Skip to content

Repo-Specific Refactor Blueprint

This document maps the target commerce architecture onto the repos that already exist in the workspace. It is intentionally pragmatic: it starts from the code that is already there and shows what each repo should own, what should move, and what should become a read model instead of a source of truth.

Current Ownership

catalog-service

Owns today:

  • catalog CRUD
  • search
  • promotions and coupons
  • import and export
  • ingestion routes
  • inventory mutations
  • inventory event publishing and consumption

Implication:

  • this repo is already the best place to anchor canonical product metadata
  • it is also the current inventory owner, even though some schema and route layers are inconsistent between SQLite-era code and newer Postgres migrations

store-hub

Owns today:

  • stores
  • terminals
  • staff
  • customers
  • locations
  • reports
  • dark store operations
  • dark store orders and ETA logic

Implication:

  • this repo should remain the owner of operational store and dark-store state
  • it should not be the canonical owner of stock truth or pricing truth

api-gateway

Owns today:

  • auth and tenant middleware
  • rate limiting
  • proxy routing
  • upstream health checks

Implication:

  • this repo is a secured edge and routing layer
  • it should gain a small number of composed BFF routes, but should not absorb domain logic that belongs in business services

sync-service

Owns today:

  • terminal registration
  • push and pull sync
  • conflict handling
  • queue processing for offline changes
  • consumption of catalog.events

Implication:

  • this repo should stay focused on terminal and offline replication
  • it should not become the owner of catalog or inventory business state

retail-agent

Owns today:

  • auth
  • releases
  • incidents
  • webhooks
  • billing integration
  • catalog enrichment
  • terminal management placeholder APIs

Implication:

  • this repo is a control plane
  • LLM-powered enrichment belongs here
  • terminal source-of-truth should still live elsewhere

Target Ownership

catalog-service should own

  • canonical product metadata
  • canonical variant metadata
  • source-system mappings
  • canonical prices
  • canonical per-location stock positions
  • stock movement ledger
  • inventory reservations
  • ingestion normalization
  • catalog and inventory domain events

store-hub should own

  • stores
  • locations
  • terminals assigned to stores
  • staff and shifts
  • dark-store metadata
  • dark-store zones
  • dark-store pickers
  • dark-store order workflow
  • dark-store metrics
  • dark-store assortment and inventory projections for fast local reads

api-gateway should own

  • authentication and policy enforcement
  • route proxying
  • a thin BFF for store-scoped catalog views

sync-service should own

  • terminal sync sessions
  • offline queues
  • conflict policies
  • replay and reconciliation

retail-agent should own

  • AI enrichment
  • release control
  • incident control
  • outbound webhook delivery
  • control-plane automation

Keep, Migrate, Deprecate

Keep in catalog-service

  • categories
  • products
  • variants
  • search index maintenance
  • ingestion routes and normalization pipeline

Add to catalog-service

  • source-system tables
  • version history tables
  • normalized price tables
  • canonical stock ledger tables
  • reservation tables
  • orderability projection tables

Deprecate in catalog-service

  • SQLite-style inventory tables as the long-term truth model
  • SQLite-style inventory_adjustments as the final ledger model
  • JSON price_lists.entries as the primary price storage model

Keep in store-hub

  • stores
  • locations
  • terminals
  • dark_stores
  • dark_store_zones
  • dark_store_pickers
  • dark_store_orders
  • dark_store_order_items
  • dark_store_metrics

Deprecate as authoritative in store-hub

  • inventory_by_location
  • dark_store_skus.quantity_on_hand
  • dark_store_skus.unit_price
  • dark_store_skus.unit_cost

These can remain temporarily as projections while downstream code is migrated.

Canonical Schema

1. Catalog Core in catalog-service

Keep existing categories, products, and variants from the Postgres schema, then add the following tables.

CREATE TABLE IF NOT EXISTS source_systems (
  id                UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id         UUID NOT NULL,
  source_type       VARCHAR(40) NOT NULL,
  name              VARCHAR(120) NOT NULL,
  vendor            VARCHAR(80),
  auth_config       JSONB NOT NULL DEFAULT '{}',
  webhook_secret    TEXT,
  is_active         BOOLEAN NOT NULL DEFAULT TRUE,
  created_at        TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_at        TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  UNIQUE (tenant_id, name)
);

CREATE TABLE IF NOT EXISTS source_entities (
  id                UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id         UUID NOT NULL,
  source_system_id  UUID NOT NULL REFERENCES source_systems(id) ON DELETE CASCADE,
  entity_type       VARCHAR(40) NOT NULL,
  external_id       VARCHAR(200) NOT NULL,
  canonical_type    VARCHAR(40) NOT NULL,
  canonical_id      UUID NOT NULL,
  metadata          JSONB NOT NULL DEFAULT '{}',
  created_at        TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_at        TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  UNIQUE (tenant_id, source_system_id, entity_type, external_id)
);

