Handling of AI pack through mmicrosoft store.

This commit is contained in:
Burak Kaan Köse
2026-04-02 15:07:05 +02:00
parent 7b369201b0
commit 8f16f553f5
26 changed files with 765 additions and 578 deletions
+3 -3
View File
@@ -68,8 +68,7 @@ public class DatabaseService : IDatabaseService
Connection.CreateTableAsync<CalendarAttachment>(),
Connection.CreateTableAsync<Reminder>(),
Connection.CreateTableAsync<MailInvitationCalendarMapping>(),
Connection.CreateTableAsync<WinoAccount>(),
Connection.CreateTableAsync<WinoAccountAddOnCache>());
Connection.CreateTableAsync<WinoAccount>());
await EnsureSchemaUpgradesAsync().ConfigureAwait(false);
await EnsureIndexesAsync().ConfigureAwait(false);
@@ -142,6 +141,8 @@ public class DatabaseService : IDatabaseService
.ExecuteAsync($"ALTER TABLE {nameof(AccountContact)} ADD COLUMN {nameof(AccountContact.ContactPictureFileId)} TEXT NULL")
.ConfigureAwait(false);
}
await Connection.ExecuteAsync("DROP TABLE IF EXISTS WinoAccountAddOnCache").ConfigureAwait(false);
}
private async Task EnsureKeyboardShortcutSchemaAsync()
@@ -208,7 +209,6 @@ SET {nameof(KeyboardShortcut.Action)} =
await Connection.ExecuteAsync("CREATE UNIQUE INDEX IF NOT EXISTS IX_MailAccountPreferences_AccountId ON MailAccountPreferences(AccountId)").ConfigureAwait(false);
await Connection.ExecuteAsync("CREATE UNIQUE INDEX IF NOT EXISTS IX_CustomServerInformation_AccountId ON CustomServerInformation(AccountId)").ConfigureAwait(false);
await Connection.ExecuteAsync("CREATE UNIQUE INDEX IF NOT EXISTS IX_WinoAccount_Email ON WinoAccount(Email)").ConfigureAwait(false);
await Connection.ExecuteAsync("CREATE UNIQUE INDEX IF NOT EXISTS IX_WinoAccountAddOnCache_AccountId ON WinoAccountAddOnCache(AccountId)").ConfigureAwait(false);
// Calendar indexes
await Connection.ExecuteAsync("CREATE INDEX IF NOT EXISTS IX_AccountCalendar_AccountId ON AccountCalendar(AccountId)").ConfigureAwait(false);
-1
View File
@@ -29,7 +29,6 @@ public static class ServicesContainerSetup
services.AddTransient<IKeyboardShortcutService, KeyboardShortcutService>();
services.AddSingleton<IWinoAccountApiClient, WinoAccountApiClient>();
services.AddSingleton<IWinoAccountProfileService, WinoAccountProfileService>();
services.AddTransient<IWinoAddOnService, WinoAddOnService>();
services.AddSingleton<IContactPictureFileService, ContactPictureFileService>();
services.AddTransient<ICalDavClient, CalDavClient>();
+24 -22
View File
@@ -11,12 +11,10 @@ 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;
@@ -137,25 +135,27 @@ public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
WinoAccountApiJsonContext.Default.ApiEnvelopeAiTextResultDto,
cancellationToken);
public Task<ApiEnvelope<CheckoutSessionResultDto>> CreateCheckoutSessionAsync(WinoAddOnProductType productId, CancellationToken cancellationToken = default)
{
var endpoint = productId switch
{
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<CheckoutSessionResultDto>.Failure("UnknownProduct"))
: SendAuthorizedRequestAsync(HttpMethod.Post, endpoint, WinoAccountApiJsonContext.Default.ApiEnvelopeCheckoutSessionResultDto, cancellationToken);
}
public Task<ApiEnvelope<CustomerPortalResultDto>> CreateCustomerPortalSessionAsync(CancellationToken cancellationToken = default)
public Task<ApiEnvelope<WinoStoreCollectionsIdTicketInfo>> CreateCollectionsIdTicketAsync(CancellationToken cancellationToken = default)
=> SendAuthorizedRequestAsync(
HttpMethod.Post,
"api/v1/billing/ai-pack/customer-portal-session",
WinoAccountApiJsonContext.Default.ApiEnvelopeCustomerPortalResultDto,
"api/v1/store/collections-id-ticket",
WinoAccountApiJsonContext.Default.ApiEnvelopeWinoStoreCollectionsIdTicketInfo,
cancellationToken);
public Task<ApiEnvelope<WinoStoreCollectionsIdTicketInfo>> CreatePurchaseIdTicketAsync(CancellationToken cancellationToken = default)
=> SendAuthorizedRequestAsync(
HttpMethod.Post,
"api/v1/store/purchase-id-ticket",
WinoAccountApiJsonContext.Default.ApiEnvelopeWinoStoreCollectionsIdTicketInfo,
cancellationToken);
public Task<ApiEnvelope<JsonElement>> SyncStoreEntitlementsAsync(string? storeIdKey, string? purchaseIdKey, CancellationToken cancellationToken = default)
=> SendAuthorizedRequestAsync(
HttpMethod.Post,
"api/v1/store/entitlements/sync",
new SyncStoreEntitlementsRequest(storeIdKey, purchaseIdKey),
WinoAccountApiJsonContext.Default.SyncStoreEntitlementsRequest,
WinoAccountApiJsonContext.Default.ApiEnvelopeJsonElement,
cancellationToken);
public async Task<string?> GetSettingsAsync(CancellationToken cancellationToken = default)
@@ -471,7 +471,7 @@ public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
return false;
}
if (!string.IsNullOrWhiteSpace(account.AccessToken) && account.AccessTokenExpiresAtUtc > DateTime.UtcNow.AddMinutes(1))
if (!string.IsNullOrWhiteSpace(account.AccessToken) && account.AccessTokenExpiresAtUtc > DateTime.UtcNow)
{
return true;
}
@@ -542,12 +542,14 @@ public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
[JsonSerializable(typeof(SummarizeRequest))]
[JsonSerializable(typeof(TranslateRequest))]
[JsonSerializable(typeof(RewriteRequest))]
[JsonSerializable(typeof(SyncStoreEntitlementsRequest))]
[JsonSerializable(typeof(ApiEnvelope<AuthResultDto>))]
[JsonSerializable(typeof(ApiEnvelope<EmailConfirmationResendResultDto>))]
[JsonSerializable(typeof(ApiEnvelope<AuthUserDto>))]
[JsonSerializable(typeof(ApiEnvelope<AiStatusResultDto>))]
[JsonSerializable(typeof(ApiEnvelope<AiTextResultDto>))]
[JsonSerializable(typeof(ApiEnvelope<CheckoutSessionResultDto>))]
[JsonSerializable(typeof(ApiEnvelope<CustomerPortalResultDto>))]
[JsonSerializable(typeof(ApiEnvelope<WinoStoreCollectionsIdTicketInfo>))]
[JsonSerializable(typeof(ApiEnvelope<JsonElement>))]
internal sealed partial class WinoAccountApiJsonContext : JsonSerializerContext;
internal sealed record SyncStoreEntitlementsRequest(string? StoreIdKey, string? PurchaseIdKey);
+56 -181
View File
@@ -1,6 +1,5 @@
#nullable enable
using System;
using System.Reflection;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
@@ -11,7 +10,6 @@ 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;
using Wino.Messaging.UI;
@@ -79,7 +77,7 @@ public sealed class WinoAccountProfileService : BaseDatabaseService, IWinoAccoun
return WinoAccountOperationResult.Failure(ApiErrorCodes.RefreshTokenInvalid);
}
if (!string.IsNullOrWhiteSpace(account.AccessToken) && account.AccessTokenExpiresAtUtc > DateTime.UtcNow.AddMinutes(1))
if (!string.IsNullOrWhiteSpace(account.AccessToken) && account.AccessTokenExpiresAtUtc > DateTime.UtcNow)
{
return WinoAccountOperationResult.Success(account);
}
@@ -123,14 +121,6 @@ public sealed class WinoAccountProfileService : BaseDatabaseService, IWinoAccoun
}
var refreshedAccount = MergeAccountProfile(account, response.Result);
var hasUnlimitedAccounts = TryGetBooleanProperty(response.Result, "HasUnlimitedAccounts", out var cachedHasUnlimitedAccounts)
? cachedHasUnlimitedAccounts
: (bool?)null;
if (hasUnlimitedAccounts.HasValue)
{
await PersistAddOnCacheAsync(refreshedAccount.Id, null, hasUnlimitedAccounts.Value).ConfigureAwait(false);
}
if (AreEquivalentProfiles(account, refreshedAccount))
{
@@ -149,18 +139,6 @@ public sealed class WinoAccountProfileService : BaseDatabaseService, IWinoAccoun
return account;
}
public async Task<WinoAccountAddOnSnapshot?> GetCachedAddOnSnapshotAsync()
{
var account = await GetActiveAccountAsync().ConfigureAwait(false);
if (account == null)
{
return null;
}
var cache = await GetAddOnCacheAsync(account.Id).ConfigureAwait(false);
return cache == null ? null : Map(cache);
}
public async Task<WinoAccount?> GetAuthenticatedAccountAsync(CancellationToken cancellationToken = default)
{
var account = await GetActiveAccountAsync().ConfigureAwait(false);
@@ -176,7 +154,7 @@ public sealed class WinoAccountProfileService : BaseDatabaseService, IWinoAccoun
return null;
}
if (account.AccessTokenExpiresAtUtc > DateTime.UtcNow.AddMinutes(1))
if (account.AccessTokenExpiresAtUtc > DateTime.UtcNow)
{
return account;
}
@@ -193,16 +171,6 @@ public sealed class WinoAccountProfileService : BaseDatabaseService, IWinoAccoun
public async Task<bool> HasActiveAccountAsync()
=> await Connection.Table<WinoAccount>().CountAsync().ConfigureAwait(false) > 0;
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
};
}
public async Task<ApiEnvelope<AuthUserDto>> GetCurrentUserAsync(CancellationToken cancellationToken = default)
{
var account = await GetAuthenticatedAccountAsync(cancellationToken).ConfigureAwait(false);
@@ -221,11 +189,7 @@ public sealed class WinoAccountProfileService : BaseDatabaseService, IWinoAccoun
if (response.Result != null)
{
var refreshedAccount = MergeAccountProfile(account, response.Result);
var hasUnlimitedAccounts = TryGetBooleanProperty(response.Result, "HasUnlimitedAccounts", out var cachedHasUnlimitedAccounts)
? cachedHasUnlimitedAccounts
: (bool?)null;
await PersistProfileDataAsync(account, refreshedAccount, hasUnlimitedAccounts).ConfigureAwait(false);
await PersistProfileDataAsync(account, refreshedAccount).ConfigureAwait(false);
}
return response;
@@ -246,11 +210,6 @@ public sealed class WinoAccountProfileService : BaseDatabaseService, IWinoAccoun
return response;
}
if (response.Result != null)
{
await PersistAddOnCacheAsync(account.Id, response.Result).ConfigureAwait(false);
}
return response;
}
@@ -263,36 +222,65 @@ public sealed class WinoAccountProfileService : BaseDatabaseService, IWinoAccoun
public async Task<ApiEnvelope<AiTextResultDto>> RewriteAsync(string html, string mode, CancellationToken cancellationToken = default)
=> await ExecuteAiOperationAsync(account => _apiClient.RewriteAsync(html, mode, cancellationToken), "rewrite", cancellationToken).ConfigureAwait(false);
public async Task<ApiEnvelope<CheckoutSessionResultDto>> CreateCheckoutSessionAsync(WinoAddOnProductType productId, CancellationToken cancellationToken = default)
public async Task<ApiEnvelope<JsonElement>> SyncStoreEntitlementsAsync(CancellationToken cancellationToken = default)
{
var account = await GetAuthenticatedAccountAsync(cancellationToken).ConfigureAwait(false);
if (account == null)
var account = await GetActiveAccountAsync().ConfigureAwait(false);
if (account == null || string.IsNullOrWhiteSpace(account.AccessToken))
{
return ApiEnvelope<CheckoutSessionResultDto>.Failure("MissingAccessToken");
return ApiEnvelope<JsonElement>.Failure("MissingAccessToken");
}
var response = await _apiClient.CreateCheckoutSessionAsync(productId, cancellationToken).ConfigureAwait(false);
string? storeIdKey = null;
string? purchaseIdKey = null;
var collectionsTicketResponse = await _apiClient.CreateCollectionsIdTicketAsync(cancellationToken).ConfigureAwait(false);
if (collectionsTicketResponse.IsSuccess && collectionsTicketResponse.Result != null)
{
storeIdKey = await _storeManagementService.GetCustomerCollectionsIdAsync(
collectionsTicketResponse.Result.ServiceTicket,
collectionsTicketResponse.Result.PublisherUserId).ConfigureAwait(false);
if (string.IsNullOrWhiteSpace(storeIdKey))
{
_logger.Warning("Failed to obtain Microsoft Store collections ID key for Wino account {Email}.", account.Email);
}
}
else
{
_logger.Warning("Failed to create Microsoft Store collections ticket for Wino account {Email}. Error code: {ErrorCode}", account.Email, collectionsTicketResponse.ErrorCode);
}
var purchaseTicketResponse = await _apiClient.CreatePurchaseIdTicketAsync(cancellationToken).ConfigureAwait(false);
if (purchaseTicketResponse.IsSuccess && purchaseTicketResponse.Result != null)
{
purchaseIdKey = await _storeManagementService.GetCustomerPurchaseIdAsync(
purchaseTicketResponse.Result.ServiceTicket,
purchaseTicketResponse.Result.PublisherUserId).ConfigureAwait(false);
if (string.IsNullOrWhiteSpace(purchaseIdKey))
{
_logger.Warning("Failed to obtain Microsoft Store purchase ID key for Wino account {Email}.", account.Email);
}
}
else
{
_logger.Warning("Failed to create Microsoft Store purchase ticket for Wino account {Email}. Error code: {ErrorCode}", account.Email, purchaseTicketResponse.ErrorCode);
}
if (string.IsNullOrWhiteSpace(storeIdKey) && string.IsNullOrWhiteSpace(purchaseIdKey))
{
return ApiEnvelope<JsonElement>.Failure("StoreEntitlementKeysMissing");
}
var response = await _apiClient.SyncStoreEntitlementsAsync(storeIdKey, purchaseIdKey, 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);
_logger.Warning("Failed to sync Microsoft Store entitlements for Wino account {Email}. Error code: {ErrorCode}", account.Email, response.ErrorCode);
return response;
}
return response;
}
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);
}
await RefreshProfileAsync(cancellationToken).ConfigureAwait(false);
await GetAiStatusAsync(cancellationToken).ConfigureAwait(false);
return response;
}
@@ -324,7 +312,7 @@ public sealed class WinoAccountProfileService : BaseDatabaseService, IWinoAccoun
var refreshResult = await RefreshProfileAsync(cancellationToken).ConfigureAwait(false);
if (refreshResult.IsSuccess && await HasAddOnAsync(targetProductType.Value, cancellationToken).ConfigureAwait(false))
if (refreshResult.IsSuccess && await _storeManagementService.HasProductAsync(targetProductType.Value).ConfigureAwait(false))
{
ReportUIChange(new WinoAccountAddOnPurchasedMessage(targetProductType.Value));
return true;
@@ -365,8 +353,6 @@ public sealed class WinoAccountProfileService : BaseDatabaseService, IWinoAccoun
}
await Connection.DeleteAllAsync<WinoAccount>().ConfigureAwait(false);
await Connection.DeleteAllAsync<WinoAccountAddOnCache>().ConfigureAwait(false);
if (account != null)
{
ReportUIChange(new WinoAccountProfileDeletedMessage(account));
@@ -395,29 +381,18 @@ public sealed class WinoAccountProfileService : BaseDatabaseService, IWinoAccoun
await Connection.InsertOrReplaceAsync(account, typeof(WinoAccount)).ConfigureAwait(false);
}
private async Task PersistProfileDataAsync(WinoAccount originalAccount, WinoAccount refreshedAccount, bool? hasUnlimitedAccounts)
private async Task PersistProfileDataAsync(WinoAccount originalAccount, WinoAccount refreshedAccount)
{
if (!AreEquivalentProfiles(originalAccount, refreshedAccount))
{
await PersistAccountAsync(refreshedAccount).ConfigureAwait(false);
PublishProfileUpdated(refreshedAccount);
}
if (hasUnlimitedAccounts.HasValue)
{
await PersistAddOnCacheAsync(refreshedAccount.Id, null, hasUnlimitedAccounts.Value).ConfigureAwait(false);
}
}
private void PublishProfileUpdated(WinoAccount account)
=> ReportUIChange(new WinoAccountProfileUpdatedMessage(account));
private async Task<bool> HasAiPackAsync(CancellationToken cancellationToken)
{
var response = await GetAiStatusAsync(cancellationToken).ConfigureAwait(false);
return response.IsSuccess && response.Result?.HasAiPack == true;
}
private async Task<ApiEnvelope<AiTextResultDto>> ExecuteAiOperationAsync(Func<WinoAccount, Task<ApiEnvelope<AiTextResultDto>>> executeAsync,
string operationName,
CancellationToken cancellationToken)
@@ -434,94 +409,9 @@ public sealed class WinoAccountProfileService : BaseDatabaseService, IWinoAccoun
_logger.Warning("Failed to {Operation} HTML with AI for Wino account {Email}. Error code: {ErrorCode}", operationName, account.Email, response.ErrorCode);
}
if (response.Quota != null)
{
await PersistAddOnCacheAsync(account.Id, Map(response.Quota)).ConfigureAwait(false);
}
return response;
}
private async Task<bool> HasUnlimitedAccountsAsync(CancellationToken cancellationToken)
{
var cachedSnapshot = await GetCachedAddOnSnapshotAsync().ConfigureAwait(false);
if (cachedSnapshot?.HasUnlimitedAccounts == true)
{
return true;
}
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 (await GetCachedAddOnSnapshotAsync().ConfigureAwait(false))?.HasUnlimitedAccounts == true;
}
return TryGetBooleanProperty(response.Result, "HasUnlimitedAccounts", out var hasUnlimitedAccounts) && hasUnlimitedAccounts;
}
private async Task<WinoAccountAddOnCache?> GetAddOnCacheAsync(Guid accountId)
=> await Connection.Table<WinoAccountAddOnCache>()
.Where(cache => cache.AccountId == accountId)
.FirstOrDefaultAsync()
.ConfigureAwait(false);
private async Task PersistAddOnCacheAsync(Guid accountId, AiStatusResultDto? aiStatus = null, bool? hasUnlimitedAccounts = null)
{
var cache = await GetAddOnCacheAsync(accountId).ConfigureAwait(false) ?? new WinoAccountAddOnCache
{
AccountId = accountId
};
if (aiStatus != null)
{
cache.HasAiPack = aiStatus.HasAiPack;
cache.AiUsageCount = aiStatus.HasAiPack ? aiStatus.Used : null;
cache.AiUsageLimit = aiStatus.HasAiPack ? aiStatus.MonthlyLimit : null;
cache.AiBillingPeriodStartUtc = aiStatus.CurrentPeriodStartUtc?.UtcDateTime;
cache.AiBillingPeriodEndUtc = aiStatus.CurrentPeriodEndUtc?.UtcDateTime;
}
if (hasUnlimitedAccounts.HasValue)
{
cache.HasUnlimitedAccounts = hasUnlimitedAccounts.Value;
}
cache.LastUpdatedUtc = DateTime.UtcNow;
await Connection.InsertOrReplaceAsync(cache, typeof(WinoAccountAddOnCache)).ConfigureAwait(false);
}
private static WinoAccountAddOnSnapshot Map(WinoAccountAddOnCache cache)
=> new(
cache.HasAiPack,
cache.AiUsageCount,
cache.AiUsageLimit,
cache.AiBillingPeriodStartUtc is DateTime periodStartUtc ? new DateTimeOffset(DateTime.SpecifyKind(periodStartUtc, DateTimeKind.Utc)) : null,
cache.AiBillingPeriodEndUtc is DateTime periodEndUtc ? new DateTimeOffset(DateTime.SpecifyKind(periodEndUtc, DateTimeKind.Utc)) : null,
cache.HasUnlimitedAccounts,
new DateTimeOffset(DateTime.SpecifyKind(cache.LastUpdatedUtc, DateTimeKind.Utc)));
private static AiStatusResultDto Map(QuotaInfoDto quota)
=> new(
quota.HasAiPack,
quota.EntitlementStatus,
quota.CurrentPeriodStartUtc,
quota.CurrentPeriodEndUtc,
quota.MonthlyLimit,
quota.Used,
quota.Remaining,
quota.Product);
private static bool AreEquivalentProfiles(WinoAccount left, WinoAccount right)
=> left.Id == right.Id &&
string.Equals(left.Email, right.Email, StringComparison.Ordinal) &&
@@ -546,21 +436,6 @@ public sealed class WinoAccountProfileService : BaseDatabaseService, IWinoAccoun
LastAuthenticatedUtc = existingAccount.LastAuthenticatedUtc
};
[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;
}
private static WinoAddOnProductType? ResolveProductType(Uri callbackUri)
{
var productCode = GetQueryParameter(callbackUri, "productCode");
-86
View File
@@ -1,86 +0,0 @@
#nullable enable
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Accounts;
namespace Wino.Services;
public sealed class WinoAddOnService : IWinoAddOnService
{
private readonly IWinoAccountProfileService _profileService;
public WinoAddOnService(IWinoAccountProfileService profileService)
{
_profileService = profileService;
}
public async Task<IReadOnlyList<WinoAddOnInfo>> GetAvailableAddOnsAsync(bool useCachedDataOnly = false, CancellationToken cancellationToken = default)
{
var cachedSnapshot = await _profileService.GetCachedAddOnSnapshotAsync().ConfigureAwait(false);
if (useCachedDataOnly)
{
return BuildAddOnInfos(cachedSnapshot);
}
var aiStatusTask = _profileService.GetAiStatusAsync(cancellationToken);
var hasUnlimitedAccountsTask = _profileService.HasAddOnAsync(WinoAddOnProductType.UNLIMITED_ACCOUNTS, cancellationToken);
await Task.WhenAll(aiStatusTask, hasUnlimitedAccountsTask).ConfigureAwait(false);
var aiStatusResponse = await aiStatusTask.ConfigureAwait(false);
var aiStatus = aiStatusResponse.IsSuccess ? aiStatusResponse.Result : null;
var hasUnlimitedAccounts = await hasUnlimitedAccountsTask.ConfigureAwait(false);
if (aiStatus == null && cachedSnapshot != null)
{
return BuildAddOnInfos(cachedSnapshot with
{
HasUnlimitedAccounts = hasUnlimitedAccounts || cachedSnapshot.HasUnlimitedAccounts
});
}
return
[
new WinoAddOnInfo(
WinoAddOnProductType.AI_PACK,
aiStatus?.HasAiPack == true,
aiStatus?.Used,
aiStatus?.MonthlyLimit,
aiStatus?.HasAiPack == true && aiStatus.MonthlyLimit is int limit && limit > 0 && aiStatus.Used is int used
? (double)used / limit * 100
: 0,
aiStatus?.CurrentPeriodEndUtc),
new WinoAddOnInfo(
WinoAddOnProductType.UNLIMITED_ACCOUNTS,
hasUnlimitedAccounts || cachedSnapshot?.HasUnlimitedAccounts == true)
];
}
private static IReadOnlyList<WinoAddOnInfo> BuildAddOnInfos(WinoAccountAddOnSnapshot? snapshot)
{
if (snapshot == null)
{
return [];
}
return
[
new WinoAddOnInfo(
WinoAddOnProductType.AI_PACK,
snapshot.HasAiPack,
snapshot.UsageCount,
snapshot.UsageLimit,
snapshot.HasAiPack && snapshot.UsageLimit is int limit && limit > 0 && snapshot.UsageCount is int used
? (double)used / limit * 100
: 0,
snapshot.BillingPeriodEndUtc),
new WinoAddOnInfo(
WinoAddOnProductType.UNLIMITED_ACCOUNTS,
snapshot.HasUnlimitedAccounts)
];
}
}