Compare commits

..
Author SHA1 Message Date
angelandClaude Sonnet 5 0a17dce032 auth: split dev-mode auth constructor and wire up dev-login/logout UI
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 5s
Squeamish about New()'s empty-domain-string sentinel for "dev mode, skip
OIDC discovery" - split into New (always makes a real OIDC discovery
call, all params required) and NewDev (no ctx/domain/credentials at all,
since none are used). main.go now branches on cfg.DevAuthEnabled to pick
the right constructor instead of main.go/config.go coordinating on when
it's safe to pass empty strings.

Also finishes out the dev-auth flow this enables: config.Load reads a
DEV_AUTH_ENABLED-aware env file and only requires Auth0 vars when dev
auth is off; a PORT config var replaces the hardcoded :8082; and the nav
UI (layout/index templates, ui router) points login/logout links at
/api/auth/dev-login and a new /api/auth/dev-logout route when dev auth
is enabled, so the whole login/logout loop works locally without a real
Auth0 app.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 23:20:15 -06:00
angel e355f6984a penpot design work summaries 2026-08-19 23:20:15 -06:00
angelandClaude Sonnet 5 53845bc95f docs: fill in AGENTS.md
Setup, common commands, architecture overview, and the database/testing
gotchas actually hit while working in this repo recently: migration
files that can drift to the point of being unrunnable (not just
stale), inconsistent platform-string casing between Go constants and
SQL objects, global (unscoped) mock-platform NOTIFY channels, the
require.Eventually-runs-on-a-goroutine hazard, and why domains/reports'
fixtures use Etsy rather than Amazon. CLAUDE.md already points here via
@AGENTS.md, so no change needed there.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XEDaCB7C2NEBgyvqEtZuxY
2026-08-19 23:20:15 -06:00
angelandClaude Sonnet 5 5b9ea6e540 reports: switch test fixtures from Amazon to Etsy to fix test isolation
domains/reports' fixtures used Amazon as their example platform - same
as domains/amazon's own tests. Both packages' test binaries run
concurrently under go test ./... by default, both wrote to the shared
mock.raw_shop_events table with platform='amazon', and the DB trigger
routed those into mock.shop_amazon_events - the exact table
domains/amazon's background-processing tests poll and assert on.
Confirmed directly: running the two packages together, domains/amazon's
TestProcessUnprocessedEvents_RetriesUnackedNotification picked up 4
events instead of 1, three of them from domains/reports' fixture shop.

Switched to Etsy instead (test-only change, no production code
touched). Verified the casing first since it mattered here:
accounts.Etsy's Go value is "Etsy" (capital), and separately Etsy's
raw_shop_events trigger checks for lowercase 'etsy' - but the view
these tests actually depend on (mock.shop_etsy_listing_event_sequence)
filters on 'Etsy', matching the Go constant, confirmed via
pg_get_viewdef. So the fixtures work correctly and, as a side effect,
never fire the lowercase-gated trigger at all - keeping
mock.shop_etsy_events untouched by these tests regardless.

Combined with the previous commit's goroutine-leak fix, go test ./...
and make test are both reliably green as single commands again.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XEDaCB7C2NEBgyvqEtZuxY
2026-08-19 23:20:15 -06:00
angelandClaude Sonnet 5 fec4047784 amazon: fix a goroutine/connection leak on ProcessEvents shutdown
listenForNotifications' notification-forwarding goroutine sent on ch
unconditionally after each WaitForNotification. If ProcessEvents' main
loop had already exited (ctx cancelled) at the exact moment this
goroutine had a notification to forward, nobody was left reading from
the unbuffered channel - the send blocked forever, the goroutine never
reached its deferred pc.Release(), and the pooled connection leaked
permanently. Real in production (a shutdown racing an in-flight
NOTIFY), not just a test artifact.

