fullstackhero/dotnet-starter-kit · error · NotFoundException

Plan not found for tenant .

Error message

Plan {planId} not found for tenant {tenantId}.

What it means

CreateSubscriptionInvoiceAsync loads the plan by planId before computing term price. If no Plan row with that Id exists, NotFoundException("Plan {planId} not found for tenant {tenantId}.") is thrown and no subscription invoice is created.

Solutions

  1. Validate the planId against the Plans table before creating the subscription invoice.
  2. Re-create or restore the missing plan row, or switch to an existing plan's Id.
  3. Avoid hard-coding plan Ids across environments; resolve plans by stable code/slug instead of GUID.
  4. Ensure seed/migrations that insert plans ran in the target environment before provisioning tenants.

Example fix

// before
await billing.CreateSubscriptionInvoiceAsync(tenantId, Guid.Parse(config["PlanId"]), ...);
// after
var plan = await db.Plans.FirstOrDefaultAsync(p => p.Code == config["PlanCode"])
    ?? throw new NotFoundException($"Plan {config["PlanCode"]} not found.");
await billing.CreateSubscriptionInvoiceAsync(tenantId, plan.Id, ...);
Defensive patterns

Strategy: validation

Validate before calling

var plan = await db.Plans.FirstOrDefaultAsync(p => p.Code == planCode)
    ?? throw new NotFoundException($"Plan {planCode} not found.");
// then use plan.Id

Type guard

bool planExists(Guid id) => db.Plans.Any(p => p.Id == id); // await

Try / catch

try { await billing.CreateSubscriptionInvoiceAsync(tenantId, planId, start, end, ct); }
catch (NotFoundException ex) { log.Error("Missing plan {PlanId}", planId); }

Prevention

When it happens

Trigger: Creating a subscription invoice with a planId that doesn't exist in the Plans table — deleted plan, wrong GUID, Id from another environment, or plan seeded after the invoice job ran.

Common situations: Tenant provisioning passing a hard-coded/stale plan Id; plans re-seeded with new GUIDs while tenants reference old ones; environment clones diverging; admin tooling sending unsaved plan Ids.

Understand the failure class

Background: "Not found" and "does not exist" errors: why "Task not found", "No such folder", and "Can't find" fire when a lookup comes back empty — this error's family across 14 libraries.

Related errors


AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15). Data as JSON: /api/errors/b071d95dddc2c153. Report an issue: GitHub.

Appendix: source

Thrown at src/Modules/Billing/Modules.Billing/Services/BillingService.cs:311

        var isRoot = callerTenantId == MultitenancyConstants.Root.Id;

        return await _db.Invoices
            .FirstOrDefaultAsync(i => i.Id == invoiceId && (isRoot || i.TenantId == callerTenantId), cancellationToken)
            .ConfigureAwait(false)
            ?? throw new NotFoundException($"Invoice {invoiceId} not found.");
    }

    public async Task<Invoice?> CreateSubscriptionInvoiceAsync(
        string tenantId,
        Guid planId,
        DateTime periodStartUtc,
        DateTime periodEndUtc,
        CancellationToken cancellationToken = default)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);

        var plan = await _db.Plans.FirstOrDefaultAsync(p => p.Id == planId, cancellationToken).ConfigureAwait(false)
            ?? throw new NotFoundException($"Plan {planId} not found for tenant {tenantId}.");

        var termPrice = plan.TermPrice;
        if (termPrice.Amount <= 0m)
        {
            // Free / trial plan — validity is still set, but there is nothing to bill.
            if (_logger.IsEnabled(LogLevel.Information))
            {
                _logger.LogInformation("[Billing] plan {PlanKey} term price is zero for tenant {TenantId}, no subscription invoice", plan.Key, tenantId);
            }
            return null;
        }

        var periodStart = DateTime.SpecifyKind(periodStartUtc, DateTimeKind.Utc);
        var periodEnd = DateTime.SpecifyKind(periodEndUtc, DateTimeKind.Utc);
        var invoiceNumber = BuildSubscriptionInvoiceNumber(tenantId, periodStart);

        // Idempotency: redelivery of the subscribe/renew event must not double-invoice the term.
        var existing = await _db.Invoices

View on GitHub (pinned to 3f2959e683)