LAFU Sales Reports

CataSync feeds 5/6: Google Merchant Center destination (Merchant API v1)

FeatureOpenView on roadmap· 12 days ago

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. Reuse gfetch, InsightsGoogleError, merchantNeedsRegistration / merchantRegisterGcp from services/core-api/src/insights/google.ts and the account lookup in services/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=500 returning JSON items rendered with the feed's rows as Google ProductInput attributes (not XML) plus a per-item hash. Engine table feed_sent (feed_id, offer_id, hash, sent_at, PRIMARY KEY (feed_id, offer_id)) for change detection (migration 017_feed_sent.sql); route POST /v1/feeds/:id/sent to record results.

Exact behaviour

  1. Data source: once per feed, create an API primary data source (POST https://merchantapi.googleapis.com/datasources/v1/accounts/{merchantId}/dataSources, primaryProductDataSource with contentLanguage: "fi", feedLabel: "FI", countries: ["FI"], displayName CataSync <feed name>). Store its name in the feed settings.googleDataSource. Never touch other data sources.
  2. Insert/update: POST https://merchantapi.googleapis.com/products/v1/accounts/{merchantId}/productInputs:insert?dataSource=<name> with offerId (feed g:id, default gla_<shop id> so old Google history and the market sync's gla_<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.
  3. Delete: items that left the feed (unpublished, filtered out, deleted on the shop): DELETE .../productInputs/{contentLanguage}~{feedLabel}~{offerId}?dataSource=<name>; remove from feed_sent.
  4. Schedule: an OMA worker job (in services/workers, same pattern as other periodic jobs) every 3 hours per enabled Google feed; plus rebuild from the MCP tool triggers a run.
  5. Statuses: daily, read item issues via the Reports API (product_view with id, offer_id, aggregated_reporting_context_status, item_issues) and store counts per feed (approved, pending, disapproved, top issues) for the UI card and MCP get.
  6. Shipping and returns: do not change Merchant Center account shipping settings automatically. Shipping goes per item (shipping attribute from shipping_price). Note in the handbook how to set account-level shipping by hand if wanted.
  7. After a run, call the existing market price sync (syncCatasyncMarketPrices) for that site.

MCP / UI

  • oma.admin_catasync_feeds gains action: "google_sync" (confirm) and Google status fields in get.
  • 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: true in 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 sync returns many more benchmarks than 2 within a few days.
  • Handbook section "Google Merchant feed".

Binding rules (read before coding)

  • Read CLAUDE.md and every .cursor/rules/*.mdc with alwaysApply: true first. 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 from src/db.mjs; core-api = TypeScript).
  • Every staff action goes through the admin MCP (admin-mcp-first.mdc): executor in services/core-api/src/agent-tools/tools-catasync.ts, confirm handler in services/core-api/src/platform/confirm.ts for writes, registry entry, zod schema in services/mcp-server/src/tools.ts, test in tools-catasync.test.ts. The UI calls the same tool (via apps/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-intl key with native en, fi, sv (Finnish must read native, not translated). No em/en dashes in prose. Run node 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 main right after each commit (rebase on origin/main first).
  • Deploy: engine with bash scripts/catasync-engine-roll.sh figaro; OMA only via the train bash scripts/deploy-quick.sh (never set OMA_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-connector with 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.

Comments (0)

No comments yet.

Join the discussion

Sign in to leave a comment on this thread.

HollahoopGet yours