Skip to content

Commit ff82a4d

Browse files
committed
refactor/platform-settings and fix bugs
1 parent 216e49b commit ff82a4d

91 files changed

Lines changed: 5343 additions & 772 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
# PlatformSettings Database Structure
2+
3+
## Overview
4+
5+
There are **3 singleton parent tables** and **4 child/collection tables**.
6+
All singleton parents support soft delete. Child collection tables do **not**
7+
have soft-delete columns (hard delete only).
8+
9+
---
10+
11+
## Singleton Parent Tables (1 row each)
12+
13+
### `homepage_settings`
14+
15+
| Column | Type | Single / List | How Populated |
16+
|---|---|---|---|
17+
| `id` | `uniqueidentifier` PK | Single | `ReferenceDataSeeder` creates, `PlatformSettingsSeeder` enriches |
18+
| `objective_ar` | `nvarchar(1000)` | Single | Seeder + Admin API `PUT /api/admin/settings/homepage` |
19+
| `objective_en` | `nvarchar(1000)` | Single | Seeder + Admin API |
20+
| `video_url` | `nvarchar(max)` | Single | Seeder + Admin API |
21+
| `cce_concepts_ar` | `nvarchar(max)` | Single | Seeder + Admin API |
22+
| `cce_concepts_en` | `nvarchar(max)` | Single | Seeder + Admin API |
23+
| `created_by_id`, `created_on`, `last_modified_by_id`, `last_modified_on` | audit | Single | Auto |
24+
| `deleted_by_id`, `deleted_on`, `is_deleted` | soft delete | Single | Auto |
25+
| `row_version` | `rowversion` | Single | Auto (concurrency) |
26+
27+
**LocalizedText mapping:** `Objective``objective_ar` / `objective_en`
28+
29+
---
30+
31+
### `about_settings`
32+
33+
| Column | Type | Single / List | How Populated |
34+
|---|---|---|---|
35+
| `id` | `uniqueidentifier` PK | Single | `ReferenceDataSeeder` creates, `PlatformSettingsSeeder` enriches |
36+
| `description_ar` | `nvarchar(1000)` | Single | Seeder + Admin API `PUT /api/admin/settings/about` |
37+
| `description_en` | `nvarchar(1000)` | Single | Seeder + Admin API |
38+
| `how_to_use_video_url` | `nvarchar(max)` | Single | Seeder + Admin API |
39+
| `created_by_id`, `created_on`, `last_modified_by_id`, `last_modified_on` | audit | Single | Auto |
40+
| `deleted_by_id`, `deleted_on`, `is_deleted` | soft delete | Single | Auto |
41+
| `row_version` | `rowversion` | Single | Auto (concurrency) |
42+
43+
**LocalizedText mapping:** `Description``description_ar` / `description_en`
44+
45+
---
46+
47+
### `policies_settings`
48+
49+
| Column | Type | Single / List | How Populated |
50+
|---|---|---|---|
51+
| `id` | `uniqueidentifier` PK | Single | `ReferenceDataSeeder` creates bare row |
52+
| `created_by_id`, `created_on`, `last_modified_by_id`, `last_modified_on` | audit | Single | Auto |
53+
| `deleted_by_id`, `deleted_on`, `is_deleted` | soft delete | Single | Auto |
54+
| `row_version` | `rowversion` | Single | Auto (concurrency) |
55+
56+
**Note:** No admin endpoint updates this table directly. It is managed
57+
indirectly through its child `policy_sections`.
58+
59+
---
60+
61+
## Child / Collection Tables (0..N rows per parent)
62+
63+
### `homepage_countries`**List** of country links
64+
65+
| Column | Type | Single / List | How Populated |
66+
|---|---|---|---|
67+
| `id` | `uniqueidentifier` PK | Single per row | Seeder + Admin API |
68+
| `homepage_settings_id` | `uniqueidentifier` FK | Single per row | Set by `SyncCountries()` domain method |
69+
| `country_id` | `uniqueidentifier` | Single per row | Seeder + Admin API |
70+
| `order_index` | `int` | Single per row | Auto (0, 1, 2...) |
71+
| `created_by_id`, `created_on`, `last_modified_by_id`, `last_modified_on` | audit | Single per row | Auto |
72+
73+
**Populated by:**
74+
- **Seeder:** `PlatformSettingsSeeder` adds 5 GCC countries (SAU, ARE, KWT, QAT, BHR)
75+
- **Admin API:** `PUT /api/admin/settings/homepage` sends `ParticipatingCountryIds: ["guid", "guid"]``SyncCountries()` adds/removes/reorders
76+
77+
---
78+
79+
### `glossary_entries`**List** of entries
80+
81+
| Column | Type | Single / List | How Populated |
82+
|---|---|---|---|
83+
| `id` | `uniqueidentifier` PK | Single per row | Seeder + Admin API |
84+
| `about_settings_id` | `uniqueidentifier` FK | Single per row | Set by `AddGlossaryEntry()` |
85+
| `term_ar` | `nvarchar(100)` | Single per row | Seeder + Admin API |
86+
| `term_en` | `nvarchar(100)` | Single per row | Seeder + Admin API |
87+
| `definition_ar` | `nvarchar(1000)` | Single per row | Seeder + Admin API |
88+
| `definition_en` | `nvarchar(1000)` | Single per row | Seeder + Admin API |
89+
| `order_index` | `int` | Single per row | Auto |
90+
| `created_by_id`, `created_on`, `last_modified_by_id`, `last_modified_on` | audit | Single per row | Auto |
91+
92+
**LocalizedText mappings:**
93+
- `Term``term_ar` / `term_en`
94+
- `Definition``definition_ar` / `definition_en`
95+
96+
**Populated by:**
97+
- **Seeder:** `PlatformSettingsSeeder` adds 4 entries (CCE, DAC, CCUS, LCOE)
98+
- **Admin API:**
99+
- `POST /api/admin/settings/about/glossary`
100+
- `PUT /api/admin/settings/about/glossary/{id}`
101+
- `DELETE /api/admin/settings/about/glossary/{id}`
102+
103+
---
104+
105+
### `knowledge_partners`**List** of partners
106+
107+
| Column | Type | Single / List | How Populated |
108+
|---|---|---|---|
109+
| `id` | `uniqueidentifier` PK | Single per row | Seeder + Admin API |
110+
| `about_settings_id` | `uniqueidentifier` FK | Single per row | Set by `AddKnowledgePartner()` |
111+
| `name_ar` | `nvarchar(200)` | Single per row | Seeder + Admin API |
112+
| `name_en` | `nvarchar(200)` | Single per row | Seeder + Admin API |
113+
| `description_ar` | `nvarchar(1000)` | Single per row | Seeder + Admin API |
114+
| `description_en` | `nvarchar(1000)` | Single per row | Seeder + Admin API |
115+
| `logo_url` | `nvarchar(max)` | Single per row | Seeder + Admin API |
116+
| `website_url` | `nvarchar(max)` | Single per row | Seeder + Admin API |
117+
| `order_index` | `int` | Single per row | Auto |
118+
| `created_by_id`, `created_on`, `last_modified_by_id`, `last_modified_on` | audit | Single per row | Auto |
119+
120+
**LocalizedText mappings:**
121+
- `Name``name_ar` / `name_en`
122+
- `Description``description_ar` / `description_en`
123+
124+
**Populated by:**
125+
- **Seeder:** `PlatformSettingsSeeder` adds 3 partners (KAPSARC, IRENA, GCEP)
126+
- **Admin API:**
127+
- `POST /api/admin/settings/about/knowledge-partners`
128+
- `PUT /api/admin/settings/about/knowledge-partners/{id}`
129+
- `DELETE /api/admin/settings/about/knowledge-partners/{id}`
130+
131+
---
132+
133+
### `policy_sections`**List** of sections
134+
135+
| Column | Type | Single / List | How Populated |
136+
|---|---|---|---|
137+
| `id` | `uniqueidentifier` PK | Single per row | Seeder + Admin API |
138+
| `policies_settings_id` | `uniqueidentifier` FK | Single per row | Set by `AddSection()` |
139+
| `type` | `int` (enum) | Single per row | Seeder + Admin API |
140+
| `title_ar` | `nvarchar(500)` | Single per row | Seeder + Admin API |
141+
| `title_en` | `nvarchar(500)` | Single per row | Seeder + Admin API |
142+
| `content_ar` | `nvarchar(max)` | Single per row | Seeder + Admin API |
143+
| `content_en` | `nvarchar(max)` | Single per row | Seeder + Admin API |
144+
| `order_index` | `int` | Single per row | Auto |
145+
| `created_by_id`, `created_on`, `last_modified_by_id`, `last_modified_on` | audit | Single per row | Auto |
146+
147+
**LocalizedText mappings:**
148+
- `Title``title_ar` / `title_en`
149+
- `Content``content_ar` / `content_en`
150+
151+
**Populated by:**
152+
- **Seeder:** `PlatformSettingsSeeder` adds 3 sections (Terms, Privacy, FAQ)
153+
- **Admin API:**
154+
- `POST /api/admin/settings/policies/sections`
155+
- `PUT /api/admin/settings/policies/sections/{id}`
156+
- `PUT /api/admin/settings/policies/sections/{id}/order`
157+
- `DELETE /api/admin/settings/policies/sections/{id}`
158+
159+
---
160+
161+
## Key Relationships
162+
163+
| Child Table | FK Column | Parent Table | Delete Behavior |
164+
|---|---|---|---|
165+
| `homepage_countries` | `homepage_settings_id` | `homepage_settings` | Cascade |
166+
| `glossary_entries` | `about_settings_id` | `about_settings` | Cascade |
167+
| `knowledge_partners` | `about_settings_id` | `about_settings` | Cascade |
168+
| `policy_sections` | `policies_settings_id` | `policies_settings` | Cascade |
169+
170+
`homepage_countries.country_id` is a **logical reference** to the `countries`
171+
table; there is no database-enforced foreign key constraint.
172+
173+
---
174+
175+
## LocalizedText Column Mappings
176+
177+
Every bilingual field is stored as two columns (`_ar` / `_en`) via EF Core
178+
owned entities (`OwnsOne`):
179+
180+
| Table | Property | AR Column | EN Column | Max Length |
181+
|---|---|---|---|---|
182+
| `homepage_settings` | `Objective` | `objective_ar` | `objective_en` | 1000 |
183+
| `about_settings` | `Description` | `description_ar` | `description_en` | 1000 |
184+
| `glossary_entries` | `Term` | `term_ar` | `term_en` | 100 |
185+
| `glossary_entries` | `Definition` | `definition_ar` | `definition_en` | 1000 |
186+
| `knowledge_partners` | `Name` | `name_ar` | `name_en` | 200 |
187+
| `knowledge_partners` | `Description` | `description_ar` | `description_en` | 1000 |
188+
| `policy_sections` | `Title` | `title_ar` | `title_en` | 500 |
189+
| `policy_sections` | `Content` | `content_ar` | `content_en` | max |
190+
191+
---
192+
193+
## Public API Read Models
194+
195+
- **Homepage:** Returns `VideoUrl`, `Objective` (ar/en), `CceConceptsAr`,
196+
`CceConceptsEn`, linked `Countries` (joined with `countries` table for
197+
name/flag/ISO), and active `HomepageSections` (from the separate
198+
`homepage_sections` content table).
199+
200+
- **About:** Returns `Description` (ar/en), `HowToUseVideoUrl`, ordered
201+
`GlossaryEntries`, and ordered `KnowledgePartners`.
202+
203+
- **Policies:** Returns ordered `PolicySections` with `Type`, `Title` (ar/en),
204+
and `Content` (ar/en) — currently as **single HTML strings**.
205+
206+
---
207+
208+
## The Problem
209+
210+
`policy_sections.content_ar` and `policy_sections.content_en` are currently
211+
**Single values** (one big HTML string per section). You want them to become
212+
a **List** so the API returns:
213+
214+
```json
215+
{
216+
"contentItems": [
217+
{ "ar": "1. القبول بالشروط", "en": "1. Acceptance of Terms" },
218+
{ "ar": "باستخدامك لهذه المنصة...", "en": "By using this platform..." }
219+
]
220+
}
221+
```
222+
223+
This would require a **new child table** following the exact same pattern as
224+
`glossary_entries` and `knowledge_partners`.
225+
226+
---
227+
228+
## Related Files
229+
230+
| Layer | Path |
231+
|---|---|
232+
| Domain | `src/CCE.Domain/PlatformSettings/` |
233+
| EF Config | `src/CCE.Infrastructure/Persistence/Configurations/PlatformSettings/` |
234+
| Migrations | `src/CCE.Infrastructure/Persistence/Migrations/` |
235+
| Commands | `src/CCE.Application/PlatformSettings/Commands/` |
236+
| Queries | `src/CCE.Application/PlatformSettings/Queries/` |
237+
| Public Queries | `src/CCE.Application/PlatformSettings/Public/Queries/` |
238+
| Internal API | `src/CCE.Api.Internal/Endpoints/` |
239+
| External API | `src/CCE.Api.External/Endpoints/` |
240+
| Seeders | `src/CCE.Seeder/Seeders/` |

