@educa-corp/sdd-framework 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/commands/generate-architecture.md +706 -0
  2. package/commands/generate-architecture.tmpl +194 -0
  3. package/commands/generate-code.md +35 -9
  4. package/commands/generate-code.tmpl +35 -9
  5. package/commands/generate-tech-docs.md +259 -246
  6. package/commands/generate-tech-docs.tmpl +21 -0
  7. package/core/FRAMEWORK_VERSION +1 -1
  8. package/core/commands/generate-architecture.md +706 -0
  9. package/core/commands/generate-code.md +35 -9
  10. package/core/commands/generate-tech-docs.md +259 -246
  11. package/core/skills/setup-ai-first/SKILL.md +12 -4
  12. package/core/templates/architecture.template.md +392 -111
  13. package/core/templates/tech-design.template.md +238 -246
  14. package/docs/01-getting-started/installation.md +47 -112
  15. package/docs/01-getting-started/quickstart.md +58 -72
  16. package/docs/01-getting-started/what-is-sdd.md +75 -0
  17. package/docs/02-concepts/architecture.md +109 -0
  18. package/docs/02-concepts/glossary.md +87 -0
  19. package/docs/02-concepts/overview.md +93 -0
  20. package/docs/02-concepts/pipeline-steps/00-setup.md +102 -0
  21. package/docs/02-concepts/pipeline-steps/01-discovery.md +129 -0
  22. package/docs/02-concepts/pipeline-steps/02-specification.md +130 -0
  23. package/docs/02-concepts/pipeline-steps/03-design-spec.md +90 -0
  24. package/docs/02-concepts/pipeline-steps/04-bdd.md +120 -0
  25. package/docs/02-concepts/pipeline-steps/05-tech-docs.md +101 -0
  26. package/docs/02-concepts/pipeline-steps/06-code.md +119 -0
  27. package/docs/02-concepts/pipeline-steps/07-dev-selftest.md +92 -0
  28. package/docs/02-concepts/pipeline-steps/08-qc-automation.md +102 -0
  29. package/docs/02-concepts/pipeline-steps/09-validate-traces.md +104 -0
  30. package/docs/02-concepts/pipeline-steps/10-feedback-loop.md +105 -0
  31. package/docs/02-concepts/pipeline-steps/README.md +92 -0
  32. package/docs/02-concepts/roles-and-hitl.md +73 -0
  33. package/docs/02-concepts/traceability.md +94 -0
  34. package/docs/03-guides/architect.md +98 -0
  35. package/docs/03-guides/developer.md +76 -0
  36. package/docs/03-guides/product-owner.md +68 -0
  37. package/docs/03-guides/tester-qa.md +70 -0
  38. package/docs/04-reference/commands.md +105 -0
  39. package/docs/04-reference/configuration.md +94 -0
  40. package/docs/04-reference/model-selection.md +68 -0
  41. package/docs/04-reference/modules.md +74 -0
  42. package/docs/04-reference/trace-schema.md +93 -0
  43. package/docs/README.md +29 -40
  44. package/docs/explain/00-setup-ai-first.md +77 -0
  45. package/docs/explain/00b-generate-architecture.md +76 -0
  46. package/docs/explain/01-define-product.md +79 -0
  47. package/docs/explain/02-generate-prd.md +78 -0
  48. package/docs/explain/03-refine-prd.md +86 -0
  49. package/docs/explain/04-review-context.md +100 -0
  50. package/docs/explain/05-generate-design-spec.md +73 -0
  51. package/docs/explain/06-generate-bdd.md +77 -0
  52. package/docs/explain/07-generate-tech-docs.md +71 -0
  53. package/docs/explain/08-review-tech-docs.md +79 -0
  54. package/docs/explain/09-generate-code.md +78 -0
  55. package/docs/explain/10-review-code.md +70 -0
  56. package/docs/explain/11-map-testids.md +69 -0
  57. package/docs/explain/12-dev-gen-test.md +66 -0
  58. package/docs/explain/13-dev-run-test.md +69 -0
  59. package/docs/explain/14-dev-smoke-test.md +67 -0
  60. package/docs/explain/15-qc-analyze.md +68 -0
  61. package/docs/explain/16-qc-plan.md +61 -0
  62. package/docs/explain/17-qc-design-test.md +61 -0
  63. package/docs/explain/18-qc-review.md +59 -0
  64. package/docs/explain/19-qc-run-test.md +67 -0
  65. package/docs/explain/20-qc-report.md +61 -0
  66. package/docs/explain/21-validate-traces.md +68 -0
  67. package/docs/explain/22-generate-spec-manifest.md +60 -0
  68. package/docs/explain/23-fix-bug.md +69 -0
  69. package/docs/explain/24-debug.md +61 -0
  70. package/docs/explain/25-report-bug.md +65 -0
  71. package/docs/explain/26-propose-scenario.md +63 -0
  72. package/docs/explain/27-learn.md +65 -0
  73. package/docs/explain/28-sync.md +70 -0
  74. package/docs/explain/29-update-framework.md +65 -0
  75. package/docs/explain/README.md +134 -0
  76. package/package.json +1 -1
  77. package/skills/setup-ai-first/SKILL.md +12 -4
  78. package/skills/setup-ai-first/SKILL.tmpl +12 -4
  79. package/templates/architecture.template.md +392 -111
  80. package/templates/tech-design.template.md +238 -246
  81. package/docs/01-getting-started/README.md +0 -19
  82. package/docs/01-getting-started/core-concepts.md +0 -102
  83. package/docs/02-guides/README.md +0 -26
  84. package/docs/02-guides/bdd-input-checklist.md +0 -68
  85. package/docs/02-guides/developer/README.md +0 -49
  86. package/docs/02-guides/developer/bdd-and-trace.md +0 -126
  87. package/docs/02-guides/developer/commands.md +0 -76
  88. package/docs/02-guides/developer/pr-checklist.md +0 -16
  89. package/docs/02-guides/developer/scenarios.md +0 -460
  90. package/docs/02-guides/developer/workflow.md +0 -121
  91. package/docs/02-guides/prd-input-checklist.md +0 -94
  92. package/docs/02-guides/product-owner/README.md +0 -81
  93. package/docs/02-guides/product-owner/commands.md +0 -30
  94. package/docs/02-guides/product-owner/handoff-checklist.md +0 -42
  95. package/docs/02-guides/product-owner/prd-writing-rules.md +0 -45
  96. package/docs/02-guides/product-owner/scenarios.md +0 -438
  97. package/docs/02-guides/tech-docs-input-checklist.md +0 -109
  98. package/docs/02-guides/tester/README.md +0 -75
  99. package/docs/02-guides/tester/bug-reporting.md +0 -117
  100. package/docs/02-guides/tester/qc-automation.md +0 -165
  101. package/docs/02-guides/tester/reading-specs.md +0 -79
  102. package/docs/02-guides/tester/scenarios.md +0 -186
  103. package/docs/02-guides/tester/spec-manifest.md +0 -130
  104. package/docs/02-guides/tester/test-checklist.md +0 -31
  105. package/docs/02-guides/tester/workflow.md +0 -77
  106. package/docs/03-concepts/README.md +0 -20
  107. package/docs/03-concepts/architecture.md +0 -248
  108. package/docs/03-concepts/mechanisms-explained.md +0 -124
  109. package/docs/03-concepts/pipeline.md +0 -278
  110. package/docs/03-concepts/traceability.md +0 -152
  111. package/docs/04-operations/README.md +0 -33
  112. package/docs/04-operations/bug-flow.md +0 -364
  113. package/docs/04-operations/publishing.md +0 -154
  114. package/docs/04-operations/sync-and-update.md +0 -522
  115. package/docs/05-reference/README.md +0 -34
  116. package/docs/05-reference/command-cheatsheet.md +0 -147
  117. package/docs/05-reference/commands.md +0 -234
  118. package/docs/05-reference/model-selection.md +0 -74
  119. package/docs/05-reference/modules.md +0 -110
  120. package/docs/05-reference/trace-schema.md +0 -154
  121. package/docs/06-commands/README.md +0 -75
  122. package/docs/06-commands/explain-debug.md +0 -32
  123. package/docs/06-commands/explain-define-product.md +0 -43
  124. package/docs/06-commands/explain-dev-gen-test.md +0 -28
  125. package/docs/06-commands/explain-dev-run-test.md +0 -24
  126. package/docs/06-commands/explain-dev-smoke-test.md +0 -25
  127. package/docs/06-commands/explain-fix-bug.md +0 -28
  128. package/docs/06-commands/explain-generate-bdd.md +0 -45
  129. package/docs/06-commands/explain-generate-code.md +0 -53
  130. package/docs/06-commands/explain-generate-design-spec.md +0 -54
  131. package/docs/06-commands/explain-generate-prd.md +0 -45
  132. package/docs/06-commands/explain-generate-spec-manifest.md +0 -20
  133. package/docs/06-commands/explain-generate-tech-docs.md +0 -56
  134. package/docs/06-commands/explain-learn.md +0 -21
  135. package/docs/06-commands/explain-map-testids.md +0 -28
  136. package/docs/06-commands/explain-propose-scenario.md +0 -24
  137. package/docs/06-commands/explain-qc-analyze.md +0 -22
  138. package/docs/06-commands/explain-qc-design-test.md +0 -20
  139. package/docs/06-commands/explain-qc-plan.md +0 -21
  140. package/docs/06-commands/explain-qc-report.md +0 -23
  141. package/docs/06-commands/explain-qc-review.md +0 -24
  142. package/docs/06-commands/explain-qc-run-test.md +0 -27
  143. package/docs/06-commands/explain-refine-prd.md +0 -51
  144. package/docs/06-commands/explain-report-bug.md +0 -24
  145. package/docs/06-commands/explain-review-code.md +0 -45
  146. package/docs/06-commands/explain-review-context.md +0 -68
  147. package/docs/06-commands/explain-review-tech-docs.md +0 -45
  148. package/docs/06-commands/explain-setup-ai-first.md +0 -25
  149. package/docs/06-commands/explain-sync.md +0 -24
  150. package/docs/06-commands/explain-update-framework.md +0 -22
  151. package/docs/06-commands/explain-validate-traces.md +0 -25
  152. package/docs/t-sample.md +0 -826
