reports: add test coverage for GetListingCountsOverTime/Report
mock.shop_amazon_listing_counts is a recursive view that computes a running inventory count: starting from the listing's base count in mock.shop_amazon_listings, it walks mock.raw_shop_events in order, applying each event as a delta (sale/refund) or an absolute reset (inventory-reset), per mock.shop_amazon_listing_event_sequence's interpretation of each row's JSON payload. This is the first test to exercise that view directly rather than just the Go code around it. Fixture uses the real SaveNewMockSale/SaveNewMockRefund/ SaveNewMockInventoryReset methods - the same entry points the simulate-sale/refund/inventory UI uses - rather than hand-rolling the JSON payload shape, so the test tracks the real payload contract. Covers the full running-count sequence (base -> sale -> refund -> reset), the unknown-listing ErrNotFound case, and GetListingCountsReport's MaxCount/MinCount over that sequence. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XEDaCB7C2NEBgyvqEtZuxY
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
package reports_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"ruben/inventory2/consts"
|
||||
"ruben/inventory2/domains/accounts"
|
||||
"ruben/inventory2/domains/reports"
|
||||
"ruben/inventory2/internal/testdb"
|
||||
)
|
||||
|
||||
// createAmazonListing creates a listing with the given base count in
|
||||
// shopID and registers its cleanup. Must be called after
|
||||
// setupAmazonMockShop, so cleanup order (LIFO) deletes the listing before
|
||||
// the shop it belongs to.
|
||||
func createAmazonListing(t *testing.T, acctStore *accounts.Store, acctID int64, shopID string, baseCount int64) (listingID string) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
|
||||
pool := testdb.Pool(t)
|
||||
|
||||
listing, err := acctStore.CreateMockListing(ctx, accounts.MockListing{
|
||||
AccountShopListingIDs: accounts.AccountShopListingIDs{
|
||||
AccountShopIDs: accounts.AccountShopIDs{
|
||||
AccountIDs: accounts.AccountIDs{AccountID: acctID},
|
||||
Platform: accounts.Amazon,
|
||||
ShopID: shopID,
|
||||
},
|
||||
},
|
||||
SKU: "test-sku",
|
||||
Name: "Test Listing",
|
||||
Description: "a listing created for a test",
|
||||
Count: baseCount,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateMockListing() error = %v", err)
|
||||
}
|
||||
|
||||
t.Cleanup(func() {
|
||||
pool.Exec(context.Background(), `
|
||||
DELETE FROM mock.shop_amazon_listings WHERE shop_id = $1 AND listing_id = $2
|
||||
`, shopID, listing.ListingID)
|
||||
})
|
||||
|
||||
return listing.ListingID
|
||||
}
|
||||
|
||||
// TestGetListingCountsOverTime exercises the actual running-count logic,
|
||||
// which lives in a recursive SQL view (mock.shop_amazon_listing_counts,
|
||||
// built on mock.shop_amazon_listing_event_sequence) rather than in Go: it
|
||||
// starts from the listing's base count and walks mock.raw_shop_events in
|
||||
// order, applying each as a delta (sale/refund) or an absolute reset
|
||||
// (inventory-reset). Uses the real SaveNewMock* methods to write those
|
||||
// events, the same entry points the simulate-sale/refund/inventory UI
|
||||
// uses, rather than hand-rolling the JSON payload shape.
|
||||
func TestGetListingCountsOverTime(t *testing.T) {
|
||||
pool := testdb.Pool(t)
|
||||
acctStore := accounts.NewStore(testdb.Logger(), pool)
|
||||
reportsStore := reports.NewStore(testdb.Logger(), pool, acctStore)
|
||||
ctx := context.Background()
|
||||
|
||||
acctID, shopID := setupAmazonMockShop(t, pool, acctStore)
|
||||
listingID := createAmazonListing(t, acctStore, acctID, shopID, 100)
|
||||
|
||||
if _, err := acctStore.SaveNewMockSale(ctx, acctID, accounts.Amazon, shopID, listingID, 10); err != nil {
|
||||
t.Fatalf("SaveNewMockSale() error = %v", err)
|
||||
}
|
||||
if _, err := acctStore.SaveNewMockRefund(ctx, acctID, accounts.Amazon, shopID, listingID, 5); err != nil {
|
||||
t.Fatalf("SaveNewMockRefund() error = %v", err)
|
||||
}
|
||||
if _, err := acctStore.SaveNewMockInventoryReset(ctx, acctID, accounts.Amazon, shopID, listingID, 50); err != nil {
|
||||
t.Fatalf("SaveNewMockInventoryReset() error = %v", err)
|
||||
}
|
||||
|
||||
got, err := reportsStore.GetListingCountsOverTime(ctx, acctID, accounts.Amazon, shopID, listingID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetListingCountsOverTime() error = %v", err)
|
||||
}
|
||||
|
||||
wantCounts := []int64{100, 110, 105, 50}
|
||||
if len(got) != len(wantCounts) {
|
||||
t.Fatalf("GetListingCountsOverTime() returned %d rows, want %d: %+v", len(got), len(wantCounts), got)
|
||||
}
|
||||
|
||||
if got[0].EventTimestamp != nil {
|
||||
t.Errorf("got[0].EventTimestamp = %v, want nil (the base count row)", got[0].EventTimestamp)
|
||||
}
|
||||
for i, row := range got {
|
||||
if row.Count != wantCounts[i] {
|
||||
t.Errorf("got[%d].Count = %d, want %d (full sequence: %+v)", i, row.Count, wantCounts[i], got)
|
||||
}
|
||||
}
|
||||
for i := 1; i < len(got); i++ {
|
||||
if got[i].EventTimestamp == nil {
|
||||
t.Errorf("got[%d].EventTimestamp = nil, want set (only the base row should be nil)", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetListingCountsOverTime_UnknownListing(t *testing.T) {
|
||||
pool := testdb.Pool(t)
|
||||
acctStore := accounts.NewStore(testdb.Logger(), pool)
|
||||
reportsStore := reports.NewStore(testdb.Logger(), pool, acctStore)
|
||||
ctx := context.Background()
|
||||
|
||||
acctID, shopID := setupAmazonMockShop(t, pool, acctStore)
|
||||
|
||||
_, err := reportsStore.GetListingCountsOverTime(ctx, acctID, accounts.Amazon, shopID, "no-such-listing")
|
||||
if !errors.Is(err, consts.ErrNotFound) {
|
||||
t.Fatalf("GetListingCountsOverTime() error = %v, want %v", err, consts.ErrNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetListingCountsReport(t *testing.T) {
|
||||
pool := testdb.Pool(t)
|
||||
acctStore := accounts.NewStore(testdb.Logger(), pool)
|
||||
reportsStore := reports.NewStore(testdb.Logger(), pool, acctStore)
|
||||
ctx := context.Background()
|
||||
|
||||
acctID, shopID := setupAmazonMockShop(t, pool, acctStore)
|
||||
listingID := createAmazonListing(t, acctStore, acctID, shopID, 100)
|
||||
|
||||
if _, err := acctStore.SaveNewMockSale(ctx, acctID, accounts.Amazon, shopID, listingID, 10); err != nil {
|
||||
t.Fatalf("SaveNewMockSale() error = %v", err)
|
||||
}
|
||||
if _, err := acctStore.SaveNewMockInventoryReset(ctx, acctID, accounts.Amazon, shopID, listingID, 20); err != nil {
|
||||
t.Fatalf("SaveNewMockInventoryReset() error = %v", err)
|
||||
}
|
||||
|
||||
report, err := reportsStore.GetListingCountsReport(ctx, acctID, accounts.Amazon, shopID, listingID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetListingCountsReport() error = %v", err)
|
||||
}
|
||||
|
||||
if report.AccountID != acctID || report.Platform != accounts.Amazon || report.ShopID != shopID || report.ListingID != listingID {
|
||||
t.Errorf("report identity = %+v, want AccountID=%d Platform=%s ShopID=%s ListingID=%s",
|
||||
report, acctID, accounts.Amazon, shopID, listingID)
|
||||
}
|
||||
|
||||
// counts over the sequence: 100 (base) -> 110 (sale +10) -> 20 (reset)
|
||||
if got := report.MaxCount().Count; got != 110 {
|
||||
t.Errorf("MaxCount().Count = %d, want 110", got)
|
||||
}
|
||||
if got := report.MinCount().Count; got != 20 {
|
||||
t.Errorf("MinCount().Count = %d, want 20", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user