CataSync feeds 5/6: Google Merchant Center destination (Merchant API v1)
Imported from GitHub issue El-SaMa/oma#248 by @El-SaMa.
Part of the Product feeds epic. Needs the Feeds UI/MCP issue live.
What
A feed with destination = google is sent straight to Google Merchant Center over Merchant API v1 (no plugin on the shop). This replaces the removed Google Listings & Ads plugin on niletech and gives CataSync many more market prices.
Where the code goes
- core-api, because the Google token lives there: new
services/core-api/src/catasync/merchant-feed.ts. Reusegfetch,InsightsGoogleError,merchantNeedsRegistration/merchantRegisterGcpfromservices/core-api/src/insights/google.tsand the account lookup inservices/core-api/src/catasync/market.ts(connected account with merchant coverage). - Items come from the engine: new route
GET /v1/feeds/:id/items?cursor=&limit=500returning JSON items rendered with the feed's rows as Google ProductInput attributes (not XML) plus a per-itemhash. Engine tablefeed_sent (feed_id, offer_id, hash, sent_at, PRIMARY KEY (feed_id, offer_id))for change detection (migration017_feed_sent.sql); routePOST /v1/feeds/:id/sentto record results.
Exact behaviour
- Data source: once per feed, create an API primary data source (
POST https://merchantapi.googleapis.com/datasources/v1/accounts/{merchantId}/dataSources,primaryProductDataSourcewithcontentLanguage: "fi",feedLabel: "FI",countries: ["FI"], displayNameCataSync <feed name>). Store its name in the feedsettings.googleDataSource. Never touch other data sources. - Insert/update:
POST https://merchantapi.googleapis.com/products/v1/accounts/{merchantId}/productInputs:insert?dataSource=<name>withofferId(feedg:id, defaultgla_<shop id>so old Google history and the market sync'sgla_<id>mapping keep working),contentLanguage: "fi",feedLabel: "FI",productAttributes(title, description, link, imageLink, additionalImageLinks, availability, availabilityDate for backorder = today + 24 days, price {amountMicros, currencyCode}, salePrice, brand, gtins, mpn, condition, itemGroupId, productTypes, shippingWeight, shipping [{country: "FI", price}]).- Send only items whose hash changed or that were last sent more than 20 days ago (Google expires products after 30 days without an update).
- Pace: max 5 requests/second, retry 429/5xx with backoff, stop the run on 401/403 and record
last_error.
- Delete: items that left the feed (unpublished, filtered out, deleted on the shop):
DELETE .../productInputs/{contentLanguage}~{feedLabel}~{offerId}?dataSource=<name>; remove fromfeed_sent. - Schedule: an OMA worker job (in
services/workers, same pattern as other periodic jobs) every 3 hours per enabled Google feed; plusrebuildfrom the MCP tool triggers a run. - Statuses: daily, read item issues via the Reports API (
product_viewwithid, offer_id, aggregated_reporting_context_status, item_issues) and store counts per feed (approved, pending, disapproved, top issues) for the UI card and MCPget. - Shipping and returns: do not change Merchant Center account shipping settings automatically. Shipping goes per item (
shippingattribute fromshipping_price). Note in the handbook how to set account-level shipping by hand if wanted. - After a run, call the existing market price sync (
syncCatasyncMarketPrices) for that site.
MCP / UI
oma.admin_catasync_feedsgainsaction: "google_sync"(confirm) and Google status fields inget.- Feed card shows: sent, pending, approved, disapproved, top 5 issues with counts, last run, last error.
- Error codes:
google_api_error(already exists, keep status/reason/message),merchant_not_set,google_not_connected.
Acceptance
- Unit tests for attribute mapping (micros, availability date, gallery, shipping) and change detection.
- Live on maxfort first if maxfort has a Merchant Center account; otherwise a dry-run mode (
dryRun: truein the MCP action: builds and validates items, sends nothing) used on niletech first, output reviewed by the operator. - Then niletech real run (operator OK in issue 6), counts verified with the Reports API, and
oma.admin_catasync_market syncreturns many more benchmarks than 2 within a few days. - Handbook section "Google Merchant feed".
Binding rules (read before coding)
- Read
CLAUDE.mdand every.cursor/rules/*.mdcwithalwaysApply: truefirst. They win over your defaults. - No OpenAI / AI calls anywhere in this feature. CataSync AI switch (
ai_enabled) stays off. Never turn it on. - Smallest correct change. Match the surrounding code style (engine = plain ESM
.mjs, mysql2 pool fromsrc/db.mjs; core-api = TypeScript). - Every staff action goes through the admin MCP (
admin-mcp-first.mdc): executor inservices/core-api/src/agent-tools/tools-catasync.ts, confirm handler inservices/core-api/src/platform/confirm.tsfor writes, registry entry, zod schema inservices/mcp-server/src/tools.ts, test intools-catasync.test.ts. The UI calls the same tool (viaapps/web/lib/catasync/admin-actions.ts), never a separate path. - UI: vertical side nav only (never a second tab row), forms in modals, shadow cards, full width (
portal-chrome.mdc). - Every string:
next-intlkey with native en, fi, sv (Finnish must read native, not translated). No em/en dashes in prose. Runnode scripts/i18n-validate.mjs. - Handbook: update
apps/web/content/admin-docs/products/catasync.md(new section) for every behaviour you add. - Secrets (tokens, Google tokens, feed tokens) never in logs, commits, handbook or chat.
- Commits: author ELSAMA only, no Co-authored-by / AI attribution. Push to
mainright after each commit (rebase onorigin/mainfirst). - Deploy: engine with
bash scripts/catasync-engine-roll.sh figaro; OMA only via the trainbash scripts/deploy-quick.sh(never setOMA_DEPLOY_SERVICES, never build on the VM). Check the live SHA contains your commit. - WordPress plugin changes: bump version, pass
scripts/wp-plugin-gate-remote.sh ops/catasync/plugins/lafu-catasync-connectorwith 0 errors 0 warnings + security check (wp-plugin-dod.mdc). Install on maxfort.fi first (test shop), only then niletech. - Never change niletech prices, statuses, shipping rows or the Code Snippets / table-rate plugins as part of this work.
- Clean up test data and temp files when done.