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)
| Item | Value |
|---|---|
Shumoul.Framework.MultiTenancy.Api .csproj <Version> | 1.0.118 |
BackEnd Shumoul.Application.csproj PackageReference | 1.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 disk | 20260718004119_Add_TenantStarterKitApplyLog_ProductCatalogColumns (unrelated to packages; nothing newer) |
SubscriptionPackageFeatureRule table origin | The 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:
| Class | Priority | File | Inserts |
|---|---|---|---|
ShumoulSubscriptionPackageSeeder | 25 | Packages/ShumoulSubscriptionPackageSeeder.cs | The 4 SubscriptionPackage rows (matched by exact FName) |
ShumoulPlanPackageSeeder | 26 | Packages/ShumoulPlanPackageSeeder.cs | SubscriptionPlanPackage rows (Monthly/Annually × 4 packages = 8 rows) |
ShumoulPackageFeatureSeeder | 27 | Packages/ShumoulPackageFeatureSeeder.cs | SubscriptionPackageFeature links (package ↔ feature) |
ShumoulPackageFeatureRuleSeeder | 28 | Packages/ShumoulPackageFeatureRuleSeeder.cs | SubscriptionPackageFeatureRule rows — 5 rows, Shumoul Starter only |
ShumoulFeatureReportActionSeeder | 29 | FeatureReportActions/ShumoulFeatureReportActionSeeder.cs | SubscriptionFeatureReportAction links (feature ↔ report action) |
How they run — confirmed from source, this task:
ICustomSeederimplementations are auto-discovered by.AddServices(typeof(ICustomSeeder), ServiceLifetime.Transient)inStartup.cs— an assembly scan, the same pattern asITransientService. 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 registeredICustomSeeder, orders themOrderBy(x => x.Priority), and awaits eachInitializeAsyncin 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) callsInitializeTenantDbAsync(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:209—app.ApplicationServices.InitializeDatabasesAsync().Wait();insideConfigure. 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 (
FNamefor packages;(PackageFName, FeatureKey)for feature links;(FeatureId, EntityName, ApplyPeriod)for rules, etc.), and inserts only what's missing —plan.SkippedAlreadyExistsis logged, never re-written. - None of the five use the old
SeedHistory-gated,!_db.X.Any()-gated pattern the originalSubscriptionPackageSeeder/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). TenantDbContextis the single central "Saas" management database — it holdsTenants,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.Apipackage on the target server/build is1.0.118(or newer, if a future bump ships alongside this). - BackEnd's
Shumoul.Application.csprojPackageReferenceforShumoul.Framework.MultiTenancy.Apimatches the same version. - No EF migration is pending (
dotnet ef migrations has-pending-model-changesagainst 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:
| Package | Monthly | Annual |
|---|---|---|
| Shumoul Starter | 49 | 588 |
| Shumoul POS | 149 | 1788 |
| Shumoul Finance Starter | 129 | 1548 |
| Shumoul Finance Advanced | 349 | 4188 |
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):
- 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). - Activate → log in → complete onboarding → confirm dashboard readiness (no
onboarding/applyneeded, consistent with every prior verification pass). - 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. - 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 throughSubscriptionFeature/SubscriptionPackageFeature, which has no code path back toApplicationRoleClaim/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
SubscriptionFeaturecatalog 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)
| Repo | Command | Result |
|---|---|---|
| MultiTenancyApi | dotnet build --no-restore | 0 errors (pre-existing warnings only) |
| MultiTenancyApi | dotnet test --no-build | 1133 total, 1127 passed, 6 failed — identical to the documented baseline, zero new failures |
| MultiTenancyApi | dotnet ef migrations has-pending-model-changes | "No changes have been made to the model since the last migration." |
| BackEnd | dotnet restore | Succeeded |
| BackEnd | dotnet build --no-restore -p:TreatWarningsAsErrors=false | 0 errors (pre-existing warnings only) |
| BackEnd | dotnet test --no-build | 792 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.
