Skip to main content
Version: Latest

11. Cash/Bank GL-Currency Linking

This chapter closes the gap tracked in Chapter 5 §5.2 and Chapter 10 §10.4: a tenant's default CashAccount/BankAccount (this framework) can now be linked to the correct GL account and currency — dbo.CashAccountCurrencies/dbo.BankAccountCurrencies — once an accounting chart template has been applied for that tenant (see Tenant Starter Kit & Product Catalog, Chapter 6 — Accounting Chart Templates).

Never creates a GL account, never creates a CashAccount/BankAccount, never posts a journal entry, voucher, or opening balance — only ever creates/updates a CashAccountCurrency/BankAccountCurrency configuration row.

11.1 Why this reuses the existing mapping-apply pipeline, not a new bridge

Investigation (see the accounting-chart-template mapping-apply flow docs) found that MultiTenancyApi's TenantAccountingChartTemplateMappingApplyService already forwards every AccountingChartTemplateMapping row for a template — including the CashOnHand/BankAccount keys — to BackEnd's internal apply-mappings endpoint, unfiltered. Only BackEnd's AccountingChartTemplateMappingApplyService decides which MappingKey values have a confirmed destination (SupportedDestinations). Before this phase, only 4 of 12 keys (Inventory, CostOfGoodsSold, SalesRevenue, SalesReturn) resolved anywhere — the rest, including CashOnHand/BankAccount, were reported Unsupported.

This phase extends exactly that one table (and its per-item write logic) with two new destination types — CashAccountCurrency and BankAccountCurrency — instead of building a parallel bridge, internal endpoint, or entitlement gate. No new MultiTenancyApi↔BackEnd bridge was introduced. The existing apply-mappings internal endpoint, its X-Shumoul-Internal-Key auth, and its Features.AccountManagement entitlement gate are all reused unchanged.

11.2 New MultiTenancyApi service: ITenantCashBankAccountingLinkService

A thin orchestration wrapper (CashBankAccountingLink/ folder), not a new bridge:

  1. Resolves which AccountingChartTemplateId to ask about — either the caller-provided one (root-admin controller, or TenantStarterKitService's own already-resolved template for the same run), or, when none is given (onboarding's best-effort call), the tenant's last successfully-applied template via TenantAccountingChartTemplateApplyLog (Status == "Success", most recent CreatedOn). No such reader existed before this phase — these logs were previously write-only audit trails.
  2. Checks Features.AccountManagement entitlement (same key every accounting apply flow already uses).
  3. Checks the resolved template actually has a CashOnHand/BankAccount mapping row at all — if not, reports NoMappingFound and does nothing.
  4. Delegates to the existing, unchanged ITenantAccountingChartTemplateMappingApplyService.PreviewAsync/ ApplyAsync — the same call TenantStarterKitService's AccountingChartMappings step already makes.
  5. Filters the returned Items down to only MappingKey in {"CashOnHand", "BankAccount"} for its own TenantCashBankAccountingLinkResult — every other mapping key result (Inventory/COGS/etc.) is ignored, not duplicated or re-processed.

