Files
inventory-plus-plus/AGENTS.md
T
angelandClaude Sonnet 5 308f16f6cc docs: add platform market research and weighted integration priority ranking
Research to inform which mock platform (Amazon, Shopify, etc.) is worth
turning into a real integration next, after Etsy. Adds GMV/seller-count
market-size data for every platform in domains/accounts/platform.go, a
weighted scoring model (GMV 45%, API/inventory-webhook completeness 30%,
growth 15%, integration cost 10%), and the resulting ranked order.

Also fills in Etsy's own row, sourced directly from
domains/platforms/etsy/generated_client rather than external docs since
it's the one platform actually live in this repo: Etsy Open API v3 has no
webhook/push mechanism at all, and only the OAuth connection flow is
built so far - polling-based order/inventory sync doesn't exist yet. Etsy
is included as a reference score (3.85/10) against the same rubric used
for the unbuilt platforms, mainly as a sanity check on the model rather
than a claim that building it first was wrong.

Flags that the existing capability table's BigCommerce row doesn't match
an actual platform in this codebase (Tiktok does, and is now researched
instead), and that Tiktok Shop's API details are unconfirmed pending a
dedicated research pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 16:23:47 -06:00

15 KiB

AGENTS.md

Go backend for a multi-platform ecommerce inventory-sync tool. Etsy is the one real, live platform integration; everything else (Amazon, BigCartel, Ebay, Ecwid, Shopify, SquareOnline, Squarespace, Tiktok, WalmartMarketplace, Wix, WooCommerce, Zoho) exists only as a mock simulation layer used for development, demos, and testing the sync/reporting logic without needing real store credentials. See README.md for the product-level roadmap.

Platform integration priority

When picking the next mock platform to turn into a real integration (after Etsy), use this weighted ranking of the 12 not-yet-live mock platforms. It scores each on GMV/market opportunity (45%), API/inventory-sync completeness (30%), growth trajectory (15%), and integration cost (10%) - see STORE_API_RESEARCH.md's "Weighted priority ranking" section for the full scoring table, per-criterion reasoning, and the market-size/API research it's built on.

  1. Shopify (9.15/10) - cleanest/most complete API (webhooks for both order and inventory events, modern GraphQL), still-strong GMV growth (+29-35% YoY), and a proven self-serve distribution channel (the Shopify App Store, where inventory-sync apps are an established category) that no other platform here has an equivalent of.
  2. Amazon (8.85/10) - largest raw dollar opportunity (~$575B in 2025 third-party GMV), and inventory-sync pain (suppressed listings, FBA stranded-inventory fees) is one of the sharpest problems this tool could solve there. domains/amazon already has the most real infrastructure of any mock platform (the ProcessEvents LISTEN/NOTIFY loop, SSE wiring), so a real SP-API integration reuses that instead of starting from zero. Scores lower than Shopify mainly because SP-API auth (LWA + AWS SigV4) and rate limiting are the most complex of any platform here, and seller growth has gone flat.
  3. Tiktok Shop (7.15/10) - hypergrowth (global GMV nearly doubled in 2025, projected to double again in 2026) is what earns it this spot, but its API details are the least confirmed of anything researched - partner.tiktokshop.com didn't yield readable docs during this pass. Needs a dedicated research pass to confirm exact webhook/endpoint names before treating this ranking as actionable.
  4. Walmart Marketplace (6.35/10) - smaller in absolute GMV than the top three, but growing ~50% YoY and a natural second-marketplace target for sellers already on Amazon; full order+inventory webhook coverage.
  5. Wix (6.35/10) - tied with Walmart on score but behind it on the growth tiebreaker; full inventory-webhook coverage and an easy API, just a smaller/less certain GMV number.
  6. WooCommerce (5.95/10) - large store count (~4-6M) but no platform-wide GMV figure exists (self-hosted plugin, no central ledger), and no dedicated inventory webhook (relies on a product.updated proxy).
  7. Square Online (5.90/10) - full inventory-webhook coverage and well-documented APIs (shared with the rest of Square's product line), but its ecommerce-specific GMV can't be isolated from Square's much larger in-person POS business.
  8. Ebay (5.75/10) - most sellers of any platform here (18.3M) but the lowest GMV-per-seller by far, plus no dedicated inventory-change webhook.
  9. Squarespace (4.75/10) - smaller GMV, no inventory webhook.
  10. Zoho (3.85/10) - tiny confirmed store count (~2,196 globally).
  11. Ecwid (3.20/10) - shrinking (-20% YoY store count).
  12. Big Cartel (1.50/10) - last, and not close: no inventory API or webhook at all is a structural dealbreaker for this tool's core use case, independent of its (also declining, -41% YoY) market size.

(BigCommerce, which appears in the API-capability table in STORE_API_RESEARCH.md, is excluded here - it isn't one of this codebase's actual mock platforms; that table predates the platform list settling on Tiktok instead.)

Etsy isn't in the ranking above (it's already built), but scores 3.85/10 as a reference point if run through the same rubric - mid-pack-and-declining GMV, and the only platform researched with zero webhook/push support of any kind (order or inventory - confirmed directly against domains/platforms/etsy/generated_client, not external docs). That's not a retroactive case against having built Etsy - it was presumably chosen for reasons this rubric doesn't score (an existing relationship, an easier path to developer credentials) - but it's a reminder that the lack of any webhook system makes real order/inventory sync a polling loop, which is exactly the kind of integration cost this rubric undercounts once a platform is more than superficially wired up. Note also that the OAuth connection flow is the only part of Etsy that's actually built so far (domains/platforms/etsy/etsy.go) - polling-based receipt/inventory sync against the live API doesn't exist yet.

