195 lines
6.6 KiB
Go
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)
|
|
}
|
|
}
|