Skip to main content
Version: Latest

15. Production / Staging Deployment Verification

15.1 Purpose and scope

A deployment-readiness pass for the four Shumoul packages (Chapter 5) and their limit enforcement (Chapter 13): confirming the Git + startup seed mechanism will deliver correct state to any environment (staging or production) with no manual SQL, and providing safe, read-only checks an operator can run after deploying. This is a verification-only pass — no source, schema, package definition, or limit was changed.

15.2 Current version state (verified)

ItemValue
Shumoul.Framework.MultiTenancy.Api .csproj <Version>1.0.118
BackEnd Shumoul.Application.csproj PackageReference1.0.118 (matches)
Pending EF model changes (dotnet ef migrations has-pending-model-changes)None — "No changes have been made to the model since the last migration."
Latest migration on disk20260718004119_Add_TenantStarterKitApplyLog_ProductCatalogColumns (unrelated to packages; nothing newer)
SubscriptionPackageFeatureRule table originThe original 20230813181647_AddAllTables migration — no schema change was ever needed for this package/limit work, only seed data

Conclusion: no migration is required for this deployment. The four packages, their entitlements, and the five Starter limits are pure seed data on top of tables that already existed before this initiative began.

15.3 Seed deployment mechanism — how it reaches a server safely

Five ICustomSeeder implementations, all in Shumoul.Framework.MultiTenancy.Api:

ClassPriorityFileInserts
ShumoulSubscriptionPackageSeeder25Packages/ShumoulSubscriptionPackageSeeder.csThe 4 SubscriptionPackage rows (matched by exact FName)
ShumoulPlanPackageSeeder26Packages/ShumoulPlanPackageSeeder.csSubscriptionPlanPackage rows (Monthly/Annually × 4 packages = 8 rows)
ShumoulPackageFeatureSeeder27Packages/ShumoulPackageFeatureSeeder.csSubscriptionPackageFeature links (package ↔ feature)
ShumoulPackageFeatureRuleSeeder28Packages/ShumoulPackageFeatureRuleSeeder.csSubscriptionPackageFeatureRule rows — 5 rows, Shumoul Starter only
ShumoulFeatureReportActionSeeder29FeatureReportActions/ShumoulFeatureReportActionSeeder.csSubscriptionFeatureReportAction links (feature ↔ report action)

How they run — confirmed from source, this task:

  • ICustomSeeder implementations are auto-discovered by .AddServices(typeof(ICustomSeeder), ServiceLifetime.Transient) in Startup.cs — an assembly scan, the same pattern as ITransientService. No manual registration is needed for a seeder to run — implementing the interface and shipping the DLL is sufficient.
  • CustomSeederRunner.RunSeedersAsync (Persistence/CustomSeederRunner.cs) resolves every registered ICustomSeeder, orders them OrderBy(x => x.Priority), and awaits each InitializeAsync in sequence — this is why Priority 25→29 above is the actual, guaranteed execution order (packages before plan-rows, before feature-links, before rules, before report-links).
  • TenancyDatabaseInitializer.InitializeDatabasesAsync (Persistence/TenancyDatabaseInitializer.cs) calls InitializeTenantDbAsync (applies any pending EF migration, then seeds the root tenant) then _seederRunner.RunSeedersAsync — both migration-apply and seeding happen automatically.
  • This is invoked unconditionally at app startup: Startup.cs:209app.ApplicationServices.InitializeDatabasesAsync().Wait(); inside Configure. Every process start (staging or production) runs this — no flag, no manual trigger, no separate deploy step.

Idempotency — confirmed from source, all five seeders:

  • Each seeder loads its embedded JSON, queries existing rows by natural key (FName for packages; (PackageFName, FeatureKey) for feature links; (FeatureId, EntityName, ApplyPeriod) for rules, etc.), and inserts only what's missingplan.SkippedAlreadyExists is logged, never re-written.
  • None of the five use the old SeedHistory-gated, !_db.X.Any()-gated pattern the original SubscriptionPackageSeeder/PlanPackagesSeeder/legacy feature seeders use (Chapter 4) — that gate is exactly why the legacy seeders would silently never pick up new rows in an already-seeded environment. These five have no such gate and are safe to run on every single startup, indefinitely.
  • No seeder deletes, updates, or overwrites any existing row of any kind.

Explicit confirmation for this task's question: editing the old JSON files (SubscriptionPackages.json, etc.) is not required for this deployment. The five new gate-free seeders and their own embedded JSON resources are the complete, authoritative, self-sufficient deployment mechanism for all four packages and their entitlements/limits.

