Files
cmautobuy/admin/service/purchase_workflow_test.go
T

228 lines
9.1 KiB
Go

package service
import (
"database/sql"
"testing"
"time"
"cmautobuy/admin/model"
"cmautobuy/admin/repository"
)
const collectedThreeDimensions = `{
"goods_id":"737116531267",
"price_granularity":"sku",
"dimensions":[{"key":"color","name":"颜色"},{"key":"size","name":"尺码"},{"key":"style","name":"款式"}],
"skus":[
{"options":{"style":"常规","size":"M码","color":"黑色"},"price_cent":3990,"available":true},
{"options":{"style":"加绒","size":"L码","color":"白色"},"price_cent":4590,"available":true},
{"options":{"style":"下架","size":"S码","color":"红色"},"price_cent":2990,"available":false}
]
}`
func seedPurchasableWorkflow(t *testing.T, db *sql.DB, sybID string) string {
t.Helper()
order := seedWorkflowOrder(t, db, sybID, "SP-1", "黑色,M")
order.PriceTwdCent = 999900
if _, err := repository.UpsertSybOrder(db, order); err != nil {
t.Fatal(err)
}
if _, err := AssociateShopeePdd(db, "SP-1", pddURLA, false); err != nil {
t.Fatal(err)
}
if err := repository.SetCollectResult(db, "737116531267", "PDD 上衣", "店铺", collectedThreeDimensions); err != nil {
t.Fatal(err)
}
key, _ := OptionKey(map[string]string{"color": "黑色", "size": "M码", "style": "常规"})
return key
}
func TestSybMapping_动态维度保存并复用(t *testing.T) {
db := newTestDB(t)
key := seedPurchasableWorkflow(t, db, "SYB-1")
detail, err := GetSybProcessingDetail(db, "SYB-1")
if err != nil {
t.Fatal(err)
}
if len(detail.PddDimensionNames) != 3 || detail.PddDimensionNames[2] != "款式" || len(detail.PddChoices) != 2 {
t.Fatalf("动态维度/可用组合不对: names=%v choices=%+v", detail.PddDimensionNames, detail.PddChoices)
}
if err := SaveSybMapping(db, "SYB-1", key, "USR-1"); err != nil {
t.Fatal(err)
}
detail, err = GetSybProcessingDetail(db, "SYB-1")
if err != nil {
t.Fatal(err)
}
if !detail.MappingValid {
t.Fatal("保存后相同蝦皮商品规格 + 当前 PDD 商品应自动复用")
}
list, err := ListSybOrdersView(db, "", SybStagePurchaseReady, 1)
if err != nil {
t.Fatal(err)
}
if list.Total != 1 || !list.Rows[0].CanPurchase || list.Rows[0].DefaultMaxPrice != "39.90" {
t.Fatalf("可采购阶段不对: %+v", list)
}
}
func TestSybMapping_同商品同规格第二条明细自动命中(t *testing.T) {
db := newTestDB(t)
key := seedPurchasableWorkflow(t, db, "SYB-1")
if err := SaveSybMapping(db, "SYB-1", key, "USR-1"); err != nil {
t.Fatal(err)
}
seedWorkflowOrder(t, db, "SYB-2", "SP-1", " 黑色,M ")
detail, err := GetSybProcessingDetail(db, "SYB-2")
if err != nil || detail == nil || !detail.MappingValid || detail.Stage != SybStagePurchaseReady {
t.Fatalf("第二条相同规格应自动命中: detail=%+v err=%v", detail, err)
}
}
func TestSybMapping_解析失败原文仍可保存且空规格被阻断(t *testing.T) {
db := newTestDB(t)
key := seedPurchasableWorkflow(t, db, "SYB-BASE")
seedWorkflowOrder(t, db, "SYB-DIRTY", "SP-1", "紅色,3XL建議80-90公斤】")
if err := SaveSybMapping(db, "SYB-DIRTY", key, "USR-1"); err != nil {
t.Fatalf("规格解析失败不应影响身份键映射: %v", err)
}
seedWorkflowOrder(t, db, "SYB-EMPTY", "SP-1", "")
if err := SaveSybMapping(db, "SYB-EMPTY", key, "USR-1"); err == nil {
t.Fatal("空规格不得保存映射")
}
if _, err := AssociateSybPdd(db, "SYB-EMPTY", pddURLA, false); err == nil {
t.Fatal("空规格不得进入采购关联流程")
}
}
func TestSybMapping_换品和选项消失都会失效(t *testing.T) {
t.Run("换 PDD 商品隔离旧映射", func(t *testing.T) {
db := newTestDB(t)
key := seedPurchasableWorkflow(t, db, "SYB-1")
SaveSybMapping(db, "SYB-1", key, "USR-1")
if _, err := AssociateShopeePdd(db, "SP-1", pddURLB, true); err != nil {
t.Fatal(err)
}
repository.SetCollectResult(db, "937122477375", "另一个商品", "店铺", stringsReplaceGoodsID(collectedThreeDimensions))
detail, _ := GetSybProcessingDetail(db, "SYB-1")
if detail.MappingValid {
t.Fatal("更换 PDD 商品后不得复用旧映射")
}
if _, err := AssociateShopeePdd(db, "SP-1", pddURLA, true); err != nil {
t.Fatal(err)
}
detail, _ = GetSybProcessingDetail(db, "SYB-1")
if !detail.MappingValid {
t.Fatal("换回原 PDD 商品后应复用原映射")
}
})
t.Run("最新采集结果删除原选项", func(t *testing.T) {
db := newTestDB(t)
key := seedPurchasableWorkflow(t, db, "SYB-1")
SaveSybMapping(db, "SYB-1", key, "USR-1")
changed := `{"goods_id":"737116531267","dimensions":[{"key":"color","name":"颜色"}],"skus":[{"options":{"color":"白色"},"price_cent":1000,"available":true}]}`
repository.SetCollectResult(db, "737116531267", "PDD 上衣", "店铺", changed)
detail, _ := GetSybProcessingDetail(db, "SYB-1")
if detail.MappingValid || detail.Stage != SybStageMappingPending {
t.Fatalf("失效映射应回到待匹配: valid=%t stage=%s", detail.MappingValid, detail.Stage)
}
})
}
func stringsReplaceGoodsID(raw string) string {
return `{"goods_id":"937122477375","price_granularity":"sku","dimensions":[{"key":"color","name":"颜色"},{"key":"size","name":"尺码"},{"key":"style","name":"款式"}],"skus":[{"options":{"style":"常规","size":"M码","color":"黑色"},"price_cent":3990,"available":true}]}`
}
func TestCreatePurchaseTasks_安全字段和重复保护(t *testing.T) {
db := newTestDB(t)
key := seedPurchasableWorkflow(t, db, "SYB-1")
if err := SaveSybMapping(db, "SYB-1", key, "USR-1"); err != nil {
t.Fatal(err)
}
admin, _, _ := prepareClientAssignmentUsers(t, db)
if err := RegisterClient(db, model.Client{ClientID: "CLIENT-1", Name: "采购机"}, true); err != nil {
t.Fatal(err)
}
result, err := CreatePurchaseTasks(db, admin,
[]PurchaseTaskRequest{{SybID: "SYB-1", MaxPriceCent: 4200}}, "CLIENT-1")
if err != nil || result.Created != 1 || len(result.Failures) != 0 {
t.Fatalf("创建结果=%+v err=%v", result, err)
}
var task model.Task
var status string
err = db.QueryRow(`SELECT status, assigned_client, syb_id, order_no, goods_id,
shopee_sku_id, pdd_goods_url, pdd_goods_id, pdd_options, quantity, max_price_cent
FROM tasks WHERE task_type='purchase' AND syb_id='SYB-1'`).Scan(
&status, &task.AssignedClient, &task.SybID, &task.OrderNo, &task.GoodsID,
&task.ShopeeSKUID, &task.PddGoodsURL, &task.PddGoodsID, &task.PddOptions,
&task.Quantity, &task.MaxPriceCent)
if err != nil {
t.Fatal(err)
}
if status != "assigned" || task.MaxPriceCent != 4200 || task.MaxPriceCent == 999900 ||
task.Quantity != 1 || task.PddOptions != key || task.AssignedClient != "CLIENT-1" {
t.Fatalf("采购任务字段不安全或不完整: status=%s task=%+v", status, task)
}
claimed, err := repository.ClaimNextTask(db, "CLIENT-1", []string{"purchase"})
if err != nil || claimed == nil || claimed.PddOptions != key ||
claimed.Quantity != 1 || claimed.MaxPriceCent != 4200 {
t.Fatalf("现有 Client 领取契约读不到完整采购参数: task=%+v err=%v", claimed, err)
}
second, err := CreatePurchaseTasks(db, admin,
[]PurchaseTaskRequest{{SybID: "SYB-1", MaxPriceCent: 4300}}, "CLIENT-1")
if err != nil || second.Created != 0 || len(second.Failures) != 1 {
t.Fatalf("重复提交应逐条拒绝: result=%+v err=%v", second, err)
}
}
func TestCreatePurchaseTasks_客户端权限和逐条失败(t *testing.T) {
db := newTestDB(t)
key := seedPurchasableWorkflow(t, db, "SYB-1")
SaveSybMapping(db, "SYB-1", key, "USR-1")
admin, buyerA, buyerB := prepareClientAssignmentUsers(t, db)
RegisterClient(db, model.Client{ClientID: "CLIENT-A"}, true)
AssignClient(db, admin, "CLIENT-A", buyerA.UserID, time.Now())
if _, err := CreatePurchaseTasks(db, buyerB,
[]PurchaseTaskRequest{{SybID: "SYB-1", MaxPriceCent: 4200}}, "CLIENT-A"); err == nil {
t.Fatal("采购员不得使用其他采购员的客户端")
}
result, err := CreatePurchaseTasks(db, buyerA,
[]PurchaseTaskRequest{{SybID: "SYB-1", MaxPriceCent: 0}, {SybID: "NOT-FOUND", MaxPriceCent: 100}}, "CLIENT-A")
if err != nil || result.Created != 0 || len(result.Failures) != 2 {
t.Fatalf("业务失败应逐条返回且不创建: result=%+v err=%v", result, err)
}
}
func TestCreatePurchaseTasks_部分业务失败不影响有效明细(t *testing.T) {
db := newTestDB(t)
key := seedPurchasableWorkflow(t, db, "SYB-1")
SaveSybMapping(db, "SYB-1", key, "USR-1")
admin, _, _ := prepareClientAssignmentUsers(t, db)
RegisterClient(db, model.Client{ClientID: "CLIENT-1"}, true)
result, err := CreatePurchaseTasks(db, admin, []PurchaseTaskRequest{
{SybID: "NOT-FOUND", MaxPriceCent: 100},
{SybID: "SYB-1", MaxPriceCent: 4200},
}, "CLIENT-1")
if err != nil || result.Created != 1 || len(result.Failures) != 1 || result.Failures[0].SybID != "NOT-FOUND" {
t.Fatalf("部分失败边界不对: result=%+v err=%v", result, err)
}
}
func TestParsePriceYuanToCent_精确转换(t *testing.T) {
for raw, want := range map[string]int64{"39.90": 3990, "1": 100, "0.01": 1, "12.3": 1230} {
got, err := ParsePriceYuanToCent(raw)
if err != nil || got != want {
t.Fatalf("%s => %d, %v; want %d", raw, got, err, want)
}
}
for _, raw := range []string{"", "0", "-1", "1.234", "abc"} {
if _, err := ParsePriceYuanToCent(raw); err == nil {
t.Fatalf("应拒绝 %q", raw)
}
}
}