Skip to content

Master Data CRUD

Master Data entities provide the classification hierarchy for outlets in the Merq platform. These four workspace-scoped entities enable organizations to structure their sales channels and geographic coverage:

  • ChannelGroup: Top-level classification (e.g., “Modern Trade”, “Traditional Trade”, “Digital”)
  • Channel: Specific channel types within a group (e.g., “Supermarket”, “Minimarket”, “Convenience Store”)
  • Account: Business entities within a channel (e.g., “Indomaret”, “Carrefour”, “Alfamart”)
  • SubArea: Geographic subdivisions for filtering and reporting (e.g., “Jakarta South”, “Surabaya Center”)

All four entities are workspace-scoped, meaning data is isolated per workspace. Cross-workspace access is prevented at the query level.

graph TD
    CG[ChannelGroup] --> C[Channel]
    C --> A[Account]
    SA[SubArea] -.independent.-> CG
    
    style CG fill:#e1f5fe
    style C fill:#e1f5fe
    style A fill:#e1f5fe
    style SA fill:#fff3e0

The hierarchy follows a strict parent-child relationship:

  • ChannelGroupChannelAccount (hierarchical, parent-delete guards enforced)
  • SubArea (independent, no parent/children relationships)
FieldTypeDescriptionConstraints
idintegerPrimary keyAuto-increment
workspace_idintegerFK to workspacesNOT NULL, indexed
namestringGroup nameVARCHAR(255), NOT NULL, unique per workspace
created_attimestampCreation timestampTIMESTAMPTZ
updated_attimestampLast update timestampTIMESTAMPTZ
deleted_attimestampSoft delete timestampTIMESTAMPTZ (nullable)
FieldTypeDescriptionConstraints
idintegerPrimary keyAuto-increment
workspace_idintegerFK to workspacesNOT NULL, indexed
namestringChannel nameVARCHAR(255), NOT NULL
channel_group_idintegerFK to channel_groupsNOT NULL, indexed
created_attimestampCreation timestampTIMESTAMPTZ
updated_attimestampLast update timestampTIMESTAMPTZ
deleted_attimestampSoft delete timestampTIMESTAMPTZ (nullable)
FieldTypeDescriptionConstraints
idintegerPrimary keyAuto-increment
workspace_idintegerFK to workspacesNOT NULL, indexed
namestringAccount nameVARCHAR(255), NOT NULL
channel_idintegerFK to channelsNOT NULL, indexed
created_attimestampCreation timestampTIMESTAMPTZ
updated_attimestampLast update timestampTIMESTAMPTZ
deleted_attimestampSoft delete timestampTIMESTAMPTZ (nullable)
FieldTypeDescriptionConstraints
idintegerPrimary keyAuto-increment
workspace_idintegerFK to workspacesNOT NULL, indexed
namestringSub area nameVARCHAR(255), NOT NULL, unique per workspace
created_attimestampCreation timestampTIMESTAMPTZ
updated_attimestampLast update timestampTIMESTAMPTZ
deleted_attimestampSoft delete timestampTIMESTAMPTZ (nullable)
MethodEndpointPermissionDescription
GET/office/v1/outlets/channel-groupschannel_group.view or channel_group.manageList (paginated)
GET/office/v1/outlets/channel-groups/:idchannel_group.view or channel_group.manageGet by ID
POST/office/v1/outlets/channel-groupschannel_group.manage (full)Create
PUT/office/v1/outlets/channel-groups/:idchannel_group.manage (full)Update
DELETE/office/v1/outlets/channel-groups/:idchannel_group.manage (full)Delete (soft)
MethodEndpointPermissionDescription
GET/office/v1/outlets/channelschannel.view or channel.manageList (paginated, filter by channel_group_id)
GET/office/v1/outlets/channels/:idchannel.view or channel.manageGet by ID
POST/office/v1/outlets/channelschannel.manage (full)Create
PUT/office/v1/outlets/channels/:idchannel.manage (full)Update
DELETE/office/v1/outlets/channels/:idchannel.manage (full)Delete (soft)
MethodEndpointPermissionDescription
GET/office/v1/outlets/accountsaccount.view or account.manageList (paginated, filter by channel_id)
GET/office/v1/outlets/accounts/:idaccount.view or account.manageGet by ID
POST/office/v1/outlets/accountsaccount.manage (full)Create
PUT/office/v1/outlets/accounts/:idaccount.manage (full)Update
DELETE/office/v1/outlets/accounts/:idaccount.manage (full)Delete (soft)
MethodEndpointPermissionDescription
GET/office/v1/outlets/sub-areassub_area.view or sub_area.manageList (paginated)
GET/office/v1/outlets/sub-areas/:idsub_area.view or sub_area.manageGet by ID
POST/office/v1/outlets/sub-areassub_area.manage (full)Create
PUT/office/v1/outlets/sub-areas/:idsub_area.manage (full)Update
DELETE/office/v1/outlets/sub-areas/:idsub_area.manage (full)Delete (soft)

Note: All entities also expose read-only GET endpoints under /app/v1/outlets/ for mobile app consumption.

