Skip to main content
Version: Latest

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/SeedWarehousesAsync create "Main Branch"/"Store 001" automatically, one-time, at tenant provisioning (before onboarding ever runs).
  • CashAccounts, BankAccounts, and SalesInvoiceTemplates have 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 any Guid/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 point WarehouseId at 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-user UserCashierSetting row (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/BankAccount entities 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 fourTenantOperationalDefaultItem with a Kind discriminator (Warehouse/CashAccount/BankAccount/SalesInvoiceTemplate), not four parallel template hierarchies. There is exactly one active row per Kind.
  • No Template wrapper 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) calls ITenantOperationalDefaultsService.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 into CashierAppsSettings/UserCashierSettings, every time a tenant applies their onboarding recommendation.
  • Root-admin, manual, on-demand: TenantOperationalDefaultsController (api/Saas/TenantOperationalDefaults) exposes the same Preview/Apply capability behind TenancyPermissions.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.