feat(t245): use ERP images as procurement references
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user