Files
inventory-plus-plus/domains/reports/reports_test.go
T
angelandClaude Sonnet 5 c8c4204a63 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
2026-08-20 00:35:22 -06:00

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)
}
}