import 'dart:convert'; import 'dart:developer'; import 'dart:io'; import 'package:dio/dio.dart'; import 'package:googleapis/androidpublisher/v3.dart' as ap; import 'package:googleapis/firestore/v1.dart' as fs; import 'package:googleapis_auth/auth_io.dart' as auth; import 'package:injectable/injectable.dart'; import 'package:mnemo_cards_backend/api/purchase/google_play_purchase_handler.dart'; import 'package:mnemo_cards_backend/api/purchase/rustore/rustore_purchase_response.dart'; import 'package:mnemo_cards_backend/database/database.dart'; import 'package:mnemo_cards_backend/user/user_drift_extension.dart'; import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart'; import 'package:mnemo_cards_common/mnemo_cards_common.dart'; import 'package:yookassa_client/yookassa_client.dart'; import 'package:drift/drift.dart' as drift; import 'payment_drift_extension.dart'; import '../../main.dart' as backend_main; import '../../packs/pack_manager.dart'; import '../../packs/products_price_resolver.dart'; import '../subscription/subscription_manager.dart'; import 'iap_repository.dart'; import 'rustore/rustore_purchase_handler.dart'; import 'yoo_money.dart'; @lazySingleton class PaymentManager { final AppDatabase _db; final PackManager _packManager; final SubscriptionManager _subscriptionManager; late final GooglePlayPurchaseHandler googlePurchaseHandler; final YooMoneyHandler _yooMoneyHandler; final RustorePurchaseHandler _rustorePurchaseHandler; final ProductsPriceResolver _productsPriceResolver; PaymentManager( this._db, this._packManager, this._subscriptionManager, this._yooMoneyHandler, this._rustorePurchaseHandler, this._productsPriceResolver, ); /// Создать платеж в базе данных Future createPayment(PaymentDto paymentDto, int userId) async { final companion = paymentDto.toCompanion(userId); final paymentId = await _db.paymentDao.createPayment(companion); final payment = await _db.paymentDao.getPaymentById(paymentId); if (payment == null) { throw Exception('Failed to create payment'); } return payment.toDto(); } /// Обновить платеж в базе данных Future updatePayment(int paymentId, PaymentDto paymentDto) async { final companion = paymentDto.toUpdateCompanion(paymentId); await _db.paymentDao.updatePaymentCompanion(companion); } /// Получить платеж по ID Future getPaymentById(int id) async { final payment = await _db.paymentDao.getPaymentById(id); return payment?.toDto(); } /// Создать обработчики платежей Google Play Future> _createPurchaseHandlers() async { return {}; } /// Выдать продукт пользователю (для промокодов и других бесплатных активаций) Future grantProductToUser(int userId, MnemoCardsProductDto product) async { await _db.transaction(() async { if (product.type == MnemoCardsProductType.pack && product.id != null) { final packId = int.tryParse(product.id!); if (packId != null) { await _db.userDao.grantPackAccess( userId: userId, packId: packId, grantType: 'promo_code', ); log('Granted pack $packId to user $userId via promo code'); } } else if (product.type == MnemoCardsProductType.subscription && product.id != null) { final planId = int.tryParse(product.id!); if (planId != null) { 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: now, finish: endDate, features: drift.Value(plan.features as List), ), ); 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 user = await _db.userDao.getUserWithDataById(payment.userId); if (user == null) { log('User not found: ${payment.userId}'); return; } // Извлечь IDs пакетов из продуктов final packIds = []; if (payment.products != null) { for (final product in payment.products!) { final productMap = product as Map; if (productMap['type'] == 'pack' && productMap['id'] != null) { packIds.add(int.parse(productMap['id'].toString())); } } } // Извлечь IDs подписок из продуктов final subscriptionIds = []; if (payment.products != null) { for (final product in payment.products!) { final productMap = product as Map; if (productMap['type'] == 'subscription' && productMap['id'] != null) { subscriptionIds.add(int.parse(productMap['id'].toString())); } } } await _db.transaction(() async { // Дать доступ к пакетам for (final packId in packIds) { await _db.userDao.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: now, finish: endDate, features: drift.Value(plan.features as List), ), ); log('Created subscription for user ${payment.userId}, plan: $subscriptionId'); } } // Обновить статус платежа await _db.paymentDao.updatePaymentStatus(payment.id, PaymentStatus.processed.name); log('Payment ${payment.id} processed successfully'); }); } /// Проверить платеж 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) { await _db.paymentDao.updatePaymentStatus(payment.id, PaymentStatus.waiting.name); return false; } // Обновить статус платежа await _db.paymentDao.updatePaymentStatus(payment.id, 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') { await _db.paymentDao.updatePaymentStatus(payment.id, 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) { log('Payment not found for token: $token'); return false; } // Проверить статус в YooKassa final yookassaPayment = await _yooMoneyHandler.checkPayment(token); if (yookassaPayment.status == 'succeeded') { await _db.paymentDao.updatePaymentStatus(payment.id, PaymentStatus.succeeded.name); await processPayment(payment); return true; } else if (yookassaPayment.status == 'canceled') { await _db.paymentDao.updatePaymentStatus(payment.id, 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, }) async { final yookassaPayment = await _yooMoneyHandler.createPayment( amount: amount, description: description, userId: userId, ); if (yookassaPayment.confirmationUrl == null) { throw Exception('Failed to create YooKassa payment URL'); } // Создать запись платежа в БД final paymentDto = PaymentDto( amount: amount, currency: 'RUB', date: DateTime.now(), status: PaymentStatus.created, paymentSystem: PaymentSystem.yookassa, packs: [], subscription: false, products: [], externalToken: yookassaPayment.id, meta: null, ); await createPayment(paymentDto, int.parse(userId)); return yookassaPayment.confirmationUrl!; } /// Получить платежи пользователя Future> getUserPayments(String userIdString) async { final userId = int.tryParse(userIdString); if (userId == null) return []; final payments = await _db.paymentDao.getPaymentsByUserId(userId); 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 != null && payment.products!.isNotEmpty) { final firstProduct = payment.products!.first as Map; final productId = firstProduct['id']?.toString(); if (productId != null) { final user = await _db.userDao.getUserById(payment.userId); await checkRustorePayment( productId: productId, subscriptionToken: payment.externalToken!, user: user != null ? await user.toUserModel() : null, ); } } } } catch (e, s) { log('Error checking payment ${payment.id}: $e'); print(s); } } }