228 lines
9.1 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|