Skip to content

0007. Product field ownership on re-sync + async image→GCS pipeline

  • Status: Accepted
  • Date: 2026-06-10
  • Deciders: POS Integration team

Context

A POS adapter syncs a provider's catalog into tenantdb.items (keyed by item_code = the provider SKU). Re-syncs happen repeatedly, and merchants keep editing products in the POS (rename, change photo, deactivate) and in Cata's own UI / Menu Manager. Two forces are in tension:

  • The POS is the source of truth for catalog structure (which products exist, their modifiers, bundles, variants).
  • Cata owns merchandising: menu composition, pricing, and display customizations — and those edits must survive a re-sync.

Separately, product images come back as the provider's CDN URL. We do not want to serve customer-facing images off a third-party CDN long-term; we want our own GCS-hosted copy. The sibling kds-management-service already solved this (service/UploadService), and this repo already has the scaffolding: a Cloud Tasks client with goroutine fallback (pkg/cloudtasks, adapter_async_service.go), a GCS client (upload_service.go), and a display-customization column pattern (items.original_name/name, CustomizeItemDisplay).

Decision

We define field ownership on re-sync, and an async image pipeline:

  1. Pricing is Cata-owned. Sync brings product info + images, never prices. The customer-facing price lives in menu_items (Menu Manager); per-outlet price variation is modelled as separate menus published to the relevant outlets — not per-outlet price overrides. (items.base_price is a product-level reference only.)
  2. Display name updates only if not customized. Sync always writes original_name; it overwrites the display name only when name = original_name (i.e. a Cata user has not renamed it). Flag-free — "customized" ⟺ name != original_name.
  3. Deactivate = hide, not delete. Provider is_active=falsevisible=false, record retained; re-activating flips it back.
  4. Removed-from-provider = leave as-is. The provider sync runs upsert-only (deleteOrphans=false); products dropped from the provider payload are not auto-deleted.
  5. Image source-of-truth split. external_image_url = the provider CDN URL (always refreshed by sync); main_image_url = our GCS copy, written asynchronously and preserved across re-syncs. After a product sync we enqueue a per-tenant image-copy job (Cloud Task, goroutine fallback locally) that downloads each external_image_url, uploads it to GCS under a content-hash object name, and writes the relative object path (e.g. {tenant}/items/{sha}.jpg, not a full URL — the app/CDN prepends the base, matching how FE-uploaded images are stored) to main_image_url via CustomizeItemDisplay. Content-hash naming makes it idempotent (unchanged image → same object; changed image → new object + updated main_image_url).

Consequences

Positive - Cata merchandising edits (name, price, image overrides) survive re-syncs. - Provider renames/photo changes/deactivations still flow through. - Images are served from our GCS; the pipeline reuses existing async + GCS scaffolding and mirrors KDS — no new schema migration.

Negative / costs - The name rule is heuristic (name = original_name): a user who renames a product to exactly the provider name is treated as "not customized." - v1 re-downloads each image every sync (idempotent on upload, but bandwidth); a future image_source_url column could skip unchanged externals. - "Removed = leave as-is" can leave stale products until manually cleaned.

Alternatives considered

  • iSeller-authoritative pricing / name — rejected: would clobber Cata merchandising on every re-sync.
  • Per-outlet price overrides (item_store / per-outlet menu_items editing) — deferred in favour of "separate menu per price tier" (simpler, no new UI).
  • Sync images synchronously — rejected: slow, couples catalog sync to image download/upload latency and failures.
  • Provenance flag column for name/image customization — rejected for v1: the name = original_name heuristic avoids a schema migration.

References

  • Plan: implementation-plans/iseller-image-sync-and-resync-strategy.md
  • Reuse: internal/service/adapter_async_service.go, pkg/cloudtasks/client.go, internal/service/upload_service.go, CustomizeItemDisplay in database/queries/items.sql.
  • KDS reference: kds-management-service/service/UploadService/Controller.go, database/item_sync.go (preserve-main-on-resync upsert).
  • Related: 0002, 0006.