Surfaced by a go test ./... hang inside domains/amazon (a goroutine
stuck in pgxpool.Pool.Close's WaitGroup.Wait) - increased cross-package
NOTIFY traffic from domains/reports' Amazon-platform fixtures made the
race easy to hit, but didn't cause it.

Fixed with a select alongside the send so the goroutine notices
ctx.Done() instead of blocking forever when nobody's listening anymore.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XEDaCB7C2NEBgyvqEtZuxY
2026-08-19 23:20:15 -06:00
angelandClaude Sonnet 5 7cbdc6a9e2 tests: migrate assertions to testify's assert/require
Replaces raw t.Error/t.Errorf/t.Fatal/t.Fatalf across every test file
that has any (domains/accounts, domains/authentication,
domains/raw_events, domains/amazon, domains/reports x2) with testify's
assert (non-halting) / require (halting) equivalents. The three
Example-based tests (server/ui/svg, server/ui/charts) have no
*testing.T at all - nothing to convert there.

require.Eventually replaces several hand-rolled polling loops in
domains/amazon/mock_test.go. Its condition function runs on a separate
goroutine (confirmed in testify's source), so calling require.* from
inside one - which two of the new Eventually calls initially did, via
the isProcessed helper - is unsafe per Go's testing rules (t.FailNow
must only be called from the test's own goroutine). Fixed by splitting
a *testing.T-free queryIsProcessed(ctx, pool, shopID, eventID) out of
isProcessed for use inside those closures specifically.

github.com/stretchr/testify promoted from an indirect to a direct
dependency (go.mod only - it was already present transitively, so
go.sum is unchanged).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XEDaCB7C2NEBgyvqEtZuxY
2026-08-19 23:20:15 -06:00
angel 6b9e45a1ab stubbed CLAUDE.md and AGENTS.md 2026-08-19 23:20:15 -06:00
angel 0d522d9f5d updated readme 2026-08-19 23:20:15 -06:00
angelandClaude Sonnet 5 9f45182a49 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-19 23:20:15 -06:00
angelandClaude Sonnet 5 621ea388c2 reports: add test coverage for GetRawShopEvents
Covers the happy path (ordering by event_timestamp DESC/event_id ASC,
Platform/ShopID fields, RawPayload round-tripping through jsonb), the
empty-shop case, and the unknown-shop ErrNotFound case (via the
accts.GetMockShop check GetRawShopEvents does before querying).

setupAmazonMockShop creates a real account + Amazon mock shop via the
same Store methods the app uses (CreateAccount, CreateMockShop) and
registers cleanup in FK-safe order.

GetListingCountsOverTime/GetListingCountsReport coverage is a separate,
larger task (needs a listing plus count-changing history feeding a DB
view) - not done here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XEDaCB7C2NEBgyvqEtZuxY
2026-08-19 23:20:15 -06:00
angelandClaude Sonnet 5 bf454ea949 amazon: reconnect the LISTEN connection on failure instead of dying
A dropped connection, a Postgres restart, or any other error on the
dedicated LISTEN connection previously propagated all the way out of
ProcessEvents, and main.go's top-level shutdown logic treats that
error channel firing the same as a fatal server error - taking down
the entire application over a hiccup on one background connection that
has nothing to do with serving HTTP traffic. This matters more with
eleven other platforms already sharing the same trigger+notify shape
in the migrations with no Go processor yet.

Adds (*Mocks).reconnectOrStop: on a real failure (not an ordinary
shutdown), logs a warning and re-establishes LISTEN after a backoff
that starts at 1s, caps at 30s, doubles on repeated immediate
failures, and resets once a reconnect actually succeeds.
ProcessEvents' select no longer returns on a LISTEN error - it loops
back in with fresh channels instead.

Verified against a genuinely killed connection (pg_terminate_backend,
targeting the backend via pg_stat_activity matched on its LISTEN
query text), not a simulated one - both in a manual check and in the
new TestProcessEvents_ReconnectsAfterListenConnectionDrops test.

Closes out all four insights from the domains/amazon design review.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XEDaCB7C2NEBgyvqEtZuxY
2026-08-19 23:20:15 -06:00
angelandClaude Sonnet 5 d579a3b8cf amazon: make the poll fallback interval configurable and observable
ProcessEvents' safety-net poll was a bare time.Minute literal inline in
its select statement - no way to verify the fallback path works without
waiting 60+ seconds in a test, no way to tune the cadence without a code
change, and no way to tell afterward whether a given loop iteration was
triggered by a real NOTIFY or by the poll timer.

Adds Mocks.pollInterval (default time.Minute) and a WithPollInterval(d)
builder, mirroring WithNotifyRetryAfter's existing pattern - deploy-time
configurable via construction, not a live/API-adjustable knob, and not
wired through .env, matching how notifyRetryAfter already works.

Adds a Debug log line on each of the two meaningful wake-up branches so
which path fired is now observable.

New test proves the poll branch actually works without waiting or
touching the shared DB trigger (which would be unsafe against the real
dev DB): it reuses the retry mechanism from the previous commit so a
second dispatch can only come from the poll timer, since nothing else
ever notifies again for the rest of the test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XEDaCB7C2NEBgyvqEtZuxY
2026-08-19 23:20:15 -06:00
angelandClaude Sonnet 5 4850bc2d31 amazon: replace fire-and-forget listener notifications with ack-based retry
Dispatching an event to the listener previously happened from an
un-awaited goroutine, with only a log line on failure - once
processed_at was set, a dropped or failed notification was permanently
and silently lost, with no way to tell it had happened.

Replaces the processed/processed_successfully booleans with a
notified_at/processed_at pair (migration 000031): the dispatcher sets
notified_at and hands the event to MockEventListener.Notify, which now
also receives an ack callback the listener calls whenever it's truly
done, synchronously or arbitrarily later. Anything still "notified" but
unacked past notifyRetryAfter (a tunable field, not a stored
per-row timestamp) gets notified again on the dispatcher's normal
poll/reactive loop - no new retry mechanism needed. ack is idempotent,
since a late ack from an earlier attempt and one from a retry can both
eventually fire for the same event.

Dispatch is deferred until after the transaction that recorded
notified_at has actually committed, so ack's independent write can't
race a still-open transaction it implicitly depends on being visible.

The real SSE listener (server/sse/db_event_publisher.go) acks inline,
since its work is synchronous.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XEDaCB7C2NEBgyvqEtZuxY
2026-08-19 23:20:15 -06:00
angelandClaude Sonnet 5 de9848679f amazon: add a Ready() signal for ProcessEvents' LISTEN registration
Tests (and any other caller) previously had no way to know when the
Postgres LISTEN behind the reactive event-processing loop had actually
registered, forcing a guessed sleep before relying on it. Mocks now
exposes Ready() <-chan struct{}, closed once listenForNotifications
successfully issues LISTEN. Purely additive - ProcessEvents' signature
is unchanged.

Updates the integration tests to wait on Ready() instead of a flat
sleep, which also cut TestProcessEvents_ReactsToNotification's runtime
from ~0.25s to ~0.06s with no flakes across repeated runs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XEDaCB7C2NEBgyvqEtZuxY
2026-08-19 23:20:15 -06:00
angel 869f98344d store api research 2026-08-19 23:20:15 -06:00
angel 4e77052a37 Claude-assisted improvements (untested)
- dev auth flow (side-step OAuth)
- db event processing integration tests
- dev scripts (eg Makefile)
- db / test db migration setup scripts.
2026-08-19 23:20:15 -06:00
angel 82efe19ae0 prototyped svg reports
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 4s
2026-08-19 23:19:41 -06:00
angel 00eed86bb2 amazon events published as server side event 2026-08-19 23:19:41 -06:00
angel 6edd46d32a mock amazon event processing stubbed 2026-08-19 23:19:41 -06:00
angel 340d26fe11 bad logs
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 4s
2026-08-19 23:19:16 -06:00
angel 002d7146d0 protoype: list raw events on mock mode reports page 2026-08-19 23:19:16 -06:00
angel e108e9da26 mock events saved to db 2026-08-19 23:19:16 -06:00
angel bff9e2bca4 mock events db schema 2026-08-19 23:19:16 -06:00
angel 8e949e419b regenerated diagrams 2026-08-19 23:19:16 -06:00
angel f5e10c3579 fixed mock mode button; shortened simulation tabs 2026-08-19 23:19:16 -06:00
angel 1026aed80f MockMode: made global in templates; increase bort of navbar in mock mode 2026-08-19 23:19:16 -06:00
angel 9bf4da775c stubbed simulations page content: set inventory 2026-08-19 23:19:16 -06:00
angel 53bbf85fe1 stubbed simulations page content: refund 2026-08-19 23:19:16 -06:00
angel fdc8aaea6a stubbed simulations page content: sale 2026-08-19 23:19:16 -06:00
11 changed files with 160 additions and 626 deletions
-69
View File
@@ -1,69 +0,0 @@
name: Claude Assistant for Gitea
on:
# Trigger on issue comments (works on both issues and pull requests in Gitea)
issue_comment:
types: [created]
# Trigger on issues being opened or assigned
issues:
types: [opened, assigned]
# Note: pull_request_review_comment has limited support in Gitea
# Use issue_comment instead which covers PR comments
jobs:
claude-assistant:
# Basic trigger detection - check for @claude in comments or issue body
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || github.event.action == 'assigned'))
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
# Note: Gitea Actions may not require id-token: write for basic functionality
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup golang environment
uses: actions/setup-go@v7
with:
go-version: 'stable'
check-latest: true
token: ${{ gitea.token }}
permissions:
contents: read
- name: Run Claude Assistant
uses: markwylde/claude-code-gitea-action@gitea
with:
allowed_tools: |
penpot-self-hosted__execute_code
penpot-self-hosted__high_level_overview
penpot-self-hosted__penpot_api_info
penpot-self-hosted__export_shape
penpot_self_hosted__execute_code
penpot_self_hosted__high_level_overview
penpot_self_hosted__penpot_api_info
penpot_self_hosted__export_shape
gitea_api_url: "https://gitea.inventory-plus-plus.com/api/v1"
gitea_token: ${{ secrets.GITEA_TOKEN }} # Use standard workflow token
# anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
# Prefer claude_code_oauth_token over anthropic_api_key (cheaper!)
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
timeout_minutes: "60"
trigger_phrase: "@claude"
assignee_trigger: "@claude"
# Needed due to bug: https://github.com/anthropics/claude-code-action/issues/1416
model: "claude-sonnet-5"
# Optional: Customize for Gitea environment
custom_instructions: |
You are working in a Gitea environment. Be aware that:
- Some GitHub Actions features may behave differently
- Focus on core functionality and avoid advanced GitHub-specific features
- Use standard git operations when possible
env:
GITEA_SERVER_URL: "https://gitea.inventory-plus-plus.com"
-28
View File
@@ -1,28 +0,0 @@
name: Tests
run-name: tests on ${{ gitea.ref }}
on: [push]
jobs:
Go tests:
runs-on: ubuntu-latest
steps:
- name: Description
run: |
echo "🧑‍🔬 $ {{ gitea.actor }} pushed branch ${{ gitea.ref }} ...00101010 ... beginning tests"
- name: Check out repository code
uses: actions/checkout@v4
- name: Update
run: |
echo "💡 The ${{ gitea.repository }} repository has been cloned to the runner."
echo "🖥️ The job is now ready to test your go code on the runner."
- name: Setup golang environment
uses: actions/setup-go@v7
with:
go-version: 'stable'
check-latest: true
token: ${{ gitea.token }}
permissions:
contents: read
- name: Run go tests
run: |
go test ./...
-1
View File
@@ -4,5 +4,4 @@
node_modules
.env
.env.*
.env.example
-10
View File
@@ -1,10 +0,0 @@
{
"mcpServers": {
"penpot-self-hosted": {
"description": "Set here, even though set globally, so it is also available for the gitea-claude action bot",
"type": "http",
"url": "https://penpot.inventory-plus-plus.com/mcp/stream?userToken=${PENPOT_MCP_USER_TOKEN}"
}
}
}
-99
View File
@@ -7,78 +7,6 @@ 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
@@ -117,12 +45,6 @@ against the live API doesn't exist yet.
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
@@ -249,27 +171,6 @@ against the live API doesn't exist yet.
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
+114 -25
View File
@@ -1,4 +1,4 @@
## Inventory++ [![Tests Passing?](https://gitea.inventory-plus-plus.com/angel/inventory-plus-plus/actions/workflows/tests.yaml/badge.svg?branch=master)](https://gitea.inventory-plus-plus.com/angel/inventory-plus-plus/actions?workflow=&scoped_workflow_source_repo_id=0&actor=0&status=0&branch=master)
##
WIP!
@@ -7,20 +7,7 @@ The benefits provided by this application will be to automatically manage shared
reducing the amount of time needed to synchronize inventory between stores.
## Where things live
Documentation is split up rather than kept in one big doc - start here:
- **[`AGENTS.md`](./AGENTS.md)** - the engineering reference: setup, `make` commands, architecture, database/migration gotchas, testing conventions, auth, and the researched platform-integration priority ranking (which platform to build next, and why).
- **[`ROADMAP.md`](./ROADMAP.md)** - in-progress and planned work, as a checklist.
- **[`research/platforms.md`](./research/platforms.md)** - the underlying API-capability and market-size research the platform-priority ranking is based on.
- **[`domains/platforms/etsy/COMPLIANCE.md`](./domains/platforms/etsy/COMPLIANCE.md)** - Etsy API usage obligations to keep satisfied when touching the Etsy integration.
- **[`diagrams/`](./diagrams)** - architecture and schema diagrams, linked from the Diagrams section below.
This project is hosted on a self-hosted [Gitea](https://gitea.inventory-plus-plus.com/angel/inventory-plus-plus) instance (not GitHub) - issues, pull requests, CI (the Tests badge above), and the wiki all live there. See `AGENTS.md`'s Gitea section for details, including the Gitea MCP server tools available to AI agents.
## Deploying
# Deploying
The application runs locally, from this directory.
It is deployed simply by running either from the root of the project,
@@ -32,7 +19,12 @@ or
go run .
```
See `AGENTS.md` for the full setup/`make` command reference (migrations, tests, dev auth, etc).
# Development
## Testing
There's a Makefile with a number of operations for running and testing the application
## Technology
@@ -40,7 +32,7 @@ See `AGENTS.md` for the full setup/`make` command reference (migrations, tests,
### Languages
#### Server side
Golang, Go Templates
Golang, Go Templates,
#### Front end
HTML, CSS, Javascript
@@ -52,6 +44,7 @@ Postgres, in docker
Using [migrate](https://github.com/golang-migrate/migrate) to manage build out the database schema, and to run
migrations.
### Tooling
#### Server side
@@ -66,14 +59,99 @@ migrations.
- tailwind: for styling the front end, using tried and testing styling paradigms, conventions, and templates.
# Architectural and Software Diagrams
# Roadmap
## Application structure
- [ ] Etsy (WIP)
- [ ] GET ETSY AUTH (WIP)
- [x] move auth state stuff to database (out of cache)
- [x] only generate a sign up link IF they click the link on the accounts page
- [ ] get api key approved
- [ ] automatically clean up access tokens and state when expired
- [ ] access tokens
- [ ] state
- [?] Get new access token using refresh token flow
- [ ] make a FK between the etsy_store_events table and etsy_users table (store_id columns don't match types)
- [ ] Auth0
- [ ] get off dev api key?
- [x] Get new access token using refresh token flow
- [ ] test
- [x] when token is expired, redirect them to the login page, then redirect them back to where they were heading to.
- [ ] Social connections login
- [x] automatically clean up access tokens and state when expired
- [x] access tokens
- [x] state
- [ ] Complete this design document?
- [ ] Complete defining this roadmap checklist
- [ ] Website displaying an audit of store events
- [ ] Start with just a list of events for a given store (use a static test store)
- [ ] ...
- [ ] Dark mode
- [ ] don't let a listing be in multiple sync groups
- [ ] Next stores on the list (at least hypothetically)
- Shopify
- WooCommerce
- BigCommerce
- Wix
- Squarespace
- Square Online
- Zoho
- Ecwid
- Big Cartel
Bigger marketplaces:
- Amazon
- Walmart Marketplace
- Ebay
See `AGENTS.md`'s Architecture section for the current, maintained breakdown of packages/directories.
## Nice to haves
- [ ] Drop in a good logger
- [ ] log all errors caught by the http server
## Constraints
- [ ] Etsy
- [ ] API Licensed Uses and Restrictions:
- [ ] Link directly back to the product information and/or image Content on Etsy, where the Application utilizes product information and/or images.
- [ ] Provide a prominently displayed email address on Your Application for third parties to contact You with any questions or issues. You shall respond to such inquiries in a timely manner.
- [ ] Use commercially reasonable efforts to provide a terms of service and privacy policy in a visible location on your Application.
- [ ] Display item Content or product information and/or images which is more than six (6) hours older than such information is on the Website, and other Etsy Content cannot be more than twenty-four (24) hours older than such Content on the Website.
- [ ] Use the API in a manner that exceeds reasonable request volume or constitutes excessive or abusive usage. Users are allocated by default, 10,000 calls per day.
- [ ] You shall not use or alter any text, logos, Etsy's Trademarks, Etsy's signature colors, Etsy's layout, or a confusingly similar layout to Etsy's layout in such a way which may suggest endorsement or affiliation by Etsy.
- [ ] Any use of the Etsy logo or Etsy's Trademarks must be used in its entirety and must not be altered or used in a misleading way.
- [ ] You shall not use a mark which is confusingly similar to Etsy's Trademarks.
- [ ] Any use of the Etsy logo or Etsy's Trademarks in Your Application shall be less prominent than the logo or mark that primarily describes the Application and Your use of the Etsy logo shall not imply any endorsement or affiliation by Etsy.
- [ ] You may publicize, issue press or blog releases of Your Application only if You state that it was created using the Etsy API and that You in no way imply that Your Application is endorsed or certified by Etsy.
- [ ] You must place or display the following notice prominently on Your Application:
"The term 'Etsy' is a trademark of Etsy, Inc. This application uses the Etsy API but is not endorsed or certified by Etsy, Inc."
- [ ] Immediately report any security deficiencies You discover to Etsy by emailing developer@etsy.com.
Looking to follow the [CQRS](https://martinfowler.com/bliki/CQRS.html) pattern.
The database will follow the [event sourcing](https://martinfowler.com/eaaDev/EventSourcing.html) database pattern.
All events (or commands) will be stored in a respective event series, and all database reads will be from views that are projections, reductions, aggregations of those event series.
## Application/directory structure
- /internal
- /site: website
- /webhooks: webhooks for platform events
- /domains: packages for each domain
- /store_events: storing and events
- /platforms: ecommerce platform domains
- /tiktok: interface with tiktok
- ... etc
- ... etc
![Application structure](./diagrams/application_structure.svg)
## Website hierarchy
- /site
# Architectural and Software Diagrams
## Database schemas
**public**
@@ -85,27 +163,38 @@ See `AGENTS.md`'s Architecture section for the current, maintained breakdown of
## Events
`domains/raw_events` is the event-sourcing store behind this - see `AGENTS.md`'s Architecture section.
### Event Sourcing Architecture
![Event sourcing architecture](./diagrams/event_sourcing.svg)
### Event Structure
![Event](./diagrams/event.svg)
### Store Event Database Tables
![Event database tables](./diagrams/event_tables.svg)
## Platform: Etsy
### Signing up
**TODO: need an account page that can create accounts ahead of time - force users to create an account first!**
![Event database tables](./diagrams/etsy/obtaining_access_token.svg)
### Models
![Models](./diagrams/etsy/models.svg)
***TODO: create a page that will take billing information and include it in this process***
See `ROADMAP.md` for what's still unbuilt in the Etsy flow (an account-creation page ahead of OAuth, billing info collection, refresh-token handling), and `domains/platforms/etsy/COMPLIANCE.md` for API usage obligations.
### Getting a new refresh token
***TODO***
### Models
![Models](./diagrams/etsy/models.svg)
## All Diagrams
-38
View File
@@ -1,38 +0,0 @@
# Roadmap
- [ ] Etsy (WIP)
- [ ] GET ETSY AUTH (WIP)
- [x] move auth state stuff to database (out of cache)
- [x] only generate a sign up link IF they click the link on the accounts page
- [ ] get api key approved
- [ ] automatically clean up access tokens and state when expired
- [ ] access tokens
- [ ] state
- [ ] account-creation page ahead of OAuth (force users to create an account before connecting a store)
- [ ] collect billing information as part of that account-creation flow
- [?] Get new access token using refresh token flow
- [ ] make a FK between the etsy_store_events table and etsy_users table (store_id columns don't match types)
- [ ] build polling-based order/inventory sync against the live Etsy API - only the OAuth connection flow is implemented so far (see `research/platforms.md`'s Etsy notes: API v3 has no webhook/push system, so this has to be a poll loop)
- see `domains/platforms/etsy/COMPLIANCE.md` for Etsy API usage obligations to keep satisfied along the way
- [ ] Auth0
- [ ] get off dev api key?
- [x] Get new access token using refresh token flow
- [ ] test
- [x] when token is expired, redirect them to the login page, then redirect them back to where they were heading to.
- [ ] Social connections login
- [x] automatically clean up access tokens and state when expired
- [x] access tokens
- [x] state
- [ ] Complete this design document?
- [ ] Complete defining this roadmap checklist
- [ ] Website displaying an audit of store events
- [ ] Start with just a list of events for a given store (use a static test store)
- [ ] ...
- [ ] Dark mode
- [ ] don't let a listing be in multiple sync groups
- [ ] Next platform to build out beyond Etsy - see `AGENTS.md`'s "Platform integration priority" section for the researched, weighted ranking (currently: Shopify, then Amazon, then Tiktok Shop) instead of picking from the raw platform list
## Nice to haves
- [ ] Drop in a good logger
- [ ] log all errors caught by the http server
+46
View File
@@ -0,0 +1,46 @@
# Store API Research
https://docs.google.com/spreadsheets/d/1xWfXn-wbiHTBeqyhnq0b46_sgLGyiYc_5N5mXKYD1R8/edit?gid=0#gid=0
| Platform | Order Event: placed | Order Event: changed | Products List | Products Look up | Inventory Look up | Inventory Update | Inventory Event: change |
|----------|----------|----------|----------|----------|----------|----------|----------|
| Shopify | orders/create | orders/cancelled, orders/delete, orders/paid, etc | /queries/products | /queries/product | /queries/product | /mutations/inventorySetQuantities | inventory_levels/update |
| WooCommerce | webhook `order.created` | webhook `order.updated`, `order.deleted` | `GET /wp-json/wc/v3/products` | `GET /wp-json/wc/v3/products/<id>` | `stock_quantity` field on product resource | `PUT /wp-json/wc/v3/products/<id>` (stock_quantity), or `/products/batch` | `product.updated` (no dedicated inventory webhook) |
| BigCommerce | webhook `store/order/created` | webhook `store/order/updated`, `store/order/statusUpdated` | `GET /v3/catalog/products` | `GET /v3/catalog/products/{product_id}` | `GET /v3/inventory/items` | `PUT /v3/inventory/adjustments/absolute` (also `/relative`) | `store/product/inventory/updated` |
| Wix | webhook `wix.ecom.v1.order.created` | webhook `wix.ecom.v1.order.updated` (also `.canceled`) | Query Products (Catalog V3) | Get Product | Query Inventory Items | Update Inventory Variants | `wix.stores.catalog.v3.inventory_item.updated` |
| Squarespace | webhook `order.create` | webhook `order.update` (FULFILLED, REFUNDED, CANCELED, MARKED_PENDING, EMAIL_UPDATED) | `GET /v2/commerce/products` | `GET /v2/commerce/products/{productIdCsvs}` | `GET /1.0/commerce/inventory/{variantIdCsvs}` | `POST /1.0/commerce/inventory/adjustments` | N/A (no inventory webhook topic) |
| Square Online | webhook `order.created` | webhook `order.updated`, `order.fulfillment.updated` | `GET /v2/catalog/list` | `GET /v2/catalog/object/{object_id}` | `POST /v2/inventory/counts/batch-retrieve` | `POST /v2/inventory/changes/batch-create` (BatchChangeInventory) | webhook `inventory.count.updated` |
| Zoho | webhook `salesorder.created` (Zoho Commerce) | webhook `salesorder.confirmed, .cancelled, .declined, .shipped, .delivered` | `GET /store/api/v1/products` | `GET /store/api/v1/products/{product_id}` | `GET /store/api/v1/variants` (`stock_on_hand`, `actual_available_stock`) | `POST /store/api/v1/inventoryadjustments` | N/A (no inventory/stock webhook event) |
| Ecwid | webhook `order.created` | webhook `order.updated`, `order.deleted` | `GET /api/v3/{storeId}/products` | `GET /api/v3/{storeId}/products/{productId}` | `GET /api/v3/{storeId}/products/{productId}` (`quantity`/`unlimited`) | `PUT /api/v3/{storeId}/products/{productId}/inventory` (`quantityDelta`) | `product.updated` webhook |
| Big Cartel | webhook `order.create` (app-approved) | webhook `order.update` (app-approved) | `GET /v1/accounts/{account_id}/products` | `GET /v1/accounts/{account_id}/products/{id}` | N/A — no dedicated inventory field/endpoint | N/A — no inventory update endpoint | N/A — no inventory-specific webhook |
| Amazon | `ORDER_CHANGE` notification (SP-API) | `ORDER_CHANGE` notification (same type, status delta) | `searchCatalogItems` (GET `/catalog/2022-04-01/items`) | `getCatalogItem` (GET `/catalog/2022-04-01/items/{asin}`) | `getInventorySummaries` (FBA Inventory API, GET `/fba/inventory/v1/summaries`) | `patchListingsItem` (PATCH `/listings/2021-08-01/items/{sellerId}/{sku}`) | `FBA_INVENTORY_AVAILABILITY_CHANGES` notification |
| Walmart Marketplace | PO created event (webhook) | Order intent to cancel / PO line auto-cancelled event (webhook); status flow Created→Acknowledged→Shipped→Delivered/Cancelled | `GET /v3/items` (getAllItems) | `GET /v3/items/{id}` (getAnItem) | `GET /v3/inventory?sku={sku}` | `PUT /v3/inventory` (also bulk via `POST /v3/feeds`) | Inventory OOS event (webhook) |
| Ebay | `FixedPriceTransaction` / `ItemSold` (Platform Notifications, legacy Trading API) | `ItemMarkedShipped` notification; also `getOrders` filtered by `lastmodifieddate` (Fulfillment API) | `GET /sell/inventory/v1/inventory_item` (getInventoryItems) | `GET /sell/inventory/v1/inventory_item/{sku}` (getInventoryItem) | `GET /sell/inventory/v1/inventory_item/{sku}` (availability.shipToLocationAvailability) | `POST /sell/inventory/v1/bulk_update_price_quantity` (bulkUpdatePriceQuantity) | N/A — no dedicated inventory-change topic found |
| Platform | Docs | Webhooks | API |
|----------|----------|----------|----------|
| Shopify | https://shopify.dev/docs/api | https://shopify.dev/docs/api/webhooks/latest?reference=toml | https://shopify.dev/docs/api/admin-graphql/latest |
| WooCommerce | https://developer.woocommerce.com/docs/apis/rest-api/ | https://developer.woocommerce.com/docs/apis/rest-api/v2/webhooks/ | https://developer.woocommerce.com/docs/apis/rest-api/v3/products/ |
| BigCommerce | https://developer.bigcommerce.com/docs | https://developer.bigcommerce.com/docs/integrations/webhooks/overview | https://developer.bigcommerce.com/docs/rest-catalog/products |
| Wix | https://dev.wix.com/docs | https://dev.wix.com/docs/build-apps/develop-your-app/api-integrations/events-and-webhooks/about-webhooks | https://dev.wix.com/docs/api-reference |
| Squarespace | https://developers.squarespace.com/commerce-apis/overview | https://developers.squarespace.com/commerce-apis/webhooksubscriptions-overview | https://developers.squarespace.com/commerce-apis/overview |
| Square Online | https://developer.squareup.com/docs | https://developer.squareup.com/docs/webhooks/overview | https://developer.squareup.com/reference/square |
| Zoho | https://www.zoho.com/commerce/api/introduction.html | https://www.zoho.com/commerce/api/webhooks.html | https://www.zoho.com/commerce/api/apis-list.html |
| Ecwid | https://docs.ecwid.com/ | https://docs.ecwid.com/webhook-automations | https://api-docs.ecwid.com/reference |
| Big Cartel | https://developers.bigcartel.com/ | https://developers.bigcartel.com/api/v1 (webhooks section, no standalone page) | https://developers.bigcartel.com/api/v1 |
| Amazon | https://developer-docs.amazon.com/sp-api/docs/welcome | https://developer-docs.amazon.com/sp-api/docs/notifications-api-v1-use-case-guide | https://developer-docs.amazon.com/sp-api/reference |
| Walmart Marketplace | https://developer.walmart.com/ | https://developer.walmart.com/doc/us/mp/us-mp-notifications/ | https://developer.walmart.com/us-marketplace/docs/inventory-api-overview |
| Ebay | https://developer.ebay.com/develop | https://developer.ebay.com/api-docs/commerce/notification/overview.html | https://developer.ebay.com/api-docs/sell/inventory/overview.html |
### Notes / caveats from research
- **Square Online**: no separate API — orders, catalog, and inventory are handled by Square's core Seller APIs (developer.squareup.com), the same ones used across all Square products. Unrelated to Squarespace despite the name.
- **Zoho**: "Zoho Commerce" (commerce.zoho.com) is the storefront product comparable to Shopify/Squarespace and owns the order/product/webhook APIs listed above. Zoho Inventory is a separate warehouse/stock-management app with its own API but no documented webhook support.
- **Big Cartel**: no true inventory API — only an `inventory_enabled` flag and `quantity_gte`/`quantity_lte` filters on products. No endpoint to set stock and no inventory-change webhook. Webhook access is gated per-app approval; exact topic names are inferred from integration examples since Big Cartel has no canonical published list.
- **Amazon SP-API**: no separate "placed" vs "changed" order topics — both flow through a single `ORDER_CHANGE` notification, differentiated by payload content.
- **WooCommerce / Ecwid**: neither has a dedicated inventory-change webhook; stock changes surface via the general `product.updated` event instead.
- **Ebay**: order-event names are less certain — developer.ebay.com pages repeatedly failed to load during research, so those values come from documented Platform Notifications event types found via search rather than a directly confirmed doc page.
-19
View File
@@ -1,19 +0,0 @@
# Etsy API compliance checklist
Obligations from Etsy's API Licensed Uses and Restrictions. Anything
touching `domains/platforms/etsy` or the Etsy-facing UI should keep these
satisfied before shipping.
- [ ] Link directly back to the product information and/or image Content on Etsy, where the Application utilizes product information and/or images.
- [ ] Provide a prominently displayed email address on Your Application for third parties to contact You with any questions or issues. You shall respond to such inquiries in a timely manner.
- [ ] Use commercially reasonable efforts to provide a terms of service and privacy policy in a visible location on your Application.
- [ ] Display item Content or product information and/or images which is more than six (6) hours older than such information is on the Website, and other Etsy Content cannot be more than twenty-four (24) hours older than such Content on the Website.
- [ ] Use the API in a manner that exceeds reasonable request volume or constitutes excessive or abusive usage. Users are allocated by default, 10,000 calls per day.
- [ ] You shall not use or alter any text, logos, Etsy's Trademarks, Etsy's signature colors, Etsy's layout, or a confusingly similar layout to Etsy's layout in such a way which may suggest endorsement or affiliation by Etsy.
- [ ] Any use of the Etsy logo or Etsy's Trademarks must be used in its entirety and must not be altered or used in a misleading way.
- [ ] You shall not use a mark which is confusingly similar to Etsy's Trademarks.
- [ ] Any use of the Etsy logo or Etsy's Trademarks in Your Application shall be less prominent than the logo or mark that primarily describes the Application and Your use of the Etsy logo shall not imply any endorsement or affiliation by Etsy.
- [ ] You may publicize, issue press or blog releases of Your Application only if You state that it was created using the Etsy API and that You in no way imply that Your Application is endorsed or certified by Etsy.
- [ ] You must place or display the following notice prominently on Your Application:
"The term 'Etsy' is a trademark of Etsy, Inc. This application uses the Etsy API but is not endorsed or certified by Etsy, Inc."
- [ ] Immediately report any security deficiencies You discover to Etsy by emailing developer@etsy.com.
-209
View File
@@ -1,209 +0,0 @@
# Platform Research
https://docs.google.com/spreadsheets/d/1xWfXn-wbiHTBeqyhnq0b46_sgLGyiYc_5N5mXKYD1R8/edit?gid=0#gid=0
| Platform | Order Event: placed | Order Event: changed | Products List | Products Look up | Inventory Look up | Inventory Update | Inventory Event: change |
|----------|----------|----------|----------|----------|----------|----------|----------|
| **Etsy (live)** | N/A — no webhook/push system in API v3 at all; poll `GET /v3/application/shops/{shop_id}/receipts` (filter `min_created`) | poll same endpoint filtered by `min_last_modified` | `GET /v3/application/shops/{shop_id}/listings` | `GET /v3/application/listings/{listing_id}` | `GET /v3/application/listings/{listing_id}/inventory` | `PUT /v3/application/listings/{listing_id}/inventory` | N/A — no inventory-change webhook; poll the inventory endpoint |
| Shopify | orders/create | orders/cancelled, orders/delete, orders/paid, etc | /queries/products | /queries/product | /queries/product | /mutations/inventorySetQuantities | inventory_levels/update |
| WooCommerce | webhook `order.created` | webhook `order.updated`, `order.deleted` | `GET /wp-json/wc/v3/products` | `GET /wp-json/wc/v3/products/<id>` | `stock_quantity` field on product resource | `PUT /wp-json/wc/v3/products/<id>` (stock_quantity), or `/products/batch` | `product.updated` (no dedicated inventory webhook) |
| BigCommerce | webhook `store/order/created` | webhook `store/order/updated`, `store/order/statusUpdated` | `GET /v3/catalog/products` | `GET /v3/catalog/products/{product_id}` | `GET /v3/inventory/items` | `PUT /v3/inventory/adjustments/absolute` (also `/relative`) | `store/product/inventory/updated` |
| Wix | webhook `wix.ecom.v1.order.created` | webhook `wix.ecom.v1.order.updated` (also `.canceled`) | Query Products (Catalog V3) | Get Product | Query Inventory Items | Update Inventory Variants | `wix.stores.catalog.v3.inventory_item.updated` |
| Squarespace | webhook `order.create` | webhook `order.update` (FULFILLED, REFUNDED, CANCELED, MARKED_PENDING, EMAIL_UPDATED) | `GET /v2/commerce/products` | `GET /v2/commerce/products/{productIdCsvs}` | `GET /1.0/commerce/inventory/{variantIdCsvs}` | `POST /1.0/commerce/inventory/adjustments` | N/A (no inventory webhook topic) |
| Square Online | webhook `order.created` | webhook `order.updated`, `order.fulfillment.updated` | `GET /v2/catalog/list` | `GET /v2/catalog/object/{object_id}` | `POST /v2/inventory/counts/batch-retrieve` | `POST /v2/inventory/changes/batch-create` (BatchChangeInventory) | webhook `inventory.count.updated` |
| Zoho | webhook `salesorder.created` (Zoho Commerce) | webhook `salesorder.confirmed, .cancelled, .declined, .shipped, .delivered` | `GET /store/api/v1/products` | `GET /store/api/v1/products/{product_id}` | `GET /store/api/v1/variants` (`stock_on_hand`, `actual_available_stock`) | `POST /store/api/v1/inventoryadjustments` | N/A (no inventory/stock webhook event) |
| Ecwid | webhook `order.created` | webhook `order.updated`, `order.deleted` | `GET /api/v3/{storeId}/products` | `GET /api/v3/{storeId}/products/{productId}` | `GET /api/v3/{storeId}/products/{productId}` (`quantity`/`unlimited`) | `PUT /api/v3/{storeId}/products/{productId}/inventory` (`quantityDelta`) | `product.updated` webhook |
| Big Cartel | webhook `order.create` (app-approved) | webhook `order.update` (app-approved) | `GET /v1/accounts/{account_id}/products` | `GET /v1/accounts/{account_id}/products/{id}` | N/A — no dedicated inventory field/endpoint | N/A — no inventory update endpoint | N/A — no inventory-specific webhook |
| Amazon | `ORDER_CHANGE` notification (SP-API) | `ORDER_CHANGE` notification (same type, status delta) | `searchCatalogItems` (GET `/catalog/2022-04-01/items`) | `getCatalogItem` (GET `/catalog/2022-04-01/items/{asin}`) | `getInventorySummaries` (FBA Inventory API, GET `/fba/inventory/v1/summaries`) | `patchListingsItem` (PATCH `/listings/2021-08-01/items/{sellerId}/{sku}`) | `FBA_INVENTORY_AVAILABILITY_CHANGES` notification |
| Walmart Marketplace | PO created event (webhook) | Order intent to cancel / PO line auto-cancelled event (webhook); status flow Created→Acknowledged→Shipped→Delivered/Cancelled | `GET /v3/items` (getAllItems) | `GET /v3/items/{id}` (getAnItem) | `GET /v3/inventory?sku={sku}` | `PUT /v3/inventory` (also bulk via `POST /v3/feeds`) | Inventory OOS event (webhook) |
| Ebay | `FixedPriceTransaction` / `ItemSold` (Platform Notifications, legacy Trading API) | `ItemMarkedShipped` notification; also `getOrders` filtered by `lastmodifieddate` (Fulfillment API) | `GET /sell/inventory/v1/inventory_item` (getInventoryItems) | `GET /sell/inventory/v1/inventory_item/{sku}` (getInventoryItem) | `GET /sell/inventory/v1/inventory_item/{sku}` (availability.shipToLocationAvailability) | `POST /sell/inventory/v1/bulk_update_price_quantity` (bulkUpdatePriceQuantity) | N/A — no dedicated inventory-change topic found |
| Platform | Docs | Webhooks | API |
|----------|----------|----------|----------|
| Etsy (live) | https://developer.etsy.com/documentation/ | N/A — no webhook/push mechanism exists in Etsy Open API v3 | https://developer.etsy.com/documentation/reference (confirmed directly against `domains/platforms/etsy/generated_client` in this repo, not external docs) |
| Shopify | https://shopify.dev/docs/api | https://shopify.dev/docs/api/webhooks/latest?reference=toml | https://shopify.dev/docs/api/admin-graphql/latest |
| WooCommerce | https://developer.woocommerce.com/docs/apis/rest-api/ | https://developer.woocommerce.com/docs/apis/rest-api/v2/webhooks/ | https://developer.woocommerce.com/docs/apis/rest-api/v3/products/ |
| BigCommerce | https://developer.bigcommerce.com/docs | https://developer.bigcommerce.com/docs/integrations/webhooks/overview | https://developer.bigcommerce.com/docs/rest-catalog/products |
| Wix | https://dev.wix.com/docs | https://dev.wix.com/docs/build-apps/develop-your-app/api-integrations/events-and-webhooks/about-webhooks | https://dev.wix.com/docs/api-reference |
| Squarespace | https://developers.squarespace.com/commerce-apis/overview | https://developers.squarespace.com/commerce-apis/webhooksubscriptions-overview | https://developers.squarespace.com/commerce-apis/overview |
| Square Online | https://developer.squareup.com/docs | https://developer.squareup.com/docs/webhooks/overview | https://developer.squareup.com/reference/square |
| Zoho | https://www.zoho.com/commerce/api/introduction.html | https://www.zoho.com/commerce/api/webhooks.html | https://www.zoho.com/commerce/api/apis-list.html |
| Ecwid | https://docs.ecwid.com/ | https://docs.ecwid.com/webhook-automations | https://api-docs.ecwid.com/reference |
| Big Cartel | https://developers.bigcartel.com/ | https://developers.bigcartel.com/api/v1 (webhooks section, no standalone page) | https://developers.bigcartel.com/api/v1 |
| Amazon | https://developer-docs.amazon.com/sp-api/docs/welcome | https://developer-docs.amazon.com/sp-api/docs/notifications-api-v1-use-case-guide | https://developer-docs.amazon.com/sp-api/reference |
| Walmart Marketplace | https://developer.walmart.com/ | https://developer.walmart.com/doc/us/mp/us-mp-notifications/ | https://developer.walmart.com/us-marketplace/docs/inventory-api-overview |
| Ebay | https://developer.ebay.com/develop | https://developer.ebay.com/api-docs/commerce/notification/overview.html | https://developer.ebay.com/api-docs/sell/inventory/overview.html |
### Notes / caveats from research
- **Square Online**: no separate API — orders, catalog, and inventory are handled by Square's core Seller APIs (developer.squareup.com), the same ones used across all Square products. Unrelated to Squarespace despite the name.
- **Zoho**: "Zoho Commerce" (commerce.zoho.com) is the storefront product comparable to Shopify/Squarespace and owns the order/product/webhook APIs listed above. Zoho Inventory is a separate warehouse/stock-management app with its own API but no documented webhook support.
- **Big Cartel**: no true inventory API — only an `inventory_enabled` flag and `quantity_gte`/`quantity_lte` filters on products. No endpoint to set stock and no inventory-change webhook. Webhook access is gated per-app approval; exact topic names are inferred from integration examples since Big Cartel has no canonical published list.
- **Amazon SP-API**: no separate "placed" vs "changed" order topics — both flow through a single `ORDER_CHANGE` notification, differentiated by payload content.
- **WooCommerce / Ecwid**: neither has a dedicated inventory-change webhook; stock changes surface via the general `product.updated` event instead.
- **Etsy**: the one row in the first table sourced from this repo's actual code rather than external docs (`domains/platforms/etsy/generated_client`, generated from Etsy's own OpenAPI spec) - so it's the most reliable row here, not the least. Etsy Open API v3 has no webhook/push system whatsoever for anything, order or inventory alike; every other platform in the table has at least *some* real-time push. The live integration in this repo currently only implements the OAuth connection flow (`GenerateConnectionURLForNewAccount`/`HandleNewAuthCode` in `domains/platforms/etsy/etsy.go`) - polling-based order/inventory sync against `GetShopReceipts`/`GetListingInventory` isn't built yet.
- **Ebay**: order-event names are less certain — developer.ebay.com pages repeatedly failed to load during research, so those values come from documented Platform Notifications event types found via search rather than a directly confirmed doc page.
- **TikTok Shop**: not in the capability table above - `partner.tiktokshop.com/docv2` is JS-rendered and didn't return usable content via fetch, so exact webhook event names and endpoint paths aren't confirmed (unlike every other row in that table, which comes from readable docs). What is confirmed via secondary sources: TikTok Shop's Partner API has webhooks covering order, product, and inventory changes, and a Product/Inventory API for listing and stock management. Treat as directionally real but needing its own dedicated research pass - with confirmed endpoint names - before implementation.
- **BigCommerce vs Tiktok**: the capability table above includes BigCommerce, but BigCommerce is not one of this codebase's actual mock platforms (see `AGENTS.md`'s platform list - `domains/accounts/platform.go`'s `allPlatforms` has `Tiktok`, not BigCommerce). BigCommerce's row is left in place since the research itself may still be useful, but it's excluded from the priority ranking below; Tiktok is included despite the weaker sourcing noted above, since it's a platform that actually exists in this codebase.
## Market-size research (2026-08-20)
Pulled to sanity-check which platforms are most worth integrating first, on
top of the API-completeness comparison above. See `AGENTS.md`'s "Platform
integration priority" section for the resulting ranking and reasoning.
Figures are mid-2026 estimates for FY2025 (or most recent trailing period)
unless noted. Anything not sourced from an SEC filing is a third-party
estimate (StoreLeads, DemandSage, Marketplace Pulse, etc.) and should be
treated as directional, not precise - methodology varies a lot between
sources, especially for "number of stores/sellers."
| Platform | Active sellers/stores | Most recent GMV | Source confidence |
|---|---|---|---|
| Amazon (3P) | ~1.9M active sellers | ~$575B (3P GMV, 2025); total Amazon GMV >$800B | Moderate - earnings-adjacent estimates |
| Shopify | ~3-6.8M active stores (methodology varies) | $378.4B (FY2025, +29% YoY) | High - SEC filings |
| eBay | ~18.3M active sellers | $79.6B (FY2025, +7% YoY) | High - SEC filings |
| Etsy (already live) | 5.6M active sellers | $11.92B GMS (FY2025, -5.3%) | High - SEC filings |
| Squarespace Commerce | ~353K live ecommerce sites | ~$7.2B (2026 est.) | Low - marketing estimate |
| WooCommerce | ~4.2-6M active stores | ~$30-35B (2025 est.; average store is tiny, ~$7-8K/yr) | Low - third-party estimate, no central ledger |
| Walmart Marketplace | ~200-250K sellers, +50% YoY growth | ~$10B (rough external estimate; Walmart doesn't break this out cleanly) | Low |
| Wix eCommerce | ~760K-3M live stores | $4.1B-$12.4B (conflicting self-reported figures) | Low |
| Tiktok Shop | ~15M sellers globally, ~500K registered / ~216K active in the US | $64.3B global (2025, nearly 2x 2024); $15.1B US (+68% YoY) | Low - third-party estimate, hypergrowth market |
| BigCommerce *(not an actual mock platform here - see caveat above)* | ~37-42K *active* stores (shrinking, shifting to enterprise) | $34B+ | Moderate |
| Square Online | n/a (bundled into Square's overall $250B GPV, mostly in-person POS) | Can't isolate | N/A |
| Zoho Commerce | ~2,196 stores globally (285 US), +34% YoY US growth off a tiny base | Not disclosed; likely small given store count | Low |
| Ecwid | ~130-164K live stores, declining (-20% YoY) | Not disclosed | Low |
| Big Cartel | ~91-192K stores (estimates vary widely), declining sharply (-41% YoY in 2026 Q1) | Not disclosed; platform targets low-AOV indie sellers | Low |
Key takeaways:
- Amazon's 3P GMV ($575B) is ~1.5x Shopify's *total* GMV despite Shopify
having 2-3x more active stores - Amazon sellers skew toward larger,
more serious operations.
- eBay has by far the most sellers (18.3M) but the lowest GMV-per-seller of
any major platform here ($79.6B / 18.3M), suggesting a lot of integration
surface for comparatively thin per-seller value - compounded by its weak
inventory-webhook story (see table above).
- WooCommerce can't be sized financially at all - it's a WordPress plugin,
not a company with a ledger - so its case rests entirely on raw store
count, not proven revenue-per-integration.
- Walmart Marketplace is small in absolute terms but growing fast (+50%
YoY) and is a natural "second marketplace" for sellers already on Amazon.
## Weighted priority ranking (2026-08-20)
Scores each of the 12 not-yet-live mock platforms (everything in
`domains/accounts/platform.go`'s `allPlatforms` except Etsy) on four 0-10
criteria, weighted and summed to a single composite score. This is a
judgment call turned into numbers, not a precise formula - the weights and
per-platform scores below are my read of the research above; treat the
*ranking* as the useful output, not the second decimal place. BigCommerce is
excluded (not an actual platform in this codebase, see caveat above).
Etsy itself is scored too, as a **reference row only** - it's already the
live integration, so it's not competing for "what to build next," but
running it through the same rubric is a useful sanity check on the model.
**Criteria & weights:**
- **GMV / market opportunity (45%)** - bucketed from the GMV figures above
(>$500B=10, $300-500B=9, $50-100B=7, $25-50B=6, $8-15B=4, $5-8B=3,
undisclosed-and-small=1). This is weighted highest because "lucrative"
is fundamentally a dollar-opportunity question.
- **API/inventory-sync completeness (30%)** - from the capability table at
the top of this doc: full order-webhook + dedicated inventory-webhook
coverage scores highest, missing inventory webhook scores mid, no
inventory API at all (Big Cartel) scores near-zero. This directly gates
how good a product experience is even possible on that platform.
- **Growth trajectory (15%)** - YoY GMV/store growth; rewards fast-growing
platforms (Tiktok, Walmart) and penalizes shrinking ones (Ecwid, Big
Cartel) as a proxy for where future opportunity is heading.
- **Integration cost, inverted (10%)** - auth complexity and existing
codebase head start (Amazon's `domains/amazon` background-processor
infra) score higher; gated/undocumented webhook access (Big Cartel)
scores lowest. Weighted lowest since it affects timeline more than
whether the integration is worth doing at all.
**Scores:**
| Platform | GMV (45%) | API (30%) | Growth (15%) | Cost (10%) | Weighted total |
|---|---|---|---|---|---|
| Shopify | 9 | 10 | 8 | 9 | **9.15** |
| Amazon | 10 | 10 | 5 | 6 | **8.85** |
| Tiktok Shop | 7 | 7 | 10 | 4 | **7.15** |
| Walmart Marketplace | 4 | 9 | 9 | 5 | **6.35** |
| Wix | 4 | 10 | 5 | 8 | **6.35** |
| WooCommerce | 6 | 6 | 5 | 7 | **5.95** |
| Square Online | 3 | 10 | 5 | 8 | **5.90** |
| Ebay | 7 | 5 | 4 | 5 | **5.75** |
| Squarespace | 3 | 6 | 6 | 7 | **4.75** |
| *Etsy (reference, already live)* | *4* | *3* | *1* | *10* | *3.85* |
| Zoho | 1 | 6 | 6 | 7 | **3.85** |
| Ecwid | 1 | 6 | 1 | 8 | **3.20** |
| Big Cartel | 1 | 2 | 1 | 3 | **1.50** |
**Ranked priority order:**
1. **Shopify** — 9.15
2. **Amazon** — 8.85
3. **Tiktok Shop** — 7.15
4. **Walmart Marketplace** — 6.35
5. **Wix** — 6.35 (behind Walmart on the growth tiebreaker: 9 vs 5)
6. **WooCommerce** — 5.95
7. **Square Online** — 5.90
8. **Ebay** — 5.75
9. **Squarespace** — 4.75
10. **Etsy** — 3.85 (reference only - already live, not competing for "what to build next")
11. **Zoho** — 3.85
12. **Ecwid** — 3.20
13. **Big Cartel** — 1.50
Notable movement from the earlier qualitative pass: Shopify edges out Amazon
once integration cost and growth are counted, not just raw GMV - Amazon's
dollar opportunity is still bigger, but Shopify is cheaper to build, easier
to get first customers through (App Store), and still growing faster.
Tiktok Shop, unresearched until this pass, lands at #3 on the strength of
its growth rate alone - but see the sourcing caveat above before acting on
that; its API details need a dedicated research pass before it's actually
buildable. Big Cartel is unambiguously last: no inventory API is a
structural dealbreaker for this specific product, independent of its market
size.
**Etsy, run through the same rubric, scores 3.85 - tied with Zoho, below
every platform except Ecwid and Big Cartel.** Its GMV is mid-pack and
declining (-5.3% YoY), and - per the caveat above - it's the *only*
platform researched with literally no webhook/push mechanism for anything,
so it scores below every platform here except Big Cartel on API
completeness too. The only criterion where it dominates is integration cost
(10/10, since it's already built). This is a useful gut-check on the model,
not a claim that building Etsy first was a mistake - Etsy was presumably
chosen for reasons this rubric doesn't capture (an existing relationship, a
founder's market knowledge, being the most approachable API to get
developer credentials for), not for having the best growth/GMV/webhook
profile. Worth remembering when weighing this ranking against Etsy's actual
day-to-day integration cost, which - per the caveat above - has turned out
to be nontrivial in practice: with no webhooks at all, real order/inventory
sync has to be built as a polling loop, which is exactly the kind of
integration cost this rubric's "Cost" column doesn't capture once a
platform is more than superficially wired up.
Sources:
- [Shopify Statistics 2026: Market Share, $116B GMV, Employees](https://www.chargeflow.io/blog/shopify-statistics)
- [Shopify 2026: $378.4B GMV, Store and Seller Data](https://termsandconditionstemplate.com/shopify-statistics-2026)
- [Amazon Third-Party Sellers Generate $575 Billion in GMV](https://english.ebrun.com/20260719/688440.shtml)
- [Amazon GMV Surpassed $800 Billion in 2025 - Marketplace Pulse](https://www.marketplacepulse.com/articles/amazon-gmv-surpassed-800-billion-in-2025)
- [Top 1.6% of Sellers Drive 50% of Amazon's 3P GMV](https://www.marketplacepulse.com/articles/top-16-of-sellers-drive-50-of-amazons-3p-gmv)
- [WooCommerce Market Share 2026: 33.4% Global Stats](https://redstagfulfillment.com/what-is-woocommerces-market-share/)
- [BigCommerce Statistics 2026](https://www.chargeflow.io/blog/bigcommerce-statistics)
- [How many Walmart Marketplace sellers are there in 2025?](https://redstagfulfillment.com/how-many-walmart-marketplace-sellers/)
- [Walmart Marketplace Grows 50% in One Year - Marketplace Pulse](https://www.marketplacepulse.com/articles/walmart-marketplace-grows-50-in-one-year)
- [eBay Gross Merchandise Volume (GMV) 2018-2026 - Marketplace Pulse](https://www.marketplacepulse.com/stats/ebay-gross-merchandise-volume-gmv)
- [eBay Statistics (2026): Active Buyers, Listings, GMV, Revenue](https://expandedramblings.com/index.php/ebay-stats/)
- [The State of Wix in 2026](https://storeleads.app/reports/wix)
- [Squarespace Subscriber and Revenue Statistics for 2026](https://backlinko.com/squarespace-users)
- [Etsy, Inc. Reports Fourth Quarter and Full Year 2025 Results](https://investors.etsy.com/news-events/press-releases/detail/218/etsy-inc-reports-fourth-quarter-and-full-year-2025-results)
- [Block Statistics (2026): Sellers, GPV, Block Revenue](https://expandedramblings.com/index.php/square-statistics/)
-128
View File
@@ -1,128 +0,0 @@
package ui_test
import (
"flag"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
"ruben/inventory2/domains/accounts"
"ruben/inventory2/domains/authentication"
etsy_platform "ruben/inventory2/domains/platforms/etsy"
"ruben/inventory2/domains/raw_events"
"ruben/inventory2/domains/reports"
"ruben/inventory2/internal/testdb"
"ruben/inventory2/server/auth"
"ruben/inventory2/server/response"
"ruben/inventory2/server/ui"
)
// update regenerates the golden files under testdata/golden from the
// current rendered output, instead of comparing against them. Review the
// diff like any other golden file before committing it, e.g.:
//
// TEST_DATABASE_URL=... go test ./server/ui/... -run TestServeTemplate_Golden -update
var update = flag.Bool("update", false, "update golden files in testdata/golden instead of comparing against them")
// TestServeTemplate_Golden is a raw-HTML snapshot/regression test: it
// exercises server/ui's real routing + templating path end-to-end (real
// templates, real Store types backed by a real Postgres connection, the
// same response.HandleResponses/HandleErrors middleware production uses -
// nothing about the render path is mocked) and diffs the rendered HTML
// against a committed golden file, so an unintentional template/markup
// change shows up as a reviewable diff instead of shipping silently.
//
// This is the "raw HTML snapshot" layer described in issue #33's UI-testing
// proposal. A browser-screenshot layer (e.g. chromedp, for catching CSS/
// layout regressions raw HTML can't) and Penpot-design-fidelity comparisons
// are separate, not-yet-implemented layers of that same proposal.
//
// Only covers logged-out pages for now (no seeded oauth session/cookie);
// extend the tests table below using testdb.SeedOAuthSession plus an
// "access_token" cookie on the request to cover authenticated pages.
//
// Like other integration tests in this repo, it's skipped (not failed) if
// TEST_DATABASE_URL is unset. If a golden file hasn't been generated yet,
// the individual case is also skipped (not failed) rather than breaking
// the suite - run with -update once, review the generated file, and commit
// it to turn that case into a real regression check.
func TestServeTemplate_Golden(t *testing.T) {
pool := testdb.Pool(t)
logger := testdb.Logger()
accts := accounts.NewStore(logger, pool)
rawEvents := raw_events.NewStore(logger, pool)
reps := reports.NewStore(logger, pool, accts)
etsy := etsy_platform.NewPlatform(
logger,
func(acctID int64) string { return "" },
"", "",
pool,
)
authr := authentication.NewDev(pool, logger)
authM := auth.NewService(logger, authr, accts)
gin.SetMode(gin.TestMode)
router := gin.New()
router.Use(
authM.Identify,
response.HandleResponses,
response.HandleErrors,
)
ui.Routes(
logger,
router.Group("/ui"),
"/ui",
rawEvents,
accts,
reps,
etsy,
authM.Authenticate(),
true,
)
tests := []struct {
name string
path string
wantStatus int
}{
{name: "home-logged-out", path: "/ui", wantStatus: http.StatusOK},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, tt.path, nil)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
require.Equal(t, tt.wantStatus, rec.Code, "unexpected status for %s", tt.path)
got := rec.Body.Bytes()
golden := filepath.Join("testdata", "golden", tt.name+".html")
if *update {
require.NoError(t, os.MkdirAll(filepath.Dir(golden), 0o755))
require.NoError(t, os.WriteFile(golden, got, 0o644))
return
}
want, err := os.ReadFile(golden)
if os.IsNotExist(err) {
t.Skipf(
"golden file %s does not exist yet; generate it once via `go test ./server/ui/... -run TestServeTemplate_Golden -update` (requires TEST_DATABASE_URL), then review and commit it",
golden,
)
}
require.NoError(t, err, "reading golden file %s", golden)
require.Equal(t, string(want), string(got), "rendered output for %s no longer matches %s; if this change is intentional, regenerate with -update", tt.path, golden)
})
}
}