Wrapper-level Status values (TenantCashBankAccountingLinkStatus): NotApplicable (tenant not found/inactive), NotEntitled, NoChartApplied, NoMappingFound, Attempted (delegated — see each item's own status for the real outcome).

11.3 BackEnd: AccountingChartTemplateMappingApplyService extension

SupportedDestinations gained two entries:

["CashOnHand"] = (AccountingChartTemplateApplyMappingDestinationType.CashAccountCurrency, null),
["BankAccount"] = (AccountingChartTemplateApplyMappingDestinationType.BankAccountCurrency, null),

Per-item resolution (only when the request actually carries one of these two keys, to avoid an unnecessary settings-cache refresh on every ordinary Inventory-only mapping apply):

  1. Default CashAccount/BankAccount — resolved via CashierAppsSettings.CashAccountId/BankAccountId (the exact Guid the Tenant Operational Defaults phase already wires). If null → MissingDependency ("apply Tenant Operational Defaults first").
  2. Default currency — the tenant's single active Is_Default Currency row. If none → MissingDependency.
  3. AccountCode → Account resolution — the same accountsByCode dictionary every other mapping key already uses (no new duplicate lookup).
  4. Idempotency key — (CashAccountId, CurrencyId) / (BankAccountId, CurrencyId), matched against non-deleted CashAccountCurrency/BankAccountCurrency rows fetched trackable up front (same pattern as the existing InventoryAccountCategory trackable fetch):
    • No existing row → create it (DryRun reports WouldApply).
    • Existing row, same AccountIdSkippedAlreadySet.
    • Existing row, different AccountIdConflict — never overwritten, regardless of Force. Both entity schemas make AccountId a non-nullable Guid, so "existing row with a missing AccountId" cannot occur — the schema itself rules out that scenario from Section 8 of the original task spec.
  5. Writes share the same transaction as the existing InventoryAccountCategory save — if either kind of write is dirty, the transaction opens; a save failure rolls back and flips every Applied item (regardless of destination type) to Failed, unchanged from the existing rollback behavior.

AccountingChartTemplateApplyMappingResultItem gained three optional fields, populated only for these two destination types: CurrencyCode, CashAccountName, BankAccountName.

11.4 Integration points

  • Tenant Operational Defaults apply (OnboardingService.ApplyRecommendationAsync) — a new best-effort try/catch block, mirroring the existing operational-defaults block, calls ITenantCashBankAccountingLinkService.ApplyAsync(session.TenantId) immediately after operational defaults apply. A NoChartApplied/NoMappingFound/NotEntitled result is expected and non-fatal — no new GL account is ever created during onboarding, and a failure here never blocks the settings-patch apply that follows.
  • TenantStarterKitService — a new step, CashBankAccountingLink, inserted between the existing AccountingChartMappings and InventoryAccountCategory steps. It reuses the same chartTemplate already resolved for the AccountingChartAccounts/AccountingChartMappings steps in the same run — it does not re-resolve "last successful apply" via the log reader (unnecessary — the template is already known). It shares the same hard dependency on AccountingChartAccounts having succeeded. Important: TenantOperationalDefaults is a separate module the Starter Kit does not orchestrate (confirmed by inspection — no reference to it exists in TenantStarterKitService). This new step does not add that integration either; if the tenant has no default CashAccount/BankAccount yet when this step runs, every item simply reports MissingDependency — a soft, per-item outcome, never a Blocked step.
  • Root-admin manual controllerTenantCashBankAccountingLinkController (api/Saas/TenantCashBankAccountingLink/Preview|Apply), gated by the new TenancyPermissions.TenantCashBankAccountingLink.View/.Apply permissions (added to MenuClaims.json on the BackEnd side too — permission seeding covers both repos).

11.5 Known issue — transient MissingDependency for a second shared-DB tenant in the same process (FIXED)

Live re-verification (see Subscription Packages & Entitlements, Chapter 16 §16.7) found that when two tenants share one physical database (Tenants.UsingSharedDb = 1) and are processed sequentially in the same long-lived host process, the second tenant processed could transiently see a stale/incorrect cached CashierAppsSettings value — surfacing as MissingDependency here even though TenantOperationalDefaults had just created that tenant's CashAccount/BankAccount moments earlier in the same request. Restarting the host process and processing that tenant first resolved it correctly, confirming this was a same-process, cross-tenant IAppSettingService caching artifact — not a defect in this chapter's logic.

Fixed — every AppSettings cache key is now tenant-scoped via a centralized IAppSettingCacheKeyBuilder; see Subscription Packages & Entitlements, Chapter 17 for the full root cause and fix, live-reverified on this exact tenant pair (171994/793084).

11.5 Entitlement

Both CashOnHand and BankAccount linking are gated by the single existing Features.AccountManagement entitlement check inside TenantAccountingChartTemplateMappingApplyService (unchanged) — no new feature key was introduced. A tenant without this entitlement never reaches the point of resolving CashierAppsSettings or writing any link; the whole apply-mappings call is blocked before the bridge is called, exactly the same behavior the four pre-existing Inventory-related keys already had.

BankAccount linking specifically requires the tenant to also have a default BankAccount in the first place — per Chapter 5 §5.3, Starter/POS tenants never get a BankAccount provisioned (entitled only via Features.AccountManagement), so BankAccount linking naturally reports MissingDependency for them — no separate entitlement check was needed for that distinction.

11.6 Live verification (dev, tenant 555001 — the designated safe write-test tenant)

Performed directly against BackEnd's internal apply-mappings endpoint (same endpoint TenantStarterKitService's AccountingChartMappings step already calls), targeting tenant 555001, which already had an applied AccountingChartTemplate (RestaurantCafe_Saudi_USStyle_COA_v1) with real CashOnHand11101 / BankAccount11102 mapping rows and matching dbo.Accounts codes from an earlier phase's testing.

  1. Before any default CashAccount/BankAccount existedapply-mappings with both keys returned MissingDependency for each, and confirmed via direct query that zero CashAccountCurrency/ BankAccountCurrency rows exist.
  2. Created a default CashAccount+BankAccount for the tenant via the existing, already-proven Tenant Operational Defaults internal endpoint (api/internal/tenant-operational-defaults/apply) — CreatedCount=2.
  3. Re-ran apply-mappings — both items Applied; confirmed via direct query that exactly one CashAccountCurrency row (pointing at Account.Code = 11101) and one BankAccountCurrency row (pointing at Account.Code = 11102) now exist, both correctly attributed (CreatedBy resolved to the tenant's real admin ApplicationUser).
  4. Ran apply-mappings a second time — both items SkippedAlreadySet; row counts unchanged (still exactly one of each) — confirms idempotency, no duplicates.
  5. No InventoryAccountCategory, journal entry, voucher, or opening balance was created at any point.

No cleanup was performed — per this tenant's designated role (safe, persistent write-verification target across several prior phases), the created CashAccount/BankAccount/link rows are left in place as expected test-tenant state, matching how earlier phases treated the same tenant.

11.7 Tests

  • MultiTenancyApi (TenantCashBankAccountingLinkServiceShould): entitlement gating (never calls the underlying service when not entitled), NotApplicable for unknown tenant, NoChartApplied when no successful apply log exists, resolving the most recent successful log while ignoring a more recent failed one, NoMappingFound when the resolved template has neither key, filtering the delegated result down to only the two relevant MappingKey items, and that PreviewAsync never calls the underlying service when there's nothing to preview.
  • BackEnd (AccountingChartTemplateMappingApplyServiceTests, extended): CashOnHand/BankAccount now resolve (no longer in the "every foundational key is Unsupported" theory); MissingDependency for missing CashAccount/BankAccount/default Currency; link creation with the correct CashAccountId/BankAccountId/CurrencyId/AccountId; SkippedAlreadySet + zero duplicates on re-apply; Conflict reported (and never overwritten, even with Force=true) when an existing link points at a different account; DryRun never saves; CurrencyCode/CashAccountName/BankAccountName populated on the result item; and that no InventoryAccountCategory row is ever touched by this key pair.

11.8 Package / release

Shumoul.Framework.MultiTenancy.Api bumped 1.0.1211.0.122 (only the Api package — the Core Shumoul.Framework.MultiTenancy package was not touched). Copied to both C:\MultiTenancy\ and local-packages/; BackEnd's PackageReference updated to match.

11.9 Remaining gaps

  • The wrapper's "resolve last successfully-applied template" reader is new — it is a plain OrderByDescending(CreatedOn).FirstOrDefault() query, not a cached/indexed lookup. Fine at current scale (one row per apply attempt, per tenant); would need attention if apply-log volume grows significantly per tenant.
  • TenantStarterKitService's new step still has no technical dependency wiring to TenantOperationalDefaultsclosed by the StarterKit Operational Defaults Orchestration phase: TenantOperationalDefaults is now its own StarterKit step, positioned before CashBankAccountingLink, so a single StarterKit Apply provisions the default CashAccount/BankAccount and links them in the correct order — no manual pre-run/re-run sequencing is required by the operator anymore. Live-verified (see that chapter) against tenant 555001 through the real, authenticated api/Saas/TenantStarterKits host.

Superseded — the previous entry about live verification only exercising BackEnd's internal endpoint directly is no longer accurate: the StarterKit Operational Defaults Orchestration phase's live verification went through the real, authenticated api/Saas/TenantStarterKits/Preview/Apply endpoints end-to-end (see that chapter's own verification section).