import 'dart:developer'; import 'package:drift_postgres/drift_postgres.dart'; import 'package:injectable/injectable.dart'; import 'package:mnemo_cards_backend/api/purchase/google_play_purchase_handler.dart'; import 'package:mnemo_cards_backend/database/database.dart'; import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart'; import 'package:mnemo_cards_common/mnemo_cards_common.dart'; import 'package:drift/drift.dart' as drift; import 'rustore/rustore_purchase_handler.dart'; import 'yoo_money.dart'; import '../../packs/product_availability_manager.dart'; import '../../repository/export.dart'; /// Result of creating YooKassa payment URL class YookassaPaymentResult { final String confirmationUrl; final String paymentId; YookassaPaymentResult({ required this.confirmationUrl, required this.paymentId, }); } @lazySingleton class PaymentManager { final PaymentRepository _paymentRepository; final SubscriptionRepository _subscriptionRepository; final UserRepository _userRepository; final ProductAvailabilityManager _productAvailabilityManager; late final GooglePlayPurchaseHandler googlePurchaseHandler; final YooMoneyHandler _yooMoneyHandler; final RustorePurchaseHandler _rustorePurchaseHandler; final AppDatabase _db; PaymentManager( this._paymentRepository, this._subscriptionRepository, this._userRepository, this._productAvailabilityManager, this._yooMoneyHandler, this._rustorePurchaseHandler, this._db, ); /// Создать платеж в базе данных Future createPayment(PaymentDto paymentDto, String userId) async { print('🔍 PaymentManager.createPayment: Starting'); print( '🔍 PaymentManager.createPayment: userId=$userId, externalToken=${paymentDto.externalToken}', ); print( '🔍 PaymentManager.createPayment: Calling paymentRepository.createPayment', ); final payment = await _paymentRepository.createPayment(paymentDto, userId); print('✅ PaymentManager.createPayment: Payment created successfully'); return payment; } /// Обновить платеж в базе данных Future updatePayment(String paymentId, PaymentDto paymentDto) async { await _paymentRepository.updatePayment(paymentId, paymentDto); } /// Получить платеж по ID Future getPaymentById(String id) async { return await _paymentRepository.getPaymentById(id); } /// Получить последний платеж для пользователя и пака /// Используется при возврате с ЮКассы без paymentId Future getLatestPaymentForUserAndPack({ required String userId, required String packId, }) async { return await _paymentRepository.getLatestPaymentForUserAndPack( userId: userId, packId: packId, ); } /// Выдать продукт пользователю (для промокодов и других бесплатных активаций) Future grantProductToUser( String userId, MnemoCardsProductDto product, ) async { await _db.transaction(() async { if (product.type == MnemoCardsProductType.pack && product.id != null) { await _productAvailabilityManager.grantProductAccess( userId: userId, product: product, grantType: '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!; final plan = await _subscriptionRepository.getPlanById(planId); if (plan != null) { final now = DateTime.now(); final endDate = now.add(Duration(days: plan.durationDays)); await _subscriptionRepository.createSubscription( UserSubscriptionsCompanion.insert( userId: userId, start: PgDateTime(now), finish: PgDateTime(endDate), features: drift.Value(plan.features.map((f) => f.name).toList()), ), ); log('Granted subscription $planId to user $userId via promo code'); } } }); } /// Обработать платеж - дать доступ к купленным пакетам и подпискам Future processPayment(Payment payment) async { if (payment.status == PaymentStatus.processed.name) { log('Payment already processed'); return; } if (payment.status != PaymentStatus.succeeded.name) { throw Exception('Payment status is not succeeded'); } // Получить пользователя final userModel = await _userRepository.getUserById(payment.userId); if (userModel == null) { log('User not found: ${payment.userId}'); return; } // Извлечь IDs пакетов из продуктов final packIds = []; try { if (payment.products.isNotEmpty && payment.products != '[]') { for (final product in payment.products) { final productMap = product as Map; if (productMap['type'] == 'pack' && productMap['id'] != null) { packIds.add(productMap['id'].toString()); } } } } catch (e) { log('Error parsing products: $e'); } // Извлечь IDs подписок из продуктов final subscriptionIds = []; try { if (payment.products.isNotEmpty && payment.products != '[]') { for (final product in payment.products) { final productMap = product as Map; if (productMap['type'] == 'subscription' && productMap['id'] != null) { subscriptionIds.add(productMap['id'].toString()); } } } } catch (e) { log('Error parsing products: $e'); } await _db.transaction(() async { // Дать доступ к пакетам for (final packId in packIds) { await _productAvailabilityManager.grantPackAccess( userId: payment.userId, packId: packId, grantType: 'purchase', ); log('Granted access to pack $packId for user ${payment.userId}'); } // Создать подписки for (final subscriptionId in subscriptionIds) { final plan = await _subscriptionRepository.getPlanById(subscriptionId); if (plan != null) { final now = DateTime.now(); final endDate = now.add(Duration(days: plan.durationDays)); await _subscriptionRepository.createSubscription( UserSubscriptionsCompanion.insert( userId: payment.userId, start: PgDateTime(now), finish: PgDateTime(endDate), features: drift.Value(plan.features.map((f) => f.name).toList()), ), ); log( 'Created subscription for user ${payment.userId}, plan: $subscriptionId', ); } } // Обновить статус платежа // Note: Payment class should have an id field when returned from queries // If payment.id doesn't work, the database needs to be regenerated // For now, we'll try to find the payment by externalToken and update it if (payment.externalToken != null && payment.externalToken!.isNotEmpty) { try { // Try to update by externalToken (if it's used as identifier) // TODO: Once database is regenerated, use payment.id directly await _paymentRepository.updatePaymentStatus( payment.externalToken!, PaymentStatus.processed.name, ); log('Payment processed successfully for user ${payment.userId}'); } catch (e) { log('Warning: Could not update payment status: $e'); log( 'Payment processed for user ${payment.userId}, but status update failed. Database regeneration may be needed.', ); } } else { log( 'Payment processed for user ${payment.userId}, but cannot update status without externalToken or payment.id', ); } }); } /// Проверить платеж Google Play Future checkGooglePayment({ required String productId, required String token, required UserModel? user, }) async { if (user == null) return false; try { // Найти платеж по external token final payment = await _db.paymentDao.getPaymentByExternalToken(token); if (payment == null) { log('Payment not found for token: $token'); return false; } // Проверить статус в Google Play final acknowledged = await googlePurchaseHandler.acknowledge( productId, token, ); if (!acknowledged) { // Use token as payment identifier since Payment.id might not be available await _db.paymentDao.updatePaymentStatus( token, PaymentStatus.waiting.name, ); return false; } // Обновить статус платежа // Use token as payment identifier await _db.paymentDao.updatePaymentStatus( token, PaymentStatus.succeeded.name, ); // Обработать платеж await processPayment(payment); return true; } catch (e) { log('Error checking Google payment: $e'); return false; } } /// Проверить платеж RuStore Future checkRustorePayment({ required String productId, required String subscriptionToken, required UserModel? user, }) async { if (user == null) return false; try { final rustorePurchaseResponse = await _rustorePurchaseHandler .checkPayment(subscriptionToken); // Найти платеж по продукту final paymentsDto = await _paymentRepository.getPaymentsByProduct( productId, ); final paymentDto = paymentsDto.isNotEmpty ? paymentsDto.first : null; if (paymentDto == null) { log('Payment not found for product: $productId'); return false; } if (rustorePurchaseResponse?.invoiceStatus.name == 'paid') { // Find payment id by productId or use subscriptionToken final token = paymentDto.externalToken ?? subscriptionToken; if (token.isNotEmpty) { await _paymentRepository.updatePaymentStatus( token, PaymentStatus.succeeded.name, ); } // Обработать платеж - нужно получить Payment из БД для processPayment final payment = await _db.paymentDao.getPaymentByExternalToken(token); if (payment != null) { await processPayment(payment); } return true; } return false; } catch (e) { log('Error checking RuStore payment: $e'); return false; } } /// Проверить платеж YooKassa Future checkYookassaPayment(String token) async { try { final paymentDto = await _paymentRepository.getPaymentByExternalToken( token, ); if (paymentDto == null) { print('Payment not found for token: $token'); return false; } // Проверить статус в YooKassa final yookassaPayment = await _yooMoneyHandler.checkPayment(token); if (yookassaPayment.status == 'succeeded') { // Use token as payment identifier await _paymentRepository.updatePaymentStatus( token, PaymentStatus.succeeded.name, ); // Обработать платеж - нужно получить Payment из БД для processPayment final payment = await _db.paymentDao.getPaymentByExternalToken(token); if (payment != null) { await processPayment(payment); } return true; } else if (yookassaPayment.status == 'canceled') { await _paymentRepository.updatePaymentStatus( token, PaymentStatus.canceled.name, ); } return false; } catch (e) { log('Error checking YooKassa payment: $e'); return false; } } /// Создать URL для оплаты через YooKassa Future createYookassaUrl({ required String amount, required String description, required String userId, List products = const [], String? packId, }) async { print('🔍 PaymentManager.createYookassaUrl: Starting'); print( '🔍 PaymentManager.createYookassaUrl: amount=$amount, userId=$userId, products=${products.length}', ); print( '🔍 PaymentManager.createYookassaUrl: Calling YooMoneyHandler.createPayment', ); final yookassaPayment = await _yooMoneyHandler.createPayment( amount: amount, description: description, userId: userId, packId: packId, ); print( '✅ PaymentManager.createYookassaUrl: YooKassa payment created, id=${yookassaPayment.id}', ); if (yookassaPayment.confirmationUrl == null) { print('❌ PaymentManager.createYookassaUrl: confirmationUrl is null'); throw Exception('Failed to create YooKassa payment URL'); } // Создать запись платежа в БД print('🔍 PaymentManager.createYookassaUrl: Creating payment in database'); final paymentDto = PaymentDto( amount: amount, currency: 'RUB', date: DateTime.now(), status: PaymentStatus.created, paymentSystem: PaymentSystem.yookassa, products: products, externalToken: yookassaPayment.id, meta: null, ); print('🔍 PaymentManager.createYookassaUrl: Calling createPayment'); await createPayment(paymentDto, userId); print('✅ PaymentManager.createYookassaUrl: Payment created in database'); print( '✅ PaymentManager.createYookassaUrl: Returning confirmationUrl and paymentId', ); return YookassaPaymentResult( confirmationUrl: yookassaPayment.confirmationUrl!, paymentId: yookassaPayment.id, ); } /// Получить платежи пользователя Future> getUserPayments(String userIdString) async { return await _paymentRepository.getPaymentsByUserId(userIdString); } /// Проверить и обработать платеж (для cron задач) Future checkAndProcessPayment(Payment payment) async { // Если платеж уже обработан, пропускаем if (payment.status == PaymentStatus.processed.name) { return; } // Если статус unknown, пропускаем if (payment.status == PaymentStatus.unknown.name) { return; } try { final system = PaymentSystem.values.firstWhere( (s) => s.name == payment.paymentSystem, orElse: () => PaymentSystem.yookassa, ); if (system == PaymentSystem.yookassa && payment.externalToken != null) { await checkYookassaPayment(payment.externalToken!); } else if (system == PaymentSystem.rustore && payment.externalToken != null) { // Для RuStore нужен productId - получаем из products if (payment.products.isNotEmpty && payment.products != '[]') { try { if (payment.products.isNotEmpty) { final firstProduct = payment.products.first; final productId = firstProduct['id']?.toString(); if (productId != null && payment.externalToken != null) { final user = await _userRepository.getUserById(payment.userId); await checkRustorePayment( productId: productId, subscriptionToken: payment.externalToken!, user: user, ); } } } catch (e) { log('Error parsing products for RuStore payment: $e'); } } } } catch (e, s) { log('Error checking payment for user ${payment.userId}: $e'); print(s); } } }