diff --git a/docs/bug-fixes.md b/docs/bug-fixes.md index 2cf1d0c..6fc788e 100644 --- a/docs/bug-fixes.md +++ b/docs/bug-fixes.md @@ -484,6 +484,26 @@ The group path now records each authoritative Amazon return as successfully proc --- +### 2026-07-17: Amazon order-id runs fetched every order before filtering + +**Description:** +An `itemize amazon -order-id ...` run still paginated the entire Amazon order history and fetched transaction details for every order. Targeted recovery runs took minutes and could cause Amazon to expire the session before reaching the requested order. + +**Test Case:** +`TestOrchestrator_fetchOrdersUsesDirectLookupForOrderID` first reproduced the issue by requiring `GetOrderDetails` for the requested ID and proving `FetchOrders` was not called. + +**Root Cause:** +The order-id filter was applied in the processing loop, after the bulk provider fetch had already completed. + +**Fix Applied:** +The orchestrator now uses the provider's direct `GetOrderDetails` operation whenever `Options.OrderID` is set, while preserving provider-fetch audit logging. + +**Verification:** +- The regression test failed before the fix and passes after it. +- Existing bulk-fetch and provider-fetch audit tests remain green. + +--- + ### 2025-09-01: No bugs discovered yet The project was developed using strict TDD methodology from the start, preventing bugs through test-first development. diff --git a/internal/application/sync/fetch.go b/internal/application/sync/fetch.go index c2ade19..7d37e0f 100644 --- a/internal/application/sync/fetch.go +++ b/internal/application/sync/fetch.go @@ -18,6 +18,26 @@ import ( // fetchOrders fetches orders from the provider based on the given options func (o *Orchestrator) fetchOrders(ctx context.Context, opts Options) ([]providers.Order, error) { + if opts.OrderID != "" { + started := time.Now() + order, err := o.provider.GetOrderDetails(ctx, opts.OrderID) + orders := make([]providers.Order, 0, 1) + if order != nil { + orders = append(orders, order) + } + var response any + if o.storage != nil { + response = summarizeOrdersForFetchLog(orders) + } + o.logProviderFetch("order_details", map[string]any{ + "order_id": opts.OrderID, + }, response, err, time.Since(started), len(orders), 0) + if err != nil { + return nil, fmt.Errorf("failed to fetch order %s: %w", opts.OrderID, err) + } + return orders, nil + } + endDate := time.Now() startDate := endDate.AddDate(0, 0, -opts.LookbackDays) diff --git a/internal/application/sync/orchestrator_test.go b/internal/application/sync/orchestrator_test.go index 769f480..cd51cb4 100644 --- a/internal/application/sync/orchestrator_test.go +++ b/internal/application/sync/orchestrator_test.go @@ -474,6 +474,40 @@ func TestOrchestrator_fetchOrders(t *testing.T) { } } +func TestOrchestrator_fetchOrdersUsesDirectLookupForOrderID(t *testing.T) { + mockProvider := new(MockProvider) + order := &MockOrder{} + mockProvider.On("GetOrderDetails", mock.Anything, "112-5922286-2695428").Return(order, nil) + orchestrator := NewOrchestrator(mockProvider, nil, nil, slog.Default()) + + orders, err := orchestrator.fetchOrders(context.Background(), Options{ + LookbackDays: 30, + OrderID: "112-5922286-2695428", + }) + + require.NoError(t, err) + assert.Equal(t, []providers.Order{order}, orders) + mockProvider.AssertNotCalled(t, "FetchOrders", mock.Anything, mock.Anything) + mockProvider.AssertExpectations(t) +} + +func TestOrchestrator_fetchOrdersReturnsDirectLookupError(t *testing.T) { + mockProvider := new(MockProvider) + mockProvider.On("GetOrderDetails", mock.Anything, "112-5922286-2695428"). + Return(nil, errors.New("session expired")) + orchestrator := NewOrchestrator(mockProvider, nil, nil, slog.Default()) + + orders, err := orchestrator.fetchOrders(context.Background(), Options{ + LookbackDays: 30, + OrderID: "112-5922286-2695428", + }) + + require.EqualError(t, err, "failed to fetch order 112-5922286-2695428: session expired") + assert.Nil(t, orders) + mockProvider.AssertNotCalled(t, "FetchOrders", mock.Anything, mock.Anything) + mockProvider.AssertExpectations(t) +} + func TestOrchestrator_fetchOrders_LogsProviderFetch(t *testing.T) { mockProvider := new(MockProvider) mockProvider.On("DisplayName").Return("Costco")