Outlet Enrichment
Outlet Enrichment
Section titled “Outlet Enrichment”Outlet Enrichment extends the base Outlet model with 12 additional fields for better classification, geographic precision, and operational grouping. This feature enables more granular reporting, territory management, and integration with Indonesian regional data.
Overview
Section titled “Overview”The enrichment feature adds classification hierarchy (Channel Group → Channel → Account), operational grouping (SubArea), timezone support, alternative GPS coordinates, and hierarchical location data (Province → City → District → Village).
Key Benefits:
- Better Classification — 4-level hierarchy for outlet categorization
- Geographic Precision — Alternative GPS coordinates for venues with poor signal
- Operational Grouping — SubArea for field team territory assignment
- Regional Integration — Links to standard Indonesian administrative divisions
- Timezone Support — Accurate time-based reporting across Indonesia’s timezones
All enrichment fields are additive and backward-compatible. Existing outlets continue to function without enrichment data.
Enrichment Fields on Outlet
Section titled “Enrichment Fields on Outlet”| Field | Type | Description | Nullable | Default |
|---|---|---|---|---|
| channel_group_id | integer | FK to channel_groups | YES | NULL |
| channel_id | integer | FK to channels | YES | NULL |
| account_id | integer | FK to accounts | YES | NULL |
| sub_area_id | integer | FK to sub_areas | YES | NULL |
| timezone | string | Timezone enum: WIB/WITA/WIT | NO | WIB |
| alt_geo_lat | float64 | Alternative latitude | NO | 0 |
| alt_geo_lng | float64 | Alternative longitude | NO | 0 |
| store_village_id | integer | FK to reg_villages | YES | NULL |
| store_district_id | integer | FK to reg_districts | YES | NULL |
| store_city_id | integer | FK to reg_cities | YES | NULL |
| store_province_id | integer | FK to reg_provinces | YES | NULL |
| remark | string | Free-form notes | NO | "" |
Classification Hierarchy
Section titled “Classification Hierarchy”The enrichment introduces a 4-level classification hierarchy for outlet categorization:
ChannelGroup (workspace-scoped) └── Channel (workspace-scoped, FK to ChannelGroup) └── Account (workspace-scoped, FK to Channel)Hierarchy Example:
- ChannelGroup: “Modern Trade”
- Channel: “Supermarket”
- Account: “Carrefour”
SubArea operates independently (no hierarchy) — used for operational grouping such as “Jakarta Central”, “Surabaya North”, or “Bandung Metro”.
Use Cases
Section titled “Use Cases”| Scenario | Classification Path |
|---|---|
| Traditional retail | Traditional Trade → Warung → Independent |
| Modern retail | Modern Trade → Supermarket → Carrefour |
| Pharmacy chain | Modern Trade → Pharmacy → Guardian |
| HoReCa | HoReCa → Restaurant → Local Chain |
| Wholesaler | Wholesale → Distributor → Regional |
Regional Location Integration
Section titled “Regional Location Integration”The store_province_id, store_city_id, store_district_id, and store_village_id fields reference the Indonesian regional database (merq-indonesian-regional service).
Architecture
Section titled “Architecture”sequenceDiagram
participant Client
participant Backend
participant Regional
participant DB
Client->>Backend: GET /office/v1/outlets/123
Backend->>DB: Query outlet with FKs
DB-->>Backend: Outlet with regional IDs
Backend->>Regional: GET /regional/provinces/31
Regional-->>Backend: Province: DKI Jakarta
Backend->>Regional: GET /regional/cities/3171
Regional-->>Backend: City: Jakarta Pusat
Backend-->>Client: Outlet with nested regional objects
Key Design Decisions:
- Separate Service — Regional data (83k+ villages) runs as independent HTTP service
- No Cross-DB FKs — Regional IDs stored as plain integers in outlets table
- HTTP Proxy — Backend proxies requests via
internal/platform/regional/client.go - Name Resolution — Happens at API response time (not DB join)
- Graceful Degradation — If regional service unreachable, returns empty names
Configuration
Section titled “Configuration”Set the regional service URL in backend environment:
REGIONAL_SERVICE_URL=http://merq-indonesian-regional:8081Regional Hierarchy
Section titled “Regional Hierarchy”Indonesian administrative divisions follow a 4-level hierarchy:
Province (provinsi) └── City (kota/kabupaten) └── District (kecamatan) └── Village (kelurahan/desa)Example:
- Province: DKI Jakarta (code: 31)
- City: Jakarta Pusat (code: 3171)
- District: Menteng (code: 317104)
- Village: Menteng (code: 3171041001)
See Regional Service API for detailed endpoint documentation.
API Changes
Section titled “API Changes”No new endpoints — enrichment fields are integrated into existing outlet endpoints.
Modified Endpoints
Section titled “Modified Endpoints”| Endpoint | Change |
|---|---|
GET /office/v1/outlets | Response includes enrichment fields + nested objects (channel_group, channel, account, sub_area, store_province, etc.) |
POST /office/v1/outlets | Accepts all 12 new fields in request body |
PUT /office/v1/outlets/:id | Accepts all 12 new fields in request body |
GET /app/v1/outlets | Mobile read-only access to enrichment fields |
Request Body Example
Section titled “Request Body Example”{ "name": "Carrefour Sudirman", "address": "Jl. Jend. Sudirman No. 123", "channel_group_id": 1, "channel_id": 5, "account_id": 10, "sub_area_id": 3, "timezone": "WIB", "alt_geo_lat": -6.2088, "alt_geo_lng": 106.8456, "store_province_id": 31, "store_city_id": 3171, "store_district_id": 317104, "store_village_id": 3171041001, "remark": "High-traffic location, peak hours 12:00-14:00"}Response Example
Section titled “Response Example”{ "success": true, "message": "Outlet retrieved", "code": "OUTLET_DETAIL", "data": { "id": 123, "name": "Carrefour Sudirman", "address": "Jl. Jend. Sudirman No. 123", "channel_group": { "id": 1, "name": "Modern Trade" }, "channel": { "id": 5, "name": "Supermarket" }, "account": { "id": 10, "name": "Carrefour" }, "sub_area": { "id": 3, "name": "Jakarta Central" }, "timezone": "WIB", "alt_geo_lat": -6.2088, "alt_geo_lng": 106.8456, "store_province": { "id": 31, "name": "DKI Jakarta", "code": "31" }, "store_city": { "id": 3171, "name": "Jakarta Pusat", "code": "3171" }, "store_district": { "id": 317104, "name": "Menteng", "code": "317104" }, "store_village": { "id": 3171041001, "name": "Menteng", "code": "3171041001" }, "remark": "High-traffic location, peak hours 12:00-14:00", "created_at": "2026-03-04T12:00:00Z", "updated_at": "2026-03-05T08:30:00Z" }}Filter Parameters
Section titled “Filter Parameters”List endpoint accepts additional filter params:
| Parameter | Type | Description |
|---|---|---|
channel_group | integer | Filter by channel group ID |
channel | integer | Filter by channel ID |
account | integer | Filter by account ID |
sub_area | integer | Filter by sub area ID |
store_province_id | integer | Filter by store province ID |
store_city_id | integer | Filter by store city ID |
Example Query:
GET /office/v1/outlets?channel_group=1&store_province_id=31&page=1&limit=20Migration
Section titled “Migration”The migration 20260304120000_outlet_enrichment.sql adds:
New Tables:
channel_groups— Top-level classificationchannels— Mid-level classification (FK to channel_groups)accounts— Leaf-level classification (FK to channels)sub_areas— Operational grouping
Outlet Table Changes:
- 12 new columns (see table above)
- Indexes on all FK columns for query performance
- Unique indexes on
(name, workspace_id)for each master table
Indexes Created:
CREATE INDEX idx_outlets_channel_group_id ON outlets (channel_group_id);CREATE INDEX idx_outlets_channel_id ON outlets (channel_id);CREATE INDEX idx_outlets_account_id ON outlets (account_id);CREATE INDEX idx_outlets_sub_area_id ON outlets (sub_area_id);CREATE INDEX idx_outlets_store_province_id ON outlets (store_province_id);CREATE INDEX idx_outlets_store_city_id ON outlets (store_city_id);To run the migration:
go run cmd/migrate/main.goRelated Master Data
Section titled “Related Master Data”Four new workspace-scoped master entities were created:
| Entity | Endpoint | Permission Key |
|---|---|---|
| ChannelGroup | /office/v1/outlets/channel-groups | channel_group.manage |
| Channel | /office/v1/outlets/channels | channel.manage |
| Account | /office/v1/outlets/accounts | account.manage |
| SubArea | /office/v1/outlets/sub-areas | sub_area.manage |
Each entity supports full CRUD operations:
- List:
GET /office/v1/outlets/{entity}(paginated, workspace-scoped) - Detail:
GET /office/v1/outlets/{entity}/:id - Create:
POST /office/v1/outlets/{entity}(requires full access permission) - Update:
PUT /office/v1/outlets/{entity}/:id(requires full access permission) - Delete:
DELETE /office/v1/outlets/{entity}/:id(requires full access permission)
Parent-Delete Guards
Section titled “Parent-Delete Guards”Hierarchy integrity is enforced:
- Cannot delete a
ChannelGroupif it has associatedChannels - Cannot delete a
Channelif it has associatedAccounts - Attempt returns 409 Conflict error
See Master Data CRUD for detailed documentation.
Workspace Scoping
Section titled “Workspace Scoping”All 4 master tables (channel_groups, channels, accounts, sub_areas) are workspace-scoped:
workspace_id BIGINT NOT NULLImplications:
workspace_idextracted from auth context on every request- All queries automatically filtered by workspace
- Master data isolated per tenant
- Cross-workspace references impossible
Example Service Layer:
func (s *channelService) workspaceID(ctx context.Context) (uint, error) { auth, err := utils.GetAuthContextValue(ctx) if err != nil || auth == nil { return 0, customerror.NewUnauthorized("unauthorized", err) } return auth.UserWorkspaceID, nil}Timezone Enum
Section titled “Timezone Enum”The timezone field accepts one of three Indonesian timezone values:
| Value | UTC Offset | Regions |
|---|---|---|
| WIB | UTC+7 | Sumatra, Java, West/Central Kalimantan |
| WITA | UTC+8 | Bali, Nusa Tenggara, South/East Kalimantan, Sulawesi |
| WIT | UTC+9 | Maluku, Papua |
Default: WIB (most common for Java-based operations)
Alternative GPS Coordinates
Section titled “Alternative GPS Coordinates”The alt_geo_lat and alt_geo_lng fields provide fallback coordinates when primary GPS (geo_lat/geo_lng) is unavailable or inaccurate.
Use Cases:
- Indoor venues with poor GPS signal (malls, airports)
- Multi-building complexes (use main entrance coordinates)
- Manual coordinate entry for legacy outlets
Defaults: 0, 0 (indicates not set)
Related Topics
Section titled “Related Topics”- Master Data CRUD — Channel Group, Channel, Account, SubArea management
- Regional Service API — Indonesian regional data integration
- Outlets User Guide — How to manage outlets with enrichment
- Workspace Scoping — Multi-tenant data isolation
- API Reference — Complete endpoint documentation