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 '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 class YookassaPaymentResult { final String confirmationUrl; final String paymentId; YookassaPaymentResult({ required this.confirmationUrl, required this.paymentId, }); } @lazySingleton class PaymentManager { final AppDatabase _db; final UserRepository _userRepository; final ProductAvailabilityManager _productAvailabilityManager; late final GooglePlayPurchaseHandler googlePurchaseHandler; final YooMoneyHandler _yooMoneyHandler; final RustorePurchaseHandler _rustorePurchaseHandler; PaymentManager( this._db, this._userRepository, this._productAvailabilityManager, this._yooMoneyHandler, this._rustorePurchaseHandler, ); /// Создать платеж в базе данных Future createPayment(PaymentDto paymentDto, String userId) async { print('🔍 PaymentManager.createPayment: Starting'); print( '🔍 PaymentManager.createPayment: userId=$userId, externalToken=${paymentDto.externalToken}', ); final companion = paymentDto.toCompanion(userId); print('🔍 PaymentManager.createPayment: Calling paymentDao.createPayment'); final paymentId = await _db.paymentDao.createPayment(companion); print('✅ PaymentManager.createPayment: Payment created, id=$paymentId'); print('🔍 PaymentManager.createPayment: Getting payment by id'); final payment = await _db.paymentDao.getPaymentById(paymentId); if (payment == null) { print( '❌ PaymentManager.createPayment: Payment not found after creation, id=$paymentId', ); throw Exception('Failed to create payment'); } print('✅ PaymentManager.createPayment: Payment retrieved successfully'); return payment.toDto(); } /// Обновить платеж в базе данных Future updatePayment(String paymentId, PaymentDto paymentDto) async { final companion = paymentDto.toUpdateCompanion(); await _db.paymentDao.updatePaymentCompanion(paymentId, companion); } /// Получить платеж по ID Future getPaymentById(String id) async { final payment = await _db.paymentDao.getPaymentById(id); return payment?.toDto(); } /// Выдать продукт пользователю (для промокодов и других бесплатных активаций) 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 _db.subscriptionDao.getPlanById(planId); if (plan != null) { final now = DateTime.now(); final endDate = now.add(Duration(days: plan.durationDays)); await _db.subscriptionDao.createUserSubscription( UserSubscriptionsCompanion.insert( userId: userId, start: PgDateTime(now), finish: PgDateTime(endDate), features: drift.Value(plan.features), ), ); 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, withData: true, ); 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 _db.subscriptionDao.getPlanById(subscriptionId); if (plan != null) { final now = DateTime.now(); final endDate = now.add(Duration(days: plan.durationDays)); await _db.subscriptionDao.createUserSubscription( UserSubscriptionsCompanion.insert( userId: payment.userId, start: PgDateTime(now), finish: PgDateTime(endDate), features: drift.Value(plan.features), ), ); 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 _db.paymentDao.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 payments = await _db.paymentDao.getPaymentsByProduct(productId); final payment = payments.isNotEmpty ? payments.first : null; if (payment == 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 = payment.externalToken ?? subscriptionToken; if (token.isNotEmpty) { await _db.paymentDao.updatePaymentStatus( token, PaymentStatus.succeeded.name, ); } 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 payment = await _db.paymentDao.getPaymentByExternalToken(token); if (payment == 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 _db.paymentDao.updatePaymentStatus( token, PaymentStatus.succeeded.name, ); await processPayment(payment); return true; } else if (yookassaPayment.status == 'canceled') { await _db.paymentDao.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 [], }) 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, ); 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 { final payments = await _db.paymentDao.getPaymentsByUserId(userIdString); return payments.map((p) => p.toDto()).toList(); } /// Проверить и обработать платеж (для 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); } } }