2026-03-16 01:33:27 +01:00
#nullable enable
using System ;
2026-03-19 01:50:14 +01:00
using System.Reflection ;
2026-03-16 01:33:27 +01:00
using System.Threading ;
using System.Threading.Tasks ;
using Serilog ;
using Wino.Core.Domain.Entities.Shared ;
2026-03-19 01:50:14 +01:00
using Wino.Core.Domain.Enums ;
2026-03-16 01:33:27 +01:00
using Wino.Core.Domain.Interfaces ;
using Wino.Core.Domain.Models.Accounts ;
2026-03-18 17:43:56 +01:00
using Wino.Mail.Api.Contracts.Ai ;
2026-03-16 01:33:27 +01:00
using Wino.Mail.Api.Contracts.Auth ;
2026-03-19 01:50:14 +01:00
using Wino.Mail.Api.Contracts.Billing ;
2026-03-16 01:33:27 +01:00
using Wino.Mail.Api.Contracts.Common ;
using Wino.Messaging.UI ;
namespace Wino.Services ;
public sealed class WinoAccountProfileService : BaseDatabaseService , IWinoAccountProfileService
{
private readonly IWinoAccountApiClient _apiClient ;
2026-03-19 01:50:14 +01:00
private readonly IStoreManagementService _storeManagementService ;
2026-03-16 01:33:27 +01:00
private readonly ILogger _logger = Log . ForContext < WinoAccountProfileService > ( ) ;
2026-03-19 01:50:14 +01:00
public WinoAccountProfileService ( IDatabaseService databaseService ,
IWinoAccountApiClient apiClient ,
IStoreManagementService storeManagementService ) : base ( databaseService )
2026-03-16 01:33:27 +01:00
{
_apiClient = apiClient ;
2026-03-19 01:50:14 +01:00
_storeManagementService = storeManagementService ;
2026-03-16 01:33:27 +01:00
}
public async Task < WinoAccountOperationResult > RegisterAsync ( string email , string password , CancellationToken cancellationToken = default )
{
var response = await _apiClient . RegisterAsync ( email , password , cancellationToken ) . ConfigureAwait ( false ) ;
var result = await PersistResponseAsync ( response ) . ConfigureAwait ( false ) ;
if ( result . IsSuccess & & result . Account ! = null )
{
ReportUIChange ( new WinoAccountSignedInMessage ( result . Account ) ) ;
}
return result ;
}
public async Task < WinoAccountOperationResult > LoginAsync ( string email , string password , CancellationToken cancellationToken = default )
{
var response = await _apiClient . LoginAsync ( email , password , cancellationToken ) . ConfigureAwait ( false ) ;
var result = await PersistResponseAsync ( response ) . ConfigureAwait ( false ) ;
if ( result . IsSuccess & & result . Account ! = null )
{
ReportUIChange ( new WinoAccountSignedInMessage ( result . Account ) ) ;
}
return result ;
}
public async Task < WinoAccountOperationResult > RefreshAsync ( CancellationToken cancellationToken = default )
{
var account = await GetActiveAccountAsync ( ) . ConfigureAwait ( false ) ;
if ( account = = null | | string . IsNullOrWhiteSpace ( account . RefreshToken ) )
{
2026-03-18 17:43:56 +01:00
_logger . Warning ( "Wino account token refresh skipped because there is no active account or refresh token." ) ;
2026-03-16 01:33:27 +01:00
return WinoAccountOperationResult . Failure ( ApiErrorCodes . RefreshTokenInvalid ) ;
}
2026-03-18 17:43:56 +01:00
_logger . Information ( "Refreshing Wino account token for {Email}" , account . Email ) ;
2026-03-16 01:33:27 +01:00
var response = await _apiClient . RefreshAsync ( account . RefreshToken , cancellationToken ) . ConfigureAwait ( false ) ;
2026-03-18 17:43:56 +01:00
var result = await PersistResponseAsync ( response ) . ConfigureAwait ( false ) ;
if ( ! result . IsSuccess )
{
_logger . Warning ( "Wino account token refresh failed for {Email}. Error code: {ErrorCode}" , account . Email , result . ErrorCode ) ;
}
return result ;
2026-03-16 01:33:27 +01:00
}
public async Task < WinoAccount ? > GetActiveAccountAsync ( )
{
var account = await Connection . Table < WinoAccount > ( ) . FirstOrDefaultAsync ( ) . ConfigureAwait ( false ) ;
return account ;
}
2026-03-18 17:43:56 +01:00
public async Task < WinoAccount ? > GetAuthenticatedAccountAsync ( CancellationToken cancellationToken = default )
{
var account = await GetActiveAccountAsync ( ) . ConfigureAwait ( false ) ;
if ( account = = null )
{
return null ;
}
if ( string . IsNullOrWhiteSpace ( account . AccessToken ) )
{
_logger . Warning ( "Wino account {Email} is missing an access token." , account . Email ) ;
return null ;
}
if ( account . AccessTokenExpiresAtUtc > DateTime . UtcNow . AddMinutes ( 1 ) )
{
return account ;
}
var refreshResult = await RefreshAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
if ( ! refreshResult . IsSuccess )
{
return null ;
}
return refreshResult . Account ? ? await GetActiveAccountAsync ( ) . ConfigureAwait ( false ) ;
}
2026-03-16 01:33:27 +01:00
public async Task < bool > HasActiveAccountAsync ( )
= > await Connection . Table < WinoAccount > ( ) . CountAsync ( ) . ConfigureAwait ( false ) > 0 ;
2026-03-19 01:50:14 +01:00
public async Task < bool > HasAddOnAsync ( WinoAddOnProductType productId , CancellationToken cancellationToken = default )
{
return productId switch
{
WinoAddOnProductType . AI_PACK = > await HasAiPackAsync ( cancellationToken ) . ConfigureAwait ( false ) ,
WinoAddOnProductType . UNLIMITED_ACCOUNTS = > await HasUnlimitedAccountsAsync ( cancellationToken ) . ConfigureAwait ( false ) ,
_ = > false
} ;
}
2026-03-18 17:43:56 +01:00
public async Task < ApiEnvelope < AuthUserDto > > GetCurrentUserAsync ( CancellationToken cancellationToken = default )
{
var account = await GetAuthenticatedAccountAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
if ( account = = null )
{
return ApiEnvelope < AuthUserDto > . Failure ( "MissingAccessToken" ) ;
}
var response = await _apiClient . GetCurrentUserAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
if ( ! response . IsSuccess )
{
_logger . Warning ( "Failed to load Wino account profile for {Email}. Error code: {ErrorCode}" , account . Email , response . ErrorCode ) ;
}
return response ;
}
public async Task < ApiEnvelope < AiStatusResultDto > > GetAiStatusAsync ( CancellationToken cancellationToken = default )
{
var account = await GetAuthenticatedAccountAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
if ( account = = null )
{
return ApiEnvelope < AiStatusResultDto > . Failure ( "MissingAccessToken" ) ;
}
var response = await _apiClient . GetAiStatusAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
if ( ! response . IsSuccess )
{
_logger . Warning ( "Failed to load AI status for Wino account {Email}. Error code: {ErrorCode}" , account . Email , response . ErrorCode ) ;
}
return response ;
}
2026-03-19 01:50:14 +01:00
public async Task < ApiEnvelope < CheckoutSessionResultDto > > CreateCheckoutSessionAsync ( WinoAddOnProductType productId , CancellationToken cancellationToken = default )
2026-03-18 17:43:56 +01:00
{
var account = await GetAuthenticatedAccountAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
if ( account = = null )
{
2026-03-19 01:50:14 +01:00
return ApiEnvelope < CheckoutSessionResultDto > . Failure ( "MissingAccessToken" ) ;
2026-03-18 17:43:56 +01:00
}
var response = await _apiClient . CreateCheckoutSessionAsync ( productId , cancellationToken ) . ConfigureAwait ( false ) ;
if ( ! response . IsSuccess )
{
_logger . Warning ( "Failed to create checkout session for product {ProductId} and Wino account {Email}. Error code: {ErrorCode}" , productId , account . Email , response . ErrorCode ) ;
}
return response ;
}
2026-03-19 01:50:14 +01:00
public async Task < ApiEnvelope < CustomerPortalResultDto > > CreateCustomerPortalSessionAsync ( CancellationToken cancellationToken = default )
{
var account = await GetAuthenticatedAccountAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
if ( account = = null )
{
return ApiEnvelope < CustomerPortalResultDto > . Failure ( "MissingAccessToken" ) ;
}
var response = await _apiClient . CreateCustomerPortalSessionAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
if ( ! response . IsSuccess )
{
_logger . Warning ( "Failed to create customer portal session for Wino account {Email}. Error code: {ErrorCode}" , account . Email , response . ErrorCode ) ;
}
return response ;
}
2026-03-16 01:33:27 +01:00
public async Task SignOutAsync ( CancellationToken cancellationToken = default )
{
var account = await GetActiveAccountAsync ( ) . ConfigureAwait ( false ) ;
if ( account ! = null & & ! string . IsNullOrWhiteSpace ( account . RefreshToken ) )
{
try
{
var result = await _apiClient . LogoutAsync ( account . RefreshToken , cancellationToken ) . ConfigureAwait ( false ) ;
if ( ! result . IsSuccess & & ! string . IsNullOrWhiteSpace ( result . ErrorCode ) )
{
_logger . Warning ( "Wino account remote sign-out failed with error code {ErrorCode}" , result . ErrorCode ) ;
}
}
catch ( Exception ex )
{
_logger . Warning ( ex , "Wino account remote sign-out failed." ) ;
}
}
await Connection . DeleteAllAsync < WinoAccount > ( ) . ConfigureAwait ( false ) ;
if ( account ! = null )
{
ReportUIChange ( new WinoAccountSignedOutMessage ( account ) ) ;
}
}
2026-03-19 01:50:14 +01:00
private async Task < WinoAccountOperationResult > PersistResponseAsync ( WinoAccountApiResult < AuthResultDto > response )
2026-03-16 01:33:27 +01:00
{
if ( ! response . IsSuccess | | response . Result = = null )
{
2026-03-19 01:50:14 +01:00
_logger . Warning ( "Wino account operation failed. Error code: {ErrorCode}. Error message: {ErrorMessage}" , response . ErrorCode , response . ErrorMessage ) ;
return WinoAccountOperationResult . Failure ( response . ErrorCode , response . ErrorMessage ) ;
2026-03-16 01:33:27 +01:00
}
var account = Map ( response . Result ) ;
await Connection . DeleteAllAsync < WinoAccount > ( ) . ConfigureAwait ( false ) ;
await Connection . InsertOrReplaceAsync ( account , typeof ( WinoAccount ) ) . ConfigureAwait ( false ) ;
return WinoAccountOperationResult . Success ( account ) ;
}
2026-03-19 01:50:14 +01:00
private async Task < bool > HasAiPackAsync ( CancellationToken cancellationToken )
{
var response = await GetAiStatusAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
return response . IsSuccess & & response . Result ? . HasAiPack = = true ;
}
private async Task < bool > HasUnlimitedAccountsAsync ( CancellationToken cancellationToken )
{
if ( await HasRemoteUnlimitedAccountsAsync ( cancellationToken ) . ConfigureAwait ( false ) )
{
return true ;
}
return await _storeManagementService . HasProductAsync ( WinoAddOnProductType . UNLIMITED_ACCOUNTS ) . ConfigureAwait ( false ) ;
}
private async Task < bool > HasRemoteUnlimitedAccountsAsync ( CancellationToken cancellationToken )
{
var response = await GetCurrentUserAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
if ( ! response . IsSuccess | | response . Result = = null )
{
return false ;
}
return TryGetBooleanProperty ( response . Result , "HasUnlimitedAccounts" , out var hasUnlimitedAccounts ) & & hasUnlimitedAccounts ;
}
[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2075", Justification = "The reflected contract property is a stable API field read from a concrete DTO instance.")]
private static bool TryGetBooleanProperty ( object instance , string propertyName , out bool value )
{
value = false ;
var property = instance . GetType ( ) . GetProperty ( propertyName , BindingFlags . Instance | BindingFlags . Public ) ;
if ( property ? . PropertyType ! = typeof ( bool ) )
{
return false ;
}
value = ( bool ) ( property . GetValue ( instance ) ? ? false ) ;
return true ;
}
2026-03-16 01:33:27 +01:00
private static WinoAccount Map ( AuthResultDto result )
= > new ( )
{
Id = result . User . UserId ,
Email = result . User . Email ,
AccountStatus = result . User . AccountStatus ,
HasPassword = result . User . HasPassword ,
HasGoogleLogin = result . User . HasGoogleLogin ,
HasFacebookLogin = result . User . HasFacebookLogin ,
AccessToken = result . AccessToken ,
AccessTokenExpiresAtUtc = result . AccessTokenExpiresAtUtc . UtcDateTime ,
RefreshToken = result . RefreshToken ,
RefreshTokenExpiresAtUtc = result . RefreshTokenExpiresAtUtc . UtcDateTime ,
LastAuthenticatedUtc = DateTime . UtcNow
} ;
}