CREATE TABLE IF NOT EXISTS catalog_ingestion_runs (
  id                UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id         UUID NOT NULL,
  source_system_id  UUID REFERENCES source_systems(id) ON DELETE SET NULL,
  ingestion_type    VARCHAR(40) NOT NULL,
  status            VARCHAR(30) NOT NULL,
  received_count    INTEGER NOT NULL DEFAULT 0,
  success_count     INTEGER NOT NULL DEFAULT 0,
  failed_count      INTEGER NOT NULL DEFAULT 0,
  error_summary     JSONB NOT NULL DEFAULT '[]',
  started_at        TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  completed_at      TIMESTAMPTZ
);

CREATE TABLE IF NOT EXISTS catalog_item_versions (
  id                UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id         UUID NOT NULL,
  variant_id        UUID NOT NULL REFERENCES variants(id) ON DELETE CASCADE,
  version_no        INTEGER NOT NULL,
  source_system_id  UUID REFERENCES source_systems(id) ON DELETE SET NULL,
  payload           JSONB NOT NULL,
  normalized_hash   TEXT NOT NULL,
  created_at        TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  UNIQUE (tenant_id, variant_id, version_no)
);

CREATE TABLE IF NOT EXISTS variant_prices (
  id                UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id         UUID NOT NULL,
  variant_id        UUID NOT NULL REFERENCES variants(id) ON DELETE CASCADE,
  location_id       UUID,
  channel           VARCHAR(40) NOT NULL DEFAULT 'default',
  currency          CHAR(3) NOT NULL DEFAULT 'INR',
  mrp               NUMERIC(12,2),
  base_price        NUMERIC(12,2) NOT NULL,
  tax_rate          NUMERIC(6,2) NOT NULL DEFAULT 0,
  effective_from    TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  effective_to      TIMESTAMPTZ,
  source_system_id  UUID REFERENCES source_systems(id) ON DELETE SET NULL,
  created_at        TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_variant_prices_lookup
  ON variant_prices (tenant_id, variant_id, location_id, channel, effective_from DESC);

Notes:

  • keep price_lists only as an optional business construct
  • make variant_prices the operational price truth
  • source mappings are mandatory if POS, ERP, seller, and dark-store feeds all need to resolve to one canonical variant

2. Inventory Core in catalog-service

Use location-aware inventory here as the canonical truth, even if it later becomes a dedicated inventory-service.

CREATE TABLE IF NOT EXISTS stock_positions (
  id                UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id         UUID NOT NULL,
  variant_id        UUID NOT NULL REFERENCES variants(id) ON DELETE CASCADE,
  location_id       UUID NOT NULL,
  quantity_on_hand  NUMERIC(18,4) NOT NULL DEFAULT 0,
  quantity_reserved NUMERIC(18,4) NOT NULL DEFAULT 0,
  quantity_available NUMERIC(18,4) GENERATED ALWAYS AS
    (quantity_on_hand - quantity_reserved) STORED,
  reorder_point     NUMERIC(18,4) NOT NULL DEFAULT 0,
  max_stock         NUMERIC(18,4),
  updated_at        TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  UNIQUE (tenant_id, variant_id, location_id)
);

CREATE TABLE IF NOT EXISTS stock_movements (
  id                UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id         UUID NOT NULL,
  variant_id        UUID NOT NULL REFERENCES variants(id) ON DELETE CASCADE,
  location_id       UUID NOT NULL,
  movement_type     VARCHAR(40) NOT NULL,
  quantity_delta    NUMERIC(18,4) NOT NULL,
  reference_type    VARCHAR(40),
  reference_id      VARCHAR(120),
  source_system_id  UUID REFERENCES source_systems(id) ON DELETE SET NULL,
  occurred_at       TIMESTAMPTZ NOT NULL,
  metadata          JSONB NOT NULL DEFAULT '{}',
  created_at        TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE IF NOT EXISTS stock_reservations (
  id                UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id         UUID NOT NULL,
  variant_id        UUID NOT NULL REFERENCES variants(id) ON DELETE CASCADE,
  location_id       UUID NOT NULL,
  order_id          VARCHAR(120) NOT NULL,
  reserved_qty      NUMERIC(18,4) NOT NULL,
  status            VARCHAR(20) NOT NULL DEFAULT 'active',
  expires_at        TIMESTAMPTZ,
  created_at        TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_at        TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  UNIQUE (tenant_id, location_id, variant_id, order_id)
);

CREATE TABLE IF NOT EXISTS orderability_projection (
  tenant_id         UUID NOT NULL,
  variant_id        UUID NOT NULL REFERENCES variants(id) ON DELETE CASCADE,
  location_id       UUID NOT NULL,
  is_orderable      BOOLEAN NOT NULL,
  reason_code       VARCHAR(40),
  available_qty     NUMERIC(18,4) NOT NULL DEFAULT 0,
  updated_at        TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  PRIMARY KEY (tenant_id, variant_id, location_id)
);

Notes:

  • stock_movements becomes the durable ledger
  • stock_positions is the current aggregate
  • stock_reservations prevents overselling
  • orderability_projection is what the UI should read for fast in-stock decisions

3. Store and Dark Store Operations in store-hub

Keep the existing operational tables:

  • stores
  • locations
  • dark_stores
  • dark_store_zones
  • dark_store_pickers
  • dark_store_orders
  • dark_store_order_items
  • dark_store_metrics

Replace stock truth in store-hub with a projection-oriented table:

CREATE TABLE IF NOT EXISTS dark_store_catalog_projection (
  tenant_id            UUID NOT NULL,
  dark_store_id        UUID NOT NULL REFERENCES dark_stores(id) ON DELETE CASCADE,
  variant_id           UUID NOT NULL,
  sku                  VARCHAR(100) NOT NULL,
  product_name         VARCHAR(300) NOT NULL,
  category_name        VARCHAR(200),
  is_active            BOOLEAN NOT NULL DEFAULT TRUE,
  is_essential         BOOLEAN NOT NULL DEFAULT FALSE,
  demand_score         NUMERIC(8,4) NOT NULL DEFAULT 0,
  velocity_rank        INTEGER,
  quantity_available   NUMERIC(18,4) NOT NULL DEFAULT 0,
  effective_price      NUMERIC(12,2) NOT NULL DEFAULT 0,
  orderable            BOOLEAN NOT NULL DEFAULT TRUE,
  source_event_id      UUID,
  refreshed_at         TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  PRIMARY KEY (tenant_id, dark_store_id, variant_id)
);

CREATE INDEX IF NOT EXISTS idx_ds_catalog_projection_sku
  ON dark_store_catalog_projection (tenant_id, dark_store_id, sku);

Notes:

  • this table is a read model only
  • it replaces dark_store_skus as the table the UI reads
  • quantity_available and effective_price are copied from canonical events, not edited directly in store-hub

4. Optional Restaurant Menu Extension in catalog-service

If food delivery is a near-term target, add the following instead of creating a separate repo immediately:

CREATE TABLE IF NOT EXISTS restaurant_menus (
  id                UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id         UUID NOT NULL,
  restaurant_id     UUID NOT NULL,
  menu_version      INTEGER NOT NULL,
  status            VARCHAR(20) NOT NULL DEFAULT 'active',
  source_system_id  UUID REFERENCES source_systems(id) ON DELETE SET NULL,
  created_at        TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  UNIQUE (tenant_id, restaurant_id, menu_version)
);

CREATE TABLE IF NOT EXISTS menu_items (
  id                UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id         UUID NOT NULL,
  menu_id           UUID NOT NULL REFERENCES restaurant_menus(id) ON DELETE CASCADE,
  external_item_id  VARCHAR(200),
  name              VARCHAR(300) NOT NULL,
  description       TEXT,
  price             NUMERIC(12,2) NOT NULL,
  tax_rate          NUMERIC(6,2) NOT NULL DEFAULT 0,
  veg_flag          BOOLEAN,
  available         BOOLEAN NOT NULL DEFAULT TRUE,
  metadata          JSONB NOT NULL DEFAULT '{}',
  created_at        TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_at        TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

Event Contracts

Standard Envelope

All domain events should use one envelope shape across catalog-service, store-hub, sync-service, and retail-agent.

{
  "event_id": "1ef9b44b-2d9f-4a2c-a8b4-1f4e6b3f9ad6",
  "event_type": "inventory.stock.adjusted",
  "source": "catalog-service",
  "tenant_id": "9c6a3381-7c14-4ed4-9ca9-4fd0e64f0f28",
  "occurred_at": "2026-03-25T10:45:12.000Z",
  "trace_id": "req_01HT...",
  "schema_version": 1,
  "data": {}
}

Events catalog-service should publish

  • catalog.product.upserted
  • catalog.variant.upserted
  • catalog.variant.deleted
  • catalog.price.updated
  • catalog.assortment.updated
  • catalog.ingestion.completed
  • inventory.stock.adjusted
  • inventory.stock.reserved
  • inventory.stock.released
  • inventory.stock.committed
  • inventory.orderability.changed

Example:

{
  "event_type": "catalog.variant.upserted",
  "source": "catalog-service",
  "tenant_id": "tenant-uuid",
  "schema_version": 1,
  "data": {
    "variant_id": "variant-uuid",
    "product_id": "product-uuid",
    "sku": "AMUL-BUTTER-500G",
    "name": "Amul Butter 500g",
    "barcode": "8901262150101",
    "category_id": "category-uuid",
    "attributes": {
      "pack_size": "500 g"
    },
    "source_system_id": "source-uuid",
    "version_no": 12
  }
}
{
  "event_type": "inventory.stock.adjusted",
  "source": "catalog-service",
  "tenant_id": "tenant-uuid",
  "schema_version": 1,
  "data": {
    "movement_id": "movement-uuid",
    "variant_id": "variant-uuid",
    "location_id": "location-uuid",
    "movement_type": "sale_committed",
    "quantity_delta": -2,
    "quantity_on_hand": 14,
    "quantity_reserved": 0,
    "quantity_available": 14,
    "reference_type": "order",
    "reference_id": "ORD-10024"
  }
}

Events store-hub should publish

  • store.location.created
  • store.location.updated
  • store.dark_store.created
  • store.dark_store.status.changed
  • store.dark_store.capacity.changed
  • store.dark_store.order.created
  • store.dark_store.order.status.changed

Example:

{
  "event_type": "store.dark_store.status.changed",
  "source": "store-hub",
  "tenant_id": "tenant-uuid",
  "schema_version": 1,
  "data": {
    "dark_store_id": "dark-store-uuid",
    "store_id": "store-uuid",
    "status": "maintenance",
    "is_active": false,
    "reason": "inventory_audit"
  }
}

Events retail-agent should publish

  • catalog.enrichment.completed
  • catalog.enrichment.failed
  • incident.created
  • incident.resolved
  • release.deployed

These should enrich the commerce domain but never become the commerce domain source of truth.

Gateway Changes

Keep direct proxying for most routes. Add only a few composed routes in api-gateway.

New BFF reads

  • GET /api/v1/stores/:storeId/catalog-view
  • GET /api/v1/dark-stores/:darkStoreId/catalog-view
  • GET /api/v1/stores/:storeId/search
  • GET /api/v1/restaurants/:restaurantId/menu

BFF behavior

/catalog-view should compose:

  • store or dark-store status from store-hub
  • canonical catalog from catalog-service
  • orderability from catalog-service
  • effective pricing from catalog-service
  • optional assortment projection from store-hub

The gateway should not perform synchronous fan-out per item. It should call one upstream endpoint that already returns a store-scoped view, or call two coarse read endpoints and merge once.

Sync-Service Changes

Align sync-service with the actual current service graph.

Required changes

  • replace product-service endpoint assumptions with catalog-service
  • replace inventory-service endpoint assumptions with catalog-service
  • keep customer-facing sync flows pointed at store-hub or the eventual owner
  • continue consuming catalog.events, but upgrade handlers for new event names

Updated mapping

  • product -> catalog-service
  • inventory_adjustment -> catalog-service
  • customer -> store-hub
  • sale -> store order or transaction service
  • payment -> billing or payment service

Repo-by-Repo Implementation Plan

Slice 1: Make inventory ownership explicit

In catalog-service:

  • add stock_positions
  • add stock_movements
  • add stock_reservations
  • publish inventory.stock.adjusted
  • publish inventory.orderability.changed

In store-hub:

  • stop writing stock truth to inventory_by_location
  • stop treating dark_store_skus.quantity_on_hand as authoritative
  • start consuming inventory events into a projection table

Slice 2: Normalize prices and source mappings

In catalog-service:

  • add source_systems
  • add source_entities
  • add catalog_item_versions
  • add variant_prices
  • move ingestion pipeline persistence to canonical IDs instead of loose SKU-only writes

Slice 3: Introduce composed read models

In catalog-service:

  • expose GET /internal/store-catalog-view?location_id=...

In store-hub:

  • expose GET /internal/dark-store-status/:id

In api-gateway:

  • add BFF route for catalog-view

Slice 4: Restaurant menu vertical

In catalog-service:

  • add menu tables
  • add POS menu adapters
  • publish menu.item.upserted
  • publish restaurant.status.changed

If you later split inventory into its own repo, the target boundary should be:

  • catalog-service: products, variants, pricing, ingestion, source mappings
  • inventory-service: stock, reservations, orderability
  • store-hub: stores, dark stores, fulfillment workflows
  • api-gateway: edge and thin BFF
  • sync-service: offline replication
  • retail-agent: enrichment and control plane

Until then, the lowest-risk path is:

  1. keep catalog and inventory together in catalog-service
  2. turn store-hub stock tables into projections
  3. standardize events
  4. add one store-scoped catalog read path