POST /office/v1/outlets/channel-groups
Content-Type: application/json
Authorization: Bearer <token>
{
"name": "Modern Trade"
}
{
"data": {
"id": 1,
"name": "Modern Trade",
"workspace_id": 1,
"created_at": "2026-03-04T00:00:00Z",
"updated_at": "2026-03-04T00:00:00Z"
},
"message": "channel group created",
"code": "CHANNEL_GROUP_CREATED"
}
POST /office/v1/outlets/channels
Content-Type: application/json
Authorization: Bearer <token>
{
"name": "Supermarket",
"channel_group_id": 1
}
{
"data": {
"id": 5,
"name": "Supermarket",
"channel_group_id": 1,
"channel_group": {
"id": 1,
"name": "Modern Trade"
},
"workspace_id": 1,
"created_at": "2026-03-04T00:00:00Z",
"updated_at": "2026-03-04T00:00:00Z"
},
"message": "channel created",
"code": "CHANNEL_CREATED"
}
POST /office/v1/outlets/accounts
Content-Type: application/json
Authorization: Bearer <token>
{
"name": "Carrefour",
"channel_id": 5
}
{
"data": {
"id": 12,
"name": "Carrefour",
"channel_id": 5,
"channel": {
"id": 5,
"name": "Supermarket",
"created_at": "2026-03-04T00:00:00Z",
"updated_at": "2026-03-04T00:00:00Z"
},
"workspace_id": 1,
"created_at": "2026-03-04T00:00:00Z",
"updated_at": "2026-03-04T00:00:00Z"
},
"message": "account created",
"code": "ACCOUNT_CREATED"
}
POST /office/v1/outlets/sub-areas
Content-Type: application/json
Authorization: Bearer <token>
{
"name": "Jakarta South"
}
{
"data": {
"id": 3,
"name": "Jakarta South",
"workspace_id": 1,
"created_at": "2026-03-04T00:00:00Z",
"updated_at": "2026-03-04T00:00:00Z"
},
"message": "sub area created",
"code": "SUB_AREA_CREATED"
}

To prevent orphaned records, the API enforces referential integrity on hierarchical entities:

ChannelGroup:

  • Cannot delete if Channels exist with this channel_group_id
  • Returns 409 Conflict error with error code CHANNEL_GROUP_HAS_CHILDREN

Channel:

  • Cannot delete if Accounts exist with this channel_id
  • Returns 409 Conflict error with error code CHANNEL_HAS_CHILDREN

Example Error Response:

{
"success": false,
"message": "Cannot delete channel group with existing channels",
"code": "CHANNEL_GROUP_HAS_CHILDREN",
"errors": {
"channel_count": 5
}
}

SubArea: No delete guards (independent entity, no children).

Implementation: Delete guards are implemented in the service layer before calling the repository delete method. The service checks for existing children using a count query.

ParamTypeDescription
keywordstringSearch by name (case-insensitive partial match)
pageintegerPage number (default: 1)
limitintegerItems per page (default: 50, max: 100)
ParamTypeDescription
channel_group_idintegerFilter by channel group ID
keywordstringSearch by name (case-insensitive partial match)
pageintegerPage number (default: 1)
limitintegerItems per page (default: 50, max: 100)
ParamTypeDescription
channel_idintegerFilter by channel ID
keywordstringSearch by name (case-insensitive partial match)
pageintegerPage number (default: 1)
limitintegerItems per page (default: 50, max: 100)
ParamTypeDescription
keywordstringSearch by name (case-insensitive partial match)
pageintegerPage number (default: 1)
limitintegerItems per page (default: 50, max: 100)

All four master entities are workspace-scoped:

  1. workspace_id Assignment: On create, the workspace_id is automatically extracted from the authenticated user’s context (utils.GetAuthContextValue) and assigned to the entity.

  2. List Endpoint Filtering: All list queries include WHERE workspace_id = ? clause, ensuring users only see data from their own workspace.

  3. Detail/Update/Delete Scoping: Single-entity queries (GET by ID, PUT, DELETE) include both id and workspace_id in the WHERE clause to prevent cross-workspace access.

  4. No Cross-Workspace Joins: Relationships (Channel → ChannelGroup, Account → Channel) are scoped to the same workspace. Cross-workspace parent assignment is prevented.

Example workspace scoping in repository:

func (r *channelRepository) GetByID(ctx context.Context, id uint, workspaceID uint) (*domain.Channel, error) {
var ch domain.Channel
err := r.db.WithContext(ctx).Preload("ChannelGroup").
Where("id = ? AND workspace_id = ?", id, workspaceID).First(&ch).Error
return &ch, err
}
EntityView PermissionManage Permission
ChannelGroupchannel_group.viewchannel_group.manage
Channelchannel.viewchannel.manage
Accountaccount.viewaccount.manage
SubAreasub_area.viewsub_area.manage

Permission Constants (defined in internal/domain/constant.go):

PermissionKeyChannelGroupManage = "channel_group.manage"
PermissionKeyChannelManage = "channel.manage"
PermissionKeyAccountManage = "account.manage"
PermissionKeySubAreaManage = "sub_area.manage"

Access Levels:

  • View permission: Allows GET (list) and GET by ID operations
  • Manage permission (full access level): Allows POST, PUT, DELETE operations

Route Registration Example:

adminRoutes.POST("", middleware.RequirePermission(middleware.PermissionRequirement{
Key: domain.PermissionKeyChannelManage,
Platform: domain.PlatformMerqAdmin,
AccessLevels: []string{domain.AccessLevelFull},
}), h.CreateChannel)
  • Outlet Enrichment — How these master entities integrate with Outlet model: /technical/backend/outlet-enrichment/
  • Outlets User Guide — Managing outlets via web admin: /user-guide/web/outlets/
  • Workspace Scoping — Multi-tenancy architecture: /technical/api/workspace-scoping/
  • RBAC System — Permission enforcement patterns: /technical/architecture/rbac-system/