feat(t242): delete unused freight orders
This commit is contained in:
@@ -65,6 +65,7 @@ var (
|
||||
ErrSourceReferenceConflict = errors.New("source reference conflict")
|
||||
ErrAssetUnavailable = errors.New("asset is unavailable")
|
||||
ErrTaskStateConflict = errors.New("task state conflict")
|
||||
ErrFreightOrderInUse = errors.New("freight order is in use")
|
||||
ErrRepositoryUnavailable = errors.New("repository unavailable")
|
||||
ErrRepositoryInvariant = errors.New("repository invariant failed")
|
||||
)
|
||||
@@ -106,6 +107,13 @@ func wrapRepositoryError(err error) error {
|
||||
"task state does not allow this operation",
|
||||
err,
|
||||
)
|
||||
case errors.Is(err, ErrFreightOrderInUse):
|
||||
return newError(
|
||||
ErrorKindConflict,
|
||||
"FREIGHT_ORDER_IN_USE",
|
||||
"freight order is already used by procurement",
|
||||
err,
|
||||
)
|
||||
case errors.Is(err, ErrRepositoryUnavailable):
|
||||
result := newError(
|
||||
ErrorKindUnavailable,
|
||||
|
||||
@@ -39,4 +39,5 @@ type FreightImageRepository interface {
|
||||
|
||||
type FreightImageCache interface {
|
||||
CacheRun(context.Context, string, string) error
|
||||
DeleteStoredImages([]string)
|
||||
}
|
||||
|
||||
@@ -169,6 +169,20 @@ func (service *FreightImageService) deleteStoredImage(storageKey string) {
|
||||
_ = service.store.Delete(ctx, storageKey)
|
||||
}
|
||||
|
||||
func (service *FreightImageService) DeleteStoredImages(storageKeys []string) {
|
||||
if len(storageKeys) == 0 {
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), freightImageCleanup)
|
||||
defer cancel()
|
||||
for _, storageKey := range storageKeys {
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
_ = service.store.Delete(ctx, storageKey)
|
||||
}
|
||||
}
|
||||
|
||||
func (service *FreightImageService) OpenItemImage(
|
||||
ctx context.Context,
|
||||
creatorSubject, itemID string,
|
||||
|
||||
@@ -118,6 +118,30 @@ func TestFreightImageOpenDoesNotRevealUnavailableItems(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFreightImageDeleteStoredImagesUsesIdempotentStoreCleanup(t *testing.T) {
|
||||
store := &freightImageStoreFake{}
|
||||
service, err := NewFreightImageService(
|
||||
&freightImageRepositoryFake{},
|
||||
&freightImageSourceFake{},
|
||||
store,
|
||||
fakeClock{},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("NewFreightImageService() error = %v", err)
|
||||
}
|
||||
service.DeleteStoredImages([]string{
|
||||
"first/image.jpg",
|
||||
"second/image.jpg",
|
||||
})
|
||||
store.mu.Lock()
|
||||
defer store.mu.Unlock()
|
||||
if len(store.deleted) != 2 ||
|
||||
store.deleted[0] != "first/image.jpg" ||
|
||||
store.deleted[1] != "second/image.jpg" {
|
||||
t.Fatalf("deleted storage keys = %#v", store.deleted)
|
||||
}
|
||||
}
|
||||
|
||||
type freightImageRepositoryFake struct {
|
||||
mu sync.Mutex
|
||||
jobs []domain.FreightItemImageJob
|
||||
|
||||
@@ -58,4 +58,9 @@ type FreightRepository interface {
|
||||
string,
|
||||
string,
|
||||
) (domain.FreightOrderDetail, error)
|
||||
DeleteFreightOrder(
|
||||
context.Context,
|
||||
string,
|
||||
string,
|
||||
) ([]string, error)
|
||||
}
|
||||
|
||||
@@ -69,6 +69,12 @@ type CreateFreightDateSyncCommand struct {
|
||||
SyncToNow bool
|
||||
}
|
||||
|
||||
type DeleteFreightOrderCommand struct {
|
||||
CreatorSubject string
|
||||
ActorUserID string
|
||||
OrderID string
|
||||
}
|
||||
|
||||
type CreateFreightSyncResult struct {
|
||||
Run domain.FreightSyncRun
|
||||
Replayed bool
|
||||
@@ -701,6 +707,44 @@ func (service *FreightService) GetOrder(
|
||||
return detail, nil
|
||||
}
|
||||
|
||||
func (service *FreightService) DeleteOrder(
|
||||
ctx context.Context,
|
||||
command DeleteFreightOrderCommand,
|
||||
) error {
|
||||
command.CreatorSubject = strings.TrimSpace(command.CreatorSubject)
|
||||
command.ActorUserID = strings.TrimSpace(command.ActorUserID)
|
||||
command.OrderID = strings.TrimSpace(command.OrderID)
|
||||
fields := map[string]string{}
|
||||
if command.CreatorSubject == "" {
|
||||
fields["creator_subject"] = "is required"
|
||||
}
|
||||
if !isUUID(command.ActorUserID) {
|
||||
fields["actor_user_id"] = "must be a UUID"
|
||||
}
|
||||
if !isUUID(command.OrderID) {
|
||||
fields["order_id"] = "must be a UUID"
|
||||
}
|
||||
if len(fields) > 0 {
|
||||
return invalidError(
|
||||
"FREIGHT_DELETE_INVALID",
|
||||
"freight order deletion is invalid",
|
||||
fields,
|
||||
)
|
||||
}
|
||||
storageKeys, err := service.repository.DeleteFreightOrder(
|
||||
ctx,
|
||||
command.CreatorSubject,
|
||||
command.OrderID,
|
||||
)
|
||||
if err != nil {
|
||||
return wrapRepositoryError(err)
|
||||
}
|
||||
if service.imageCache != nil {
|
||||
service.imageCache.DeleteStoredImages(storageKeys)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *FreightService) normalize(
|
||||
source domain.FreightSourceBatch,
|
||||
) (domain.FreightImportBatch, error) {
|
||||
|
||||
@@ -236,6 +236,7 @@ type recordingFreightImageCache struct {
|
||||
creatorSubject string
|
||||
runID string
|
||||
err error
|
||||
deleted []string
|
||||
}
|
||||
|
||||
func (cache *recordingFreightImageCache) CacheRun(
|
||||
@@ -248,6 +249,12 @@ func (cache *recordingFreightImageCache) CacheRun(
|
||||
return cache.err
|
||||
}
|
||||
|
||||
func (cache *recordingFreightImageCache) DeleteStoredImages(
|
||||
storageKeys []string,
|
||||
) {
|
||||
cache.deleted = append(cache.deleted, storageKeys...)
|
||||
}
|
||||
|
||||
func TestCreateFreightOrderSyncTimeoutUsesLiveCleanupContext(t *testing.T) {
|
||||
repository := &syncTrackingRepository{}
|
||||
source := &blockingOrderSource{}
|
||||
@@ -378,6 +385,56 @@ func validFreightOrderSyncCommand(idempotencyKey string) CreateFreightSyncComman
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteFreightOrderValidatesConflictAndCleansImages(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 = ErrFreightOrderInUse
|
||||
err = service.DeleteOrder(context.Background(), command)
|
||||
assertUsecaseError(t, err, ErrorKindConflict, "FREIGHT_ORDER_IN_USE")
|
||||
if len(imageCache.deleted) != 2 {
|
||||
t.Fatalf("conflicting delete cleaned images = %#v", imageCache.deleted)
|
||||
}
|
||||
|
||||
command.OrderID = "invalid"
|
||||
err = service.DeleteOrder(context.Background(), command)
|
||||
assertUsecaseError(t, err, ErrorKindInvalid, "FREIGHT_DELETE_INVALID")
|
||||
if repository.deleteCalls != 2 {
|
||||
t.Fatalf("invalid delete reached repository %d times", repository.deleteCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFreightDateQuerySplitsIntoSevenDayWindows(t *testing.T) {
|
||||
source := &recordingDateSource{}
|
||||
service := &FreightService{source: source}
|
||||
@@ -581,12 +638,17 @@ func (*blockingOrderSource) QueryCreatedRange(
|
||||
}
|
||||
|
||||
type syncTrackingRepository struct {
|
||||
mu sync.Mutex
|
||||
startCalls int
|
||||
completeCalls int
|
||||
failCalls int
|
||||
failCode string
|
||||
failContextErr error
|
||||
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(
|
||||
@@ -682,6 +744,17 @@ func (*syncTrackingRepository) GetFreightOrder(
|
||||
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
|
||||
@@ -800,3 +873,11 @@ func (*dateCaptureRepository) GetFreightOrder(
|
||||
) (domain.FreightOrderDetail, error) {
|
||||
return domain.FreightOrderDetail{}, nil
|
||||
}
|
||||
|
||||
func (*dateCaptureRepository) DeleteFreightOrder(
|
||||
context.Context,
|
||||
string,
|
||||
string,
|
||||
) ([]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user