265 lines
11 KiB
Go
265 lines
11 KiB
Go
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},
|
||
}
|
||
if got := len(SybStageOptions()) - 1; got != len(cases) {
|
||
t.Fatalf("筛选项有 %d 个业务阶段,但测试只覆盖 %d 个", got, len(cases))
|
||
}
|
||
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("无法解析更新时间时不应永久停留在正常采集中")
|
||
}
|
||
}
|