product availability
Some checks are pending
Backend CI / test (push) Waiting to run
Backend CI / build (push) Blocked by required conditions
Deploy Mnemo Cards / Deploy Backend (push) Waiting to run
Deploy Mnemo Cards / Deploy Web App (push) Blocked by required conditions
Deploy Mnemo Cards / Final Verification (push) Blocked by required conditions
Some checks are pending
Backend CI / test (push) Waiting to run
Backend CI / build (push) Blocked by required conditions
Deploy Mnemo Cards / Deploy Backend (push) Waiting to run
Deploy Mnemo Cards / Deploy Web App (push) Blocked by required conditions
Deploy Mnemo Cards / Final Verification (push) Blocked by required conditions
This commit is contained in:
parent
e3afe9888c
commit
829542714a
19 changed files with 219 additions and 207 deletions
|
|
@ -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<Set<String>>(),
|
||||
),
|
||||
);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<dynamic>),
|
||||
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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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<bool> _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/<packId>/tests')
|
||||
|
|
@ -900,16 +890,6 @@ class PacksApiV2 {
|
|||
}
|
||||
}
|
||||
|
||||
/// Helper method to check if user has subscription access
|
||||
Future<bool> _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, {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -193,7 +193,10 @@ class TelegramBotApiV2 {
|
|||
if (userId != null && userId.isNotEmpty) {
|
||||
// Get specific user info
|
||||
final users = <UserModel>[];
|
||||
final user = await _userRepository.getUserById(userId, includePacks: true);
|
||||
final user = await _userRepository.getUserById(
|
||||
userId,
|
||||
includePacks: true,
|
||||
);
|
||||
if (user != null) {
|
||||
users.add(user);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String>().toSet();
|
||||
final userPackIds = userPacks
|
||||
.map((p) => p.id)
|
||||
.whereType<String>()
|
||||
.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',
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -135,13 +135,18 @@ class PaymentDao extends DatabaseAccessor<AppDatabase>
|
|||
|
||||
/// Обновить статус платежа
|
||||
Future<void> 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');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -397,18 +397,11 @@ class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
|
|||
|
||||
/// Проверить, есть ли у пользователя доступ к паку
|
||||
Future<bool> 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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<UserRepository>()),
|
||||
getIt.get<CheckPaymentTask>(),
|
||||
AddFreePacks(getIt.get<FreePacksDistributor>(), getIt.get<UserRepository>()),
|
||||
AddFreePacks(
|
||||
getIt.get<FreePacksDistributor>(),
|
||||
getIt.get<UserRepository>(),
|
||||
getIt.get<ProductAvailabilityManager>(),
|
||||
),
|
||||
Backup(backupDir),
|
||||
GeneratePromocodes(database),
|
||||
DiscountCampaignTask(getIt.get<DiscountsManager>(), database),
|
||||
|
|
|
|||
|
|
@ -65,7 +65,6 @@ extension CardPackToDto on CardPack {
|
|||
}
|
||||
|
||||
extension CardPackToModel on CardPack {
|
||||
|
||||
Future<CardPackModel> toModel() async {
|
||||
return CardPackModel(
|
||||
id: id,
|
||||
|
|
|
|||
|
|
@ -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<List<CardPackModel>> 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<void> giveFreePacksToUser(UserModel user) async {
|
||||
|
|
@ -29,12 +27,15 @@ class FreePacksDistributor {
|
|||
await givePacksToUser(user, packs);
|
||||
}
|
||||
|
||||
Future<void> givePacksToUser(UserModel user, List<CardPackModel> packs) async {
|
||||
Future<void> givePacksToUser(
|
||||
UserModel user,
|
||||
List<CardPackModel> 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',
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -235,4 +235,3 @@ class PackRepository {
|
|||
await _db.packDao.updateVoiceUrl(voiceId, voiceUrl);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ class UserRepository {
|
|||
/// Получить пользователя по externalUserId
|
||||
Future<UserModel?> 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<bool> hasPackAccess(String userId, String packId) async {
|
||||
return await _db.userDao.hasPackAccess(userId, packId);
|
||||
}
|
||||
|
||||
/// Дать пользователю доступ к паку
|
||||
Future<void> grantPackAccess({
|
||||
required String userId,
|
||||
required String packId,
|
||||
String grantType = 'purchase',
|
||||
}) async {
|
||||
await _db.userDao.grantPackAccess(
|
||||
userId: userId,
|
||||
packId: packId,
|
||||
grantType: grantType,
|
||||
);
|
||||
}
|
||||
|
||||
/// Отозвать доступ к паку
|
||||
Future<void> revokePackAccess(String userId, String packId) async {
|
||||
await _db.userDao.revokePackAccess(userId, packId);
|
||||
}
|
||||
|
||||
/// Получить UserData пользователя
|
||||
Future<UserData?> getUserData(String userId) async {
|
||||
return await _db.userDao.getUserData(userId);
|
||||
|
|
@ -179,4 +156,3 @@ class UserRepository {
|
|||
await _db.userDao.updateLastOnline(userId);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue