← Code knowledge base
Reviewed Flutter implementation · 2026-10-27

MobileTiersController.cs

backend/src/NeoBanking.Api/Controllers/MobileTiersController.cs

Immutable source snapshot based on revision de78e63ed65c. Original line numbers are retained. This is implementation evidence; check the public API catalogue before adapting it.

The mobile /api/v1/mobile routes belong to the example backend. Company credentials stay on that backend. Some sections outside the documented product scope may be omitted.

Lines 1–316

using System.Net.Http;using System.Text.Json;using Microsoft.AspNetCore.Authorization;using Microsoft.AspNetCore.Mvc;using Microsoft.EntityFrameworkCore;using NeoBanking.Api.Cards;using NeoBanking.Api.Tiers;using NeoBanking.Application.Common;using NeoBanking.Application.DTOs.Tiers;using NeoBanking.Application.Security;using NeoBanking.Application.UseCases.Hoppa;using NeoBanking.Infrastructure.Persistence; namespace NeoBanking.Api.Controllers; [Authorize(Policy = AuthorizationPolicyNames.User)][Route("api/v1/mobile/tiers")]public sealed class MobileTiersController : HoppaProxyControllerBase{    private static readonly string[] SelectedTierKeys =        ["SelectedTierId", "selectedTierId", "TierId", "tierId", "PackageId", "packageId"];     private readonly NeoBankingDbContext _dbContext;     public MobileTiersController(NeoBankingDbContext dbContext, IProxyHoppaRequestUseCase proxyHoppa)        : base(proxyHoppa)    {        _dbContext = dbContext;    }     /// <summary>    /// The tiers this customer may choose, plus the one they are on. The provider    /// lists every tier of the company, so hidden, inactive and other-account-type    /// tiers are removed here (<see cref="CustomerTierCatalog"/>).    /// </summary>    [HttpGet]    public async Task<ActionResult<JsonElement?>> ListTiers(CancellationToken cancellationToken)    {        if (!TryGetCurrentUserId(out var userId))        {            return MissingIdentity<JsonElement?>();        }         var catalogTask = LoadCatalogAsync("mobile.tiers.list.failed", "We could not list tiers.", cancellationToken);        var customerTask = LoadCustomerAsync(userId, cancellationToken);        await Task.WhenAll(catalogTask, customerTask);         var catalog = await catalogTask;        if (!catalog.IsSuccess || catalog.Value is not JsonElement tiers)        {            return ToActionResult(catalog);        }         var customer = await customerTask;        return Ok(CustomerTierCatalog.Filter(tiers, customer.AccountType, customer.CurrentTierId));    }     [HttpGet("card-tier/{cardTierId}")]    public async Task<ActionResult<JsonElement?>> GetCardTier(string cardTierId, CancellationToken cancellationToken)    {        if (!TryGetCurrentUserId(out _))        {            return MissingIdentity<JsonElement?>();        }         return ToActionResult(await SendHoppaAsync<object?>(            HttpMethod.Get,            $"/api/v2/tiers/card-tier/{Segment(cardTierId)}",            null,            "mobile.tiers.card_tier.failed",            "We could not load card tier.",            cancellationToken));    }     [HttpPost("select")]    [HttpPost("current")]    public async Task<ActionResult<JsonElement?>> SelectTier(        [FromBody] ChangeUserTierRequestDto request,        CancellationToken cancellationToken)    {        if (!TryGetCurrentUserId(out var userId))        {            return MissingIdentity<JsonElement?>();        }         if (request.TierId is null)        {            return BadRequest(new            {                code = "mobile.tiers.tier_id_required",                message = "Select a tier to continue."            });        }         // The provider accepts any company tier from our API key, so the same rule        // that hides a tier from the list has to refuse it here.        var catalogTask = LoadCatalogAsync("mobile.tiers.select.failed", "We could not select tier.", cancellationToken);        var customerTask = LoadCustomerAsync(userId, cancellationToken);        await Task.WhenAll(catalogTask, customerTask);         var catalog = await catalogTask;        if (!catalog.IsSuccess)        {            return ToActionResult(catalog);        }         var customer = await customerTask;        var offered = catalog.Value is JsonElement tiers            ? CardLimitPolicy.FindTier(                CustomerTierCatalog.Filter(tiers, customer.AccountType, customer.CurrentTierId),                request.TierId.Value)            : null;        if (offered is null)        {            return BadRequest(new            {                code = "mobile.tiers.not_available",                message = "This plan is not available for your account."            });        }         return ToActionResult(await SendHoppaAsync(            HttpMethod.Post,            $"/api/v2/users/{Segment(userId)}/tier",            new SetUserTierRequestDto            {                TierId = request.TierId.Value,                TierCycle = request.TierCycle            },            "mobile.tiers.select.failed",            "We could not select tier.",            cancellationToken));    }     [HttpGet("current")]    public async Task<ActionResult<JsonElement?>> GetCurrentTier(CancellationToken cancellationToken)    {        if (!TryGetCurrentUserId(out var userId))        {            return MissingIdentity<JsonElement?>();        }         var userResult = await LoadHoppaUserAsync(userId, "We could not load current tier.", cancellationToken);         if (!userResult.IsSuccess)        {            return ToActionResult(userResult);        }         if (userResult.Value is not JsonElement user)        {            return Ok(new { });        }         var payload = UnwrapPayload(user);        if (!TryGetInt32(payload, out var selectedTierId, SelectedTierKeys))        {            return Ok(new { });        }         return Ok(new        {            Id = selectedTierId,            TierId = selectedTierId,            SelectedTierId = selectedTierId,            UserId = TryGetInt32(payload, out var upstreamUserId, "Id", "id", "UserId", "userId") ? upstreamUserId : (int?)null,            TierCycle = TryGetString(payload, "TierBillingCycle", "tierBillingCycle", "TierCycle", "tierCycle"),            Status = TryGetString(payload, "Status", "status")        });    }     private Task<ApplicationResult<JsonElement?>> LoadCatalogAsync(        string failureCode,        string failureMessage,        CancellationToken cancellationToken) =>        SendHoppaAsync<object?>(HttpMethod.Get, "/api/v2/tiers", null, failureCode, failureMessage, cancellationToken);     private Task<ApplicationResult<JsonElement?>> LoadHoppaUserAsync(        string userId,        string failureMessage,        CancellationToken cancellationToken) =>        SendHoppaAsync<object?>(            HttpMethod.Get,            $"/api/v2/users/{Segment(userId)}",            null,            "mobile.tiers.current.failed",            failureMessage,            cancellationToken);     /// <summary>    /// The account type and tier that decide what the customer is offered. The live    /// provider user wins; without it the account type stored at signup is used and    /// no tier counts as current.    /// </summary>    private async Task<(string AccountType, int? CurrentTierId)> LoadCustomerAsync(        string userId,        CancellationToken cancellationToken)    {        var userResult = await LoadHoppaUserAsync(userId, "We could not load your account.", cancellationToken);        if (userResult.IsSuccess && userResult.Value is JsonElement user)        {            var payload = UnwrapPayload(user);            var accountType = TryGetString(payload, "AccountType", "accountType");            int? currentTierId = TryGetInt32(payload, out var selectedTierId, SelectedTierKeys) ? selectedTierId : null;            if (!string.IsNullOrWhiteSpace(accountType))            {                return (CustomerTierCatalog.NormalizeAccountType(accountType), currentTierId);            }             return (await GetLocalAccountTypeAsync(cancellationToken), currentTierId);        }         return (await GetLocalAccountTypeAsync(cancellationToken), null);    }     private async Task<string> GetLocalAccountTypeAsync(CancellationToken cancellationToken)    {        if (!TryGetLocalUserId(out var localUserId) || !Guid.TryParse(localUserId, out var parsedUserId))        {            return "personal";        }         var metadataJson = await _dbContext.Users            .AsNoTracking()            .Where(user => user.Id == parsedUserId)            .Select(user => user.MetadataJson)            .SingleOrDefaultAsync(cancellationToken);        if (string.IsNullOrWhiteSpace(metadataJson))        {            return "personal";        }         try        {            using var document = JsonDocument.Parse(metadataJson);            return document.RootElement.ValueKind == JsonValueKind.Object &&                   document.RootElement.TryGetProperty("accountType", out var value) &&                   value.ValueKind == JsonValueKind.String                ? CustomerTierCatalog.NormalizeAccountType(value.GetString())                : "personal";        }        catch (JsonException)        {            return "personal";        }    }     private static JsonElement UnwrapPayload(JsonElement element)    {        if (element.ValueKind != JsonValueKind.Object)        {            return element;        }         foreach (var propertyName in new[] { "Data", "data", "User", "user" })        {            if (element.TryGetProperty(propertyName, out var payload) &&                payload.ValueKind == JsonValueKind.Object)            {                return payload;            }        }         return element;    }     private static bool TryGetInt32(JsonElement element, out int value, params string[] propertyNames)    {        value = 0;        if (element.ValueKind != JsonValueKind.Object)        {            return false;        }         foreach (var propertyName in propertyNames)        {            if (!element.TryGetProperty(propertyName, out var property))            {                continue;            }             if (property.ValueKind == JsonValueKind.Number &&                property.TryGetInt32(out value))            {                return true;            }             if (property.ValueKind == JsonValueKind.String &&                int.TryParse(property.GetString(), out value))            {                return true;            }        }         return false;    }     private static string? TryGetString(JsonElement element, params string[] propertyNames)    {        if (element.ValueKind != JsonValueKind.Object)        {            return null;        }         foreach (var propertyName in propertyNames)        {            if (element.TryGetProperty(propertyName, out var property) &&                property.ValueKind is JsonValueKind.String or JsonValueKind.Number)            {                return property.ToString();            }        }         return null;    }}
HOPPA DEVELOPER ASSISTANT

What are you building?

From a question to an implementation.

Get a step-by-step flow, example code and the docs behind it. Choose a starting point or describe your feature.

Answers use the public API guide and example app code. Check the linked reference before shipping. Explore code sources ↗

Your question and recent conversation are sent to our AI provider. Don’t include API keys, personal data or card details.