# 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/raw_events` - the [event-sourcing](https://martinfowler.com/eaaDev/EventSourcing.html) store: `Save` appends an `Event`, `LoadEventsForStore` replays a platform+store's series. Other domains (`reports`, `domains/accounts/mocks.go`) read from views/queries that project over this event series rather than mutating their own standalone state. See also the [CQRS](https://martinfowler.com/bliki/CQRS.html) note above. - `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 ` 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_`, `mock.shop__listings`, and for Amazon specifically `mock.shop__events` (the background processor's queue). Some platforms also have a recursive `mock.shop__listing_counts` view (built on `mock.shop__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__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. ## Gitea - This project is hosted on a self-hosted **Gitea** instance at `gitea.inventory-plus-plus.com` (repo: `angel/inventory-plus-plus`), not GitHub - the `origin` remote points at it over SSH. Issues, pull requests, wiki, and CI (Gitea Actions - see the Tests badge at the top of `README.md`) all live there rather than on GitHub, even though the tooling/workflow (Actions YAML, PR-based review) looks GitHub-shaped. - A Gitea MCP server is available in agent sessions (tools prefixed `mcp__angel__...` - e.g. `issue_read`/`issue_write`, `pull_request_read`/`pull_request_write`, `list_pull_requests`/`list_issues`, `list_branches`, `wiki_read`/ `wiki_write`) for reading/managing issues, PRs, branches, releases, and the wiki without shelling out to `git`/`gh`. There is no GitHub CLI (`gh`) equivalent here - use these MCP tools or `git` directly instead. - The Gitea MCP server has **no Projects API** - it cannot read or modify Gitea Project boards. Don't attempt to automate Project-board changes (e.g. moving an issue between columns) through it; that has to be done manually in the Gitea UI. ## 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.