{"record":{"id":"5256c5b253ce3c39","repo":"fullstackhero/dotnet-starter-kit","slug":"tenant-context-is-required-approvetopuprequestcommandhandler","errorCode":null,"errorMessage":"Tenant context is required.","messagePattern":"Tenant context is required\\.","errorType":"exception","errorClass":"UnauthorizedException","httpStatus":401,"severity":"error","filePath":"src/Modules/Billing/Modules.Billing/Features/v1/Wallets/ApproveTopupRequest/ApproveTopupRequestCommandHandler.cs","lineNumber":23,"sourceCode":"using FSH.Modules.Billing.Data;\nusing FSH.Modules.Billing.Services;\nusing Mediator;\nusing Microsoft.EntityFrameworkCore;\n\nnamespace FSH.Modules.Billing.Features.v1.Wallets.ApproveTopupRequest;\n\npublic sealed class ApproveTopupRequestCommandHandler(\n    BillingDbContext db,\n    IBillingService billing,\n    IMultiTenantContextAccessor<AppTenantInfo> tenantAccessor)\n    : ICommandHandler<ApproveTopupRequestCommand, Guid>\n{\n    public async ValueTask<Guid> Handle(ApproveTopupRequestCommand command, CancellationToken cancellationToken)\n    {\n        ArgumentNullException.ThrowIfNull(command);\n\n        var callerTenantId = tenantAccessor.MultiTenantContext?.TenantInfo?.Id\n            ?? throw new UnauthorizedException(\"Tenant context is required.\");\n        var isRoot = callerTenantId == MultitenancyConstants.Root.Id;\n\n        var request = await db.TopupRequests\n            .FirstOrDefaultAsync(r => r.Id == command.Id, cancellationToken)\n            .ConfigureAwait(false)\n            ?? throw new NotFoundException($\"Top-up request {command.Id} not found.\");\n\n        if (!isRoot && request.TenantId != callerTenantId)\n        {\n            throw new UnauthorizedException(\"You can only approve top-up requests for your own tenant.\");\n        }\n\n        // For root, operate on the request's own tenant; for non-root, callerTenantId equals request.TenantId.\n        var invoice = await billing.CreateTopupInvoiceAsync(request.TenantId, command.Id, cancellationToken)\n            .ConfigureAwait(false);\n\n        return invoice.Id;\n    }","sourceCodeStart":5,"sourceCodeEnd":41,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Billing/Modules.Billing/Features/v1/Wallets/ApproveTopupRequest/ApproveTopupRequestCommandHandler.cs#L5-L41","documentation":"ApproveTopupRequestCommandHandler needs the caller's tenant id to enforce that only root may approve requests of other tenants and that non-root callers approve only their own. It reads tenantAccessor.MultiTenantContext?.TenantInfo?.Id and throws UnauthorizedException('Tenant context is required.') when it is null, since without a tenant the ownership check is impossible.","triggerScenarios":"POSTing an approve-top-up-request command without a Finbuckle-resolvable tenant (missing __tenant__ header/host mapping), or calling the handler from Hangfire/CLI without a tenant context.","commonSituations":"Approving from an admin tool that omits the tenant header; host-header rewriting by a proxy breaking tenant resolution; tests instantiating the handler with an unconfigured tenant accessor.","solutions":["Send the approve request with the tenant identifier set (header __tenant__ or mapped tenant host).","Verify Finbuckle middleware registration/order in the API host.","For approving other tenants' requests, use root credentials with root tenant context resolved.","In tests, mock ITenantAccessor to return a non-null TenantInfo.Id."],"exampleFix":"// before\nawait client.PostAsync($\"/api/v1/wallets/topup-requests/{id}/approve\", null);\n// after\nvar req = new HttpRequestMessage(HttpMethod.Post, $\"/api/v1/wallets/topup-requests/{id}/approve\");\nreq.Headers.Add(\"__tenant__\", \"acme\");\nawait client.SendAsync(req);","handlingStrategy":"try-catch","validationCode":"if (tenantAccessor.MultiTenantContext?.TenantInfo?.Id is null)\n    throw new InvalidOperationException(\"ApproveTopupRequest requires tenant context; set __tenant__ header.\");","typeGuard":"bool HasTenant(ITenantAccessor a) => a.MultiTenantContext?.TenantInfo?.Id is not null;","tryCatchPattern":"try { await api.ApproveTopupRequestAsync(id); }\ncatch (UnauthorizedException ex) when (ex.Message == \"Tenant context is required.\")\n{\n    // add tenant header and retry once\n}","preventionTips":["Centralize tenant header injection in the API client used by admin tooling.","Forbid manual handler construction in tests without a tenant-aware ITenantAccessor.","Audit Hangfire/CLI callers of wallet handlers for tenant scopes.","Verify tenant resolution after any reverse-proxy or host-header change."],"tags":["multitenancy","authorization","wallet","billing"],"backgroundTag":"authentication-required","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"}