Files
inventory-plus-plus/AGENTS.md
T
angel cdd2202839
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 4s
Tests / Go tests (push) Successful in 13s
docs: document self-hosted Gitea and its MCP tooling for agents
Make sure AI agents know this project is hosted on a self-hosted Gitea
instance rather than GitHub, and that mcp__angel__* MCP tools (with no
Projects API) are available for interacting with it.
2026-08-20 23:59:48 -06:00

16 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/raw_events - the event-sourcing 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 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 <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.

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.