package/docs/t-sample.md DELETED
@@ -1,826 +0,0 @@
1
- # Store Management — Branch Configuration & Price List Technical Design Document
2
-
3
- <!-- @trace.domain: store-management -->
4
- <!-- @trace.uc: LOYAL-23-UC1, LOYAL-23-UC2 -->
5
- <!-- @trace.prd: LOYAL-23 -->
6
-
7
- > **Related doc:** [Store Management — Configuration (LOYAL-16)](./store-management-configuration-technical-design.md) — covers store info, images, delivery/payment settings, branch display toggle.
8
-
9
- ## 1. Overview
10
-
11
- The Branch Configuration module allows Merchants (admin accounts) to configure which branch serves as the **order-receiving branch (chi nhánh nhận đơn)** for their Zalo Mini App storefront. Branch base data comes from KVS API (via `IKvsBranchService`); loyalty-owned config (`isPrimary`) is stored in `BranchConfig` DB table. This module only selects which branch is primary. The module also displays the applied price list (always "Bảng giá chung", read-only).
12
-
13
- This module operates **post-onboarding** and is accessed via a dedicated popup "Cấu hình chi nhánh và giá bán", separate from the store settings popup (LOYAL-16).
14
-
15
- ### Goals
16
- - Allow Merchants to change the primary branch (chi nhánh nhận đơn) for order receiving
17
- - Warn Merchants when the configured primary branch becomes invalid (deleted/deactivated in KVS)
18
- - Display the applied price list (always "Bảng giá chung", read-only)
19
- - Preserve existing orders' branch assignment when primary branch changes
20
- - Enforce admin-only access
21
-
22
- ### Business Actors
23
-
24
- | Actor | Description | Channel |
25
- |-------|-------------|---------|
26
- | Merchant (admin) | Store owner with admin permissions who configures primary branch | KVS → KVLoyalty Widget → Portal (iframe) → StoreManagement API |
27
-
28
- ---
29
-
30
- ## 2. Architecture Overview
31
-
32
- ### 2.1 High-level Architecture
33
-
34
- ```
35
- ┌─────────────┐ ┌─────────────────┐ ┌──────────────────────┐
36
- │ KVS App │────▶│ KVLoyalty Widget │────▶│ KVLoyalty Portal │
37
- │ (Partner) │ │ (CDN JS) │ │ (Angular 19) │
38
- └─────────────┘ └─────────────────┘ └──────────┬───────────┘
39
- │ REST (Bearer)
40
-
41
- ┌──────────────────────┐
42
- │ API Gateway │
43
- │ (HAProxy) │
44
- └──────────┬───────────┘
45
-
46
- ┌─────────────────────────┤
47
- ▼ ▼
48
- ┌──────────────────┐ ┌─────────────────┐
49
- │ StoreManagement │ │ Identity API │
50
- │ API │ │ │
51
- └────────┬─────────┘ └─────────────────┘
52
-
53
- ┌────────┴─────────┐
54
- ▼ ▼
55
- ┌──────────────┐ ┌──────────────┐
56
- │ SQL Server │ │ KVS API │
57
- │ (Sharded DB) │ │ (Branch data)│
58
- └──────────────┘ └──────────────┘
59
- ```
60
-
61
- > **Note:** Branch base data (name, phone, address, isActive) is fetched live from KVS API via `IKvsBranchService` (same pattern as Product). Loyalty-owned config (`isPrimary`, `isDisplayedOnStorefront`) is stored in `BranchConfig` DB table.
62
-
63
- ### 2.2 Communication Patterns
64
-
65
- | Pattern | Usage | Scope |
66
- |---------|-------|-------|
67
- | KVS → Widget → Portal → API | Token Exchange (Bearer, RS256) — Merchant actions via Portal iframe | LOYAL-23-UC1 |
68
- | Portal → StoreManagement API | REST — Read branch list (reuse LOYAL-16 API), update `isPrimary` flag in BranchConfig | LOYAL-23-UC1, UC2 |
69
- | StoreManagement API → KVS API | REST (KVS-Merchant-Id header) — Fetch branch base data via `IKvsBranchService` (Redis cache 5min) | LOYAL-23-UC1 |
70
-
71
- ---
72
-
73
- ## 3. Data Model
74
-
75
- ### 3.1 Entity Design
76
-
77
- This module uses the **API-sourced model + DB config** pattern introduced in LOYAL-16-UC1. See [LOYAL-16 §3.1](./store-management-configuration-technical-design.md) for full entity definitions.
78
-
79
- #### BranchModel (API-sourced POCO — see `core-entities.md` §3)
80
-
81
- Branch base data is fetched live from KVS API via `IKvsBranchService`. Not a DB entity.
82
-
83
- | Field | Type | Usage in LOYAL-23 |
84
- |-------|------|-------------------|
85
- | `externalId` | `string` | KVS branch ID — used as join key with BranchConfig |
86
- | `name` | `string` | Display name in dropdown and warning messages |
87
- | `isActive` | `boolean` | From KVS API. When primary BranchConfig exists but `isActive = false` → branch is DEACTIVATED → warning shown |
88
- | `createdAt` | `datetime` | From KVS API. Used for sorting branches in dropdown (newest first — SC2) |
89
-
90
- #### BranchConfig (DB Entity — see LOYAL-16 §3.1)
91
-
92
- Loyalty-owned config per branch. `isPrimary` is the order-receiving branch flag.
93
-
94
- | Field | Type | Usage in LOYAL-23 |
95
- |-------|------|-------------------|
96
- | `branchExternalId` | `string` | Cross-reference → KVS branch ID |
97
- | `isPrimary` | `boolean` | `true` = this branch is the order-receiving branch. Only 1 per Merchant. Changed via PUT endpoint. |
98
- | `primaryBranchName` | `string?` | Snapshot of branch name at time `isPrimary` was set. Used to display warning message when branch is deleted from KVS (KVS API no longer returns the branch, so name is unavailable). Updated on every `isPrimary` swap. Only meaningful when `isPrimary = true`. |
99
- | `isDisplayedOnStorefront` | `boolean` | Managed by LOYAL-16 — not modified by LOYAL-23 |
100
-
101
- **Branch state transitions (merged view):**
102
-
103
- ```
104
- Normal: BranchConfig.isPrimary = true, BranchModel found, isActive = true → ✅ Valid
105
- Deactivated: BranchConfig.isPrimary = true, BranchModel found, isActive = false → ⚠️ Warning "đã ngừng hoạt động" (name from BranchModel)
106
- Deleted: BranchConfig.isPrimary = true, BranchModel NOT found in KVS response → ⚠️ Warning "đã bị xóa" (name from BranchConfig.primaryBranchName)
107
- Changed: BranchConfig.isPrimary = false, BranchModel found, isActive = true → Old primary (after merchant selects new)
108
- ```
109
-
110
- **Constraints:**
111
- - Exactly 1 BranchConfig per Merchant with `isPrimary = true` (enforced in application logic)
112
- - `isPrimary` is a KVLoyalty-owned field — stored in `BranchConfig` DB table
113
- - When a new KVS branch appears without a `BranchConfig` record, one is auto-created with `isPrimary = false`
114
-
115
- ### 3.2 Entity Relationships
116
-
117
- ```
118
- Merchant (1:N) → BranchConfig (only 1 has isPrimary = true = chi nhánh nhận đơn)
119
- ├── branchExternalId ───── BranchModel (KVS API)
120
- │ ├── name, contactNumber, address [read-only]
121
- │ └── isActive [read-only]
122
- ├── isPrimary [determines order-receiving branch]
123
- └── isDisplayedOnStorefront [managed by LOYAL-16]
124
- ```
125
-
126
- > **Note:** Order–Branch association (BR2: orders keep original branch) will be designed in the Order module scope.
127
-
128
- ### 3.3 Data Source Boundaries
129
-
130
- **LOYAL-23 scope: READ merged branch list (KVS API + BranchConfig DB) + WRITE `isPrimary` flag in BranchConfig.**
131
-
132
- Branch base data (name, phone, address, isActive) is fetched live from KVS API — no SyncData dependency.
133
-
134
- | Responsibility | LOYAL-23 Scope | Handled By |
135
- |---|---|---|
136
- | ✅ Read branch list (KVS API + BranchConfig merge) | Yes | Reuse `GET /v1/store-management/branches` (LOYAL-16) |
137
- | ✅ Update `isPrimary` flag | Yes | StoreManagement API (swap on BranchConfig rows) |
138
- | ❌ Branch base data (name, phone, address) | Read-only | KVS API via `IKvsBranchService` (Redis cache 5min) |
139
- | ❌ Products/categories/inventory display | No | API-sourced via `IProductCatalogService` → `IKvsProductService` → KVS API (Redis 10min). Storefront queries use isPrimary branch to determine which branch's products to fetch from KVS. |
140
-
141
- **Data flow when primary branch changes:**
142
-
143
- ```
144
- Merchant selects new branch in Portal
145
-
146
- PUT /branch-configuration { branchExternalId: "..." }
147
-
148
- StoreManagement API: UPSERT BranchConfig SET isPrimary = true/false
149
- + UPDATE BranchConfig SET primaryBranchName = @newBranchName WHERE isPrimary = true
150
-
151
- Done — next storefront product load uses new isPrimary branch to fetch products from KVS API
152
- ```
153
-
154
- ### 3.4 Multi-tenant & Sharding
155
-
156
- Same as other store-management modules:
157
- - All entities carry `MerchantId` = `TenantId`
158
- - EF Core Global Query Filters enforce tenant isolation
159
- - Shard resolution via `IShardResolver.ResolveAsync(appId, tenantId)` during auth middleware
160
-
161
- ---
162
-
163
- ## 4. API Contracts
164
-
165
- ### 4.1 Endpoints
166
-
167
- ```
168
- GET /v1/store-management/branches # REUSE (LOYAL-16) — branch list for dropdown
169
- PUT /v1/store-management/storefront/branch-configuration # NEW — Update order-receiving branch (isPrimary swap)
170
- ```
171
-
172
- > **`GET /v1/store-management/branches`** already exists (LOYAL-16, `StorefrontController`). Returns `ApiResponse<List<BranchDisplayDto>>` with merged view: KVS branch data (`BranchExternalId, Name, ContactNumber, Address, IsActive`) + BranchConfig DB flags (`IsPrimary, IsDisplayedOnStorefront`).
173
- >
174
- > `BranchDisplayDto` already includes `IsPrimary` field (from BranchConfig DB table). Portal uses `branchExternalId` (string) as the branch identifier.
175
-
176
- ### 4.2 Request/Response Models
177
-
178
- #### BranchDisplayDto (defined in LOYAL-16 — reused here)
179
- <!-- uses BranchExternalId as identifier per LOYAL-16 Branch→BranchConfig split -->
180
-
181
- ```csharp
182
- public class BranchDisplayDto
183
- {
184
- public string BranchExternalId { get; set; } = default!; // KVS branch ID — join key
185
- public string Name { get; set; } = default!; // From BranchModel (KVS API)
186
- public string? ContactNumber { get; set; } // From BranchModel (KVS API)
187
- public string? Address { get; set; } // From BranchModel (KVS API)
188
- public bool IsActive { get; set; } // From BranchModel (KVS API)
189
- public DateTime CreatedAt { get; set; } // From BranchModel (KVS API) — used for sorting (newest first, SC2)
190
- public bool IsPrimary { get; set; } // From BranchConfig (DB) — true = chi nhánh nhận đơn
191
- public bool IsDisplayedOnStorefront { get; set; } // From BranchConfig (DB) — managed by LOYAL-16
192
- }
193
- ```
194
-
195
- > **Warning detection (client-side):** Portal loads branch list (merged KVS + BranchConfig). Two invalid cases:
196
- > 1. **Deactivated:** isPrimary BranchConfig exists AND KVS branch has `isActive = false` → warning "đã ngừng hoạt động". Branch name available from `BranchModel.name`.
197
- > 2. **Deleted:** isPrimary BranchConfig exists AND NO matching branch in KVS response → warning "đã bị xóa". Branch name from `BranchConfig.primaryBranchName` (snapshot stored at time of isPrimary set).
198
- >
199
- > For the **deactivated** case, the merge logic includes the branch in `BranchDisplayDto[]` with `IsActive = false`. For the **deleted** case, the API returns a metadata field `deletedPrimaryBranch` (see below) since there's no BranchModel to merge with.
200
-
201
- #### InvalidPrimaryBranchInfo (NEW — returned alongside BranchDisplayDto[] when primary branch is deleted)
202
-
203
- ```csharp
204
- public class BranchListResponse
205
- {
206
- public List<BranchDisplayDto> Branches { get; set; } = new();
207
- public InvalidPrimaryBranchInfo? DeletedPrimaryBranch { get; set; } // non-null when isPrimary branch not found in KVS
208
- }
209
-
210
- public class InvalidPrimaryBranchInfo
211
- {
212
- public string BranchExternalId { get; set; } = default!; // from BranchConfig.branchExternalId
213
- public string BranchName { get; set; } = default!; // from BranchConfig.primaryBranchName (snapshot)
214
- }
215
- ```
216
- >
217
- > **Price list (UC2):** Hardcoded in Portal as "Bảng giá chung" (read-only label). No backend field needed — per UC2-BR1, it's always fixed.
218
-
219
- #### Update Branch Configuration (NEW)
220
-
221
- ```csharp
222
- // PUT /v1/store-management/storefront/branch-configuration
223
- // Request:
224
-
225
- public class UpdateBranchConfigurationRequest
226
- {
227
- public string BranchExternalId { get; set; } = null!; // KVS branch ID (cross-reference key)
228
- }
229
-
230
- // Response: ApiResponse<object>
231
- ```
232
-
233
- **Validation Rules (FluentValidation):**
234
-
235
- ```csharp
236
- public class UpdateBranchConfigurationValidator : AbstractValidator<UpdateBranchConfigurationRequest>
237
- {
238
- public UpdateBranchConfigurationValidator()
239
- {
240
- RuleFor(x => x.BranchExternalId)
241
- .NotEmpty().WithMessage("Vui lòng chọn chi nhánh nhận đơn");
242
- }
243
- }
244
- ```
245
-
246
- **Handler logic (UpdateBranchConfigurationHandler):**
247
- 1. Fetch branches from KVS API via `IKvsBranchService.GetBranchesAsync()` — validate `BranchExternalId` exists and `isActive = true`
248
- 2. Load current primary BranchConfig — if `BranchExternalId` == current primary → return error `BRANCH_NOT_CHANGED`
249
- 3. Get branch name from KVS response: `newBranchName = branches.First(b => b.ExternalId == request.BranchExternalId).Name`
250
- 4. Begin transaction:
251
- a. `UPDATE BranchConfig SET isPrimary = false, primaryBranchName = null WHERE isPrimary = true AND merchantId = @tenantId`
252
- b. `UPSERT BranchConfig SET isPrimary = true, primaryBranchName = @newBranchName WHERE branchExternalId = @newBranchExternalId AND merchantId = @tenantId`
253
- c. Commit transaction
254
- 5. Return success
255
- 6. On error → rollback, return `BRANCH_CONFIG_SAVE_FAILED`
256
-
257
- ### 4.3 Error Codes
258
-
259
- | Code | HTTP Status | Description | Trace |
260
- |------|-------------|-------------|-------|
261
- | `BRANCH_NOT_FOUND` | 400 Bad Request | Selected branch does not exist or does not belong to current tenant | UC1-SC1 |
262
- | `BRANCH_NOT_ACTIVE` | 400 Bad Request | Selected branch is not active (isActive = false) | UC1-SC1 |
263
- | `BRANCH_NOT_CHANGED` | 400 Bad Request | Selected branch is the same as current primary (Save button should be disabled client-side) | UC1-SC7 |
264
- | `BRANCH_CONFIG_SAVE_FAILED` | 500 Internal | System error during branch configuration save | UC1-SC12 |
265
-
266
- ### 4.5 UI Component Mapping — Branch Configuration Popup (Portal — Angular 19)
267
-
268
- > **Source:** Figma — `Merchant` file, node `78:35333` ("Thiết lập đồng bộ" / "Cấu hình chi nhánh và giá bán")
269
- > **Stack:** Angular 19 standalone, Signals, ngx-bootstrap modal, `bk-*` design system
270
- > <!-- @figma.url: https://www.figma.com/design/2AAbMqr0IwzvQffuUDZb4A/Merchant?node-id=78-35333 -->
271
-
272
- #### 4.5.1 Component Hierarchy
273
-
274
- ```
275
- StoreHomeComponent (existing — features/store-management/store-home/)
276
- └── Opens modal via BsModalService ──▶
277
- BranchConfigDialogComponent (NEW — popup "Cấu hình chi nhánh và giá bán")
278
- ├── Header: "Cấu hình chi nhánh và giá bán" + close (✕) button
279
-
280
- ├── Section 1: Chi nhánh nhận đơn
281
- │ ├── Title: "Chi nhánh nhận đơn" (bk-fw-semibold bk-text-md)
282
- │ ├── Subtitle: "Chi nhánh được chọn dùng để đồng bộ sản phẩm, tồn kho và nhận đơn giao đi từ Zalo."
283
- │ ├── Dropdown select: branch list (active branches, sorted createdAt DESC)
284
- │ └── BranchWarningAlertComponent (conditional — only when primary branch is invalid)
285
- │ ├── Warning icon (orange)
286
- │ ├── Message text (dynamic per case):
287
- │ │ ├── Deleted: "Chi nhánh '{name}' đã bị xóa. Vui lòng chọn chi nhánh khác"
288
- │ │ └── Deactivated: "Chi nhánh '{name}' đã ngừng hoạt động. Vui lòng chọn chi nhánh khác"
289
- │ └── "Đã hiểu" button → dismisses alert
290
-
291
- ├── Section 2: Thiết lập giá (bordered card, bg gray)
292
- │ ├── Title: "Thiết lập giá" (bk-fw-semibold bk-text-md)
293
- │ ├── Subtitle: "Danh sách bảng giá được lọc theo chi nhánh đã chọn..."
294
- │ ├── Label: "Bảng giá bán"
295
- │ └── Read-only input: "Bảng giá chung" (select arrow hidden)
296
-
297
- └── Footer: Action Bar
298
- ├── "Bỏ qua" → secondary button (close without saving)
299
- └── "Lưu" → primary button (disabled when branch unchanged)
300
- ```
301
-
302
- #### 4.5.2 Component File Mapping
303
-
304
- | Component | Path | Type | Purpose |
305
- |-----------|------|------|---------|
306
- | `BranchConfigDialogComponent` | `features/store-management/store-home/branch-config-dialog/` | Feature | Branch configuration popup — dropdown + save logic |
307
- | `BranchWarningAlertComponent` | `features/store-management/store-home/branch-config-dialog/branch-warning-alert/` | Child | Conditional warning banner when branch is invalid |
308
-
309
- #### 4.5.3 State Management (Signals)
310
-
311
- ```typescript
312
- // BranchConfigDialogComponent — signal-based state
313
- selectedBranchId = signal<string | null>(null); // KVS branchExternalId of selected branch in dropdown
314
- currentPrimaryBranchId = signal<string | null>(null); // original isPrimary branch (from GET /branches)
315
- availableBranches = signal<IBranchDisplay[]>([]); // from GET /v1/store-management/branches (reuse LOYAL-16)
316
- deletedPrimaryBranch = signal<IInvalidPrimaryBranchInfo | null>(null); // non-null when isPrimary branch deleted from KVS
317
- isBranchValid = signal(true); // false when primary branch deactivated OR deleted
318
- branchWarningMessage = signal<string | null>(null); // derived — see derivation logic below
319
- warningDismissed = signal(false); // "Đã hiểu" clicked
320
- branchDropdownPlaceholder = signal('Chọn chi nhánh'); // shown when isBranchValid = false (SC9)
321
- priceListName = signal('Bảng giá chung'); // always fixed (UC2)
322
- saving = signal(false); // loading state during save
323
- isSaveDisabled = computed(() => // Lưu button state
324
- this.saving() ||
325
- this.selectedBranchId() === this.currentPrimaryBranchId() ||
326
- this.selectedBranchId() === null
327
- );
328
- ```
329
-
330
- **Branch list filter + sort (client-side, SC2):**
331
- ```typescript
332
- // API GET /branches returns ALL branches (including isActive = false) so client can detect SC9 warnings.
333
- // For dropdown rendering: filter active + sort newest first (createdAt DESC).
334
- const displayBranches = branches
335
- .filter(b => b.isActive)
336
- .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
337
- availableBranches.set(displayBranches);
338
- ```
339
-
340
- **Warning message derivation (client-side, SC9):**
341
- ```typescript
342
- // After loading branch list response:
343
- // Case 1: Deactivated — isPrimary branch found in list with isActive = false
344
- const primaryBranch = branches.find(b => b.isPrimary);
345
- if (primaryBranch && !primaryBranch.isActive) {
346
- isBranchValid.set(false);
347
- selectedBranchId.set(null); // dropdown shows placeholder "Chọn chi nhánh"
348
- branchWarningMessage.set(`Chi nhánh '${primaryBranch.name}' đã ngừng hoạt động. Vui lòng chọn chi nhánh khác`);
349
- }
350
- // Case 2: Deleted — deletedPrimaryBranch present in response (no matching BranchModel from KVS)
351
- if (response.deletedPrimaryBranch) {
352
- isBranchValid.set(false);
353
- selectedBranchId.set(null); // dropdown shows placeholder "Chọn chi nhánh"
354
- branchWarningMessage.set(`Chi nhánh '${response.deletedPrimaryBranch.branchName}' đã bị xóa. Vui lòng chọn chi nhánh khác`);
355
- }
356
- ```
357
-
358
- **Portal TypeScript model** reuses `IBranchDisplay` from LOYAL-16 (see [LOYAL-16 §4.5.3](./store-management-configuration-technical-design.md)):
359
- - Branch identifier is `branchExternalId: string` (KVS branch ID)
360
- - `isDeleted` removed — KVS API only returns active branches; inactive = `isActive: false`
361
- - `IUpdateBranchConfigurationRequest.branchExternalId: string` (was `branchId: number`)
362
-
363
- #### 4.5.4 Modal Configuration
364
-
365
- ```typescript
366
- // Opened from StoreHomeComponent (e.g., "Cấu hình chi nhánh" button)
367
- this.modalService.show(BranchConfigDialogComponent, {
368
- class: 'bk-modal bk-modal-md bk-modal-center', // medium popup per Figma (640px)
369
- animated: true,
370
- backdrop: 'static',
371
- keyboard: false,
372
- });
373
- ```
374
-
375
- #### 4.5.5 Figma → Design System Mapping
376
-
377
- | Figma Element | Design System Class | Notes |
378
- |---------------|---------------------|-------|
379
- | Dialog container | `bk-modal bk-modal-md bk-modal-center` | Medium centered modal (640×497px per Figma) |
380
- | Close (✕) button | `bk-modal-close` / icon `xmark` (KV_Icon_Kit) | Top-right, circular 32px button |
381
- | Section title ("Chi nhánh nhận đơn") | `bk-fw-semibold bk-text-md` | 16px semi-bold, #15171a |
382
- | Section subtitle | `bk-text-sm` | 14px regular, #3e464f |
383
- | Branch dropdown | `bk-form-control` (Kendo DropDownList or ngx-bootstrap) | Full width, border-radius 8px, 32px height |
384
- | Warning alert | `bk-alert bk-alert-warning` | bg #fff9f2, border #ffdbb3, border-radius 8px |
385
- | Warning icon | `ik-triangle-warning` (KV_Icon_Kit) | 24px, color #ff8800 |
386
- | "Đã hiểu" button | `bk-btn bk-btn-outline-neutral bk-btn-sm` | 32px height, inside alert action area |
387
- | Price section card | Custom — `bg: var(--bg/layer/level-1, #f7f8f9)`, `border: 1px solid var(--divider/block, #e8eaed)`, `border-radius: 12px`, `padding: 16px` | Contained card for "Thiết lập giá" section |
388
- | "Bảng giá bán" label | `bk-label` | 14px regular, #15171a |
389
- | "Bảng giá chung" input | `bk-form-control` with `readonly` | Select suffix arrow has `opacity: 0` (hidden) |
390
- | "Bỏ qua" button | `bk-btn bk-btn-outline-neutral` | 40px height, min-width 72px |
391
- | "Lưu" button | `bk-btn bk-btn-primary` | 40px height, min-width 72px, bg #0070f4. Disabled when branch unchanged |
392
-
393
- ---
394
-
395
- ## 5. Key Flows (Sequence Diagrams)
396
-
397
- ### 5.1 Happy Path — Open Branch Configuration Popup
398
-
399
- ```mermaid
400
- sequenceDiagram
401
- participant M as Merchant
402
- participant SH as StoreHomeComponent
403
- participant MS as BsModalService
404
- participant BC as BranchConfigDialogComponent
405
- participant BS as BranchConfigService
406
- participant API as StoreManagement API
407
- participant KVS as KVS API
408
- participant DB as SQL Server
409
-
410
- M->>SH: Click "Cấu hình chi nhánh"
411
- SH->>MS: show(BranchConfigDialogComponent, { class: 'bk-modal-md' })
412
- MS->>BC: Create + render dialog
413
- BC->>BC: ngOnInit() → loading.set(true)
414
- BC->>BS: getBranches()
415
- BS->>API: GET /v1/store-management/branches (reuse LOYAL-16)
416
- API->>KVS: IKvsBranchService.GetBranchesAsync() (Redis cache 5min)
417
- KVS-->>API: List<BranchModel> (name, phone, address, isActive)
418
- API->>DB: SELECT BranchConfig WHERE merchantId = @tenantId
419
- API->>API: Merge BranchModel[] + BranchConfig[] on branchExternalId
420
- API->>API: Auto-create BranchConfig for new KVS branches (isDisplayedOnStorefront = true)
421
- API-->>BS: ApiResponse<List<BranchDisplayDto>> (includes isPrimary)
422
- BS-->>BC: branch list
423
- BC->>BC: Find branch with isPrimary = true → set currentPrimaryBranchId (branchExternalId)
424
- alt No branch has isPrimary = true → primary branch was deactivated in KVS
425
- BC->>BC: isBranchValid.set(false), show BranchWarningAlertComponent
426
- end
427
- BC->>BC: loading.set(false) → render popup
428
- Note over BC: "Lưu" button disabled (branch unchanged)
429
- ```
430
-
431
- ### 5.2 Happy Path — Save Branch Configuration (SC1)
432
-
433
- ```mermaid
434
- sequenceDiagram
435
- participant M as Merchant (Portal)
436
- participant BC as BranchConfigDialogComponent
437
- participant BS as BranchConfigService
438
- participant GW as API Gateway
439
- participant SM as StoreManagement API
440
- participant KVS as KVS API
441
- participant DB as SQL Server
442
- participant NS as NotificationService
443
-
444
- M->>BC: Select "Chi nhánh 2" from dropdown
445
- BC->>BC: selectedBranchId.set(newBranchExternalId)
446
- Note over BC: "Lưu" button enabled (branch changed)
447
-
448
- M->>BC: Click "Lưu"
449
- BC->>BC: saving.set(true), disable "Lưu" button
450
- BC->>BS: updateBranchConfiguration({ branchExternalId: newBranchExternalId })
451
- BS->>GW: PUT /v1/store-management/storefront/branch-configuration
452
- GW->>SM: Forward (Bearer token)
453
- SM->>KVS: IKvsBranchService.GetBranchesAsync() — validate branch exists and isActive
454
- SM->>SM: Validate: branchExternalId in KVS response, isActive = true
455
- SM->>DB: Begin transaction
456
- SM->>DB: UPDATE BranchConfig SET isPrimary = false WHERE isPrimary = true AND merchantId = @tenantId
457
- SM->>DB: UPSERT BranchConfig SET isPrimary = true WHERE branchExternalId = @newId AND merchantId = @tenantId
458
- SM->>DB: Commit transaction
459
- SM-->>BC: ApiResponse success
460
- BC->>NS: showSuccess("Lưu thiết lập thành công")
461
- BC->>BC: modalRef.hide()
462
- ```
463
-
464
- ### 5.3 Branch Invalid Warning Flow (SC9, SC10, SC11)
465
-
466
- ```mermaid
467
- sequenceDiagram
468
- participant M as Merchant (Portal)
469
- participant BC as BranchConfigDialogComponent
470
- participant API as StoreManagement API
471
- participant KVSAPI as KVS API
472
- participant DB as SQL Server
473
-
474
- M->>BC: Open popup → GET /branches
475
- BC->>API: GET /v1/store-management/branches
476
- API->>KVSAPI: IKvsBranchService.GetBranchesAsync()
477
- KVSAPI-->>API: List<BranchModel>
478
- API->>DB: SELECT BranchConfig WHERE merchantId = @tenantId
479
- API->>API: Merge BranchModel[] + BranchConfig[] on branchExternalId
480
-
481
- alt Case 1: Deactivated — KVS returns branch with isActive = false
482
- API->>API: isPrimary BranchConfig matched, BranchModel.isActive = false
483
- API-->>BC: BranchListResponse { branches: [...includes inactive], deletedPrimaryBranch: null }
484
- BC->>BC: Find isPrimary branch with isActive = false → isBranchValid = false
485
- BC->>BC: branchWarningMessage = "Chi nhánh '{name}' đã ngừng hoạt động..."
486
- else Case 2: Deleted — KVS does NOT return the branch
487
- API->>API: isPrimary BranchConfig has no matching BranchModel
488
- API->>DB: Read BranchConfig.primaryBranchName for deleted branch name
489
- API-->>BC: BranchListResponse { branches: [...active only], deletedPrimaryBranch: { branchExternalId, branchName } }
490
- BC->>BC: deletedPrimaryBranch present → isBranchValid = false
491
- BC->>BC: branchWarningMessage = "Chi nhánh '{branchName}' đã bị xóa..."
492
- end
493
-
494
- BC->>BC: Show BranchWarningAlertComponent
495
- BC->>BC: selectedBranchId = null → dropdown shows placeholder "Chọn chi nhánh"
496
-
497
- alt Merchant clicks "Đã hiểu"
498
- M->>BC: Click "Đã hiểu"
499
- BC->>BC: warningDismissed.set(true) → hide alert
500
- end
501
-
502
- Note over DB: Storefront shows empty state "Không có hàng hóa nào!" until Merchant selects new branch (cover bởi LOYAL-26-UC1 BR25 trigger #3)
503
- ```
504
-
505
- ### 5.4 Save Error — Rollback Flow (SC12)
506
-
507
- ```mermaid
508
- sequenceDiagram
509
- participant M as Merchant (Portal)
510
- participant BC as BranchConfigDialogComponent
511
- participant BS as BranchConfigService
512
- participant SM as StoreManagement API
513
- participant DB as SQL Server
514
- participant NS as NotificationService
515
-
516
- M->>BC: Select "Chi nhánh 2", click "Lưu"
517
- BC->>BC: saving.set(true)
518
- BC->>BS: updateBranchConfiguration({ branchExternalId: newBranchExternalId })
519
- BS->>SM: PUT /v1/store-management/storefront/branch-configuration
520
- SM->>DB: Begin transaction
521
- SM->>DB: UPDATE BranchConfig SET isPrimary = false / true
522
- SM->>DB: ❌ Error during commit
523
- SM->>DB: Rollback transaction
524
- SM-->>BS: ApiResponse { success: false, errorCode: "BRANCH_CONFIG_SAVE_FAILED" }
525
- BS-->>BC: Error response
526
- BC->>NS: showError("Đã có lỗi xảy ra. Vui lòng thử lại.")
527
- BC->>BC: saving.set(false)
528
- Note over DB: BranchConfig.isPrimary unchanged — still "Chi nhánh 1"
529
- ```
530
-
531
- ### 5.5 Concurrent Access — Last-Write-Wins (SC13)
532
-
533
- ```mermaid
534
- sequenceDiagram
535
- participant A as Admin A (Portal)
536
- participant B as Admin B (Portal)
537
- participant SM as StoreManagement API
538
- participant DB as SQL Server
539
-
540
- A->>SM: PUT /branch-configuration { branchExternalId: "kvs-branch-2" }
541
- SM->>DB: Swap BranchConfig.isPrimary: branch-1 → false, branch-2 → true
542
- SM-->>A: Success → toast "Lưu thiết lập thành công"
543
-
544
- Note over A,B: Shortly after...
545
-
546
- B->>SM: PUT /branch-configuration { branchExternalId: "kvs-branch-3" }
547
- SM->>DB: Swap BranchConfig.isPrimary: branch-2 → false, branch-3 → true
548
- SM-->>B: Success → toast "Lưu thiết lập thành công"
549
-
550
- Note over DB: Final state: BranchConfig(kvs-branch-3).isPrimary = true (last write wins)
551
- ```
552
-
553
- ### 5.6 Cancel/Close Without Saving (SC5, SC6)
554
-
555
- ```mermaid
556
- sequenceDiagram
557
- participant M as Merchant
558
- participant BC as BranchConfigDialogComponent
559
-
560
- M->>BC: Open popup "Cấu hình chi nhánh và giá bán"
561
- BC->>BC: Load branch list
562
-
563
- alt Click "Bỏ qua"
564
- M->>BC: Click "Bỏ qua"
565
- BC->>BC: modalRef.hide() → no API call
566
- else Click close (✕)
567
- M->>BC: Click ✕ button
568
- BC->>BC: modalRef.hide() → no API call
569
- end
570
-
571
- Note over BC: No data saved — configuration unchanged
572
- ```
573
-
574
- ### 5.7 Recovery from Warning State (SC14)
575
-
576
- > **Context:** Merchant opens popup while primary branch is invalid (KVS deleted/deactivated). Warning banner is shown, dropdown is empty (placeholder), Save is disabled. Merchant selects a valid branch from the dropdown → Save enables → save succeeds → popup closes.
577
-
578
- ```mermaid
579
- sequenceDiagram
580
- participant M as Merchant (Portal)
581
- participant BC as BranchConfigDialogComponent
582
- participant WA as BranchWarningAlertComponent
583
- participant BS as BranchConfigService
584
- participant SM as StoreManagement API
585
- participant KVS as KVS API
586
- participant DB as SQL Server
587
- participant NS as NotificationService
588
-
589
- Note over BC: Initial state from §5.3 — isBranchValid=false,<br/>selectedBranchId=null, warning banner shown,<br/>Save disabled (placeholder "Chọn chi nhánh")
590
-
591
- M->>BC: Select "Chi nhánh 2" from dropdown
592
- BC->>BC: selectedBranchId.set("kvs-branch-2")
593
- BC->>BC: isSaveDisabled = computed(...) → false (selectedBranchId ≠ null && ≠ currentPrimaryBranchId)
594
- Note over BC: Warning banner state — implementation-defined:<br/>banner stays visible until save success (re-evaluates on next popup open).<br/>"Đã hiểu" (SC11) is the only explicit dismiss path before save.
595
-
596
- M->>BC: Click "Lưu"
597
- BC->>BC: saving.set(true)
598
- BC->>BS: updateBranchConfiguration({ branchExternalId: "kvs-branch-2" })
599
- BS->>SM: PUT /v1/store-management/storefront/branch-configuration
600
- SM->>KVS: IKvsBranchService.GetBranchesAsync() — validate kvs-branch-2 exists, isActive=true
601
- SM->>DB: Begin transaction
602
- SM->>DB: UPDATE BranchConfig SET isPrimary=false, primaryBranchName=null WHERE isPrimary=true
603
- SM->>DB: UPSERT BranchConfig SET isPrimary=true, primaryBranchName='Chi nhánh 2' WHERE branchExternalId='kvs-branch-2'
604
- SM->>DB: Commit transaction
605
- SM-->>BC: ApiResponse success
606
- BC->>NS: showSuccess("Lưu thiết lập thành công")
607
- BC->>BC: modalRef.hide()
608
-
609
- Note over DB: BranchConfig.isPrimary now points to Chi nhánh 2 (valid).<br/>Next Consumer storefront load fetches products from kvs-branch-2 via KVS API.<br/>Empty state on storefront resolves automatically.
610
- ```
611
-
612
- **Key integration points:**
613
-
614
- | Step | State transition | Verified by |
615
- |------|------------------|-------------|
616
- | Select branch from dropdown | `selectedBranchId: null → "kvs-branch-2"` | SC14 (When tôi chọn chi nhánh) |
617
- | Save button computed | `isSaveDisabled: true → false` | SC14 (Then hệ thống kích hoạt nút "Lưu") |
618
- | Save backend swap | `BranchConfig.isPrimary: invalid-branch → "kvs-branch-2"` | SC14 (Then hệ thống cập nhật cấu hình) |
619
- | Storefront empty state resolves | Consumer next load fetches from new branch | Cross-system effect (cover bởi LOYAL-26-UC1) |
620
-
621
- ### 5.8 Cancel During Warning State (SC15)
622
-
623
- > **Context:** Merchant opens popup while primary branch is invalid, then clicks "Bỏ qua" or ✕ **without** selecting a new branch. Popup closes; BranchConfig stays pointing at invalid branch; warning persists; Consumer storefront keeps showing empty state. Differs from SC5/SC6 in **state preservation assertion**, not in flow logic.
624
-
625
- ```mermaid
626
- sequenceDiagram
627
- participant M as Merchant (Portal)
628
- participant BC as BranchConfigDialogComponent
629
- participant DB as SQL Server
630
- participant ZMA as Zalo Mini App (Consumer)
631
-
632
- Note over BC: Initial state from §5.3 — isBranchValid=false,<br/>selectedBranchId=null, warning banner shown,<br/>BranchConfig.isPrimary still points to invalid branch
633
-
634
- alt Click "Bỏ qua"
635
- M->>BC: Click "Bỏ qua"
636
- BC->>BC: modalRef.hide() — no API call
637
- else Click close (✕)
638
- M->>BC: Click ✕ button
639
- BC->>BC: modalRef.hide() — no API call
640
- end
641
-
642
- Note over DB: BranchConfig.isPrimary UNCHANGED — still points to invalid (deleted/deactivated) branch.<br/>No reset, no auto-select, no notification.
643
-
644
- Note over ZMA: Consumer storefront continues to show empty state "Không có hàng hóa nào!"<br/>(cover bởi LOYAL-26-UC1 BR25 trigger #3)
645
-
646
- Note over M: Next time Merchant opens popup → §5.3 flow re-runs → warning banner shown again.
647
- ```
648
-
649
- **Implementation note:** Same `modalRef.hide()` call as SC5/SC6 — no special branch needed in code. Tests assert that after cancel:
650
- - `BranchConfig.isPrimary` row in DB unchanged
651
- - Re-opening the popup re-renders the warning banner (idempotent §5.3 flow)
652
- - No API call made on cancel
653
-
654
- ---
655
-
656
- ## 6. Integration Points
657
-
658
- | Integration | Direction | Method | Description |
659
- |-------------|-----------|--------|-------------|
660
- | Portal → StoreManagement API | Outbound (client) | REST (Bearer RS256) | Branch list GET (reuse LOYAL-16) + branch config PUT |
661
- | StoreManagement API → KVS API | Outbound (server) | REST (KVS-Merchant-Id header) | Fetch branch base data via `IKvsBranchService` (Redis cache 5min) |
662
-
663
- ### 6.1 Event Bus (Kafka)
664
-
665
- No new Kafka events introduced by LOYAL-23. Branch base data (name, phone, address, isActive) is fetched live from KVS API — no SyncData dependency.
666
-
667
- ### 6.2 Cross-Service Dependencies
668
-
669
- | Dependent Service | What's Needed | Contract | Status |
670
- |---|---|---|---|
671
- | Identity API | Admin gate at token exchange | `POST /api/v1/portal/token-exchange` | ✅ Exists |
672
- | StoreManagement API | Branch list for dropdown | `GET /v1/store-management/branches` — `BranchDisplayDto` includes `IsPrimary` | ✅ Exists |
673
- | KVS API | Branch base data (via IKvsBranchService) | `GET /api/v3/branches` (Redis cache 5min) | ✅ Exists (shared with LOYAL-16) |
674
-
675
- > **No new cross-service dependencies.** Only new endpoint: `PUT /storefront/branch-configuration`. `BranchDisplayDto` already includes `IsPrimary` from BranchConfig DB table.
676
-
677
- ---
678
-
679
- ## 7. Security & Authorization
680
-
681
- ### 7.1 Authentication
682
-
683
- **Primary flow:** KVS → KVLoyalty Widget → Token Exchange → Portal Session Token (Bearer RS256, 30 min TTL)
684
-
685
- Same as LOYAL-16. All Portal → StoreManagement API requests carry `Authorization: Bearer <portalSessionToken>`.
686
-
687
- ### 7.2 Authorization Rules
688
-
689
- > **Admin gate is enforced upstream by Identity API token-exchange (out of scope per BR1).** See §11 (Cross-cutting & Assumptions) for the non-admin denial reference flow.
690
-
691
- | Action | Required Role/Permission | Description | Trace |
692
- |--------|--------------------------|-------------|-------|
693
- | View branch configuration | Admin | Popup mounted only after successful token-exchange; admin role enforced upstream | — (out of scope) |
694
- | Update primary branch | Admin | PUT request reaches API only via authenticated portal session; tenant isolation via `MerchantId` filter | SC1 |
695
-
696
- ---
697
-
698
- ## 8. Error Handling & Edge Cases
699
-
700
- | Scenario | Strategy | Details | Trace |
701
- |----------|----------|---------|-------|
702
- | Primary branch deactivated/deleted in KVS | Warning in popup (client-side) + empty state on Consumer storefront | KVS API returns `isActive = false` OR branch missing → Portal shows warning. **Consumer storefront shows empty state "Không có hàng hóa nào!"** (cover bởi LOYAL-26-UC1 BR25 trigger #3). No auto-replacement. | SC9, SC10, BR3 |
703
- | KVS API unavailable | Graceful degradation | GET /branches returns empty list. Portal shows empty branch dropdown. Config cannot be saved until KVS recovers. | SC9, BR3 |
704
- | Dismiss branch warning | Hide alert (client-side) | "Đã hiểu" hides alert. No API call. | SC11, BR3 |
705
- | Save error | Rollback + error toast | "Đã có lỗi xảy ra. Vui lòng thử lại." Transaction rolled back. | SC12, BR5 |
706
- | Concurrent edits | Last-write-wins | No conflict detection. Second save overwrites first. | SC13, BR5 |
707
- | Cancel/close popup (normal state) | No save | "Bỏ qua" or ✕ closes popup. No API call. | SC5, SC6, BR6 |
708
- | Cancel/close popup (warning state) | No save, state preserved | "Bỏ qua" or ✕ closes popup. `BranchConfig.isPrimary` unchanged (still points to invalid branch). Warning re-renders on next open; Consumer storefront empty state persists. | SC15, BR3, BR6 |
709
- | Recovery from warning state | Save enable on branch select | After choosing valid branch from dropdown, `isSaveDisabled` becomes `false`. Standard save flow follows (§5.7). | SC14, BR3, BR6 |
710
- | Branch unchanged | Save disabled | "Lưu" disabled when `selectedBranchId === currentPrimaryBranchId`. Server rejects with `BRANCH_NOT_CHANGED`. | SC7, BR1 |
711
- | Branch field empty (normal state) | Not possible | Dropdown pre-selects current primary. No empty option. | SC8, BR1 |
712
- | Orders keep original branch | No change | Order–Branch association designed in Order module scope. | SC3, BR2 |
713
- | Price list | Hardcoded | Always "Bảng giá chung", read-only in Portal. No backend field. | UC2-SC1, UC2-SC2, UC2-BR1 |
714
-
715
- ---
716
-
717
- ## 9. Design Decisions
718
-
719
- | # | Decision | Rationale | Alternatives Considered |
720
- |---|----------|-----------|-------------------------|
721
- | 1 | **Use `BranchConfig.isPrimary` for chi nhánh nhận đơn** | `isPrimary` is a loyalty-owned config field, stored in `BranchConfig` DB table alongside `isDisplayedOnStorefront`. Branch base data (name, phone, address) comes from KVS API. | Store in MerchantSetting JSON — rejected: isPrimary is per-branch, not per-merchant |
722
- | 2 | **Branch base data from KVS API (not DB sync)** | Follows API-sourced model pattern (same as Product). Branch data always fresh from KVS. No sync drift. | SyncData pattern — rejected: eliminated Hybrid entity pattern per architecture alignment |
723
- | 3 | **Reuse `GET /branches` + `BranchDisplayDto` with `IsPrimary`** | API already exists (LOYAL-16). Merged view includes KVS data + BranchConfig flags. No new GET endpoint needed. | Dedicated GET /branch-configuration — rejected: duplicates available data |
724
- | 4 | **Warning detection client-side** | Portal compares branch list: if no branch has `IsPrimary = true` in active list → invalid. KVS `isActive = false` triggers warning. | Backend warning API — rejected: Portal has all the data |
725
- | 5 | **Separate popup from LOYAL-16** | Distinct concern (branch selection vs store info). Confirmed by distinct Figma frames. | Merge into LOYAL-16 popup — rejected: too many concerns |
726
- | 6 | **Last-write-wins** | Consistent with LOYAL-16. Per BR5. | OCC with Revision — rejected per BR5 |
727
- | 7 | **Empty state on Consumer storefront when branch invalid** | Empty state signals fail-safe: Consumer cannot order products from a branch that no longer exists. Avoids serving orphan products. Aligns with LOYAL-26-UC1 BR25 trigger #3. | (a) Frozen old data — rejected: Consumer may try to buy SP from non-existent branch → order errors. (b) Auto-select next branch — rejected per BR3 (no proactive replacement). |
728
- | 8 | **Price list hardcoded** | Per UC2-BR1, always "Bảng giá chung". Frontend constant. | Store in DB — rejected: unnecessary for fixed value |
729
-
730
- ### NFR-to-Design Mapping
731
-
732
- | NFR Category | PRD Requirement | Design Decision |
733
- |---|---|---|
734
- | Multi-tenant isolation | Tenant data never shared | `MerchantId` on BranchConfig + EF Core Global Query Filter |
735
- | Data consistency | Concurrent: last-write-wins (BR5) | DB transaction for BranchConfig.isPrimary swap, no OCC |
736
- | Data consistency | Orders not affected (BR2) | Order–Branch association designed in Order module scope |
737
- | Responsiveness | Changes reflected immediately (BR4) | isPrimary saved synchronously |
738
- | Cross-system consistency | Invalid branch → Consumer storefront empty state (BR3) | Storefront product fetch returns empty when isPrimary branch invalid (cover bởi LOYAL-26-UC1 BR25 trigger #3); resolves automatically once Merchant selects a valid branch |
739
-
740
- ---
741
-
742
- ## 10. UC Coverage
743
-
744
- | UC | Feature | Sections Covered | Status |
745
- |----|---------|------------------|--------|
746
- | LOYAL-23-UC1 | Cấu hình chi nhánh nhận đơn và đồng bộ dữ liệu | §1–§9 (all sections) | ✅ Covered |
747
- | LOYAL-23-UC2 | Xem và áp dụng bảng giá bán cho gian hàng | §4.2 (price list in UI), §4.5 (Section 2), §8, §9 (#8) | ✅ Covered |
748
-
749
- ### UC1 Scenario Coverage
750
-
751
- | Scenario | Section | Business Rule |
752
- |----------|---------|---------------|
753
- | SC1: Cập nhật chi nhánh nhận đơn | §4.2 (PUT), §5.2 | BR1, BR6 |
754
- | SC2: Chỉ hiển thị chi nhánh active, sắp xếp mới→cũ | §4.2 (GET), §4.5.3 (client filter+sort), §5.1 | BR1 |
755
- | SC3: Giữ nguyên chi nhánh đơn cũ | §3.1 (Order), §8 (delegated to Order module) | BR2 |
756
- | SC4: Đồng bộ sản phẩm theo chi nhánh mới | §3.3 (products API-sourced — isPrimary change causes next product fetch to use new branch via KVS API) | BR4 |
757
- | SC5: "Bỏ qua" (normal state) | §5.6 | BR6 |
758
- | SC6: Close (✕) (normal state) | §5.6 | BR6 |
759
- | SC7: Save disabled khi không đổi | §4.3, §4.5.3, §8 | BR1 |
760
- | SC8: Không bỏ trống chi nhánh (normal state) | §4.5.3, §8 | BR1 |
761
- | SC9: Cảnh báo chi nhánh invalid (Scenario Outline: deleted + deactivated) | §4.5.1, §5.3, §8 | BR3 |
762
- | SC10: Gian hàng Zalo empty state khi CN invalid | §3.3, §5.3, §8, §9 (#7), §9 NFR | BR3 |
763
- | SC11: Đóng cảnh báo "Đã hiểu" | §4.5.3, §5.3, §8 | BR3 |
764
- | SC12: Lỗi lưu → rollback | §4.3, §5.4, §8 | BR5 |
765
- | SC13: Concurrent → last-write-wins | §5.5, §9 (#6), §8 | BR5 |
766
- | SC14: Recovery from warning state | §5.7, §8 | BR3, BR6 |
767
- | SC15: Cancel during warning state | §5.8, §8 | BR3, BR6 |
768
-
769
- ### UC2 Scenario Coverage
770
-
771
- | Scenario | Section | Business Rule |
772
- |----------|---------|---------------|
773
- | UC2-SC1: "Bảng giá chung" read-only | §4.5.1 (Section 2), §8 | UC2-BR1 |
774
- | UC2-SC2: Giữ nguyên bảng giá khi đổi chi nhánh | §8, §9 (#8) | UC2-BR1 |
775
-
776
- ---
777
-
778
- ## 11. Cross-cutting & Assumptions (Out-of-Scope Reference)
779
-
780
- This section documents upstream concerns that LOYAL-23 **depends on but does not implement**. They are kept here for cross-team context and onboarding clarity.
781
-
782
- ### 11.1 Admin Gate (Identity API token-exchange)
783
-
784
- > Per [BDD BR1](../../../kvloyalty-business/specs/features/store-management/LOYAL-23-UC1-cau-hinh-chi-nhanh-dong-bo-du-lieu.feature): "Permission 'tài khoản admin' enforce ở Portal middleware (**out of scope UC này**)."
785
-
786
- Admin role enforcement happens at the KVS → KVLoyalty Widget → Identity API token-exchange boundary, before the Portal popup is ever mounted. LOYAL-23 implementation assumes any caller reaching `BranchConfigDialogComponent` or the `PUT /branch-configuration` endpoint is already an authenticated admin in the current tenant.
787
-
788
- **Reference flow — non-admin denial** (cross-cutting, not traced to any LOYAL-23 SC):
789
-
790
- ```mermaid
791
- sequenceDiagram
792
- participant U as Non-Admin User
793
- participant KVS as KVS App
794
- participant W as KVLoyalty Widget
795
- participant ID as Identity API
796
-
797
- U->>KVS: Truy cập "Cấu hình chi nhánh"
798
- KVS->>W: kvloyalty.init(appId, tenantId, branchId, userId, kvsJwt)
799
- W->>ID: POST /api/v1/portal/token-exchange { tenantId, branchId, userId }
800
- ID->>ID: Validate KVS JWT → check role
801
- ID-->>W: 403 Forbidden (non-admin user)
802
- W-->>KVS: Error callback — access denied
803
- Note over U: Hệ thống từ chối truy cập tính năng — popup không được mount
804
- ```
805
-
806
- **Owned by:** Identity API team. See [portal-auth-technical-design.md](../authen/portal-auth-technical-design.md).
807
-
808
- ### 11.2 Consumer Storefront Empty State (LOYAL-26-UC1)
809
-
810
- When `BranchConfig.isPrimary` points to a deleted/deactivated branch, the Consumer-facing Zalo Mini App storefront renders an empty state "Không có hàng hóa nào!". This UI is implemented in LOYAL-26-UC1, BR25 trigger #3. LOYAL-23 only ensures `isPrimary` reflects the invalid state correctly so that the Storefront product fetch returns empty.
811
-
812
- **Owned by:** Storefront team. See LOYAL-26-UC1-SC12.
813
-
814
- ### 11.3 Order–Branch Association (Order Module)
815
-
816
- When `BranchConfig.isPrimary` changes, existing orders must keep their original branch assignment (per [BDD BR2](../../../kvloyalty-business/specs/features/store-management/LOYAL-23-UC1-cau-hinh-chi-nhanh-dong-bo-du-lieu.feature)). LOYAL-23 does not touch the Order entity; this guarantee depends on the Order module snapshotting `branchId` at order creation time.
817
-
818
- **Owned by:** Order module team (design pending).
819
-
820
- ---
821
-
822
- ## Figma Design References
823
-
824
- <!-- @figma.url: https://www.figma.com/design/2AAbMqr0IwzvQffuUDZb4A/Merchant?node-id=78-35333 -->
825
- - Design: [Figma — Cấu hình chi nhánh và giá bán](https://www.figma.com/design/2AAbMqr0IwzvQffuUDZb4A/Merchant?node-id=78-35333)
826
- - Exported: 2026-03-20