3. Root Cause & Architecture Decision
3.1 Why the onboarding warnings appeared
Three onboarding warnings prompted this phase:
- "WarehouseId was not set because no default warehouse was created in Phase 1."
- "CashAccountId was not set because no default cash account was created in Phase 1."
- "SalesInvoiceTemplateId was not set because automatic invoice template creation is outside Phase 1."
Investigation of ApplicationDbSeeder.cs (Shumoul.Infrastructure/Persistence/Initialization/) found:
- Branch and Warehouse already exist for every tenant.
SeedBranchesAsync/SeedWarehousesAsynccreate "Main Branch"/"Store 001" automatically, one-time, at tenant provisioning (before onboarding ever runs). CashAccounts,BankAccounts, andSalesInvoiceTemplateshave zero seed logic anywhere — no code in the repository ever inserted a row into any of these three tables. Every tenant started with them completely empty.- The deeper reason these Ids never get set isn't only missing data — it's a deliberate, blanket rule
in the onboarding patch engine.
InternalOnboardingSettingsPatchService.ApplyGroupAsync<T>skips anyGuid/Guid?property unconditionally, regardless of whether a valid target record exists (see Business Onboarding & Smart Configuration, Chapter 11). So even though a warehouse already existed, the generic onboarding-apply path would never pointWarehouseIdat it. This guard is intentional (never blindly assign an externally-supplied entity reference) and is not weakened by this phase — see §7.2. - A second, more consequential finding:
CashierAppsSettings.WarehouseId/CashAccountId/ SalesInvoiceTemplateId(the tenant-wide settings-store fields the onboarding warning is about) are not read by real POS/cashier code. The actual live default a cashier/device uses is a per-userUserCashierSettingrow (same property names, a real per-(UserId, BranchId)table row), populated today only via a manual settings screen, never seeded. Silencing the onboarding warning and making POS actually work by default are two different fixes — this phase does both (see §7.3). CashAccount/BankAccountentities have no GL/currency dependency at creation time — the optional GL+currency link (CashAccountCurrency/BankAccountCurrency) is a separate join table. A cash box or bank record can be created safely with zero accounting entitlement.
3.2 Chosen architecture
Two different problems, two different fixes — deliberately not one uniform system:
A. Universal reference data (Currencies, PaymentMethods, DeviceTypes/Models, Months, ZATCA x6,
LookupGroups, AddressTypes, BarcodeLabelFields/Templates/Fields)
These have exactly one variant, always — there is no per-tenant, per-activity, or per-package customization concept for "what currencies exist" or "what a Simplified Tax Invoice document type code is." Building a new SaaS-side template family (with Preview/Apply, entitlement filtering, per-activity variation) for data that never varies would be pure overhead.
Decision: keep the existing ApplicationDbSeeder JSON + entity + matching-key code in BackEnd
as-is, and de-gate it — remove the one-time SeedsHistory wrapper for these 16 specific seed steps so
they run (insert-missing-only) on every SeedDatabaseAsync invocation instead of only the first. Every
one of these methods already has its own per-row matching key (Code/SystemName/CurrencyCode+Name/Field/
No — see Chapter 8), so this is safe by construction. This reuses
100% of already-written, already-proven logic — see Chapter 8 for the
exact list and the one real bug this pass found and fixed.
B. Genuinely new operational defaults (Warehouse*, CashAccount, BankAccount, SalesInvoiceTemplate)
These have zero pre-existing seed logic (Warehouse excepted, which keeps its existing, compatible,
one-time seeder — see §3.3) and benefit from the same SaaS-control-plane pattern already proven by
AccountingChartTemplates/CostCenterTemplates/ProductCatalogTemplates: a central source of truth,
gate-free JSON seeding, explicit Preview/Apply, entitlement-aware filtering, and a central apply log for
cross-tenant visibility.
Decision: a new, deliberately lean SaaS template family — TenantOperationalDefaults — cloning the
CostCenterTemplates pattern (chosen as the closest existing analog) but flattened, since there is no
per-BusinessActivity variation to model:
- One entity, not four —
TenantOperationalDefaultItemwith aKinddiscriminator (Warehouse/CashAccount/BankAccount/SalesInvoiceTemplate), not four parallel template hierarchies. There is exactly one active row perKind. - No
Templatewrapper entity, no BusinessActivity resolution, no Preview/Apply-per-item-type controllers — one seeder, one Preview/Apply service, one BackEnd internal endpoint handling all four kinds in a single call. - Full detail: Chapter 4 — Defaults Catalog, Chapter 6 — API Reference.
3.3 Warehouse is a special case — not touched
SeedWarehousesAsync already exists, already runs automatically (one-time, at tenant provisioning), and
is already compatible with the new mechanism's own "insert only if the tenant has zero rows of this kind"
check — both paths agree on the same safe semantics regardless of which one ran first. SeedBranchesAsync/
SeedWarehousesAsync/SeedDeviceAsync were left exactly as they were (still one-time-gated) — they are
out of scope for the three named warnings and de-gating them would add no benefit (a single "Main Branch"/
"Store 001"/"Cashier 001" per tenant is exactly what the one-time gate already guarantees safely).
3.4 Hybrid automatic + root-admin model (Option D)
Per the task's own framing of four options (A: provision at tenant creation; B: provision during onboarding apply; C: root-admin StarterKit step only; D: hybrid), this phase implements Option D:
- Automatic, minimal, in-process:
OnboardingService.ApplyRecommendationAsync(MultiTenancyApi) callsITenantOperationalDefaultsService.ApplyAsync(tenantId)directly — not through the root-admin-gated HTTP controller — immediately before the generic settings-patch bridge call. This provisions exactly the four operational-default kinds (entitlement-filtered) and wires the resulting Ids intoCashierAppsSettings/UserCashierSettings, every time a tenant applies their onboarding recommendation. - Root-admin, manual, on-demand:
TenantOperationalDefaultsController(api/Saas/TenantOperationalDefaults) exposes the same Preview/Apply capability behindTenancyPermissions.TenantOperationalDefaults.Apply, for support/ops re-runs on an existing tenant (e.g. a tenant whose subscription was upgraded after onboarding and now qualifies for a previously entitlement-skipped default). - Never exposed to tenant self-login — the root-admin controller requires a root-admin permission a normal tenant user's JWT never carries; the automatic path is a direct in-process service call with no HTTP surface a tenant could reach.
See Chapter 7 for the exact call sequence and why the generic patch engine's Guid-block is never touched.