backend/src/CCE.Api.External/appsettings.Production.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"Infrastructure": {
99
"SqlConnectionString": "Server=db52197.public.databaseasp.net; Database=db52197; User Id=db52197; Password=3Mm!x5#Y?rR9; Encrypt=True; TrustServerCertificate=True; MultipleActiveResultSets=True;",
1010
"RedisConnectionString": "rediss://default:gQAAAAAAAYY8AAIgcDIwYmNkMjFmM2Q0NDk0MGRiOWZhZjczNDE1NmMwZjFlMw@game-elk-99900.upstash.io:6379",
11+
"MediaUploadsRoot": "./wwwroot/media/",
1112
"MeilisearchUrl": "http://localhost:7700",
1213
"MeilisearchMasterKey": "dev-meili-master-key-change-me",
1314
"OutputCacheTtlSeconds": 60

backend/src/CCE.Api.Internal/Endpoints/AboutSettingsEndpoints.cs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,9 @@ public static IEndpointRouteBuilder MapAboutSettingsEndpoints(this IEndpointRout
3232

3333
about.MapPut("", async (UpdateAboutSettingsRequest body, IMediator mediator, CancellationToken ct) =>
3434
{
35-
var rowVersion = string.IsNullOrEmpty(body.RowVersion)
36-
? System.Array.Empty<byte>()
37-
: System.Convert.FromBase64String(body.RowVersion);
3835
var cmd = new UpdateAboutSettingsCommand(
3936
body.DescriptionAr, body.DescriptionEn,
40-
body.HowToUseVideoUrl, rowVersion);
37+
body.HowToUseVideoUrl);
4138
var result = await mediator.Send(cmd, ct).ConfigureAwait(false);
4239
return result.ToHttpResult();
4340
})
@@ -121,8 +118,7 @@ public static IEndpointRouteBuilder MapAboutSettingsEndpoints(this IEndpointRout
121118
public sealed record UpdateAboutSettingsRequest(
122119
string DescriptionAr,
123120
string DescriptionEn,
124-
string? HowToUseVideoUrl,
125-
string RowVersion);
121+
string? HowToUseVideoUrl);
126122

127123
public sealed record CreateGlossaryEntryRequest(
128124
string TermAr,

backend/src/CCE.Api.Internal/Endpoints/HomepageSettingsEndpoints.cs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,13 @@ public static IEndpointRouteBuilder MapHomepageSettingsEndpoints(this IEndpointR
2626

2727
settings.MapPut("", async (UpdateHomepageSettingsRequest body, IMediator mediator, CancellationToken ct) =>
2828
{
29-
var rowVersion = string.IsNullOrEmpty(body.RowVersion)
30-
? System.Array.Empty<byte>()
31-
: System.Convert.FromBase64String(body.RowVersion);
3229
var cmd = new UpdateHomepageSettingsCommand(
3330
body.VideoUrl,
3431
body.ObjectiveAr,
3532
body.ObjectiveEn,
3633
body.CceConceptsAr,
3734
body.CceConceptsEn,
38-
body.ParticipatingCountryIds,
39-
rowVersion);
35+
body.ParticipatingCountryIds);
4036
var result = await mediator.Send(cmd, ct).ConfigureAwait(false);
4137
return result.ToHttpResult();
4238
})
@@ -53,5 +49,4 @@ public sealed record UpdateHomepageSettingsRequest(
5349
string ObjectiveEn,
5450
string CceConceptsAr,
5551
string CceConceptsEn,
56-
System.Collections.Generic.IReadOnlyList<System.Guid> ParticipatingCountryIds,
57-
string RowVersion);
52+
System.Collections.Generic.IReadOnlyList<System.Guid> ParticipatingCountryIds);

backend/src/CCE.Api.Internal/Endpoints/PoliciesSettingsEndpoints.cs

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
using CCE.Application.Common;
33
using CCE.Application.PlatformSettings.Commands.CreatePolicySection;
44
using CCE.Application.PlatformSettings.Commands.DeletePolicySection;
5-
using CCE.Application.PlatformSettings.Commands.UpdatePoliciesSettings;
5+
using CCE.Application.PlatformSettings.Commands.ReorderPolicySection;
66
using CCE.Application.PlatformSettings.Commands.UpdatePolicySection;
77
using CCE.Application.PlatformSettings.Queries.GetPoliciesSettings;
88
using CCE.Domain;
@@ -27,18 +27,6 @@ public static IEndpointRouteBuilder MapPoliciesSettingsEndpoints(this IEndpointR
2727
.RequireAuthorization(Permissions.Page_PolicyEdit)
2828
.WithName("GetPoliciesSettings");
2929

30-
policies.MapPut("", async (UpdatePoliciesSettingsRequest body, IMediator mediator, CancellationToken ct) =>
31-
{
32-
var rowVersion = string.IsNullOrEmpty(body.RowVersion)
33-
? System.Array.Empty<byte>()
34-
: System.Convert.FromBase64String(body.RowVersion);
35-
var cmd = new UpdatePoliciesSettingsCommand(rowVersion);
36-
var result = await mediator.Send(cmd, ct).ConfigureAwait(false);
37-
return result.ToHttpResult();
38-
})
39-
.RequireAuthorization(Permissions.Page_PolicyEdit)
40-
.WithName("UpdatePoliciesSettings");
41-
4230
policies.MapPost("/sections", async (
4331
CreatePolicySectionRequest body,
4432
IMediator mediator, CancellationToken ct) =>
@@ -64,6 +52,18 @@ public static IEndpointRouteBuilder MapPoliciesSettingsEndpoints(this IEndpointR
6452
.RequireAuthorization(Permissions.Page_PolicyEdit)
6553
.WithName("UpdatePolicySection");
6654

55+
policies.MapPut("/sections/{id:guid}/order", async (
56+
System.Guid id,
57+
ReorderPolicySectionRequest body,
58+
IMediator mediator, CancellationToken ct) =>
59+
{
60+
var cmd = new ReorderPolicySectionCommand(id, body.OrderIndex);
61+
var result = await mediator.Send(cmd, ct).ConfigureAwait(false);
62+
return result.ToHttpResult();
63+
})
64+
.RequireAuthorization(Permissions.Page_PolicyEdit)
65+
.WithName("ReorderPolicySection");
66+
6767
policies.MapDelete("/sections/{id:guid}", async (
6868
System.Guid id,
6969
IMediator mediator, CancellationToken ct) =>
@@ -78,9 +78,6 @@ public static IEndpointRouteBuilder MapPoliciesSettingsEndpoints(this IEndpointR
7878
}
7979
}
8080

81-
public sealed record UpdatePoliciesSettingsRequest(
82-
string RowVersion);
83-
8481
public sealed record CreatePolicySectionRequest(
8582
int Type,
8683
string TitleAr,
@@ -93,3 +90,5 @@ public sealed record UpdatePolicySectionRequest(
9390
string TitleEn,
9491
string ContentAr,
9592
string ContentEn);
93+
94+
public sealed record ReorderPolicySectionRequest(int OrderIndex);

backend/src/CCE.Api.Internal/appsettings.Production.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"SqlConnectionString": "Server=db52197.public.databaseasp.net; Database=db52197; User Id=db52197; Password=3Mm!x5#Y?rR9; Encrypt=True; TrustServerCertificate=True; MultipleActiveResultSets=True;",
1010
"RedisConnectionString": "rediss://default:gQAAAAAAAYY8AAIgcDIwYmNkMjFmM2Q0NDk0MGRiOWZhZjczNDE1NmMwZjFlMw@game-elk-99900.upstash.io:6379",
1111
"LocalUploadsRoot": "./backend/uploads/",
12+
"MediaUploadsRoot": "./wwwroot/media/",
1213
"ClamAvHost": "localhost",
1314
"ClamAvPort": 3310
1415
},

backend/src/CCE.Application/Common/Interfaces/ICceDbContext.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,5 +76,7 @@ public interface ICceDbContext
7676
void Delete<T>(T entity) where T : class;
7777
void DeleteRange<T>(System.Collections.Generic.IEnumerable<T> entities) where T : class;
7878

79+
void SetExpectedRowVersion<T>(T entity, byte[] expectedRowVersion) where T : class;
80+
7981
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
8082
}

backend/src/CCE.Application/Messages/SystemCode.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,11 @@ public static class SystemCode
159159
public const string CON027 = "CON027"; // Resource deleted
160160
public const string CON028 = "CON028"; // Resource published
161161

162+
// ─── Media Success ───
163+
public const string CON029 = "CON029"; // Media uploaded
164+
public const string CON036 = "CON036"; // Media updated
165+
public const string CON037 = "CON037"; // Media deleted
166+
162167
// ─── Community Success ───
163168
public const string CON030 = "CON030"; // Topic created
164169
public const string CON031 = "CON031"; // Post created

0 commit comments

Comments
 (0)