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
150 lines
5.4 KiB
Go
150 lines
5.4 KiB
Go
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)
|
|
}
|
|
}
|