feat(t224): add incremental freight sync
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"cmroubao/backend-api/internal/domain"
|
||||
)
|
||||
@@ -52,6 +55,125 @@ func TestFreightNormalizationRejectsConflictingIdentityAndInvalidTime(
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
@@ -86,3 +208,122 @@ func validFreightSource() domain.FreightSourceBatch {
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
var errDateSourceFailure = errors.New("date source failed")
|
||||
|
||||
type recordingDateSource struct {
|
||||
calls [][2]string
|
||||
failOnCall int
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (repository *dateCaptureRepository) CreateFreightSync(
|
||||
_ context.Context,
|
||||
run domain.FreightSyncRun,
|
||||
_, _ string,
|
||||
) (domain.FreightSyncRun, bool, error) {
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user