README had drifted: an unresolved merge-conflict marker left over from a past edit, a directory-structure diagram that no longer matches the repo (/internal/site, /internal/domains/platforms/tiktok - actual layout is server/, domains/, etc per AGENTS.md), and a duplicated dev-workflow blurb that AGENTS.md already documents more accurately. Splits the roadmap checklist out to ROADMAP.md and the Etsy API compliance checklist to domains/platforms/etsy/COMPLIANCE.md (next to the code it governs, where someone touching that integration will actually look for it) instead of burying both in one large README. README itself becomes a short front door with a "Where things live" index up top, since scattering docs across files only helps if there's an obvious map to them. Moves the CQRS/event-sourcing architecture note into AGENTS.md's Architecture section (with a new domains/raw_events bullet) rather than leaving it as prose in README, since AGENTS.md is the maintained engineering reference and that's where a reader would already be looking for how the domains are structured. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
262 lines
15 KiB
Markdown
262 lines
15 KiB
Markdown
# 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 <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.
|