Setup

  1. Postgres running locally, with an app_client role/database matching .env.example's DATABASE_URL.
  2. cp .env.example .env and fill in real values (Auth0, Etsy). For local dev-only work you don't need real Auth0/Etsy secrets if DEV_AUTH_ENABLED is true (see Auth below) - changeme placeholders are fine.
  3. Create the dedicated test database once: createdb -O app_client inventory_2_test. make test migrates it automatically after that.

Commands

  • make dev (default: bare make) - applies pending migrations, starts the Tailwind watcher in the background, runs the server in the foreground. Ctrl-C stops both.
  • make run - just go run ., no migrations or CSS.
  • make test - runs the full test suite against the dedicated inventory_2_test database (migrates it first). Safe to run anytime; never touches real data.
  • make test-against-dev-db - same suite, pointed at the real dev database instead. Useful for checking behavior against real data; tests clean up after themselves via t.Cleanup, but it's still touching your dev DB.
  • make migrate-up / migrate-down / migrate-version - wrap the migrate CLI using DATABASE_URL from .env, so the DSN is never hand-typed. migrate-test-* variants target TEST_DATABASE_URL.
  • go build ./..., go vet ./..., gofmt -l . - standard, expected clean before considering work done.
  • go test ./... -race - the test suite is safe to run this way (see Testing below for what made that true).

Architecture

  • domains/ - business logic, one package per bounded concern (accounts, authentication, raw_events, reports, amazon, platforms/etsy). Each domain owns its own DB access; there's no shared ORM/repository layer. Looking to loosely follow CQRS: writes go through domain Store methods, reads are mostly separate query methods on the same Store.
  • domains/accounts/mocks.go - the mock-platform simulation layer: CreateMockShop, CreateMockListing, SaveNewMockSale / SaveNewMockRefund / SaveNewMockInventoryReset, etc. These are the real entry points production code (and the simulate-sale/refund/ inventory UI) uses - prefer them over hand-rolled SQL when writing tests or new features that need mock shop/listing/event data.
  • domains/amazon/mock.go - the one platform with a live background processor on top of the mock layer: a Postgres LISTEN/NOTIFY-driven loop ((*Mocks).ProcessEvents) that reacts to newly inserted mock events and dispatches them to a MockEventListener (wired to Server-Sent Events in main.go, so the UI updates live). No other platform has this yet; if adding one, read this file's history/comments first - it went through several correctness passes (reconnect-on-failure, ack-based retry, configurable poll fallback) worth not re-discovering from scratch.
  • server/ - HTTP layer (Gin). server/api is the JSON/HTMX API, server/ui renders HTML pages, server/auth is session/auth middleware, server/sse is the Server-Sent Events plumbing.
  • config/config.go - all runtime configuration comes from environment variables (loaded from .env via godotenv), never hardcoded. If you add a new required external dependency (a new secret, a new service URL), add it here, not as a literal in the code that uses it.
  • internal/testdb - shared test fixtures: Pool(t) (connects via TEST_DATABASE_URL, skips the test if unset), Logger(), NewUserID(t), SeedOAuthUser/SeedOAuthSession.

