{"record":{"id":"f24e7615dc964e47","repo":"fullstackhero/dotnet-starter-kit","slug":"you-can-only-reject-top-up-requests-for-your-own-tenant","errorCode":null,"errorMessage":"You can only reject top-up requests for your own tenant.","messagePattern":"You can only reject top-up requests for your own tenant\\.","errorType":"exception","errorClass":"UnauthorizedException","httpStatus":403,"severity":"error","filePath":"src/Modules/Billing/Modules.Billing/Features/v1/Wallets/RejectTopupRequest/RejectTopupRequestCommandHandler.cs","lineNumber":33,"sourceCode":"    IMultiTenantContextAccessor<AppTenantInfo> tenantAccessor)\n    : ICommandHandler<RejectTopupRequestCommand, Guid>\n{\n    public async ValueTask<Guid> Handle(RejectTopupRequestCommand 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 reject top-up requests for your own tenant.\");\n        }\n\n        if (request.Status != TopupRequestStatus.Pending)\n        {\n            throw new CustomException(\n                $\"Top-up request {command.Id} cannot be rejected because it is {request.Status} (only Pending requests can be rejected).\",\n                (IEnumerable<string>?)null,\n                HttpStatusCode.Conflict);\n        }\n\n        request.Reject(command.Reason);\n        await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);\n        return request.Id;\n    }\n}\n","sourceCodeStart":15,"sourceCodeEnd":49,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Billing/Modules.Billing/Features/v1/Wallets/RejectTopupRequest/RejectTopupRequestCommandHandler.cs#L15-L49","documentation":"After loading the top-up request, non-root callers may only reject requests belonging to their own tenant. If request.TenantId differs from the caller's tenant Id, UnauthorizedException(\"You can only reject top-up requests for your own tenant.\") is thrown (HTTP 403 semantics). Only the Root tenant can reject requests of any tenant.","triggerScenarios":"A tenant-authenticated caller passes the Id of a top-up request that belongs to a different tenant (or the root tenant) — e.g. guessing/scraping GUIDs or a root-issued Id reused by a tenant user.","commonSituations":"Shared link to a top-up request between tenants; multi-tenant admin tooling logged in with the wrong tenant token; reusing test IDs across tenants.","solutions":["Log in with credentials/tokens of the tenant that owns the top-up request, or have the Root tenant perform the rejection.","Filter your UI's top-up request list by the caller's tenant so foreign Ids are never offered.","Do not persist or share top-up request Ids across tenants; treat them as tenant-scoped resources.","If legitimate cross-tenant rejection is required, perform it as the Root tenant."],"exampleFix":"// before\n// tenant user rejects arbitrary id\nawait mediator.Send(new RejectTopupRequestCommand(id));\n// after\nvar req = await db.TopupRequests.FindAsync(id);\nif (req == null || req.TenantId != callerTenantId) throw new UnauthorizedException(\"...own tenant.\");","handlingStrategy":"validation","validationCode":"var req = await db.TopupRequests.FindAsync(id);\nif (req is null || req.TenantId != callerTenantId)\n    return Forbid(); // or 404","typeGuard":"bool canReject(TopupRequest r, string callerTenantId, bool isRoot) => isRoot || r.TenantId == callerTenantId;","tryCatchPattern":"try { await mediator.Send(cmd); }\ncatch (UnauthorizedException ex) when (ex.Message.Contains(\"own tenant\")) { return Forbid(); }","preventionTips":["Only surface top-up request Ids that belong to the caller's tenant in the UI.","Don't share top-up request links across tenants.","Use Root credentials for cross-tenant administration."],"tags":["authorization","multitenancy","billing"],"backgroundTag":"permission-denied","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"}