feat(t237): import freight orders synchronously

This commit is contained in:
QiuSW
2026-07-29 12:11:56 +08:00
parent 4bce1e162c
commit 8d5b88f4a2
18 changed files with 653 additions and 97 deletions
@@ -3,6 +3,7 @@ package usecase
import (
"context"
"errors"
"sync"
"testing"
"time"
@@ -98,6 +99,176 @@ func TestCreateFreightOrderSyncStopsBeforePersistingWhenOCRIsInvalid(t *testing.
}
}
func TestCreateFreightOrderSyncReturnsCommittedResult(t *testing.T) {
repository := &syncTrackingRepository{}
service, err := NewFreightService(
repository,
&recordingDateSource{},
fakeClock{},
&sequenceIDs{},
time.Minute,
)
if err != nil {
t.Fatalf("NewFreightService() error = %v", err)
}
if service.orderTimeout != 55*time.Second {
t.Fatalf("order timeout = %s", service.orderTimeout)
}
result, err := service.CreateOrderSync(
context.Background(),
validFreightOrderSyncCommand("sync-success"),
)
if err != nil {
t.Fatalf("CreateOrderSync() error = %v", err)
}
if result.Replayed || result.Run.Status != domain.FreightSyncSucceeded ||
result.Run.OrderCount != 1 || result.Run.ItemCount != 1 ||
result.Run.StartedAt == nil || result.Run.FinishedAt == nil {
t.Fatalf("CreateOrderSync() result = %+v", result)
}
repository.mu.Lock()
defer repository.mu.Unlock()
if repository.startCalls != 1 || repository.completeCalls != 1 ||
repository.failCalls != 0 {
t.Fatalf(
"start/complete/fail calls = %d/%d/%d",
repository.startCalls,
repository.completeCalls,
repository.failCalls,
)
}
}
func TestCreateFreightOrderSyncTimeoutUsesLiveCleanupContext(t *testing.T) {
repository := &syncTrackingRepository{}
source := &blockingOrderSource{}
service, err := NewFreightService(
repository,
source,
fakeClock{},
&sequenceIDs{},
time.Minute,
)
if err != nil {
t.Fatalf("NewFreightService() error = %v", err)
}
service.orderTimeout = 10 * time.Millisecond
_, err = service.CreateOrderSync(
context.Background(),
validFreightOrderSyncCommand("sync-timeout"),
)
assertUsecaseError(t, err, ErrorKindUnavailable, "FREIGHT_SYNC_TIMEOUT")
repository.mu.Lock()
defer repository.mu.Unlock()
if repository.failCalls != 1 ||
repository.failCode != "FREIGHT_SYNC_TIMEOUT" ||
repository.failContextErr != nil {
t.Fatalf(
"failure calls/code/context = %d/%q/%v",
repository.failCalls,
repository.failCode,
repository.failContextErr,
)
}
}
func TestCreateFreightOrderSyncCancellationUsesLiveCleanupContext(t *testing.T) {
repository := &syncTrackingRepository{}
source := &blockingOrderSource{started: make(chan struct{})}
service, err := NewFreightService(
repository,
source,
fakeClock{},
&sequenceIDs{},
time.Minute,
)
if err != nil {
t.Fatalf("NewFreightService() error = %v", err)
}
ctx, cancel := context.WithCancel(context.Background())
result := make(chan error, 1)
go func() {
_, runErr := service.CreateOrderSync(
ctx,
validFreightOrderSyncCommand("sync-canceled"),
)
result <- runErr
}()
select {
case <-source.started:
case <-time.After(time.Second):
t.Fatal("order sync did not reach source")
}
cancel()
err = <-result
assertUsecaseError(t, err, ErrorKindUnavailable, "FREIGHT_SYNC_CANCELED")
repository.mu.Lock()
defer repository.mu.Unlock()
if repository.failCalls != 1 ||
repository.failCode != "FREIGHT_SYNC_CANCELED" ||
repository.failContextErr != nil {
t.Fatalf(
"failure calls/code/context = %d/%q/%v",
repository.failCalls,
repository.failCode,
repository.failContextErr,
)
}
}
func TestCreateFreightOrderSyncRejectsConcurrentRequest(t *testing.T) {
repository := &syncTrackingRepository{}
source := &blockingOrderSource{
started: make(chan struct{}),
release: make(chan struct{}),
}
service, err := NewFreightService(
repository,
source,
fakeClock{},
&sequenceIDs{},
time.Minute,
)
if err != nil {
t.Fatalf("NewFreightService() error = %v", err)
}
firstResult := make(chan error, 1)
go func() {
_, runErr := service.CreateOrderSync(
context.Background(),
validFreightOrderSyncCommand("sync-first"),
)
firstResult <- runErr
}()
select {
case <-source.started:
case <-time.After(time.Second):
t.Fatal("first order sync did not reach source")
}
startedAt := time.Now()
_, err = service.CreateOrderSync(
context.Background(),
validFreightOrderSyncCommand("sync-second"),
)
assertUsecaseError(t, err, ErrorKindConflict, "FREIGHT_SYNC_BUSY")
if time.Since(startedAt) > 100*time.Millisecond {
t.Fatalf("busy response took %s", time.Since(startedAt))
}
close(source.release)
if err := <-firstResult; err != nil {
t.Fatalf("first CreateOrderSync() error = %v", err)
}
}
func validFreightOrderSyncCommand(idempotencyKey string) CreateFreightSyncCommand {
return CreateFreightSyncCommand{
CreatorSubject: "local-admin",
ActorUserID: "00000000-0000-4000-8000-000000000099",
IdempotencyKey: idempotencyKey,
OrderNumber: "ORDER-123",
}
}
func TestFreightDateQuerySplitsIntoSevenDayWindows(t *testing.T) {
source := &recordingDateSource{}
service := &FreightService{source: source}
@@ -261,6 +432,144 @@ type recordingDateSource struct {
ensureErr error
}
type blockingOrderSource struct {
started chan struct{}
release chan struct{}
once sync.Once
}
func (source *blockingOrderSource) QueryOrder(
ctx context.Context,
_ string,
) (domain.FreightSourceBatch, error) {
if source.started != nil {
source.once.Do(func() {
close(source.started)
})
}
if source.release != nil {
select {
case <-source.release:
case <-ctx.Done():
return domain.FreightSourceBatch{}, ctx.Err()
}
} else {
<-ctx.Done()
return domain.FreightSourceBatch{}, ctx.Err()
}
return validFreightSource(), nil
}
func (*blockingOrderSource) QueryCreatedRange(
context.Context,
string,
string,
) (domain.FreightSourceBatch, error) {
return domain.FreightSourceBatch{}, errors.New("unexpected date query")
}
type syncTrackingRepository struct {
mu sync.Mutex
startCalls int
completeCalls int
failCalls int
failCode string
failContextErr error
}
func (*syncTrackingRepository) CreateFreightSync(
_ context.Context,
run domain.FreightSyncRun,
_, _ string,
) (domain.FreightSyncRun, bool, error) {
return run, true, nil
}
func (repository *syncTrackingRepository) StartFreightSync(
context.Context,
string,
time.Time,
) error {
repository.mu.Lock()
defer repository.mu.Unlock()
repository.startCalls++
return nil
}
func (repository *syncTrackingRepository) CompleteFreightSync(
context.Context,
domain.FreightSyncRun,
domain.FreightImportBatch,
time.Time,
) error {
repository.mu.Lock()
defer repository.mu.Unlock()
repository.completeCalls++
return nil
}
func (*syncTrackingRepository) CompleteFreightDateSync(
context.Context,
domain.FreightSyncRun,
domain.FreightImportBatch,
time.Time,
time.Time,
) error {
return errors.New("unexpected date completion")
}
func (repository *syncTrackingRepository) FailFreightSync(
ctx context.Context,
_ string,
code string,
_ time.Time,
) error {
repository.mu.Lock()
defer repository.mu.Unlock()
repository.failCalls++
repository.failCode = code
repository.failContextErr = ctx.Err()
return nil
}
func (*syncTrackingRepository) RecoverFreightSyncs(
context.Context,
time.Time,
) (int64, error) {
return 0, nil
}
func (*syncTrackingRepository) GetFreightSync(
context.Context,
string,
string,
) (domain.FreightSyncRun, error) {
return domain.FreightSyncRun{}, nil
}
func (*syncTrackingRepository) GetFreightSyncWatermark(
context.Context,
string,
) (*domain.FreightSyncWatermark, error) {
return nil, nil
}
func (*syncTrackingRepository) ListFreightOrders(
context.Context,
string,
int,
) ([]domain.FreightOrder, error) {
return nil, nil
}
func (*syncTrackingRepository) GetFreightOrder(
context.Context,
string,
string,
) (domain.FreightOrderDetail, error) {
return domain.FreightOrderDetail{}, nil
}
func (source *recordingDateSource) EnsureAuthenticated(context.Context) error {
source.ensureCalls++
return source.ensureErr