Database & migrations

  • Migrations live in database_migrations/, run via golang-migrate (migrate CLI). Every migration needs a paired .up.sql/.down.sql.
  • Migration files can drift from the live schema, and drift can mean "unrunnable," not just "stale docs." This has happened at least twice: an interval-literal syntax error that failed on any fresh database, and a column that existed in the live dev DB but not in what the migration files would produce (fixed by adding a migration that reconciles the two, then migrate force <version> on the DB that already matched by hand). When something about the schema seems off, verify against the live DB (psql \d, pg_get_viewdef(...)) before trusting the migration files, and especially before assuming a fresh database would come up the same way the long-running dev one does.
  • The mock-platform schema (mock.*) follows a consistent per-platform pattern: mock.shop_<platform>, mock.shop_<platform>_listings, and for Amazon specifically mock.shop_<platform>_events (the background processor's queue). Some platforms also have a recursive mock.shop_<platform>_listing_counts view (built on mock.shop_<platform>_listing_event_sequence) that computes a running inventory count from mock.raw_shop_events - it's real per-platform logic living in SQL, not a passthrough, and worth reading with pg_get_viewdef(..., true) rather than assuming it matches another platform's shape.
  • Platform-string casing is inconsistent and matters. Most accounts.Platform constants are lowercase/snake_case ("amazon", "big_cartel", ...) matching what SQL triggers/views expect, but Etsy/Tiktok/Wix are capitalized Go-side ("Etsy", not "etsy"). Some SQL objects for those platforms expect the capitalized form (e.g. the Etsy listing-counts view filters on 'Etsy'), others expect lowercase (e.g. the Etsy raw_shop_events trigger checks 'etsy'). Don't assume; check the actual SQL (pg_get_viewdef, trigger definitions) for the specific object you're relying on before writing a fixture or a query.
  • The mock_shop_<platform>_event_inserted NOTIFY channels are global, not scoped by shop/account. Any insert into raw_shop_events for that platform notifies every currently-listening connection for that platform, regardless of which test or code path caused it. See Testing below for how this bit two test packages at once.

Testing

  • Tests are real integration tests against a real Postgres database (internal/testdb.Pool(t)), not mocks/stubs of the DB layer - this codebase is mostly thin Store methods wrapping SQL, so testing the Go code in isolation would miss most of the actual risk.
  • Assertions use github.com/stretchr/testify's assert/require throughout - require for anything the rest of the test depends on (halts immediately), assert for independent checks. Use require.Eventually for "poll until true or timeout" patterns instead of hand-rolling one.
  • require.Eventually's condition function runs on a separate goroutine. Never call require.*/t.Fatal* (which invoke t.FailNow()) from inside one - Go's testing package requires FailNow to only be called from the test's own goroutine; calling it from a spawned one doesn't panic cleanly, it just silently reports the wrong thing. If a helper used inside an Eventually closure needs to check an error, give it a *testing.T-free variant that returns (value, error) and let the closure decide what "not yet satisfied" means.
  • Every test cleans up its own rows via t.Cleanup, in FK-safe order (children before parents). Use unique, randomly-generated IDs (testdb.NewUserID(t), uuid.NewString()) so tests are safe to run concurrently with themselves and with real dev data under test-against-dev-db.
  • Pick a mock platform deliberately when writing new fixtures that touch mock.raw_shop_events. domains/amazon's tests process every unprocessed event system-wide (that's correct behavior for its background worker, not a bug) and react to the global Amazon NOTIFY channel - any other package's tests that insert Amazon-platform mock events risk being picked up by domains/amazon's tests when go test ./... runs both packages' binaries concurrently (Go's default). domains/reports, for example, deliberately uses Etsy as its example platform for exactly this reason. If you're not testing domains/amazon itself, don't use Amazon as your fixture platform.
  • Never disable/re-enable a shared DB trigger to simulate a "notify never fires" scenario in a test, even temporarily - it's unsafe to do against a database anything else might be using concurrently (including a real make dev session someone has running). If you need to prove a timing/fallback path works, find a way to construct the scenario through normal writes instead (see domains/amazon/mock_test.go's poll-fallback test for an example: it reuses the retry mechanism to guarantee a second dispatch can only come from the poll timer, without touching any trigger).
  • Killing a real Postgres backend connection (pg_terminate_backend, found via pg_stat_activity) is a legitimate, safe way to test reconnect/failure-recovery logic when properly scoped - see domains/amazon/mock_test.go's terminateListenConnection. Be aware its PID lookup matches on query text globally, not "this test's connection specifically" - check for other live processes (e.g. a running make dev) before running a test like this against a shared database.

Auth

  • Real auth is Auth0-backed OIDC (domains/authentication). For local dev, set DEV_AUTH_ENABLED=true and hit GET /api/auth/dev-login?user_id=whoever to mint a real session (writes directly to oauth_users/oauth_tokens, same shape a real login produces) without any Auth0 round-trip. Different user_id values let you test multiple identities/accounts side by side. Never enable this outside local development.

Commit conventions

  • Commit messages: imperative mood subject line, no period, body explains why (what problem existed, what the fix changes) rather than re-describing the diff. One logical change per commit - e.g. a bugfix found while doing unrelated work gets its own commit, not folded into the original task's.
  • Work summaries: see work-summaries/ for dated records of past sessions' changes and reasoning - useful context before touching an area someone else (human or agent) recently worked on.