901 lines
23 KiB
Go
901 lines
23 KiB
Go
package usecase
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"cmroubao/backend-api/internal/domain"
|
|
)
|
|
|
|
func TestFreightNormalizationHashExcludesInternalIDs(t *testing.T) {
|
|
source := validFreightSource()
|
|
firstService := &FreightService{ids: &sequenceIDs{next: 10}}
|
|
first, err := firstService.normalize(source)
|
|
if err != nil {
|
|
t.Fatalf("first normalize error = %v", err)
|
|
}
|
|
secondService := &FreightService{ids: &sequenceIDs{next: 100}}
|
|
second, err := secondService.normalize(source)
|
|
if err != nil {
|
|
t.Fatalf("second normalize error = %v", err)
|
|
}
|
|
if first.Orders[0].ID == second.Orders[0].ID {
|
|
t.Fatal("test IDs did not differ")
|
|
}
|
|
if first.Orders[0].CanonicalSHA256 !=
|
|
second.Orders[0].CanonicalSHA256 {
|
|
t.Fatalf(
|
|
"canonical hash depends on internal ID: %s != %s",
|
|
first.Orders[0].CanonicalSHA256,
|
|
second.Orders[0].CanonicalSHA256,
|
|
)
|
|
}
|
|
changed := validFreightSource()
|
|
changedPrice := *changed.Orders[0].Items[0].OriginalUnitPriceMinor + 1
|
|
changed.Orders[0].Items[0].OriginalUnitPriceMinor = &changedPrice
|
|
changedBatch, err := firstService.normalize(changed)
|
|
if err != nil {
|
|
t.Fatalf("changed price normalize error = %v", err)
|
|
}
|
|
if first.Orders[0].CanonicalSHA256 ==
|
|
changedBatch.Orders[0].CanonicalSHA256 ||
|
|
first.Orders[0].Items[0].CanonicalSHA256 ==
|
|
changedBatch.Orders[0].Items[0].CanonicalSHA256 {
|
|
t.Fatal("canonical hash ignored original unit price")
|
|
}
|
|
}
|
|
|
|
func TestFreightNormalizationRejectsConflictingIdentityAndInvalidTime(
|
|
t *testing.T,
|
|
) {
|
|
duplicate := validFreightSource()
|
|
duplicate.Orders[0].Items = append(
|
|
duplicate.Orders[0].Items,
|
|
duplicate.Orders[0].Items[0],
|
|
)
|
|
service := &FreightService{ids: &sequenceIDs{}}
|
|
if _, err := service.normalize(duplicate); err == nil {
|
|
t.Fatal("duplicate item normalize error = nil")
|
|
}
|
|
|
|
invalidTime := validFreightSource()
|
|
value := "not-a-time"
|
|
invalidTime.Orders[0].SourceCreatedAt = &value
|
|
if _, err := service.normalize(invalidTime); err == nil {
|
|
t.Fatal("invalid source time normalize error = nil")
|
|
}
|
|
}
|
|
|
|
func TestParseERPTimeAcceptsConfirmedMinuteAndSecondPrecision(t *testing.T) {
|
|
shanghaiExpected := time.Date(2026, 7, 29, 4, 34, 0, 0, time.UTC)
|
|
shanghaiSecondExpected := time.Date(
|
|
2026,
|
|
7,
|
|
29,
|
|
4,
|
|
34,
|
|
56,
|
|
0,
|
|
time.UTC,
|
|
)
|
|
testCases := []struct {
|
|
name string
|
|
value string
|
|
expected time.Time
|
|
}{
|
|
{"space minute", "2026-07-29 12:34", shanghaiExpected},
|
|
{"T minute", "2026-07-29T12:34", shanghaiExpected},
|
|
{"space second", "2026-07-29 12:34:56", shanghaiSecondExpected},
|
|
{"T second", "2026-07-29T12:34:56", shanghaiSecondExpected},
|
|
{
|
|
"RFC3339",
|
|
"2026-07-29T12:34:56+08:00",
|
|
shanghaiSecondExpected,
|
|
},
|
|
}
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
actual, err := parseERPTime(&testCase.value)
|
|
if err != nil || actual == nil || !actual.Equal(testCase.expected) {
|
|
t.Fatalf(
|
|
"parseERPTime(%q) = %v, %v; want %v",
|
|
testCase.value,
|
|
actual,
|
|
err,
|
|
testCase.expected,
|
|
)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseERPTimeRejectsUnknownMinuteFormats(t *testing.T) {
|
|
for _, value := range []string{
|
|
"2026-02-30 12:34",
|
|
"2026-07-29 12",
|
|
"2026-07-29 12:34 extra",
|
|
"29/07/2026 12:34",
|
|
} {
|
|
if actual, err := parseERPTime(&value); err == nil || actual != nil {
|
|
t.Fatalf("parseERPTime(%q) = %v, %v", value, actual, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFreightNormalizationAcceptsDetailMinutePrecisionTime(t *testing.T) {
|
|
source := validFreightSource()
|
|
value := "2026-07-29 12:34"
|
|
source.Orders[0].SourceCreatedAt = &value
|
|
service := &FreightService{ids: &sequenceIDs{}}
|
|
result, err := service.normalize(source)
|
|
expected := time.Date(2026, 7, 29, 4, 34, 0, 0, time.UTC)
|
|
if err != nil || len(result.Orders) != 1 ||
|
|
result.Orders[0].SourceCreatedAt == nil ||
|
|
!result.Orders[0].SourceCreatedAt.Equal(expected) {
|
|
t.Fatalf("normalize minute time = %+v, %v", result.Orders, err)
|
|
}
|
|
}
|
|
|
|
func TestFreightSourceErrorCodesAreSourceNeutral(t *testing.T) {
|
|
cases := []struct {
|
|
err error
|
|
want string
|
|
}{
|
|
{domain.ErrFreightSourceNotConfigured, "ERP_NOT_CONFIGURED"},
|
|
{domain.ErrFreightSourceSessionNeeded, "ERP_SESSION_REQUIRED"},
|
|
{domain.ErrFreightSourceNotFound, "ERP_FREIGHT_NOT_FOUND"},
|
|
{domain.ErrFreightSourceProtocol, "ERP_RESPONSE_INVALID"},
|
|
{domain.ErrFreightSourceOCRInvalid, "OCR_SERVICE_INVALID"},
|
|
{domain.ErrFreightSourceLoginRejected, "ERP_LOGIN_REJECTED"},
|
|
{errors.New("temporary source failure"), "ERP_UNAVAILABLE"},
|
|
}
|
|
for _, testCase := range cases {
|
|
if got := freightSourceErrorCode(testCase.err); got != testCase.want {
|
|
t.Fatalf("freightSourceErrorCode(%v) = %q, want %q", testCase.err, got, testCase.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCreateFreightOrderSyncStopsBeforePersistingWhenOCRIsInvalid(t *testing.T) {
|
|
repository := &dateCaptureRepository{}
|
|
service, err := NewFreightService(
|
|
repository,
|
|
&recordingDateSource{ensureErr: domain.ErrFreightSourceOCRInvalid},
|
|
fakeClock{},
|
|
&sequenceIDs{},
|
|
time.Minute,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("NewFreightService() error = %v", err)
|
|
}
|
|
_, err = service.CreateOrderSync(context.Background(), CreateFreightSyncCommand{
|
|
CreatorSubject: "local-admin",
|
|
ActorUserID: "00000000-0000-4000-8000-000000000099",
|
|
IdempotencyKey: "ocr-invalid",
|
|
OrderNumber: "ORDER-123",
|
|
})
|
|
if !errors.Is(err, domain.ErrFreightSourceOCRInvalid) || repository.createCalls != 0 {
|
|
t.Fatalf("CreateOrderSync() error/calls = %v / %d", err, repository.createCalls)
|
|
}
|
|
}
|
|
|
|
func TestCreateFreightOrderSyncReturnsCommittedResult(t *testing.T) {
|
|
repository := &syncTrackingRepository{}
|
|
imageCache := &recordingFreightImageCache{
|
|
err: errors.New("best effort image failure"),
|
|
}
|
|
service, err := NewFreightService(
|
|
repository,
|
|
&recordingDateSource{},
|
|
fakeClock{},
|
|
&sequenceIDs{},
|
|
time.Minute,
|
|
WithFreightImageCache(imageCache),
|
|
)
|
|
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)
|
|
}
|
|
if imageCache.calls != 1 ||
|
|
imageCache.creatorSubject != "local-admin" ||
|
|
imageCache.runID != result.Run.ID {
|
|
t.Fatalf("image cache calls = %+v", imageCache)
|
|
}
|
|
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,
|
|
)
|
|
}
|
|
}
|
|
|
|
type recordingFreightImageCache struct {
|
|
calls int
|
|
creatorSubject string
|
|
runID string
|
|
err error
|
|
deleted []string
|
|
}
|
|
|
|
func (cache *recordingFreightImageCache) CacheRun(
|
|
_ context.Context,
|
|
creatorSubject, runID string,
|
|
) error {
|
|
cache.calls++
|
|
cache.creatorSubject = creatorSubject
|
|
cache.runID = runID
|
|
return cache.err
|
|
}
|
|
|
|
func (cache *recordingFreightImageCache) DeleteStoredImages(
|
|
storageKeys []string,
|
|
) {
|
|
cache.deleted = append(cache.deleted, storageKeys...)
|
|
}
|
|
|
|
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 TestDeleteFreightOrderValidatesConflictsAndCleansImages(t *testing.T) {
|
|
repository := &syncTrackingRepository{
|
|
deleteStorageKeys: []string{"first/image.jpg", "second/image.jpg"},
|
|
}
|
|
imageCache := &recordingFreightImageCache{}
|
|
service, err := NewFreightService(
|
|
repository,
|
|
&recordingDateSource{},
|
|
fakeClock{},
|
|
&sequenceIDs{},
|
|
time.Minute,
|
|
WithFreightImageCache(imageCache),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("NewFreightService() error = %v", err)
|
|
}
|
|
command := DeleteFreightOrderCommand{
|
|
CreatorSubject: " local-admin ",
|
|
ActorUserID: "00000000-0000-4000-8000-000000000099",
|
|
OrderID: "00000000-0000-4000-8000-000000000100",
|
|
}
|
|
if err := service.DeleteOrder(context.Background(), command); err != nil {
|
|
t.Fatalf("DeleteOrder() error = %v", err)
|
|
}
|
|
if repository.deleteCalls != 1 ||
|
|
repository.deleteCreatorSubject != "local-admin" ||
|
|
repository.deleteOrderID != command.OrderID {
|
|
t.Fatalf("delete repository call = %+v", repository)
|
|
}
|
|
if len(imageCache.deleted) != 2 ||
|
|
imageCache.deleted[0] != "first/image.jpg" ||
|
|
imageCache.deleted[1] != "second/image.jpg" {
|
|
t.Fatalf("deleted image keys = %#v", imageCache.deleted)
|
|
}
|
|
|
|
repository.deleteErr = ErrFreightOrderHasReference
|
|
err = service.DeleteOrder(context.Background(), command)
|
|
assertUsecaseError(
|
|
t,
|
|
err,
|
|
ErrorKindConflict,
|
|
"FREIGHT_ORDER_HAS_REFERENCE",
|
|
)
|
|
if len(imageCache.deleted) != 2 {
|
|
t.Fatalf("reference conflict cleaned images = %#v", imageCache.deleted)
|
|
}
|
|
|
|
repository.deleteErr = ErrFreightOrderHasPurchaseTask
|
|
err = service.DeleteOrder(context.Background(), command)
|
|
assertUsecaseError(
|
|
t,
|
|
err,
|
|
ErrorKindConflict,
|
|
"FREIGHT_ORDER_HAS_PURCHASE_TASK",
|
|
)
|
|
if len(imageCache.deleted) != 2 {
|
|
t.Fatalf("task conflict cleaned images = %#v", imageCache.deleted)
|
|
}
|
|
|
|
command.OrderID = "invalid"
|
|
err = service.DeleteOrder(context.Background(), command)
|
|
assertUsecaseError(t, err, ErrorKindInvalid, "FREIGHT_DELETE_INVALID")
|
|
if repository.deleteCalls != 3 {
|
|
t.Fatalf("invalid delete reached repository %d times", repository.deleteCalls)
|
|
}
|
|
}
|
|
|
|
func TestFreightDateQuerySplitsIntoSevenDayWindows(t *testing.T) {
|
|
source := &recordingDateSource{}
|
|
service := &FreightService{source: source}
|
|
|
|
result, err := service.queryCreatedRange(
|
|
context.Background(),
|
|
domain.FreightSyncRun{
|
|
Mode: domain.FreightSyncCreatedRange,
|
|
CreatedFrom: "2026-07-01",
|
|
CreatedTo: "2026-07-15",
|
|
},
|
|
)
|
|
|
|
if err != nil {
|
|
t.Fatalf("queryCreatedRange() error = %v", err)
|
|
}
|
|
want := [][2]string{
|
|
{"2026-07-01", "2026-07-07"},
|
|
{"2026-07-08", "2026-07-14"},
|
|
{"2026-07-15", "2026-07-15"},
|
|
}
|
|
if len(source.calls) != len(want) {
|
|
t.Fatalf("calls = %#v", source.calls)
|
|
}
|
|
for index := range want {
|
|
if source.calls[index] != want[index] {
|
|
t.Fatalf("call %d = %#v, want %#v", index, source.calls[index], want[index])
|
|
}
|
|
}
|
|
if result.Query.CreatedFrom == nil ||
|
|
*result.Query.CreatedFrom != "2026-07-01" ||
|
|
result.Query.CreatedTo == nil ||
|
|
*result.Query.CreatedTo != "2026-07-15" {
|
|
t.Fatalf("merged query = %+v", result.Query)
|
|
}
|
|
}
|
|
|
|
func TestFreightDateQueryStopsOnMiddleWindowFailure(t *testing.T) {
|
|
source := &recordingDateSource{failOnCall: 2}
|
|
service := &FreightService{source: source}
|
|
|
|
_, err := service.queryCreatedRange(
|
|
context.Background(),
|
|
domain.FreightSyncRun{
|
|
Mode: domain.FreightSyncCreatedRange,
|
|
CreatedFrom: "2026-07-01",
|
|
CreatedTo: "2026-07-15",
|
|
},
|
|
)
|
|
|
|
if !errors.Is(err, errDateSourceFailure) || len(source.calls) != 2 {
|
|
t.Fatalf("queryCreatedRange() error/calls = %v / %#v", err, source.calls)
|
|
}
|
|
}
|
|
|
|
func TestCreateFreightSyncToNowUsesWatermarkOverlapDay(t *testing.T) {
|
|
watermarkTime := time.Date(2026, 7, 25, 16, 5, 0, 0, time.UTC)
|
|
repository := &dateCaptureRepository{
|
|
watermark: &domain.FreightSyncWatermark{
|
|
CreatorSubject: "local-admin",
|
|
SourceSystem: domain.FreightSourceShunyunbao,
|
|
LastSuccessfulTo: watermarkTime,
|
|
},
|
|
}
|
|
service, err := NewFreightService(
|
|
repository,
|
|
&recordingDateSource{},
|
|
fakeClock{},
|
|
&sequenceIDs{},
|
|
time.Minute,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("NewFreightService() error = %v", err)
|
|
}
|
|
|
|
result, err := service.CreateDateSync(
|
|
context.Background(),
|
|
CreateFreightDateSyncCommand{
|
|
CreatorSubject: "local-admin",
|
|
ActorUserID: "00000000-0000-4000-8000-000000000099",
|
|
IdempotencyKey: "sync-to-now",
|
|
SyncToNow: true,
|
|
},
|
|
)
|
|
|
|
if err != nil {
|
|
t.Fatalf("CreateDateSync() error = %v", err)
|
|
}
|
|
if result.Run.CreatedFrom != "2026-07-25" ||
|
|
result.Run.CreatedTo != "2026-07-26" ||
|
|
result.Run.WatermarkThrough == nil ||
|
|
!result.Run.WatermarkThrough.Equal(fakeClock{}.Now()) {
|
|
t.Fatalf("sync-to-now run = %+v", result.Run)
|
|
}
|
|
}
|
|
|
|
func TestCreateFreightManualRangeRejectsMoreThanSevenDays(t *testing.T) {
|
|
service, _ := NewFreightService(
|
|
&dateCaptureRepository{},
|
|
&recordingDateSource{},
|
|
fakeClock{},
|
|
&sequenceIDs{},
|
|
time.Minute,
|
|
)
|
|
|
|
_, err := service.CreateDateSync(
|
|
context.Background(),
|
|
CreateFreightDateSyncCommand{
|
|
CreatorSubject: "local-admin",
|
|
ActorUserID: "00000000-0000-4000-8000-000000000099",
|
|
IdempotencyKey: "too-wide",
|
|
CreatedFrom: "2026-07-19",
|
|
CreatedTo: "2026-07-26",
|
|
},
|
|
)
|
|
|
|
assertUsecaseError(t, err, ErrorKindInvalid, "FREIGHT_SYNC_INVALID")
|
|
}
|
|
|
|
func validFreightSource() domain.FreightSourceBatch {
|
|
shop := "测试店铺"
|
|
sourceCreatedAt := "2026-07-28 08:00:00"
|
|
orderStatus := "0"
|
|
purchaseStatus := "1"
|
|
thumb := "190"
|
|
itemPurchaseStatus := "0"
|
|
quantity := 2
|
|
originalUnitPriceMinor := int64(12950)
|
|
canceled := false
|
|
return domain.FreightSourceBatch{
|
|
SchemaVersion: 1,
|
|
Query: domain.FreightSourceQuery{
|
|
Mode: domain.FreightSyncOrderNumber,
|
|
},
|
|
Orders: []domain.FreightSourceOrder{{
|
|
ExternalStockID: "12",
|
|
SourceCode: "SOURCE-12",
|
|
ShopName: &shop,
|
|
SourceCreatedAt: &sourceCreatedAt,
|
|
OrderStatus: &orderStatus,
|
|
PurchaseStatus: &purchaseStatus,
|
|
IsCanceled: &canceled,
|
|
Items: []domain.FreightSourceItem{{
|
|
ExternalItemID: "88",
|
|
Title: "商品",
|
|
ProductSpec: "黑色,L",
|
|
SKU: "黑色,L",
|
|
Quantity: &quantity,
|
|
ProductThumbRef: &thumb,
|
|
OriginalUnitPriceMinor: &originalUnitPriceMinor,
|
|
OriginalCurrency: domain.FreightCurrencyTWD,
|
|
PurchaseStatus: &itemPurchaseStatus,
|
|
}},
|
|
}},
|
|
}
|
|
}
|
|
|
|
var errDateSourceFailure = errors.New("date source failed")
|
|
|
|
type recordingDateSource struct {
|
|
calls [][2]string
|
|
failOnCall int
|
|
ensureCalls int
|
|
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
|
|
deleteCalls int
|
|
deleteCreatorSubject string
|
|
deleteOrderID string
|
|
deleteStorageKeys []string
|
|
deleteErr 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 (repository *syncTrackingRepository) DeleteFreightOrder(
|
|
_ context.Context,
|
|
creatorSubject, orderID string,
|
|
) ([]string, error) {
|
|
repository.deleteCalls++
|
|
repository.deleteCreatorSubject = creatorSubject
|
|
repository.deleteOrderID = orderID
|
|
return append([]string(nil), repository.deleteStorageKeys...),
|
|
repository.deleteErr
|
|
}
|
|
|
|
func (source *recordingDateSource) EnsureAuthenticated(context.Context) error {
|
|
source.ensureCalls++
|
|
return source.ensureErr
|
|
}
|
|
|
|
func (source *recordingDateSource) QueryOrder(
|
|
context.Context,
|
|
string,
|
|
) (domain.FreightSourceBatch, error) {
|
|
return validFreightSource(), nil
|
|
}
|
|
|
|
func (source *recordingDateSource) QueryCreatedRange(
|
|
_ context.Context,
|
|
createdFrom, createdTo string,
|
|
) (domain.FreightSourceBatch, error) {
|
|
source.calls = append(source.calls, [2]string{createdFrom, createdTo})
|
|
if source.failOnCall == len(source.calls) {
|
|
return domain.FreightSourceBatch{}, errDateSourceFailure
|
|
}
|
|
return domain.FreightSourceBatch{
|
|
SchemaVersion: 1,
|
|
Query: domain.FreightSourceQuery{
|
|
Mode: domain.FreightSyncCreatedRange,
|
|
CreatedFrom: &createdFrom,
|
|
CreatedTo: &createdTo,
|
|
},
|
|
Orders: []domain.FreightSourceOrder{},
|
|
}, nil
|
|
}
|
|
|
|
type dateCaptureRepository struct {
|
|
watermark *domain.FreightSyncWatermark
|
|
createCalls int
|
|
}
|
|
|
|
func (repository *dateCaptureRepository) CreateFreightSync(
|
|
_ context.Context,
|
|
run domain.FreightSyncRun,
|
|
_, _ string,
|
|
) (domain.FreightSyncRun, bool, error) {
|
|
repository.createCalls++
|
|
return run, false, nil
|
|
}
|
|
|
|
func (*dateCaptureRepository) StartFreightSync(
|
|
context.Context,
|
|
string,
|
|
time.Time,
|
|
) error {
|
|
return nil
|
|
}
|
|
|
|
func (*dateCaptureRepository) CompleteFreightSync(
|
|
context.Context,
|
|
domain.FreightSyncRun,
|
|
domain.FreightImportBatch,
|
|
time.Time,
|
|
) error {
|
|
return nil
|
|
}
|
|
|
|
func (*dateCaptureRepository) CompleteFreightDateSync(
|
|
context.Context,
|
|
domain.FreightSyncRun,
|
|
domain.FreightImportBatch,
|
|
time.Time,
|
|
time.Time,
|
|
) error {
|
|
return nil
|
|
}
|
|
|
|
func (*dateCaptureRepository) FailFreightSync(
|
|
context.Context,
|
|
string,
|
|
string,
|
|
time.Time,
|
|
) error {
|
|
return nil
|
|
}
|
|
|
|
func (*dateCaptureRepository) RecoverFreightSyncs(
|
|
context.Context,
|
|
time.Time,
|
|
) (int64, error) {
|
|
return 0, nil
|
|
}
|
|
|
|
func (*dateCaptureRepository) GetFreightSync(
|
|
context.Context,
|
|
string,
|
|
string,
|
|
) (domain.FreightSyncRun, error) {
|
|
return domain.FreightSyncRun{}, nil
|
|
}
|
|
|
|
func (repository *dateCaptureRepository) GetFreightSyncWatermark(
|
|
context.Context,
|
|
string,
|
|
) (*domain.FreightSyncWatermark, error) {
|
|
return repository.watermark, nil
|
|
}
|
|
|
|
func (*dateCaptureRepository) ListFreightOrders(
|
|
context.Context,
|
|
string,
|
|
int,
|
|
) ([]domain.FreightOrder, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (*dateCaptureRepository) GetFreightOrder(
|
|
context.Context,
|
|
string,
|
|
string,
|
|
) (domain.FreightOrderDetail, error) {
|
|
return domain.FreightOrderDetail{}, nil
|
|
}
|
|
|
|
func (*dateCaptureRepository) DeleteFreightOrder(
|
|
context.Context,
|
|
string,
|
|
string,
|
|
) ([]string, error) {
|
|
return nil, nil
|
|
}
|