15.4 Where the seeders write — central SaaS DB only, never a tenant database

  • All five seeders depend on ITenancyRepositoryAsync, which wraps exactly one EF context: TenantDbContext (Tenants/TenantDbContext.cs), configured with a single, fixed connection string read once at startup (DatabaseSettings.ConnectionString — see [reference: Dev DB Network Access] for the equivalent dev-machine env var name; no connection string value appears in this document).
  • TenantDbContext is the single central "Saas" management database — it holds Tenants, SubscriptionPackage, SubscriptionPackageFeature, SubscriptionPackageFeatureRule, SubscriptionPlanPackage, SubscriptionFeatureReportAction, and related tables. It is not per-tenant and does not switch connection strings per request.
  • The tenant's own operational database (ApplicationDbContext, BackEnd repo, ERP business data — invoices, stock, GL, etc.) is a completely separate context in a separate repository. None of the five package seeders reference it, inject it, or write to it in any way.
  • Conclusion: deploying these seeders can only ever affect the one central subscription-management database. No tenant's operational data is touched by this deployment.

15.5 Pre-deployment checklist

  • Shumoul.Framework.MultiTenancy.Api package on the target server/build is 1.0.118 (or newer, if a future bump ships alongside this).
  • BackEnd's Shumoul.Application.csproj PackageReference for Shumoul.Framework.MultiTenancy.Api matches the same version.
  • No EF migration is pending (dotnet ef migrations has-pending-model-changes against the target environment returns "No changes", exactly as confirmed above in dev).
  • Standard deploy proceeds — no special production configuration, feature flag, or manual step is required beyond the existing deploy pipeline. Application startup performs both the migration-apply check and the seeder run automatically.
  • No manual SQL is required, under any circumstance, for this specific deployment.

15.6 Post-deployment verification — safe, read-only checks

15.6.1 Application log check

Search server logs after the first startup following deployment for:

Started to seed Shumoul subscription packages. Count=4
Completed seeding Shumoul subscription packages. Inserted=4 (first deploy only)
Completed seeding Shumoul subscription packages. Inserted=0 (every subsequent restart)

An Inserted=0 on a restart after the first successful deploy is the expected, healthy steady state — it confirms the seeder ran and found nothing missing, not that it failed to run.

15.6.2 Pricing API check (read-only, safe on production)

GET api/Subscription/Packages/pricing

Expected: 8 rows total (4 packages × Monthly/Annually), each with a distinct SubscriptionPlanPackage.Id, and (dev-verified) prices:

PackageMonthlyAnnual
Shumoul Starter49588
Shumoul POS1491788
Shumoul Finance Starter1291548
Shumoul Finance Advanced3494188

This single call may be run against production without further approval — it is unauthenticated, read-only, and returns only public pricing data already intended for display.

15.6.3 Read-only SQL checks (do NOT execute against production without explicit approval)

