{"record":{"id":"ddf3fcc65169ddc0","repo":"fullstackhero/dotnet-starter-kit","slug":"subscription-cannot-be-backdated","errorCode":null,"errorMessage":"Subscription cannot be backdated.","messagePattern":"Subscription cannot be backdated\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"src/BuildingBlocks/Shared/Multitenancy/AppTenantInfo.cs","lineNumber":58,"sourceCode":"    public bool IsActive { get; set; }\n    public DateTime ValidUpto { get; set; }\n    public string? Issuer { get; set; }\n\n    /// <summary>Plan name used to resolve quota defaults (e.g. \"free\", \"pro\"). Null falls back to <c>QuotaOptions.DefaultPlan</c>.</summary>\n    public string? Plan { get; set; }\n\n    /// <summary>Per-tenant quota overrides. Serialized as JSON by the tenant store; empty by default.</summary>\n    public Dictionary<QuotaResource, long> QuotaLimits { get; set; } = new();\n\n    public void AddValidity(int months) =>\n        ValidUpto = ValidUpto.AddMonths(months);\n\n    public void SetValidity(in DateTime validTill)\n    {\n        var normalized = validTill;\n        ValidUpto = ValidUpto < normalized\n            ? normalized\n            : throw new InvalidOperationException(\"Subscription cannot be backdated.\");\n    }\n\n    public void Activate()\n    {\n        if (Id == MultitenancyConstants.Root.Id)\n        {\n            throw new InvalidOperationException(\"Invalid Tenant\");\n        }\n\n        IsActive = true;\n    }\n\n    public void Deactivate()\n    {\n        if (Id == MultitenancyConstants.Root.Id)\n        {\n            throw new InvalidOperationException(\"Invalid Tenant\");\n        }","sourceCodeStart":40,"sourceCodeEnd":76,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/BuildingBlocks/Shared/Multitenancy/AppTenantInfo.cs#L40-L76","documentation":"AppTenantInfo.SetValidity extends a tenant's subscription validity to the new expiry date, but only if the date is later than the current ValidUpto. Passing an earlier (or equal) date throws InvalidOperationException, preventing accidental shortening of a paid subscription. Note the comparison is ValidUpto < normalized — an equal date also throws.","triggerScenarios":"Calling SetValidity(validTill) with a validTill less than or equal to the tenant's current ValidUpto, e.g. during EnsureDemoTenantsExistAsync seeding with a stale/hard-coded expiry date after it was already extended.","commonSituations":"Seeding demo tenants on a database where the subscription was already extended; re-running seeders with fixed dates; clock/config mistakes passing dates in the past; admin UI allowing operators to enter an earlier expiry.","solutions":["Only call SetValidity with a date later than the current ValidUpto — check before calling.","In seeders, make expiry dates relative to UtcNow instead of hard-coded.","Add a separate explicit downgrade/shorten method if backdating is ever legitimately required.","Guard the caller: skip SetValidity when normalized <= tenant.ValidUpto."],"exampleFix":"// before\nsubscription.SetValidity(DateTime.Parse(\"2026-01-01\"));\n\n// after\nvar newExpiry = DateTime.UtcNow.AddYears(1);\nif (newExpiry > tenant.ValidUpto)\n    tenant.SetValidity(newExpiry);","handlingStrategy":"validation","validationCode":"if (newExpiry <= tenant.ValidUpto)\n    throw new ArgumentException(\"New expiry must be later than the current subscription expiry.\");\ntenant.SetValidity(newExpiry);","typeGuard":"bool CanExtend(AppTenantInfo t, DateTime validTill) => t.ValidUpto < validTill;","tryCatchPattern":"try {\n    tenant.SetValidity(newExpiry);\n} catch (InvalidOperationException ex) when (ex.Message == \"Subscription cannot be backdated.\") {\n    logger.LogWarning(\"Skipped backdated expiry {Expiry} for tenant {TenantId}\", newExpiry, tenant.Id);\n}","preventionTips":["Compute expiry dates relative to UtcNow in seeders and tests — never hard-code.","Only call SetValidity for extensions; handle renewals-before-expiry differently.","In admin UIs, disable submitting an expiry earlier than the current one."],"tags":["multitenancy","subscription","domain-logic"],"backgroundTag":"invalid-state-transition","analyzedSha":"3f2959e683e9f83f13e55e1678c9119f63c7e8e5","analyzedAt":"2026-09-15T22:20:53.684Z","contentChangedAt":"2026-09-15T22:20:53.684Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}