Files
goauto/server/app/goauto/sybinnercode/apply_test.go
T

195 lines
6.6 KiB
Go

package sybinnercode
import (
"context"
"errors"
"testing"
"go-admin/app/goauto/models"
"go-admin/app/goauto/sybclient"
"github.com/google/uuid"
)
type fakeWriter struct {
fakeMatchReader
deleteCalls, createCalls, updateCalls int
unknownAfterUpdate bool
nextID int64
}
func (f *fakeWriter) DeleteInnerCode(_ context.Context, detailID int64) error {
f.deleteCalls++
for stockID, stock := range f.stocks {
for i := range stock.Details {
if stock.Details[i].ID == detailID {
stock.Details[i].Raw["innerExpCode"] = ""
f.stocks[stockID] = stock
return nil
}
}
}
return errors.New("detail missing")
}
func (f *fakeWriter) CreateInnerCodeDetail(_ context.Context, stockID int64, _ string) (int64, error) {
f.createCalls++
f.nextID++
stock := f.stocks[stockID]
stock.Details = append(stock.Details, detail(f.nextID, "", 1, "", "", ""))
f.stocks[stockID] = stock
return f.nextID, nil
}
func (f *fakeWriter) UpdateDetailCode(_ context.Context, stockID, detailID int64, code string) error {
f.updateCalls++
stock := f.stocks[stockID]
for i := range stock.Details {
if stock.Details[i].ID == detailID {
stock.Details[i].Raw["innerExpCode"] = code
f.stocks[stockID] = stock
if f.unknownAfterUpdate {
return sybclient.ErrWriteResultUnknown
}
return nil
}
}
return errors.New("detail missing")
}
func readyRecordAndWriter(t *testing.T, code2 bool) (*Service, *fakeWriter, uint64) {
t.Helper()
db := testDB(t)
items := []models.SYBInnerCodeItem{{BusinessDate: "2026-08-28", Code: "IC-1", Ordinal: 1, SourceRow: 2}}
qty := 1
if code2 {
items = append(items, models.SYBInnerCodeItem{BusinessDate: "2026-08-28", Code: "IC-2", Ordinal: 2, SourceRow: 3})
qty = 2
}
record := models.SYBInnerCodeRecord{BusinessDate: "2026-08-28", OrderNumber: "ORDER-1", Stall: "A#1", SpecKey: "黑色,L", SpecRaw: "黑色,L", SourceSKURaw: "SKU-1", Status: models.SYBInnerCodePending, CreatedBy: 1, ImportRequestID: uuid.NewString(), Items: items}
jobID := createMatchJob(t, db, []models.SYBInnerCodeRecord{record})
writer := &fakeWriter{fakeMatchReader: fakeMatchReader{rows: map[string][]sybclient.StockRow{"ORDER-1": {{ID: 10, Code: "ORDER-1"}}}, stocks: map[int64]sybclient.StockDetail{10: {ID: 10, Code: "ORDER-1", Details: []sybclient.DetailItem{detail(20, "黑色,L", qty, "SKU-1", "A#1", "")}}}}, nextID: 20}
if err := RunMatchJob(context.Background(), db, writer, jobID); err != nil {
t.Fatal(err)
}
var saved models.SYBInnerCodeRecord
if err := db.First(&saved).Error; err != nil {
t.Fatal(err)
}
return NewService(db), writer, saved.ID
}
func TestQueueAndRunApplyWritesEachCodeWithCheckpoints(t *testing.T) {
service, writer, id := readyRecordAndWriter(t, true)
preview, err := service.PreviewApply(context.Background(), []uint64{id})
if err != nil {
t.Fatal(err)
}
if preview.Records != 1 || preview.InboundCodes != 2 || preview.PlaceholderDetails != 1 {
t.Fatalf("preview=%+v", preview)
}
result, err := service.QueueApply(context.Background(), 1, ApplyRequest{RequestID: uuid.NewString(), IDs: []uint64{id}})
if err != nil {
t.Fatal(err)
}
if err := RunApplyBatch(context.Background(), service.db, writer, result.BatchID); err != nil {
t.Fatal(err)
}
var record models.SYBInnerCodeRecord
service.db.First(&record, id)
if record.Status != models.SYBInnerCodeUpdated {
t.Fatalf("record=%+v", record)
}
if writer.createCalls != 1 || writer.updateCalls != 2 {
t.Fatalf("create=%d update=%d", writer.createCalls, writer.updateCalls)
}
if countCode(writer.stocks[10], "IC-1") != 1 || countCode(writer.stocks[10], "IC-2") != 1 {
t.Fatalf("stock=%+v", writer.stocks[10])
}
var checkpoints int64
service.db.Model(&models.SYBInnerCodeCheckpoint{}).Where("record_id = ?", id).Count(&checkpoints)
if checkpoints < 6 {
t.Fatalf("checkpoints=%d", checkpoints)
}
}
func TestUnknownWriteResultNeedsCheckAndReadOnlyRecheckConfirms(t *testing.T) {
service, writer, id := readyRecordAndWriter(t, false)
writer.unknownAfterUpdate = true
result, err := service.QueueApply(context.Background(), 1, ApplyRequest{RequestID: uuid.NewString(), IDs: []uint64{id}})
if err != nil {
t.Fatal(err)
}
if err := RunApplyBatch(context.Background(), service.db, writer, result.BatchID); err != nil {
t.Fatal(err)
}
var record models.SYBInnerCodeRecord
service.db.First(&record, id)
if record.Status != models.SYBInnerCodeNeedsCheck || writer.updateCalls != 1 {
t.Fatalf("record=%+v calls=%d", record, writer.updateCalls)
}
status, _, err := Recheck(context.Background(), service.db, writer, id)
if err != nil {
t.Fatal(err)
}
if status != models.SYBInnerCodeUpdated || writer.updateCalls != 1 {
t.Fatalf("status=%s calls=%d", status, writer.updateCalls)
}
}
func TestApplyRequestIsIdempotent(t *testing.T) {
service, _, id := readyRecordAndWriter(t, false)
request := ApplyRequest{RequestID: uuid.NewString(), IDs: []uint64{id}}
first, err := service.QueueApply(context.Background(), 1, request)
if err != nil {
t.Fatal(err)
}
second, err := service.QueueApply(context.Background(), 1, request)
if err != nil {
t.Fatal(err)
}
if first != second {
t.Fatalf("first=%+v second=%+v", first, second)
}
var count int64
service.db.Model(&models.SYBInnerCodeApplyBatch{}).Count(&count)
if count != 1 {
t.Fatalf("batches=%d", count)
}
}
func TestDatabaseLeaseAllowsOnlyOneOwner(t *testing.T) {
db := testDB(t)
first, err := acquireLease(context.Background(), db, "owner-a")
if err != nil || !first {
t.Fatalf("first=%v err=%v", first, err)
}
second, err := acquireLease(context.Background(), db, "owner-b")
if err != nil {
t.Fatal(err)
}
if second {
t.Fatal("second owner acquired active lease")
}
releaseLease(db, "owner-a")
second, err = acquireLease(context.Background(), db, "owner-b")
if err != nil || !second {
t.Fatalf("after release=%v err=%v", second, err)
}
}
func TestRecoverInterruptedSeparatesQueuedAndApplying(t *testing.T) {
db := testDB(t)
records := []models.SYBInnerCodeRecord{{BusinessDate: "2026-08-28", OrderNumber: "Q", Stall: "", SpecKey: "Q", SpecRaw: "Q", Status: models.SYBInnerCodeQueued, CreatedBy: 1, ImportRequestID: uuid.NewString()}, {BusinessDate: "2026-08-28", OrderNumber: "A", Stall: "", SpecKey: "A", SpecRaw: "A", Status: models.SYBInnerCodeApplying, CreatedBy: 1, ImportRequestID: uuid.NewString()}}
for i := range records {
if err := db.Create(&records[i]).Error; err != nil {
t.Fatal(err)
}
}
if err := RecoverInterrupted(db); err != nil {
t.Fatal(err)
}
db.Order("id").Find(&records)
if records[0].Status != models.SYBInnerCodeReady || records[1].Status != models.SYBInnerCodeNeedsCheck {
t.Fatalf("records=%+v", records)
}
}