Handling of paddle purchases and add-ons.

This commit is contained in:
Burak Kaan Köse
2026-03-19 01:50:14 +01:00
parent f306f6eb1c
commit b0ee5c9974
26 changed files with 779 additions and 513 deletions
+92 -12
View File
@@ -11,9 +11,12 @@ using System.Text.Json.Serialization.Metadata;
using System.Threading;
using System.Threading.Tasks;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Accounts;
using Wino.Mail.Api.Contracts.Ai;
using Wino.Mail.Api.Contracts.Auth;
using Wino.Mail.Api.Contracts.Billing;
using Wino.Mail.Api.Contracts.Common;
namespace Wino.Services;
@@ -46,13 +49,13 @@ public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
_ownsHttpClient = true;
}
public Task<ApiEnvelope<AuthResultDto>> RegisterAsync(string email, string password, CancellationToken cancellationToken = default)
public Task<WinoAccountApiResult<AuthResultDto>> RegisterAsync(string email, string password, CancellationToken cancellationToken = default)
=> SendAuthRequestAsync("api/v1/auth/register", new RegisterRequest(email, password), WinoAccountApiJsonContext.Default.RegisterRequest, cancellationToken);
public Task<ApiEnvelope<AuthResultDto>> LoginAsync(string email, string password, CancellationToken cancellationToken = default)
public Task<WinoAccountApiResult<AuthResultDto>> LoginAsync(string email, string password, CancellationToken cancellationToken = default)
=> SendAuthRequestAsync("api/v1/auth/login", new LoginRequest(email, password), WinoAccountApiJsonContext.Default.LoginRequest, cancellationToken);
public Task<ApiEnvelope<AuthResultDto>> RefreshAsync(string refreshToken, CancellationToken cancellationToken = default)
public Task<WinoAccountApiResult<AuthResultDto>> RefreshAsync(string refreshToken, CancellationToken cancellationToken = default)
=> SendAuthRequestAsync("api/v1/auth/refresh", new RefreshRequest(refreshToken), WinoAccountApiJsonContext.Default.RefreshRequest, cancellationToken);
public async Task<ApiEnvelope<JsonElement>> LogoutAsync(string refreshToken, CancellationToken cancellationToken = default)
@@ -84,20 +87,27 @@ public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
public Task<ApiEnvelope<AiStatusResultDto>> GetAiStatusAsync(CancellationToken cancellationToken = default)
=> SendAuthorizedRequestAsync("api/v1/ai/status", WinoAccountApiJsonContext.Default.ApiEnvelopeAiStatusResultDto, cancellationToken);
public Task<ApiEnvelope<string>> CreateCheckoutSessionAsync(string productId, CancellationToken cancellationToken = default)
public Task<ApiEnvelope<CheckoutSessionResultDto>> CreateCheckoutSessionAsync(WinoAddOnProductType productId, CancellationToken cancellationToken = default)
{
var endpoint = productId switch
{
"ai-pack-monthly" => "api/v1/billing/ai-pack/checkout-session",
"unlimited-accounts" => "api/v1/billing/unlimited-accounts/checkout-session",
WinoAddOnProductType.AI_PACK => "api/v1/billing/ai-pack/checkout-session",
WinoAddOnProductType.UNLIMITED_ACCOUNTS => "api/v1/billing/unlimited-accounts/checkout-session",
_ => string.Empty
};
return string.IsNullOrWhiteSpace(endpoint)
? Task.FromResult(ApiEnvelope<string>.Failure("UnknownProduct"))
: SendAuthorizedRequestAsync(HttpMethod.Post, endpoint, WinoAccountApiJsonContext.Default.ApiEnvelopeString, cancellationToken);
? Task.FromResult(ApiEnvelope<CheckoutSessionResultDto>.Failure("UnknownProduct"))
: SendAuthorizedRequestAsync(HttpMethod.Post, endpoint, WinoAccountApiJsonContext.Default.ApiEnvelopeCheckoutSessionResultDto, cancellationToken);
}
public Task<ApiEnvelope<CustomerPortalResultDto>> CreateCustomerPortalSessionAsync(CancellationToken cancellationToken = default)
=> SendAuthorizedRequestAsync(
HttpMethod.Post,
"api/v1/billing/ai-pack/customer-portal-session",
WinoAccountApiJsonContext.Default.ApiEnvelopeCustomerPortalResultDto,
cancellationToken);
public async Task<string?> GetSettingsAsync(CancellationToken cancellationToken = default)
{
try
@@ -141,7 +151,7 @@ public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
}
}
private async Task<ApiEnvelope<AuthResultDto>> SendAuthRequestAsync<TRequest>(string endpoint, TRequest request, JsonTypeInfo<TRequest> typeInfo, CancellationToken cancellationToken)
private async Task<WinoAccountApiResult<AuthResultDto>> SendAuthRequestAsync<TRequest>(string endpoint, TRequest request, JsonTypeInfo<TRequest> typeInfo, CancellationToken cancellationToken)
{
try
{
@@ -156,14 +166,83 @@ public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
? null
: JsonSerializer.Deserialize(payload, WinoAccountApiJsonContext.Default.ApiEnvelopeAuthResultDto);
return envelope ?? ApiEnvelope<AuthResultDto>.Failure($"HTTP {(int)response.StatusCode} {response.ReasonPhrase}".Trim());
if (envelope?.IsSuccess == true && envelope.Result != null)
{
return WinoAccountApiResult<AuthResultDto>.Success(envelope.Result);
}
var errorCode = envelope?.ErrorCode ?? $"HTTP {(int)response.StatusCode} {response.ReasonPhrase}".Trim();
var errorMessage = ExtractErrorMessage(payload) ?? response.ReasonPhrase;
return WinoAccountApiResult<AuthResultDto>.Failure(errorCode, errorMessage);
}
catch (Exception ex)
{
return ApiEnvelope<AuthResultDto>.Failure(ex.Message);
return WinoAccountApiResult<AuthResultDto>.Failure(ex.GetType().Name, ex.Message);
}
}
private static string? ExtractErrorMessage(string? payload)
{
if (string.IsNullOrWhiteSpace(payload))
{
return null;
}
try
{
using var document = JsonDocument.Parse(payload);
return TryGetErrorMessage(document.RootElement);
}
catch (JsonException)
{
return null;
}
}
private static string? TryGetErrorMessage(JsonElement element)
{
if (TryGetStringProperty(element, "errorMessage", out var errorMessage))
{
return errorMessage;
}
if (TryGetStringProperty(element, "message", out var message))
{
return message;
}
if (TryGetStringProperty(element, "detail", out var detail))
{
return detail;
}
if (element.ValueKind == JsonValueKind.Object && element.TryGetProperty("error", out var errorElement))
{
return TryGetErrorMessage(errorElement);
}
return null;
}
private static bool TryGetStringProperty(JsonElement element, string propertyName, out string? value)
{
value = null;
if (element.ValueKind != JsonValueKind.Object || !element.TryGetProperty(propertyName, out var property))
{
return false;
}
if (property.ValueKind != JsonValueKind.String)
{
return false;
}
value = property.GetString();
return !string.IsNullOrWhiteSpace(value);
}
private Task<ApiEnvelope<TResponse>> SendAuthorizedRequestAsync<TResponse>(string endpoint, JsonTypeInfo<ApiEnvelope<TResponse>> typeInfo, CancellationToken cancellationToken)
=> SendAuthorizedRequestAsync(HttpMethod.Get, endpoint, typeInfo, cancellationToken);
@@ -234,6 +313,7 @@ public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
[JsonSerializable(typeof(ApiEnvelope<AuthResultDto>))]
[JsonSerializable(typeof(ApiEnvelope<AuthUserDto>))]
[JsonSerializable(typeof(ApiEnvelope<AiStatusResultDto>))]
[JsonSerializable(typeof(ApiEnvelope<string>))]
[JsonSerializable(typeof(ApiEnvelope<CheckoutSessionResultDto>))]
[JsonSerializable(typeof(ApiEnvelope<CustomerPortalResultDto>))]
[JsonSerializable(typeof(ApiEnvelope<JsonElement>))]
internal sealed partial class WinoAccountApiJsonContext : JsonSerializerContext;