Master Data CRUD
Overview
Section titled “Overview”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.
Hierarchy
Section titled “Hierarchy”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:
- ChannelGroup → Channel → Account (hierarchical, parent-delete guards enforced)
- SubArea (independent, no parent/children relationships)
Data Models
Section titled “Data Models”ChannelGroup Model
Section titled “ChannelGroup Model”| Field | Type | Description | Constraints |
|---|---|---|---|
| id | integer | Primary key | Auto-increment |
| workspace_id | integer | FK to workspaces | NOT NULL, indexed |
| name | string | Group name | VARCHAR(255), NOT NULL, unique per workspace |
| created_at | timestamp | Creation timestamp | TIMESTAMPTZ |
| updated_at | timestamp | Last update timestamp | TIMESTAMPTZ |
| deleted_at | timestamp | Soft delete timestamp | TIMESTAMPTZ (nullable) |
Channel Model
Section titled “Channel Model”| Field | Type | Description | Constraints |
|---|---|---|---|
| id | integer | Primary key | Auto-increment |
| workspace_id | integer | FK to workspaces | NOT NULL, indexed |
| name | string | Channel name | VARCHAR(255), NOT NULL |
| channel_group_id | integer | FK to channel_groups | NOT NULL, indexed |
| created_at | timestamp | Creation timestamp | TIMESTAMPTZ |
| updated_at | timestamp | Last update timestamp | TIMESTAMPTZ |
| deleted_at | timestamp | Soft delete timestamp | TIMESTAMPTZ (nullable) |
Account Model
Section titled “Account Model”| Field | Type | Description | Constraints |
|---|---|---|---|
| id | integer | Primary key | Auto-increment |
| workspace_id | integer | FK to workspaces | NOT NULL, indexed |
| name | string | Account name | VARCHAR(255), NOT NULL |
| channel_id | integer | FK to channels | NOT NULL, indexed |
| created_at | timestamp | Creation timestamp | TIMESTAMPTZ |
| updated_at | timestamp | Last update timestamp | TIMESTAMPTZ |
| deleted_at | timestamp | Soft delete timestamp | TIMESTAMPTZ (nullable) |
SubArea Model
Section titled “SubArea Model”| Field | Type | Description | Constraints |
|---|---|---|---|
| id | integer | Primary key | Auto-increment |
| workspace_id | integer | FK to workspaces | NOT NULL, indexed |
| name | string | Sub area name | VARCHAR(255), NOT NULL, unique per workspace |
| created_at | timestamp | Creation timestamp | TIMESTAMPTZ |
| updated_at | timestamp | Last update timestamp | TIMESTAMPTZ |
| deleted_at | timestamp | Soft delete timestamp | TIMESTAMPTZ (nullable) |
API Endpoints
Section titled “API Endpoints”ChannelGroup Endpoints
Section titled “ChannelGroup Endpoints”| Method | Endpoint | Permission | Description |
|---|---|---|---|
| GET | /office/v1/outlets/channel-groups | channel_group.view or channel_group.manage | List (paginated) |
| GET | /office/v1/outlets/channel-groups/:id | channel_group.view or channel_group.manage | Get by ID |
| POST | /office/v1/outlets/channel-groups | channel_group.manage (full) | Create |
| PUT | /office/v1/outlets/channel-groups/:id | channel_group.manage (full) | Update |
| DELETE | /office/v1/outlets/channel-groups/:id | channel_group.manage (full) | Delete (soft) |
Channel Endpoints
Section titled “Channel Endpoints”| Method | Endpoint | Permission | Description |
|---|---|---|---|
| GET | /office/v1/outlets/channels | channel.view or channel.manage | List (paginated, filter by channel_group_id) |
| GET | /office/v1/outlets/channels/:id | channel.view or channel.manage | Get by ID |
| POST | /office/v1/outlets/channels | channel.manage (full) | Create |
| PUT | /office/v1/outlets/channels/:id | channel.manage (full) | Update |
| DELETE | /office/v1/outlets/channels/:id | channel.manage (full) | Delete (soft) |
Account Endpoints
Section titled “Account Endpoints”| Method | Endpoint | Permission | Description |
|---|---|---|---|
| GET | /office/v1/outlets/accounts | account.view or account.manage | List (paginated, filter by channel_id) |
| GET | /office/v1/outlets/accounts/:id | account.view or account.manage | Get by ID |
| POST | /office/v1/outlets/accounts | account.manage (full) | Create |
| PUT | /office/v1/outlets/accounts/:id | account.manage (full) | Update |
| DELETE | /office/v1/outlets/accounts/:id | account.manage (full) | Delete (soft) |
SubArea Endpoints
Section titled “SubArea Endpoints”| Method | Endpoint | Permission | Description |
|---|---|---|---|
| GET | /office/v1/outlets/sub-areas | sub_area.view or sub_area.manage | List (paginated) |
| GET | /office/v1/outlets/sub-areas/:id | sub_area.view or sub_area.manage | Get by ID |
| POST | /office/v1/outlets/sub-areas | sub_area.manage (full) | Create |
| PUT | /office/v1/outlets/sub-areas/:id | sub_area.manage (full) | Update |
| DELETE | /office/v1/outlets/sub-areas/:id | sub_area.manage (full) | Delete (soft) |
Note: All entities also expose read-only GET endpoints under /app/v1/outlets/ for mobile app consumption.
Example Requests and Responses
Section titled “Example Requests and Responses”ChannelGroup Create
Section titled “ChannelGroup Create”POST /office/v1/outlets/channel-groupsContent-Type: application/jsonAuthorization: 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"}Channel Create
Section titled “Channel Create”POST /office/v1/outlets/channelsContent-Type: application/jsonAuthorization: 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"}Account Create
Section titled “Account Create”POST /office/v1/outlets/accountsContent-Type: application/jsonAuthorization: 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"}SubArea Create
Section titled “SubArea Create”POST /office/v1/outlets/sub-areasContent-Type: application/jsonAuthorization: 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"}Parent-Delete Guards
Section titled “Parent-Delete Guards”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 Conflicterror with error codeCHANNEL_GROUP_HAS_CHILDREN
Channel:
- Cannot delete if Accounts exist with this
channel_id - Returns
409 Conflicterror with error codeCHANNEL_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.
Filter Parameters
Section titled “Filter Parameters”ChannelGroup List
Section titled “ChannelGroup List”| Param | Type | Description |
|---|---|---|
keyword | string | Search by name (case-insensitive partial match) |
page | integer | Page number (default: 1) |
limit | integer | Items per page (default: 50, max: 100) |
Channel List
Section titled “Channel List”| Param | Type | Description |
|---|---|---|
channel_group_id | integer | Filter by channel group ID |
keyword | string | Search by name (case-insensitive partial match) |
page | integer | Page number (default: 1) |
limit | integer | Items per page (default: 50, max: 100) |
Account List
Section titled “Account List”| Param | Type | Description |
|---|---|---|
channel_id | integer | Filter by channel ID |
keyword | string | Search by name (case-insensitive partial match) |
page | integer | Page number (default: 1) |
limit | integer | Items per page (default: 50, max: 100) |
SubArea List
Section titled “SubArea List”| Param | Type | Description |
|---|---|---|
keyword | string | Search by name (case-insensitive partial match) |
page | integer | Page number (default: 1) |
limit | integer | Items per page (default: 50, max: 100) |
Workspace Scoping
Section titled “Workspace Scoping”All four master entities are workspace-scoped:
-
workspace_idAssignment: On create, theworkspace_idis automatically extracted from the authenticated user’s context (utils.GetAuthContextValue) and assigned to the entity. -
List Endpoint Filtering: All list queries include
WHERE workspace_id = ?clause, ensuring users only see data from their own workspace. -
Detail/Update/Delete Scoping: Single-entity queries (GET by ID, PUT, DELETE) include both
idandworkspace_idin the WHERE clause to prevent cross-workspace access. -
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}RBAC Permissions
Section titled “RBAC Permissions”| Entity | View Permission | Manage Permission |
|---|---|---|
| ChannelGroup | channel_group.view | channel_group.manage |
| Channel | channel.view | channel.manage |
| Account | account.view | account.manage |
| SubArea | sub_area.view | sub_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)Related Topics
Section titled “Related Topics”- 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/