From 829542714a840771e13d35274eaa23d83d84c76b Mon Sep 17 00:00:00 2001 From: Dmitry Date: Thu, 8 Jan 2026 19:58:13 +0300 Subject: [PATCH] product availability --- .../lib/api/di/injector.config.dart | 134 ++++++++++-------- .../lib/api/purchase/payment_manager.dart | 19 ++- .../lib/api/v2/admin_analytics_api_v2.dart | 16 +-- .../lib/api/v2/admin_users_api_v2.dart | 23 ++- .../lib/api/v2/packs_api_v2.dart | 46 ++---- .../lib/api/v2/purchases_api_v2.dart | 52 ++++--- .../lib/api/v2/telegram_bot_api_v2.dart | 5 +- .../lib/api/v2/users_api_v2.dart | 5 +- .../lib/cron/add_free_packs.dart | 15 +- .../lib/cron/check_admins.dart | 4 +- .../lib/database/daos/payment_dao.dart | 17 ++- .../lib/database/daos/user_dao.dart | 7 - mnemo_cards_backend/lib/main.dart | 7 +- .../lib/packs/card_pack_drift_extension.dart | 1 - .../lib/packs/free_packs_distributor.dart | 27 ++-- .../lib/packs/pack_dto_converter.dart | 8 +- .../lib/packs/pack_repository.dart | 1 - .../lib/user/user_manager.dart | 13 +- .../lib/user/user_repository.dart | 26 +--- 19 files changed, 219 insertions(+), 207 deletions(-) diff --git a/mnemo_cards_backend/lib/api/di/injector.config.dart b/mnemo_cards_backend/lib/api/di/injector.config.dart index 95693eb..1460ad6 100644 --- a/mnemo_cards_backend/lib/api/di/injector.config.dart +++ b/mnemo_cards_backend/lib/api/di/injector.config.dart @@ -20,6 +20,7 @@ import '../../packs/free_packs_distributor.dart' as _i1062; import '../../packs/pack_dto_converter.dart' as _i433; import '../../packs/pack_manager.dart' as _i833; import '../../packs/pack_repository.dart' as _i258; +import '../../packs/product_availability_manager.dart' as _i797; import '../../packs/products_price_resolver.dart' as _i908; import '../../promo_codes/promo_codes_manager.dart' as _i151; import '../../statistics/achievement_manager.dart' as _i802; @@ -158,20 +159,6 @@ extension GetItInjectableX on _i174.GetIt { gh.lazySingleton<_i985.TasksApiV2>( () => _i985.TasksApiV2(gh<_i586.TaskManager>()), ); - gh.lazySingleton<_i1009.PaymentManager>( - () => _i1009.PaymentManager( - gh<_i1072.AppDatabase>(), - gh<_i950.UserRepository>(), - gh<_i988.YooMoneyHandler>(), - gh<_i222.RustorePurchaseHandler>(), - ), - ); - gh.lazySingleton<_i151.PromoCodesManager>( - () => _i151.PromoCodesManager( - gh<_i1072.AppDatabase>(), - gh<_i1009.PaymentManager>(), - ), - ); gh.factory<_i1015.AdminPacksApiV2>( () => _i1015.AdminPacksApiV2( gh<_i1072.AppDatabase>(), @@ -184,57 +171,15 @@ extension GetItInjectableX on _i174.GetIt { gh<_i1072.AppDatabase>(), ), ); - gh.lazySingleton<_i273.PromocodesApiV2>( - () => _i273.PromocodesApiV2(gh<_i151.PromoCodesManager>()), - ); gh.lazySingleton<_i908.ProductsPriceResolver>( () => _i908.ProductsPriceResolver( gh<_i891.DiscountsManager>(), gh<_i950.UserRepository>(), ), ); - gh.lazySingleton<_i1062.FreePacksDistributor>( - () => _i1062.FreePacksDistributor( - gh<_i950.UserRepository>(), - gh<_i258.PackRepository>(), - ), - ); - gh.lazySingleton<_i735.CheckPaymentTask>( - () => _i735.CheckPaymentTask( - gh<_i1009.PaymentManager>(), - gh<_i1072.AppDatabase>(), - ), - ); gh.lazySingleton<_i858.DiscountsApiV2>( () => _i858.DiscountsApiV2(gh<_i891.DiscountsManager>()), ); - gh.lazySingleton<_i280.UserManager>( - () => _i280.UserManager( - gh<_i1072.AppDatabase>(), - gh<_i950.UserRepository>(), - gh<_i1062.FreePacksDistributor>(), - gh<_i71.SessionTracker>(), - gh<_i1029.StatisticsCalculator>(), - gh<_i802.AchievementManager>(), - gh<_i909.WordStatisticsManager>(), - ), - ); - gh.lazySingleton<_i247.UsersApiV2>( - () => _i247.UsersApiV2( - gh<_i280.UserManager>(), - gh<_i1009.PaymentManager>(), - gh<_i1029.StatisticsCalculator>(), - gh<_i950.UserRepository>(), - gh<_i1072.AppDatabase>(), - ), - ); - gh.lazySingleton<_i483.AdminAuthApiV2>( - () => _i483.AdminAuthApiV2( - gh<_i240.TelegramAuthCodeService>(), - gh<_i280.UserManager>(), - gh<_i108.JwtService>(), - ), - ); gh.lazySingleton<_i433.PackDtoConverter>( () => _i433.PackDtoConverter( gh<_i908.ProductsPriceResolver>(), @@ -248,6 +193,37 @@ extension GetItInjectableX on _i174.GetIt { gh<_i433.PackDtoConverter>(), ), ); + gh.lazySingleton<_i797.ProductAvailabilityManager>( + () => _i797.ProductAvailabilityManager( + gh<_i833.PackManager>(), + gh<_i1072.AppDatabase>(), + publicPackIds: gh>(), + ), + ); + gh.lazySingleton<_i1062.FreePacksDistributor>( + () => _i1062.FreePacksDistributor( + gh<_i797.ProductAvailabilityManager>(), + gh<_i258.PackRepository>(), + ), + ); + gh.lazySingleton<_i280.UserManager>( + () => _i280.UserManager( + gh<_i1072.AppDatabase>(), + gh<_i950.UserRepository>(), + gh<_i1062.FreePacksDistributor>(), + gh<_i71.SessionTracker>(), + gh<_i1029.StatisticsCalculator>(), + gh<_i802.AchievementManager>(), + gh<_i909.WordStatisticsManager>(), + ), + ); + gh.lazySingleton<_i483.AdminAuthApiV2>( + () => _i483.AdminAuthApiV2( + gh<_i240.TelegramAuthCodeService>(), + gh<_i280.UserManager>(), + gh<_i108.JwtService>(), + ), + ); gh.lazySingleton<_i560.UserManager>( () => _i560.UserManager( gh<_i1072.AppDatabase>(), @@ -260,12 +236,13 @@ extension GetItInjectableX on _i174.GetIt { gh.lazySingleton<_i561.MnemoShelf>( () => _i561.MnemoShelf(gh<_i280.UserManager>()), ); - gh.lazySingleton<_i69.PurchasesApiV2>( - () => _i69.PurchasesApiV2( - gh<_i1009.PaymentManager>(), - gh<_i833.PackManager>(), - gh<_i950.UserRepository>(), + gh.lazySingleton<_i1009.PaymentManager>( + () => _i1009.PaymentManager( gh<_i1072.AppDatabase>(), + gh<_i950.UserRepository>(), + gh<_i797.ProductAvailabilityManager>(), + gh<_i988.YooMoneyHandler>(), + gh<_i222.RustorePurchaseHandler>(), ), ); gh.lazySingleton<_i800.PacksApiV2>( @@ -273,11 +250,17 @@ extension GetItInjectableX on _i174.GetIt { gh<_i833.PackManager>(), gh<_i259.TestManager>(), gh<_i258.PackRepository>(), - gh<_i950.UserRepository>(), + gh<_i797.ProductAvailabilityManager>(), gh<_i1072.AppDatabase>(), gh<_i747.MinioService>(), ), ); + gh.lazySingleton<_i735.CheckPaymentTask>( + () => _i735.CheckPaymentTask( + gh<_i1009.PaymentManager>(), + gh<_i1072.AppDatabase>(), + ), + ); gh.lazySingleton<_i170.TestsApiV2>( () => _i170.TestsApiV2( gh<_i259.TestManager>(), @@ -285,6 +268,15 @@ extension GetItInjectableX on _i174.GetIt { gh<_i1072.AppDatabase>(), ), ); + gh.lazySingleton<_i69.PurchasesApiV2>( + () => _i69.PurchasesApiV2( + gh<_i1009.PaymentManager>(), + gh<_i833.PackManager>(), + gh<_i950.UserRepository>(), + gh<_i797.ProductAvailabilityManager>(), + gh<_i1072.AppDatabase>(), + ), + ); gh.lazySingleton<_i52.AuthApiV2>( () => _i52.AuthApiV2( gh<_i1072.AppDatabase>(), @@ -294,6 +286,24 @@ extension GetItInjectableX on _i174.GetIt { gh<_i240.TelegramAuthCodeService>(), ), ); + gh.lazySingleton<_i247.UsersApiV2>( + () => _i247.UsersApiV2( + gh<_i280.UserManager>(), + gh<_i1009.PaymentManager>(), + gh<_i1029.StatisticsCalculator>(), + gh<_i950.UserRepository>(), + gh<_i1072.AppDatabase>(), + ), + ); + gh.lazySingleton<_i151.PromoCodesManager>( + () => _i151.PromoCodesManager( + gh<_i1072.AppDatabase>(), + gh<_i1009.PaymentManager>(), + ), + ); + gh.lazySingleton<_i273.PromocodesApiV2>( + () => _i273.PromocodesApiV2(gh<_i151.PromoCodesManager>()), + ); return this; } } diff --git a/mnemo_cards_backend/lib/api/purchase/payment_manager.dart b/mnemo_cards_backend/lib/api/purchase/payment_manager.dart index 0cc8e5f..a785b5b 100644 --- a/mnemo_cards_backend/lib/api/purchase/payment_manager.dart +++ b/mnemo_cards_backend/lib/api/purchase/payment_manager.dart @@ -10,6 +10,7 @@ import 'package:drift/drift.dart' as drift; import 'payment_drift_extension.dart'; import 'rustore/rustore_purchase_handler.dart'; import 'yoo_money.dart'; +import '../../packs/product_availability_manager.dart'; import '../../user/user_repository.dart'; /// Result of creating YooKassa payment URL @@ -27,6 +28,7 @@ class YookassaPaymentResult { class PaymentManager { final AppDatabase _db; final UserRepository _userRepository; + final ProductAvailabilityManager _productAvailabilityManager; late final GooglePlayPurchaseHandler googlePurchaseHandler; final YooMoneyHandler _yooMoneyHandler; final RustorePurchaseHandler _rustorePurchaseHandler; @@ -34,6 +36,7 @@ class PaymentManager { PaymentManager( this._db, this._userRepository, + this._productAvailabilityManager, this._yooMoneyHandler, this._rustorePurchaseHandler, ); @@ -79,13 +82,12 @@ class PaymentManager { ) async { await _db.transaction(() async { if (product.type == MnemoCardsProductType.pack && product.id != null) { - final packId = product.id!; - await _userRepository.grantPackAccess( + await _productAvailabilityManager.grantProductAccess( userId: userId, - packId: packId, + product: product, grantType: 'promo_code', ); - log('Granted pack $packId to user $userId via promo code'); + log('Granted pack ${product.id} to user $userId via promo code'); } else if (product.type == MnemoCardsProductType.subscription && product.id != null) { final planId = product.id!; @@ -99,7 +101,7 @@ class PaymentManager { userId: userId, start: PgDateTime(now), finish: PgDateTime(endDate), - features: drift.Value(plan.features as List), + features: drift.Value(plan.features), ), ); log('Granted subscription $planId to user $userId via promo code'); @@ -120,7 +122,10 @@ class PaymentManager { } // Получить пользователя - final userModel = await _userRepository.getUserById(payment.userId, withData: true); + final userModel = await _userRepository.getUserById( + payment.userId, + withData: true, + ); if (userModel == null) { log('User not found: ${payment.userId}'); return; @@ -160,7 +165,7 @@ class PaymentManager { await _db.transaction(() async { // Дать доступ к пакетам for (final packId in packIds) { - await _userRepository.grantPackAccess( + await _productAvailabilityManager.grantPackAccess( userId: payment.userId, packId: packId, grantType: 'purchase', diff --git a/mnemo_cards_backend/lib/api/v2/admin_analytics_api_v2.dart b/mnemo_cards_backend/lib/api/v2/admin_analytics_api_v2.dart index 9a3b221..97ea9f5 100644 --- a/mnemo_cards_backend/lib/api/v2/admin_analytics_api_v2.dart +++ b/mnemo_cards_backend/lib/api/v2/admin_analytics_api_v2.dart @@ -61,25 +61,15 @@ class AdminAnalyticsApiV2 { final packCount = await _db.packDao.countPacks(); // Get all packs and filter in memory - final enabledPacks = await _db.packDao.getAllPacks( - enabledOnly: true, - ); + final enabledPacks = await _db.packDao.getAllPacks(enabledOnly: true); final enabledPackCount = enabledPacks.length; final paymentCount = await _db.paymentDao.countAllPayments(); // Get recent users (last 10) - final allUsers = await _userRepository.getAllUsers( - limit: 10, - ); + final allUsers = await _userRepository.getAllUsers(limit: 10); final recentUsers = allUsers - .map( - (u) => { - 'id': u.id, - 'name': u.name, - 'email': u.email, - }, - ) + .map((u) => {'id': u.id, 'name': u.name, 'email': u.email}) .toList(); // Get top packs by user count diff --git a/mnemo_cards_backend/lib/api/v2/admin_users_api_v2.dart b/mnemo_cards_backend/lib/api/v2/admin_users_api_v2.dart index 1b53283..f42b6e5 100644 --- a/mnemo_cards_backend/lib/api/v2/admin_users_api_v2.dart +++ b/mnemo_cards_backend/lib/api/v2/admin_users_api_v2.dart @@ -160,7 +160,10 @@ class AdminUsersApiV2 { }, statusCode: 400); } - final userModel = await _userRepository.getUserById(userId, includePacks: true); + final userModel = await _userRepository.getUserById( + userId, + includePacks: true, + ); if (userModel == null) { return _json({ 'error': 'User not found', @@ -219,7 +222,9 @@ class AdminUsersApiV2 { if (isUpdate) { // Update existing user - final existingUserModel = await _userRepository.getUserById(userDto.id!); + final existingUserModel = await _userRepository.getUserById( + userDto.id!, + ); if (existingUserModel == null) { return _json({ 'error': 'User not found', @@ -254,7 +259,10 @@ class AdminUsersApiV2 { await _userRepository.updateUserPartial(updates); // Get updated user - final updatedUserModel = await _userRepository.getUserById(userDto.id!, includePacks: true); + final updatedUserModel = await _userRepository.getUserById( + userDto.id!, + includePacks: true, + ); if (updatedUserModel == null) { return _json({ 'error': 'Database error', @@ -275,7 +283,9 @@ class AdminUsersApiV2 { : 'admin_created_${DateTime.now().millisecondsSinceEpoch}'; // Check if user with this externalUserId already exists - final existingUserModel = await _userRepository.getUserByExternalId(externalUserId); + final existingUserModel = await _userRepository.getUserByExternalId( + externalUserId, + ); if (existingUserModel != null) { return _json({ 'error': 'User already exists', @@ -307,7 +317,10 @@ class AdminUsersApiV2 { userData: userDataCompanion, ); - final createdUserModel = await _userRepository.getUserById(userId, includePacks: true); + final createdUserModel = await _userRepository.getUserById( + userId, + includePacks: true, + ); if (createdUserModel == null) { return _json({ 'error': 'Database error', diff --git a/mnemo_cards_backend/lib/api/v2/packs_api_v2.dart b/mnemo_cards_backend/lib/api/v2/packs_api_v2.dart index 800ca2d..9fa8f82 100644 --- a/mnemo_cards_backend/lib/api/v2/packs_api_v2.dart +++ b/mnemo_cards_backend/lib/api/v2/packs_api_v2.dart @@ -12,12 +12,11 @@ import 'package:mnemo_cards_backend/packs/card_image_storage.dart'; import 'package:mnemo_cards_backend/packs/card_model_extension.dart'; import 'package:mnemo_cards_backend/packs/pack_manager.dart' show PackManager; import 'package:mnemo_cards_backend/packs/pack_repository.dart'; +import 'package:mnemo_cards_backend/packs/product_availability_manager.dart'; import 'package:mnemo_cards_backend/packs/voice_storage.dart'; import 'package:mnemo_cards_backend/storage/minio_config.dart'; import 'package:mnemo_cards_backend/storage/minio_service.dart'; import 'package:mnemo_cards_backend/tests/test_manager.dart'; -import 'package:mnemo_cards_backend/user/user_repository.dart'; -import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart'; import 'package:mnemo_cards_common/mnemo_cards_common.dart'; import 'package:shelf/shelf.dart'; import 'package:shelf_open_api/shelf_open_api.dart'; @@ -33,7 +32,7 @@ class PacksApiV2 { final PackManager _packManager; final TestManager _testManager; final PackRepository _packRepository; - final UserRepository _userRepository; + final ProductAvailabilityManager _productAvailabilityManager; final AppDatabase _db; final MinioService _minioService; @@ -41,7 +40,7 @@ class PacksApiV2 { this._packManager, this._testManager, this._packRepository, - this._userRepository, + this._productAvailabilityManager, this._db, this._minioService, ); @@ -66,12 +65,6 @@ class PacksApiV2 { headers: {'Content-Type': 'application/json'}, ); - Response _conflict(String message) => Response( - 409, - body: jsonEncode({'error': 'Conflict', 'message': message}), - headers: {'Content-Type': 'application/json'}, - ); - Response _forbidden([String? message]) => Response( 403, body: jsonEncode({ @@ -289,10 +282,11 @@ class PacksApiV2 { final packJson = packDto.toJson(); if (user != null) { - final isPurchased = - await _isPackPurchased(user.id!, packId) || - (await _hasSubscriptionAccess(user)); - packJson['isPurchased'] = isPurchased; + final isAvailable = await _productAvailabilityManager.isPackAvailable( + packId: packId, + user: user, + ); + packJson['isPurchased'] = isAvailable; } return _ok(packJson); @@ -331,10 +325,11 @@ class PacksApiV2 { // Include purchase status in response if user is authenticated if (user != null) { - final isPurchased = - await _isPackPurchased(user.id!, packId) || - (await _hasSubscriptionAccess(user)); - packJson['isPurchased'] = isPurchased; + final isAvailable = await _productAvailabilityManager.isPackAvailable( + packId: packId, + user: user, + ); + packJson['isPurchased'] = isAvailable; } return _ok(packJson); @@ -834,11 +829,6 @@ class PacksApiV2 { } } - /// Check if user purchased a pack - Future _isPackPurchased(String userId, String packId) async { - return await _userRepository.hasPackAccess(userId, packId); - } - /// GET /api/v2/packs/{packId}/tests /// Get tests for a pack @Route.get('/packs//tests') @@ -900,16 +890,6 @@ class PacksApiV2 { } } - /// Helper method to check if user has subscription access - Future _hasSubscriptionAccess(UserModel user) async { - // Check subscription features from user model - final subscription = user.subscriptionModel; - if (subscription == null) { - return false; - } - return subscription.features.contains(SubscriptionFeatureEnum.packs); - } - /// Convert Drift VoiceModel to VoiceDto VoiceDto _voiceModelToDto( drift.VoiceModel voice, { diff --git a/mnemo_cards_backend/lib/api/v2/purchases_api_v2.dart b/mnemo_cards_backend/lib/api/v2/purchases_api_v2.dart index a6f9a2c..db33ad3 100644 --- a/mnemo_cards_backend/lib/api/v2/purchases_api_v2.dart +++ b/mnemo_cards_backend/lib/api/v2/purchases_api_v2.dart @@ -6,7 +6,7 @@ import 'package:mnemo_cards_backend/api/authorize/helpers.dart'; import 'package:mnemo_cards_backend/api/purchase/payment_manager.dart'; import 'package:mnemo_cards_backend/database/database.dart'; import 'package:mnemo_cards_backend/packs/pack_manager.dart'; -import 'package:mnemo_cards_backend/packs/products_price_resolver.dart'; +import 'package:mnemo_cards_backend/packs/product_availability_manager.dart'; import 'package:mnemo_cards_backend/user/user_repository.dart'; import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart'; import 'package:mnemo_cards_common/mnemo_cards_common.dart'; @@ -23,12 +23,14 @@ class PurchasesApiV2 { final PaymentManager _paymentManager; final PackManager _packManager; final UserRepository _userRepository; + final ProductAvailabilityManager _productAvailabilityManager; final AppDatabase _db; PurchasesApiV2( this._paymentManager, this._packManager, this._userRepository, + this._productAvailabilityManager, this._db, ); @@ -101,11 +103,14 @@ class PurchasesApiV2 { print( '🔍 createPackPurchase: Checking pack access for userId=${user.id}, packId=$packId', ); - // Check if already purchased - final hasAccess = await _userRepository.hasPackAccess(user.id!, packId); - if (hasAccess) { - print('❌ createPackPurchase: Pack already purchased'); - return _badRequest('Pack is already purchased'); + // Check if already purchased or available through subscription + final isAvailable = await _productAvailabilityManager.isPackAvailable( + packId: packId, + user: user, + ); + if (isAvailable) { + print('❌ createPackPurchase: Pack already available'); + return _badRequest('Pack is already available'); } print('✅ createPackPurchase: Pack not purchased yet'); @@ -207,10 +212,13 @@ class PurchasesApiV2 { return _notFound('Pack not found'); } - // Check if already purchased - final hasAccess = await _userRepository.hasPackAccess(user.id!, productId); - if (hasAccess) { - return _badRequest('Pack is already purchased'); + // Check if already purchased or available through subscription + final isAvailable = await _productAvailabilityManager.isPackAvailable( + packId: productId, + user: user, + ); + if (isAvailable) { + return _badRequest('Pack is already available'); } // Get price (with discounts if applicable) @@ -370,24 +378,30 @@ class PurchasesApiV2 { return _notFound('Pack not found'); } - // Check purchase status - final isPurchased = await _userRepository.hasPackAccess(user.id!, packId); + // Check availability (includes both direct purchase and subscription) + final isAvailable = await _productAvailabilityManager.isPackAvailable( + packId: packId, + user: user, + ); - // Check subscription access + // Check direct purchase access separately + final hasDirectAccess = await _db.userDao.hasPackAccess(user.id!, packId); + + // Check subscription access separately final activeSubscription = await _db.subscriptionDao .getActiveSubscription(user.id!); final hasSubscriptionAccess = activeSubscription != null && - (activeSubscription.features is List && - (activeSubscription.features as List).contains( - SubscriptionFeatureEnum.packs.name, - )); + activeSubscription.features.contains( + SubscriptionFeatureEnum.packs.name, + ); return _ok({ 'packId': packId, - 'isPurchased': isPurchased, - 'purchased': isPurchased, + 'isPurchased': hasDirectAccess, + 'purchased': hasDirectAccess, 'hasSubscriptionAccess': hasSubscriptionAccess, + 'isAvailable': isAvailable, }); } catch (e, s) { developer.log( diff --git a/mnemo_cards_backend/lib/api/v2/telegram_bot_api_v2.dart b/mnemo_cards_backend/lib/api/v2/telegram_bot_api_v2.dart index cf8b379..10803a0 100644 --- a/mnemo_cards_backend/lib/api/v2/telegram_bot_api_v2.dart +++ b/mnemo_cards_backend/lib/api/v2/telegram_bot_api_v2.dart @@ -193,7 +193,10 @@ class TelegramBotApiV2 { if (userId != null && userId.isNotEmpty) { // Get specific user info final users = []; - final user = await _userRepository.getUserById(userId, includePacks: true); + final user = await _userRepository.getUserById( + userId, + includePacks: true, + ); if (user != null) { users.add(user); } diff --git a/mnemo_cards_backend/lib/api/v2/users_api_v2.dart b/mnemo_cards_backend/lib/api/v2/users_api_v2.dart index 588b3f3..575e64e 100644 --- a/mnemo_cards_backend/lib/api/v2/users_api_v2.dart +++ b/mnemo_cards_backend/lib/api/v2/users_api_v2.dart @@ -258,7 +258,10 @@ class UsersApiV2 { } // Convert UserData to UserDataDto - final userDataModel = await _userRepository.getUserById(user.id!, withData: true); + final userDataModel = await _userRepository.getUserById( + user.id!, + withData: true, + ); if (userDataModel?.userData == null) { return _json({'error': 'user_data_not_found'}, statusCode: 404); } diff --git a/mnemo_cards_backend/lib/cron/add_free_packs.dart b/mnemo_cards_backend/lib/cron/add_free_packs.dart index 00cd281..0ee58d8 100644 --- a/mnemo_cards_backend/lib/cron/add_free_packs.dart +++ b/mnemo_cards_backend/lib/cron/add_free_packs.dart @@ -1,4 +1,5 @@ import 'package:mnemo_cards_backend/packs/free_packs_distributor.dart'; +import 'package:mnemo_cards_backend/packs/product_availability_manager.dart'; import 'package:mnemo_cards_backend/user/user_repository.dart'; import 'task.dart' as task; @@ -6,8 +7,13 @@ import 'task.dart' as task; class AddFreePacks with task.Task { final FreePacksDistributor _freePacksDistributor; final UserRepository _userRepository; + final ProductAvailabilityManager _productAvailabilityManager; - AddFreePacks(this._freePacksDistributor, this._userRepository); + AddFreePacks( + this._freePacksDistributor, + this._userRepository, + this._productAvailabilityManager, + ); @override String get name => 'add_free_packs'; @@ -32,7 +38,10 @@ class AddFreePacks with task.Task { for (final user in allUsers) { if (user.id == null) continue; final userPacks = await _userRepository.getUserPacks(user.id!); - final userPackIds = userPacks.map((p) => p.id).whereType().toSet(); + final userPackIds = userPacks + .map((p) => p.id) + .whereType() + .toSet(); // Если у пользователя нет хотя бы одного из freePacks if (!freePackIds.every((packId) => userPackIds.contains(packId))) { @@ -49,7 +58,7 @@ class AddFreePacks with task.Task { try { for (final pack in freePacks) { if (pack.id == null) continue; - await _userRepository.grantPackAccess( + await _productAvailabilityManager.grantPackAccess( userId: userId, packId: pack.id!, grantType: 'free', diff --git a/mnemo_cards_backend/lib/cron/check_admins.dart b/mnemo_cards_backend/lib/cron/check_admins.dart index 3a484db..a4e721d 100644 --- a/mnemo_cards_backend/lib/cron/check_admins.dart +++ b/mnemo_cards_backend/lib/cron/check_admins.dart @@ -27,7 +27,9 @@ class CheckAdminsTask with task.Task { print('$name processing admin ID: $externalUserId'); if (externalUserId.isNotEmpty) { // Найти токен по externalUserId - final rawUser = await _userRepository.getUserByExternalId(externalUserId); + final rawUser = await _userRepository.getUserByExternalId( + externalUserId, + ); final userId = rawUser?.id; if (userId == null) { diff --git a/mnemo_cards_backend/lib/database/daos/payment_dao.dart b/mnemo_cards_backend/lib/database/daos/payment_dao.dart index 3e149ae..1237a3c 100644 --- a/mnemo_cards_backend/lib/database/daos/payment_dao.dart +++ b/mnemo_cards_backend/lib/database/daos/payment_dao.dart @@ -135,13 +135,18 @@ class PaymentDao extends DatabaseAccessor /// Обновить статус платежа Future updatePaymentStatus(String externalToken, String status) async { - print('🔍 PaymentDao.updatePaymentStatus: token=$externalToken, status=$status'); - final result = await (update(payments)..where((p) => p.externalToken.equals(externalToken))).write( - PaymentsCompanion( - status: Value(status), - updatedAt: Value(PgDateTime(DateTime.now())), - ), + print( + '🔍 PaymentDao.updatePaymentStatus: token=$externalToken, status=$status', ); + final result = + await (update( + payments, + )..where((p) => p.externalToken.equals(externalToken))).write( + PaymentsCompanion( + status: Value(status), + updatedAt: Value(PgDateTime(DateTime.now())), + ), + ); print('✅ PaymentDao.updatePaymentStatus: result=$result'); } diff --git a/mnemo_cards_backend/lib/database/daos/user_dao.dart b/mnemo_cards_backend/lib/database/daos/user_dao.dart index 6a213b2..7a3a124 100644 --- a/mnemo_cards_backend/lib/database/daos/user_dao.dart +++ b/mnemo_cards_backend/lib/database/daos/user_dao.dart @@ -397,18 +397,11 @@ class UserDao extends DatabaseAccessor with _$UserDaoMixin { /// Проверить, есть ли у пользователя доступ к паку Future hasPackAccess(String userId, String packId) async { - print('🔍 UserDao.hasPackAccess: userId=$userId, packId=$packId'); - print( - '🔍 UserDao.hasPackAccess: SQL: SELECT * FROM user_packs WHERE user_id = ? AND pack_id = ? LIMIT 1', - ); final query = select(userPacks) ..where((up) => up.userId.equals(userId) & up.packId.equals(packId)) ..limit(1); final results = await query.get(); - print( - '✅ UserDao.hasPackAccess: Result count=${results.length}, hasAccess=${results.isNotEmpty}', - ); return results.isNotEmpty; } diff --git a/mnemo_cards_backend/lib/main.dart b/mnemo_cards_backend/lib/main.dart index 41bff9a..b639f32 100644 --- a/mnemo_cards_backend/lib/main.dart +++ b/mnemo_cards_backend/lib/main.dart @@ -8,6 +8,7 @@ import 'package:mnemo_cards_backend/cron/cron_executor.dart'; import 'package:mnemo_cards_backend/cron/generate_promocodes.dart'; import 'package:mnemo_cards_backend/database/database.dart'; import 'package:mnemo_cards_backend/discounts/discounts_manager.dart'; +import 'package:mnemo_cards_backend/packs/product_availability_manager.dart'; import 'package:mnemo_cards_backend/user/user_manager.dart'; import 'package:mnemo_cards_backend/tests/test_manager.dart'; import 'package:mnemo_cards_backend/user/user_repository.dart'; @@ -123,7 +124,11 @@ void main() async { DeleteOldArchives(), CheckAdminsTask(getIt.get()), getIt.get(), - AddFreePacks(getIt.get(), getIt.get()), + AddFreePacks( + getIt.get(), + getIt.get(), + getIt.get(), + ), Backup(backupDir), GeneratePromocodes(database), DiscountCampaignTask(getIt.get(), database), diff --git a/mnemo_cards_backend/lib/packs/card_pack_drift_extension.dart b/mnemo_cards_backend/lib/packs/card_pack_drift_extension.dart index 09dcda5..a824b3d 100644 --- a/mnemo_cards_backend/lib/packs/card_pack_drift_extension.dart +++ b/mnemo_cards_backend/lib/packs/card_pack_drift_extension.dart @@ -65,7 +65,6 @@ extension CardPackToDto on CardPack { } extension CardPackToModel on CardPack { - Future toModel() async { return CardPackModel( id: id, diff --git a/mnemo_cards_backend/lib/packs/free_packs_distributor.dart b/mnemo_cards_backend/lib/packs/free_packs_distributor.dart index 467a345..cdf5210 100644 --- a/mnemo_cards_backend/lib/packs/free_packs_distributor.dart +++ b/mnemo_cards_backend/lib/packs/free_packs_distributor.dart @@ -1,26 +1,24 @@ import 'package:injectable/injectable.dart'; -import 'package:mnemo_cards_backend/user/user_repository.dart'; +import 'package:mnemo_cards_backend/packs/product_availability_manager.dart'; import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart'; import 'pack_repository.dart'; @lazySingleton class FreePacksDistributor { - final UserRepository _userRepository; + final ProductAvailabilityManager _productAvailabilityManager; final PackRepository _packRepository; - FreePacksDistributor(this._userRepository, this._packRepository); + FreePacksDistributor(this._productAvailabilityManager, this._packRepository); Future> getFreePacks() async { final allPacks = await _packRepository.getAllPacks(enabledOnly: true); // Фильтруем паки с нулевой ценой или без цены - return allPacks - .where((pack) { - final priceStr = pack.price; - if (priceStr == null || priceStr.isEmpty) return true; - final price = double.tryParse(priceStr); - return price == null || price == 0; - }) - .toList(); + return allPacks.where((pack) { + final priceStr = pack.price; + if (priceStr == null || priceStr.isEmpty) return true; + final price = double.tryParse(priceStr); + return price == null || price == 0; + }).toList(); } Future giveFreePacksToUser(UserModel user) async { @@ -29,12 +27,15 @@ class FreePacksDistributor { await givePacksToUser(user, packs); } - Future givePacksToUser(UserModel user, List packs) async { + Future givePacksToUser( + UserModel user, + List packs, + ) async { if (user.id == null) return; for (final pack in packs) { if (pack.id == null) continue; - await _userRepository.grantPackAccess( + await _productAvailabilityManager.grantPackAccess( userId: user.id!, packId: pack.id!, grantType: 'free', diff --git a/mnemo_cards_backend/lib/packs/pack_dto_converter.dart b/mnemo_cards_backend/lib/packs/pack_dto_converter.dart index c0caa7f..f4296d3 100644 --- a/mnemo_cards_backend/lib/packs/pack_dto_converter.dart +++ b/mnemo_cards_backend/lib/packs/pack_dto_converter.dart @@ -20,11 +20,9 @@ class PackDtoConverter { CardPackModel model, UserModel? userModel, ) async { - // Check access via purchases list (source of truth is user_packs table in DB) - // user.purchases is a cached list of pack IDs the user has access to - bool available = userModel != null && - (userModel.purchases.contains(model.id?.toString()) || - userModel.admin); + bool available = + userModel != null && + (userModel.packs.contains(model.id?.toString()) || userModel.admin); if (userModel != null && !available) { available = userModel.subscriptionModel?.features.contains( diff --git a/mnemo_cards_backend/lib/packs/pack_repository.dart b/mnemo_cards_backend/lib/packs/pack_repository.dart index 8137491..97d9342 100644 --- a/mnemo_cards_backend/lib/packs/pack_repository.dart +++ b/mnemo_cards_backend/lib/packs/pack_repository.dart @@ -235,4 +235,3 @@ class PackRepository { await _db.packDao.updateVoiceUrl(voiceId, voiceUrl); } } - diff --git a/mnemo_cards_backend/lib/user/user_manager.dart b/mnemo_cards_backend/lib/user/user_manager.dart index 6edfbcb..75ed2c5 100644 --- a/mnemo_cards_backend/lib/user/user_manager.dart +++ b/mnemo_cards_backend/lib/user/user_manager.dart @@ -119,7 +119,9 @@ class UserManager { String? name, }) async { // Check if user exists by externalId - var existingUserModel = await _userRepository.getUserByExternalId(externalId); + var existingUserModel = await _userRepository.getUserByExternalId( + externalId, + ); if (existingUserModel != null) { print('User found $name $email $telegram'); @@ -134,7 +136,9 @@ class UserManager { if (shouldUpdateEmail || shouldUpdateTelegram) { final updatedModel = existingUserModel.copyWith( email: shouldUpdateEmail ? email : existingUserModel.email, - telegram: shouldUpdateTelegram ? telegram : existingUserModel.telegram, + telegram: shouldUpdateTelegram + ? telegram + : existingUserModel.telegram, ); await _userRepository.updateUser(updatedModel); existingUserModel = updatedModel; @@ -169,7 +173,10 @@ class UserManager { userData: userDataCompanion, ); - final createdUserModel = await _userRepository.getUserById(userId, includePacks: true); + final createdUserModel = await _userRepository.getUserById( + userId, + includePacks: true, + ); if (createdUserModel == null) { throw Exception('Failed to create user'); } diff --git a/mnemo_cards_backend/lib/user/user_repository.dart b/mnemo_cards_backend/lib/user/user_repository.dart index ab080a7..74d9e8b 100644 --- a/mnemo_cards_backend/lib/user/user_repository.dart +++ b/mnemo_cards_backend/lib/user/user_repository.dart @@ -57,7 +57,7 @@ class UserRepository { /// Получить пользователя по externalUserId Future getUserByExternalId(String externalId) async { final user = await _db.userDao.getUserByExternalId(externalId); - + if (user == null) return null; return await user.toUserModel(); @@ -141,29 +141,6 @@ class UserRepository { return await Future.wait(packs.map((p) => p.toModel())); } - /// Проверить, есть ли у пользователя доступ к паку - Future hasPackAccess(String userId, String packId) async { - return await _db.userDao.hasPackAccess(userId, packId); - } - - /// Дать пользователю доступ к паку - Future grantPackAccess({ - required String userId, - required String packId, - String grantType = 'purchase', - }) async { - await _db.userDao.grantPackAccess( - userId: userId, - packId: packId, - grantType: grantType, - ); - } - - /// Отозвать доступ к паку - Future revokePackAccess(String userId, String packId) async { - await _db.userDao.revokePackAccess(userId, packId); - } - /// Получить UserData пользователя Future getUserData(String userId) async { return await _db.userDao.getUserData(userId); @@ -179,4 +156,3 @@ class UserRepository { await _db.userDao.updateLastOnline(userId); } } -