9. Onboarding
9.1 Device (Solution Unit) Onboarding — OTP Flow
The OTP itself is obtained by the tenant admin directly from ZATCA's own portal, out of band — it arrives at
the ERP only as an HTTP header on the POST api/EInvoicing/Onboarding call, never from configuration. A
Otp: 123345 value present in appsettings.Development.json/appsettings.production.json's
ZatcaPortalSettings section is a leftover fixed testing value, not a live onboarding mechanism — confirmed
by the absence of any code path that reads a ZatcaPortalSettings.Otp property (no such property exists on
the class at all).
9.2 Onboarding / Certificate Lifecycle Services
| Service | Responsibility |
|---|---|
OnboardingProcessFlowService | Orchestrates Compliance CSID → Compliance checks → Production CSID, sequentially, for Initialize(type, otp) |
OnboardingValidatorService | TenantIsOnboarded()/CheckOnboardingStatus() — reads FatooraSecretSettings.Onboarded, throws BadRequestException if not onboarded |
OnboardingComplianceCsidService | Requests the Compliance CSID from ZATCA (via ClientCsidService.Compliance) |
OnboardingProductionCsidService | Requests/renews the Production CSID (via ClientCsidService.Production); throws if FatooraSecretSettings.Onboarded == false |
OnboardingComplianceCheckService | Runs the "compliance checks" step (SimplifiedInvoiceCompliance, StandardInvoiceCompliance — the file StandardTaxInvoiceCompliance.cs actually declares a class named StandardInvoiceCompliance, a filename/class-name mismatch) |
X509CertificateService | Generates the CSR, generates/imports the EC key pair, caches the resulting certificate in the Windows certificate store |
ECKeyPairHelper | Generates the EC key pair (SecObjectIdentifiers.SecP256k1) |
9.3 Authentication During Onboarding
The Compliance CSID request (the very first onboarding step) correctly does not attach Basic-Auth — this matches ZATCA's spec, since the caller doesn't have a CSID yet at that point; it authenticates with the CSR and OTP only.
Every step after that — the Production CSID request (both initial issuance and renewal) — does require ZATCA-side Basic-Auth. This is exactly the call path the Phase 0/0.1/0.2 authentication finding and fix covered — see Chapter 7 — Security § 7.2. Before the Phase 0.2 fix, any onboarding attempt that reached the Production CSID step would have received an unauthenticated request rejection from ZATCA.
9.4 Onboarding Events (MediatR)
All 8 events use MediatR (INotification/INotificationHandler<T>), not the platform's generic Notification
Framework:
| Event | Published from | Handler action |
|---|---|---|
OnboardingCompletedEvent | ClientCsidService | Persists ProductionCsidSettings/FatooraSecretSettings. Logging only |
OnboardingRenewalEvent | Onboarding.ProcessFlowService | Removes the X.509 cert from the certificate store |
SwitchToProductionModeEvent | Controller, via IMediator.Publish | Validates the mode transition, clears/sets app settings |
SingleComplianceCheckDoneEvent | ClientComplianceService | Flips a boolean flag per invoice type in ComplianceChecksSettings |
CompilanceChecksFailureEvent | ClientComplianceService | Marks OTP Invalid = true, clears settings to restart onboarding |
OtpIsProvidedEvent | Onboarding.ProcessFlowService | Validates OTP format/expiry; throws BadRequestException on failure |
OtpIsRejectedEvent | ClientCsidService | Sets Invalid = true in settings |
OnboardingStartOverEvent | Onboarding.ProcessFlowService | Revokes/removes the X.509 cert; throws if not revoked on the ZATCA portal first |
Every handler does only state persistence, certificate-store operations, logging, or throws a validation exception — none call any communication/notification interface. See Chapter 12 — Known Limitations for what this means practically (no alert if onboarding fails or a certificate is about to expire).
9.5 No ERP Workflow Triggers Onboarding
Onboarding, like invoice submission, is reachable only via its dedicated controller endpoint — no ERP business workflow initiates it automatically. A tenant admin (or whatever caller has the OTP) drives the process manually, one HTTP call at a time.
