Appearance
Template Service
The Template service manages email and document templates for Microtec ERP. It stores bilingual (Arabic/English) templates with Handlebars-style placeholder syntax and renders them on demand with caller-supplied data. The Notification service is its primary consumer.
Overview
| Property | Value |
|---|---|
| Service | Template.Apis |
| CAE placement | Private CAE |
| Gateway route prefix | /template-apis/ |
| Source | Platforms/Src/InfrastructureServices/ (Template sub-module) |
| Primary consumer | Notification.Apis |
| Template syntax | Handlebars () |
| Languages | Arabic (ar), English (en) |
| Auth realm | microtec (Keycloak) |
Architecture
Templates are stored per-tenant. Each tenant can override the default platform templates to match their brand, language, and communication style.
Domain Model
Template Types
| Type | Enum | Usage |
|---|---|---|
TemplateType.Email | Full HTML email bodies | |
| SMS | TemplateType.Sms | Short plain-text messages |
| Push | TemplateType.Push | Short push notification title + body |
| Document | TemplateType.Document | PDF/printed document layouts |
| InApp | TemplateType.InApp | In-app notification messages |
API Endpoints
Template Management
| Method | Route | Operation |
|---|---|---|
GET | /Templates | List all templates (paged) |
GET | /Templates/{Id} | Get template with all language variants |
GET | /Templates/ByCode/{code} | Get template by code |
POST | /Templates | Create new template |
PUT | /Templates | Update template |
DELETE | /Templates/{Id} | Delete custom template (system templates protected) |
Template Content (per Language)
| Method | Route | Operation |
|---|---|---|
GET | /Templates/{Id}/Contents | List all language variants |
PUT | /Templates/{Id}/Contents | Update content for a language |
POST | /Templates/{Id}/Preview | Render template with sample data |
Render Endpoint
| Method | Route | Operation |
|---|---|---|
POST | /Templates/Render | Render a template by code with caller-supplied data |
System Template Codes
Platform templates are pre-seeded for every new tenant during provisioning:
| Code | Type | Description |
|---|---|---|
INVOICE_POSTED | Customer invoice notification | |
PAYMENT_RECEIVED | Payment receipt confirmation | |
PAYROLL_SALARY_CREDITED | SMS | Salary credit alert |
WORKFLOW_APPROVAL_REQUIRED | Push | Approval request notification |
WORKFLOW_APPROVED | Workflow approval confirmation | |
WORKFLOW_REJECTED | Workflow rejection with comment | |
TENANT_WELCOME | New tenant onboarding email | |
SUBSCRIPTION_EXPIRY_WARNING | Subscription expiry reminder (7, 3, 1 day) | |
SUBSCRIPTION_RENEWED | Subscription renewal confirmation | |
USER_PASSWORD_RESET | Password reset link | |
REPORT_READY | Scheduled report delivery | |
LEAVE_REQUEST_SUBMITTED | Push | Leave request notification to manager |
LEAVE_REQUEST_APPROVED | Push | Leave approval notification to employee |
System Templates
Templates with IsSystemTemplate = true cannot be deleted. They can be customized (body and subject) per-tenant but the code and placeholder schema remain fixed.
Template Syntax
Templates use placeholders. HTML email templates support full HTML and inline CSS:
html
<!-- INVOICE_POSTED — Arabic (ar) variant -->
<html dir="rtl" lang="ar">
<body style="font-family: Arial, sans-serif; direction: rtl;">
<h2>عزيزي {{CustomerName}}</h2>
<p>نحيطكم علماً بإصدار الفاتورة رقم <strong>{{InvoiceNumber}}</strong></p>
<table>
<tr>
<td>تاريخ الفاتورة</td>
<td>{{InvoiceDate}}</td>
</tr>
<tr>
<td>إجمالي المبلغ</td>
<td>{{TotalAmount}} ريال</td>
</tr>
</table>
<p>شكراً لثقتكم بنا.</p>
<p>{{CompanyName}}</p>
</body>
</html>html
<!-- INVOICE_POSTED — English (en) variant -->
<html dir="ltr" lang="en">
<body style="font-family: Arial, sans-serif;">
<h2>Dear {{CustomerName}},</h2>
<p>Invoice <strong>{{InvoiceNumber}}</strong> has been issued.</p>
<table>
<tr><td>Invoice Date</td><td>{{InvoiceDate}}</td></tr>
<tr><td>Total Amount</td><td>SAR {{TotalAmount}}</td></tr>
</table>
<p>Thank you for your business.</p>
<p>{{CompanyName}}</p>
</body>
</html>Render API
The Notification service calls the Render endpoint internally. External callers can also use it for preview purposes:
Request
http
POST /template-apis/Templates/Render
Authorization: Bearer <token>
Content-Type: application/json
{
"templateCode": "INVOICE_POSTED",
"language": "ar",
"data": {
"CustomerName": "شركة الأمل للتجارة",
"InvoiceNumber": "INV-2026-00123",
"InvoiceDate": "2026-01-15",
"TotalAmount": "12,500.00",
"CompanyName": "ميكروتيك للتقنية"
}
}Response
json
{
"success": true,
"data": {
"subject": "فاتورة رقم INV-2026-00123",
"body": "<html dir=\"rtl\">...</html>",
"plainTextBody": "عزيزي شركة الأمل للتجارة، الفاتورة رقم INV-2026-00123..."
}
}CQRS Examples
AddTemplateCommand
csharp
// Commands/AddTemplate/AddTemplateCommand.cs
public record AddTemplateCommand(AddTemplateDto Dto) : ICommand<int>;
// Commands/AddTemplate/Dto/AddTemplateDto.cs
public record AddTemplateDto(
string Code,
string NameAr,
string NameEn,
TemplateType Type,
List<TemplateContentDto> Contents,
List<TemplatePlaceholderDto> Placeholders);
public record TemplateContentDto(
string Language,
string Subject,
string Body,
string PlainTextBody);
public record TemplatePlaceholderDto(
string Key,
string Description,
bool IsRequired,
string SampleValue);RenderTemplateQuery
csharp
// Queries/RenderTemplate/RenderTemplateQuery.cs
public record RenderTemplateQuery(
string TemplateCode,
string Language,
Dictionary<string, string> Data) : IQuery<RenderedTemplateDto>;
// Queries/RenderTemplate/RenderTemplateQueryHandler.cs
public class RenderTemplateQueryHandler(
IUnitOfWork<TemplateDbContext> unitOfWork,
ITemplateRenderer renderer,
ITenantProvider tenant) : IRequestHandler<RenderTemplateQuery, ApiResponse<RenderedTemplateDto>>
{
public async Task<ApiResponse<RenderedTemplateDto>> Handle(
RenderTemplateQuery request, CancellationToken ct)
{
// Try tenant-specific template first, fall back to system template
var content = await unitOfWork.Repository<TemplateContent>()
.GetQueryable()
.AsNoTracking()
.Where(c =>
c.Template.Code == request.TemplateCode &&
c.Language == request.Language &&
(c.Template.TenantId == tenant.TenantId || c.Template.IsSystemTemplate))
.OrderByDescending(c => c.Template.TenantId == tenant.TenantId) // tenant override first
.FirstOrDefaultAsync(ct)
?? throw new NotFoundException($"Template '{request.TemplateCode}' not found for language '{request.Language}'");
var rendered = renderer.Render(content, request.Data);
return ApiResponse.Success(rendered);
}
}Localization Strategy
| Language | Code | Direction | Character Set |
|---|---|---|---|
| Arabic | ar | RTL | Unicode / UTF-8 |
| English | en | LTR | ASCII / UTF-8 |
Every template must have both ar and en content entries. The Notification service passes the language preference from the original request (defaulting to ar if not specified).
Missing Language Variant
If a template exists but lacks a content entry for the requested language, the render endpoint returns HTTP 404. Always seed both language variants when creating a new template.
Template Preview (Admin Tool)
Platform admins can preview templates directly from the BusinessOwners portal:
http
POST /template-apis/Templates/{Id}/Preview
Authorization: Bearer <token>
{
"language": "ar",
"sampleData": {
"CustomerName": "شركة تجريبية",
"InvoiceNumber": "INV-TEST-001"
}
}Returns the fully rendered HTML for inspection before going live.
Configuration
json
// appsettings.json (Template.Apis)
{
"ConnectionStrings": {
"AdminConnection": "Server=...;Database=MicrotecAdmin;..."
},
"Keycloak": {
"Authority": "https://<keycloak-host>/realms/microtec",
"Audience": "account"
},
"TemplateDefaults": {
"DefaultLanguage": "ar",
"MaxBodySizeKb": 512
}
}Deployment
The Template service is listed in the pipeline service config:
Devops/azure/config/container-backend/services-config.json → "template"On first tenant provisioning, the PlatformsWorker seeds all system templates into the tenant's template store from the embedded resource defaults in the Template service.