feat(t245): use ERP images as procurement references

This commit is contained in:
QiuSW
2026-07-29 17:29:20 +08:00
parent a20590b4b7
commit 15074692a3
22 changed files with 1057 additions and 117 deletions
@@ -0,0 +1,281 @@
package sqlite_test
import (
"bytes"
"context"
"image"
"image/jpeg"
"testing"
"time"
"cmroubao/backend-api/internal/domain"
"cmroubao/backend-api/internal/platform/assetstore"
repository "cmroubao/backend-api/internal/repository/sqlite"
"cmroubao/backend-api/internal/usecase"
)
func TestProcurementAutomaticallyCopiesAndCanReplaceERPReference(
t *testing.T,
) {
db := openDatabase(t)
repositories, _ := repository.New(db)
ctx := context.Background()
now := time.Date(2026, 7, 29, 8, 0, 0, 0, time.UTC)
userID := uuid(1700)
seedFreightUser(t, db, userID, now)
run := freightRun(1701, userID, now)
createAndStartFreightRun(t, repositories, run, "auto-reference", "1")
batch := freightBatch(1710, "a", "b")
firstThumb := "190100"
secondThumb := "190101"
batch.Orders[0].Items[0].ProductThumbRef = &firstThumb
batch.Orders[0].Items[1].ProductThumbRef = &secondThumb
if err := repositories.CompleteFreightSync(
ctx,
run,
batch,
now.Add(time.Second),
); err != nil {
t.Fatalf("CompleteFreightSync() error = %v", err)
}
orders, _ := repositories.ListFreightOrders(ctx, "local-admin", 10)
detail, _ := repositories.GetFreightOrder(
ctx,
"local-admin",
orders[0].ID,
)
files, err := assetstore.New(t.TempDir())
if err != nil {
t.Fatalf("assetstore.New() error = %v", err)
}
firstImage := saveReadyFreightImage(
t,
ctx,
repositories,
files,
detail.Items[0],
firstThumb,
now.Add(2*time.Second),
)
ids := &procurementIDs{next: 1800}
service, err := usecase.NewProcurementService(
repositories,
procurementClock{now: now.Add(3 * time.Second)},
ids,
usecase.WithProcurementReferenceStore(files),
)
if err != nil {
t.Fatalf("NewProcurementService() error = %v", err)
}
command := usecase.CreateProcurementRequestCommand{
CreatorSubject: "local-admin",
ActorUserID: userID,
FreightOrderItemID: detail.Items[0].ID,
ConfirmProcurementNeeded: true,
}
created, err := service.CreateRequest(ctx, command)
if err != nil ||
created.Request.Status != domain.ProcurementReady ||
created.Request.ReferenceAssetID == nil ||
created.Request.ReferenceOrigin == nil ||
*created.Request.ReferenceOrigin != domain.ProcurementReferenceERP ||
created.Request.ReferenceSourceImageSHA256 == nil ||
*created.Request.ReferenceSourceImageSHA256 != firstImage.SHA256 {
t.Fatalf("automatic request = %+v, %v", created, err)
}
autoAsset, err := repositories.GetAsset(
ctx,
"local-admin",
*created.Request.ReferenceAssetID,
)
if err != nil || autoAsset.StorageKey == firstImage.StorageKey {
t.Fatalf("automatic asset = %+v, %v", autoAsset, err)
}
assertStoredImageExists(t, ctx, files, firstImage.StorageKey)
assertStoredImageExists(t, ctx, files, autoAsset.StorageKey)
replayed, err := service.CreateRequest(ctx, command)
if err != nil || !replayed.Replayed ||
replayed.Request.ID != created.Request.ID ||
replayed.Request.ReferenceAssetID == nil ||
*replayed.Request.ReferenceAssetID != autoAsset.ID {
t.Fatalf("automatic replay = %+v, %v", replayed, err)
}
var assetCount int
if err := db.QueryRow(
`SELECT count(*) FROM assets WHERE creator_subject = ?`,
"local-admin",
).Scan(&assetCount); err != nil || assetCount != 1 {
t.Fatalf("asset count after replay = %d, %v", assetCount, err)
}
manualID := uuid(1900)
manualNormalized := putTestImage(t, ctx, files, manualID)
manual := domain.Asset{
ID: manualID,
CreatorSubject: "local-admin",
Purpose: domain.AssetPurposeTaskReference,
MediaType: manualNormalized.MediaType,
SizeBytes: manualNormalized.SizeBytes,
SHA256: manualNormalized.SHA256,
StorageKey: manualNormalized.StorageKey,
CreatedAt: now.Add(4 * time.Second),
}
if _, _, err := repositories.CreateAssetIdempotent(
ctx,
manual,
"manual-reference",
repeatHex("a"),
); err != nil {
t.Fatalf("CreateAssetIdempotent() error = %v", err)
}
replaced, err := service.BindReference(
ctx,
usecase.BindProcurementReferenceCommand{
CreatorSubject: "local-admin",
ActorUserID: userID,
RequestID: created.Request.ID,
ImageAssetID: manual.ID,
},
)
if err != nil ||
replaced.ReferenceOrigin == nil ||
*replaced.ReferenceOrigin != domain.ProcurementReferenceManual ||
replaced.ReferenceAssetID == nil ||
*replaced.ReferenceAssetID != manual.ID {
t.Fatalf("manual replacement = %+v, %v", replaced, err)
}
if _, err := repositories.GetAsset(
ctx,
"local-admin",
autoAsset.ID,
); err == nil {
t.Fatal("replaced automatic asset remains in database")
}
assertStoredImageMissing(t, ctx, files, autoAsset.StorageKey)
assertStoredImageExists(t, ctx, files, firstImage.StorageKey)
assertStoredImageExists(t, ctx, files, manual.StorageKey)
legacy, _ := usecase.NewProcurementService(
repositories,
procurementClock{now: now.Add(5 * time.Second)},
ids,
)
pending, err := legacy.CreateRequest(
ctx,
usecase.CreateProcurementRequestCommand{
CreatorSubject: "local-admin",
ActorUserID: userID,
FreightOrderItemID: detail.Items[1].ID,
ConfirmProcurementNeeded: true,
},
)
if err != nil || pending.Request.Status != domain.ProcurementNeedsImage {
t.Fatalf("pending request = %+v, %v", pending, err)
}
saveReadyFreightImage(
t,
ctx,
repositories,
files,
detail.Items[1],
secondThumb,
now.Add(6*time.Second),
)
upgraded, err := service.CreateRequest(
ctx,
usecase.CreateProcurementRequestCommand{
CreatorSubject: "local-admin",
ActorUserID: userID,
FreightOrderItemID: detail.Items[1].ID,
ConfirmProcurementNeeded: true,
},
)
if err != nil || !upgraded.Replayed ||
upgraded.Request.ID != pending.Request.ID ||
upgraded.Request.Status != domain.ProcurementReady {
t.Fatalf("upgraded request = %+v, %v", upgraded, err)
}
}
func saveReadyFreightImage(
t *testing.T,
ctx context.Context,
repositories *repository.Store,
files *assetstore.Store,
item domain.FreightOrderItem,
thumb string,
now time.Time,
) domain.FreightItemImage {
t.Helper()
normalized := putTestImage(t, ctx, files, item.ID)
candidate := domain.FreightItemImage{
CreatorSubject: "local-admin",
FreightOrderItemID: item.ID,
ProductThumbRef: thumb,
Status: domain.FreightItemImageReady,
MediaType: normalized.MediaType,
SizeBytes: normalized.SizeBytes,
SHA256: normalized.SHA256,
StorageKey: normalized.StorageKey,
UpdatedAt: now,
}
if _, err := repositories.SaveFreightItemImage(ctx, candidate); err != nil {
t.Fatalf("SaveFreightItemImage() error = %v", err)
}
return candidate
}
func putTestImage(
t *testing.T,
ctx context.Context,
files *assetstore.Store,
id string,
) usecase.NormalizedReferenceImage {
t.Helper()
var content bytes.Buffer
if err := jpeg.Encode(
&content,
image.NewRGBA(image.Rect(0, 0, 8, 6)),
nil,
); err != nil {
t.Fatalf("jpeg.Encode() error = %v", err)
}
normalized, err := files.Put(
ctx,
id,
"image/jpeg",
bytes.NewReader(content.Bytes()),
)
if err != nil {
t.Fatalf("files.Put() error = %v", err)
}
return normalized
}
func assertStoredImageExists(
t *testing.T,
ctx context.Context,
files *assetstore.Store,
key string,
) {
t.Helper()
content, err := files.Open(ctx, key)
if err != nil {
t.Fatalf("files.Open(%q) error = %v", key, err)
}
_ = content.Close()
}
func assertStoredImageMissing(
t *testing.T,
ctx context.Context,
files *assetstore.Store,
key string,
) {
t.Helper()
if content, err := files.Open(ctx, key); err == nil {
_ = content.Close()
t.Fatalf("files.Open(%q) succeeded after cleanup", key)
}
}