All SELECT-only, no connection string included. Run against the central Saas DB (TenantDbContext's target), never a tenant database:

-- 1. All four packages exist, no duplicates
SELECT FName, Name, COUNT(*) AS RowCount
FROM Saas.SubscriptionPackages
WHERE FName IN ('Shumoul Starter', 'Shumoul POS', 'Shumoul Finance Starter', 'Shumoul Finance Advanced')
GROUP BY FName, Name;
-- Expect: 4 rows, RowCount = 1 each

-- 2. Plan-package (pricing) rows exist — 2 per package
SELECT sp.FName, COUNT(*) AS PlanPackageRowCount
FROM Saas.SubscriptionPlanPackages spp
JOIN Saas.SubscriptionPackages sp ON sp.Id = spp.PackageId
WHERE sp.FName IN ('Shumoul Starter', 'Shumoul POS', 'Shumoul Finance Starter', 'Shumoul Finance Advanced')
GROUP BY sp.FName;
-- Expect: 4 rows, PlanPackageRowCount = 2 each (Monthly + Annually)

-- 3. Package -> feature links, no duplicates
SELECT sp.FName, sf.FKey, COUNT(*) AS LinkCount
FROM Saas.SubscriptionPackageFeatures spf
JOIN Saas.SubscriptionPackages sp ON sp.Id = spf.PackageId
JOIN Saas.SubscriptionFeatures sf ON sf.Id = spf.FeatureId
WHERE sp.FName IN ('Shumoul Starter', 'Shumoul POS', 'Shumoul Finance Starter', 'Shumoul Finance Advanced')
GROUP BY sp.FName, sf.FKey
HAVING COUNT(*) > 1;
-- Expect: 0 rows (no duplicate feature link for any package)

-- 4. Package-feature-rule rows exist ONLY for Shumoul Starter
SELECT sp.FName, COUNT(*) AS RuleCount
FROM Saas.SubscriptionPackageFeatureRules spfr
JOIN Saas.SubscriptionPackageFeatures spf ON spf.Id = spfr.FeatureId
JOIN Saas.SubscriptionPackages sp ON sp.Id = spf.PackageId
GROUP BY sp.FName;
-- Expect: exactly one row: FName = 'Shumoul Starter', RuleCount = 5
-- Any other package appearing here, or Starter's count != 5, is a real defect — stop and report.

-- 5. No duplicate package-feature-rule rows (same feature link + entity + period seeded twice)
SELECT FeatureId, EntityName, ApplyPeriod, COUNT(*) AS DupeCount
FROM Saas.SubscriptionPackageFeatureRules
GROUP BY FeatureId, EntityName, ApplyPeriod
HAVING COUNT(*) > 1;
-- Expect: 0 rows

-- 6. Feature -> report-action links exist, no platform/admin leakage
SELECT sf.FKey, arn.ActionKey
FROM Saas.SubscriptionFeatureReportActions sfra
JOIN Saas.SubscriptionFeatures sf ON sf.Id = sfra.FeatureId
JOIN Saas.AppReportNameActions arn ON arn.Id = sfra.ReportNameActionId
WHERE arn.ActionKey LIKE 'Permission.AdministrativeReports.%'
OR arn.ActionKey LIKE '%Tenancy%';
-- Expect: 0 rows — no platform/administrative/tenancy-scoped report ever linked through a customer feature

(Table/schema names above follow the Saas.* schema convention documented in Chapter 3; adjust only if that chapter's naming has since changed — do not guess a different schema.)

15.7 Controlled test plan — Starter limits and one signup

Staging (preferred, run without further approval if a staging environment is available):

  1. Register one test tenant on Shumoul Starter, using a clearly-marked test email (+deploy-verify@-style address, matching the convention already used for QA — see Chapter 14 §14.3).
  2. Activate → log in → complete onboarding → confirm dashboard readiness (no onboarding/apply needed, consistent with every prior verification pass).
  3. Attempt to create a second branch, second user, second POS device, a 6th product, and an 11th sales invoice in the current month — each must be blocked with the existing localized "Package limit exceeded" message (Chapter 13), not a raw exception.
  4. Optionally repeat step 3's second-branch check on a fresh POS or Finance Starter tenant to reconfirm no cross-package leakage — already live-verified twice in Chapter 14 §14.8 and does not need to be repeated on every deploy.

Production: do not create a test tenant, do not attempt any of the write operations in step 3, and do not run onboarding/apply or TenantStarterKits/Preview/Apply, unless the user explicitly approves a specific, scoped action. If approved, only §15.6.2 (pricing) and the read-only SQL in §15.6.3 should be run first; anything beyond that requires a separate, explicit go-ahead.

15.8 Security re-confirmation

Re-derived from source this pass, matching Chapter 6:

  • No TenancyPermissions.* value is ever granted to a customer tenant through any of the four packages — package entitlement flows only through SubscriptionFeature/SubscriptionPackageFeature, which has no code path back to ApplicationRoleClaim/root-admin permissions.
  • No package includes Features.TenantStarterKitManagement-style template-administration features (the Starter Kit apply flow is root-admin-gated separately, per Chapter 10, independent of any customer's subscribed package).
  • No package includes any SaaS subscription-management feature (packages/plans/pricing administration) — that surface is host-only and outside the SubscriptionFeature catalog entirely.
  • §15.6.3 query 6 is the concrete, repeatable, read-only check for platform/administrative report leakage specifically.

15.9 Build and test verification (this pass)

RepoCommandResult
MultiTenancyApidotnet build --no-restore0 errors (pre-existing warnings only)
MultiTenancyApidotnet test --no-build1133 total, 1127 passed, 6 failed — identical to the documented baseline, zero new failures
MultiTenancyApidotnet ef migrations has-pending-model-changes"No changes have been made to the model since the last migration."
BackEnddotnet restoreSucceeded
BackEnddotnet build --no-restore -p:TreatWarningsAsErrors=false0 errors (pre-existing warnings only)
BackEnddotnet test --no-build792 total, 757 passed, 35 failed — identical to the documented baseline, zero new failures

No source file changed in either repo during this pass — both test runs are a reconfirmation, not a verification of new behavior.

15.10 Outcome

No bug was found. No package definition, limit, or business logic was changed. No migration is required. The five seeders are a safe, gate-free, idempotent, central-DB-only mechanism that will deliver all four packages and the Starter limits to any environment purely through the standard Git-deploy + application-startup path — no manual SQL, no special production step, beyond deploying the already-committed Shumoul.Framework.MultiTenancy.Api 1.0.118 package.