Files
cmautobuy/admin/service/syb_workflow_test.go
T

269 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"database/sql"
"fmt"
"strings"
"testing"
"cmautobuy/admin/model"
"cmautobuy/admin/repository"
)
func seedWorkflowOrder(t *testing.T, db *sql.DB, sybID, goodsID, rawSpec string) model.SybOrder {
t.Helper()
order := model.SybOrder{
SybID: sybID, OrderNo: "ORDER-" + sybID, Title: "测试商品",
ProductSpec: rawSpec, ShopeeGoodsID: goodsID, Quantity: 1, SybData: `{}`,
}
if _, err := repository.UpsertSybOrder(db, order); err != nil {
t.Fatalf("准备顺运宝明细失败: %v", err)
}
return order
}
func TestUpsertSybOrder_补建骨架并由报表提升来源(t *testing.T) {
db := newSyncTestDB(t)
seedWorkflowOrder(t, db, "SYB-1", "SP-1", "黑色, M")
product, err := repository.GetShopeeProductByGoodsID(db, "SP-1")
if err != nil || product == nil || product.Source != "syb" || product.Title != "测试商品" {
t.Fatalf("顺运宝骨架不正确: product=%+v err=%v", product, err)
}
context, err := repository.GetSybOrderContext(db, "SYB-1")
if err != nil || context == nil || context.Order.SpecKey != "黑色, M" {
t.Fatalf("规格身份键不正确: context=%+v err=%v", context, err)
}
if err := repository.UpsertShopeeProduct(db, "SP-1", "报表标题", "正常", "MAIN-1"); err != nil {
t.Fatal(err)
}
product, err = repository.GetShopeeProductByGoodsID(db, "SP-1")
if err != nil || product.Source != "report" || product.Title != "报表标题" {
t.Fatalf("报表导入应提升骨架来源: product=%+v err=%v", product, err)
}
seedWorkflowOrder(t, db, "SYB-2", "SP-1", "白色,L")
product, _ = repository.GetShopeeProductByGoodsID(db, "SP-1")
if product.Source != "report" || product.Title != "报表标题" {
t.Fatalf("后续同步不得覆盖报表商品: %+v", product)
}
}
func TestListSybOrdersView_空规格阻断且普通规格直接进入PDD阶段(t *testing.T) {
db := newSyncTestDB(t)
seedWorkflowOrder(t, db, "SYB-EMPTY", "SP-EMPTY", "")
seedWorkflowOrder(t, db, "SYB-PDD", "SP-PDD", "黑色,M")
cases := []struct{ stage, wantID string }{
{SybStageSpecMissing, "SYB-EMPTY"},
{SybStagePddMissing, "SYB-PDD"},
}
for _, tc := range cases {
result, err := ListSybOrdersView(db, "", "", tc.stage, 1)
if err != nil {
t.Fatal(err)
}
if result.Total != 1 || len(result.Rows) != 1 || result.Rows[0].SybID != tc.wantID {
t.Fatalf("阶段 %s 结果错误: %+v", tc.stage, result)
}
if result.Rows[0].StageText == "" || result.Rows[0].ActionText == "" {
t.Fatalf("阶段 %s 必须有文字和下一步: %+v", tc.stage, result.Rows[0])
}
}
}
func TestListSybOrdersView_店铺与订单号和阶段组合筛选(t *testing.T) {
db := newSyncTestDB(t)
for _, order := range []model.SybOrder{
{SybID: "SHOP-A", OrderNo: "ORDER-A", ShopName: "台北服饰旗舰店", Title: "纯棉上衣", ProductSpec: "黑色,M", ShopeeGoodsID: "SP-A", Quantity: 1, SybData: `{}`},
{SybID: "SHOP-B", OrderNo: "ORDER-B", ShopName: "台南生活馆", Title: "牛仔裤", ProductSpec: "", ShopeeGoodsID: "SP-B", Quantity: 1, SybData: `{}`},
} {
if _, err := repository.UpsertSybOrder(db, order); err != nil {
t.Fatal(err)
}
}
result, err := ListSybOrdersView(db, "ORDER-A,NOT-FOUND", " 服饰 ", SybStagePddMissing, 1)
if err != nil || result.Total != 1 || len(result.Rows) != 1 || result.Rows[0].SybID != "SHOP-A" || result.Rows[0].ShopName != "台北服饰旗舰店" {
t.Fatalf("组合筛选错误: result=%+v err=%v", result, err)
}
if result.OrderNoQuery != "ORDER-A,NOT-FOUND" || result.OrderNoCount != 2 ||
result.MatchedOrderCount != 1 || len(result.UnmatchedOrderNos) != 1 || result.UnmatchedOrderNos[0] != "NOT-FOUND" {
t.Fatalf("订单号命中统计错误: %+v", result)
}
if !result.IsFiltered {
t.Fatal("店铺条件应标记为筛选结果")
}
}
func TestParseSybOrderNos_多分隔符去重与限制(t *testing.T) {
got, err := ParseSybOrderNos(" A,B;C;D\tE\nA ")
if err != nil || strings.Join(got, ",") != "A,B,C,D,E" {
t.Fatalf("多分隔符解析错误: got=%v err=%v", got, err)
}
tooMany := make([]string, 101)
for i := range tooMany {
tooMany[i] = fmt.Sprintf("ORDER-%03d", i)
}
if _, err := ParseSybOrderNos(strings.Join(tooMany, ",")); !IsValidationError(err) || ValidationField(err) != "order_no" {
t.Fatalf("超过 100 个订单号应返回字段校验错误: %v", err)
}
if _, err := ParseSybOrderNos(strings.Repeat("A", 192)); !IsValidationError(err) {
t.Fatalf("超长单号应返回校验错误: %v", err)
}
if _, err := ParseSybOrderNos(strings.Repeat("A,", 2001)); !IsValidationError(err) {
t.Fatalf("超长原始输入应返回校验错误: %v", err)
}
}
func TestSybStageFor_全部筛选阶段与逐行推导一致(t *testing.T) {
key, err := OptionKey(map[string]string{"color": "黑色", "size": "M码", "style": "常规"})
if err != nil {
t.Fatal(err)
}
ready := repository.SybOrderContext{
Order: model.SybOrder{SpecKey: "黑色,M", Quantity: 1},
PddGoodsID: "737116531267", PddCollectStatus: string(model.CollectCollected),
PddSkusJSON: collectedThreeDimensions, MappingOptionKey: key,
}
copyWith := func(change func(*repository.SybOrderContext)) repository.SybOrderContext {
value := ready
change(&value)
return value
}
cases := []struct {
want string
action string
context repository.SybOrderContext
}{
{SybStageSpecMissing, "核对缺失规格", copyWith(func(c *repository.SybOrderContext) { c.Order.SpecKey = "" })},
{SybStagePddMissing, "关联 PDD", copyWith(func(c *repository.SybOrderContext) { c.PddGoodsID, c.PddCollectStatus = "", "" })},
{SybStagePddPending, "创建采集任务", copyWith(func(c *repository.SybOrderContext) { c.PddCollectStatus = string(model.CollectPending) })},
{SybStagePddCollecting, "查看采集状态", copyWith(func(c *repository.SybOrderContext) {
c.PddCollectStatus = string(model.CollectCollecting)
c.PddUpdatedAt = model.NowISO()
c.HasActiveCollectTask = true
})},
{SybStagePddCollectingStale, "重新创建采集任务", copyWith(func(c *repository.SybOrderContext) {
c.PddCollectStatus = string(model.CollectCollecting)
c.PddUpdatedAt = model.NowISO()
})},
{SybStagePddFailed, "重新采集", copyWith(func(c *repository.SybOrderContext) { c.PddCollectStatus = string(model.CollectFailed) })},
{SybStageMappingPending, "匹配规格", copyWith(func(c *repository.SybOrderContext) { c.MappingOptionKey = "" })},
{SybStageTaskCreated, "查看采购任务", copyWith(func(c *repository.SybOrderContext) { c.HasActiveTask = true })},
{SybStagePurchaseCompleted, "查看采购结果", copyWith(func(c *repository.SybOrderContext) {
c.HasSucceededPurchaseTask, c.HasManualReviewPurchaseTask = true, true
c.Order.SpecKey = ""
})},
{SybStagePurchaseReview, "核对采购订单", copyWith(func(c *repository.SybOrderContext) {
c.HasManualReviewPurchaseTask = true
c.MappingOptionKey = ""
})},
{SybStagePurchaseBlocked, "核对采购数量", copyWith(func(c *repository.SybOrderContext) { c.Order.Quantity = 0 })},
{SybStagePurchaseReady, "创建采购任务", ready},
}
// AI 规格匹配是映射来源筛选,不会取代订单当前的业务阶段。
if got := len(SybStageOptions()) - 1; got != len(cases)+1 {
t.Fatalf("筛选项有 %d 个业务阶段或来源筛选,但测试覆盖 %d 个业务阶段", got, len(cases))
}
if ParseSybStage(SybStageAIMatched) != SybStageAIMatched || SybStageLabel(SybStageAIMatched) != "AI规格匹配" {
t.Fatal("AI 规格匹配筛选项未正确注册")
}
for _, tc := range cases {
stage, text, _, action := sybStageFor(tc.context)
wantText := SybStageLabel(tc.want)
if tc.want == SybStageSpecMissing {
// 筛选项为了工具条宽度使用短文案,行详情继续明确这是采购数据异常。
wantText = "采购数据异常:顺运宝未提供规格"
}
if stage != tc.want || text != wantText || action != tc.action {
t.Fatalf("阶段 %s 推导错误: stage=%s text=%q action=%q", tc.want, stage, text, action)
}
}
}
func TestSybStageOptions_缺规格筛选文案简洁且详情原因完整(t *testing.T) {
var filterText string
for _, option := range SybStageOptions() {
if option.Value == SybStageSpecMissing {
filterText = option.Text
break
}
}
if filterText != "顺运宝未提供规格" {
t.Fatalf("缺规格筛选文案错误: %q", filterText)
}
stage, stageText, help, _ := sybStageFor(repository.SybOrderContext{})
if stage != SybStageSpecMissing || stageText != "采购数据异常:顺运宝未提供规格" ||
!strings.Contains(help, "不能匹配或创建采购任务") {
t.Fatalf("缩短筛选项不得丢失详情原因: stage=%q text=%q help=%q", stage, stageText, help)
}
}
func TestSybOrderViewFor_批量动作能力按阶段隔离(t *testing.T) {
base := repository.SybOrderContext{
Order: model.SybOrder{SybID: "SYB-1", SpecKey: "黑色,M", Quantity: 1},
PddGoodsID: "737116531267",
}
cases := []struct {
status model.CollectStatus
mappingKey string
wantCollect, wantPurchase bool
}{
{status: model.CollectPending, wantCollect: true},
{status: model.CollectFailed, wantCollect: true},
{status: model.CollectCollecting},
{status: model.CollectCollected},
}
for _, tc := range cases {
context := base
context.PddCollectStatus = string(tc.status)
context.MappingOptionKey = tc.mappingKey
if tc.status == model.CollectCollecting {
context.PddUpdatedAt = model.NowISO()
context.HasActiveCollectTask = true
}
view := sybOrderViewFor(context)
if view.CanCollect != tc.wantCollect || view.CanPurchase != tc.wantPurchase {
t.Errorf("status=%s: CanCollect=%v CanPurchase=%v", tc.status, view.CanCollect, view.CanPurchase)
}
if view.SelectionHint == "" {
t.Errorf("status=%s: 应给出选择能力说明", tc.status)
}
}
key, err := OptionKey(map[string]string{"color": "黑色", "size": "M码", "style": "常规"})
if err != nil {
t.Fatal(err)
}
ready := base
ready.PddCollectStatus = string(model.CollectCollected)
ready.PddSkusJSON = collectedThreeDimensions
ready.MappingOptionKey = key
view := sybOrderViewFor(ready)
if view.CanCollect || !view.CanPurchase {
t.Fatalf("采购就绪行应只允许采购:CanCollect=%v CanPurchase=%v", view.CanCollect, view.CanPurchase)
}
for _, change := range []func(*repository.SybOrderContext){
func(c *repository.SybOrderContext) { c.HasSucceededPurchaseTask = true },
func(c *repository.SybOrderContext) { c.HasManualReviewPurchaseTask = true },
} {
locked := ready
change(&locked)
lockedView := sybOrderViewFor(locked)
if lockedView.CanCollect || lockedView.CanPurchase {
t.Fatalf("采购完成或待核对行不得再提供批量动作:%+v", lockedView)
}
}
}
func TestSybCollectingIsStale_有效任务但时间损坏时允许恢复(t *testing.T) {
context := repository.SybOrderContext{
PddCollectStatus: string(model.CollectCollecting),
PddUpdatedAt: "not-a-time",
HasActiveCollectTask: true,
}
if !sybCollectingIsStale(context) {
t.Fatal("无法解析更新时间时不应永久停留在正常采集中")
}
}