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

This commit is contained in:
Dmitry 2026-01-08 19:58:13 +03:00
parent e3afe9888c
commit 829542714a
19 changed files with 219 additions and 207 deletions

View file

@ -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;
}
}

View file

@ -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',

View file

@ -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

View file

@ -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',

View file

@ -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, {

View file

@ -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(

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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',

View file

@ -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) {

View file

@ -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');
}

View file

@ -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;
}

View file

@ -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),

View file

@ -65,7 +65,6 @@ extension CardPackToDto on CardPack {
}
extension CardPackToModel on CardPack {
Future<CardPackModel> toModel() async {
return CardPackModel(
id: id,

View file

@ -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',

View file

@ -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(

View file

@ -235,4 +235,3 @@ class PackRepository {
await _db.packDao.updateVoiceUrl(voiceId, voiceUrl);
}
}

View file

@ -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');
}

View file

@ -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);
}
}