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:
- 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_priceis a product-level reference only.) - Display name updates only if not customized. Sync always writes
original_name; it overwrites the displaynameonly whenname = original_name(i.e. a Cata user has not renamed it). Flag-free — "customized" ⟺name != original_name. - Deactivate = hide, not delete. Provider
is_active=false→visible=false, record retained; re-activating flips it back. - Removed-from-provider = leave as-is. The provider sync runs upsert-only
(
deleteOrphans=false); products dropped from the provider payload are not auto-deleted. - 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 eachexternal_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) tomain_image_urlviaCustomizeItemDisplay. Content-hash naming makes it idempotent (unchanged image → same object; changed image → new object + updatedmain_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_nameheuristic 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,CustomizeItemDisplayindatabase/queries/items.sql. - KDS reference:
kds-management-service/service/UploadService/Controller.go,database/item_sync.go(preserve-main-on-resync upsert). - Related: 0002, 0006.