2025-11-16 11:25:27 +00:00
|
|
|
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';
|
2025-12-13 13:27:05 +00:00
|
|
|
import 'package:mnemo_cards_backend/database/database.dart';
|
2025-11-16 11:25:27 +00:00
|
|
|
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';
|
2025-12-13 13:27:05 +00:00
|
|
|
import 'package:drift/drift.dart' as drift;
|
|
|
|
|
|
|
|
|
|
import 'payment_drift_extension.dart';
|
|
|
|
|
import '../../main.dart' as backend_main;
|
2025-11-16 11:25:27 +00:00
|
|
|
|
|
|
|
|
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 {
|
2025-12-13 13:27:05 +00:00
|
|
|
final AppDatabase _db;
|
2025-11-16 11:25:27 +00:00
|
|
|
final PackManager _packManager;
|
|
|
|
|
final SubscriptionManager _subscriptionManager;
|
|
|
|
|
late final GooglePlayPurchaseHandler googlePurchaseHandler;
|
|
|
|
|
final YooMoneyHandler _yooMoneyHandler;
|
|
|
|
|
final RustorePurchaseHandler _rustorePurchaseHandler;
|
|
|
|
|
final ProductsPriceResolver _productsPriceResolver;
|
|
|
|
|
|
|
|
|
|
PaymentManager(
|
2025-12-13 13:27:05 +00:00
|
|
|
this._db,
|
2025-11-16 11:25:27 +00:00
|
|
|
this._packManager,
|
|
|
|
|
this._subscriptionManager,
|
|
|
|
|
this._yooMoneyHandler,
|
|
|
|
|
this._rustorePurchaseHandler,
|
|
|
|
|
this._productsPriceResolver,
|
|
|
|
|
);
|
|
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
/// Создать платеж в базе данных
|
|
|
|
|
Future<PaymentDto> 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();
|
2025-11-16 11:25:27 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
/// Обновить платеж в базе данных
|
|
|
|
|
Future<void> updatePayment(int paymentId, PaymentDto paymentDto) async {
|
|
|
|
|
final companion = paymentDto.toUpdateCompanion(paymentId);
|
|
|
|
|
await _db.paymentDao.updatePaymentCompanion(companion);
|
2025-11-16 11:25:27 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
/// Получить платеж по ID
|
|
|
|
|
Future<PaymentDto?> getPaymentById(int id) async {
|
|
|
|
|
final payment = await _db.paymentDao.getPaymentById(id);
|
|
|
|
|
return payment?.toDto();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Создать обработчики платежей Google Play
|
|
|
|
|
Future<Map<String, GooglePlayPurchaseHandler>>
|
|
|
|
|
_createPurchaseHandlers() async {
|
|
|
|
|
// TODO: Implement proper Google Play purchase handlers with service account
|
|
|
|
|
// For now, return empty map
|
|
|
|
|
return {};
|
2025-11-16 11:25:27 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
/// Обработать платеж - дать доступ к купленным пакетам и подпискам
|
|
|
|
|
Future<void> processPayment(Payment payment) async {
|
|
|
|
|
if (payment.status == PaymentStatus.processed.name) {
|
2025-11-16 11:25:27 +00:00
|
|
|
log('Payment already processed');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
if (payment.status != PaymentStatus.succeeded.name) {
|
|
|
|
|
throw Exception('Payment status is not succeeded');
|
2025-11-16 11:25:27 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
// Получить пользователя
|
|
|
|
|
final user = await _db.userDao.getUserWithDataById(payment.userId);
|
|
|
|
|
if (user == null) {
|
|
|
|
|
log('User not found: ${payment.userId}');
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-11-16 11:25:27 +00:00
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
// Извлечь IDs пакетов из продуктов
|
|
|
|
|
final packIds = <int>[];
|
|
|
|
|
if (payment.products != null) {
|
|
|
|
|
for (final product in payment.products!) {
|
|
|
|
|
final productMap = product as Map<String, dynamic>;
|
|
|
|
|
if (productMap['type'] == 'pack' && productMap['id'] != null) {
|
|
|
|
|
packIds.add(int.parse(productMap['id'].toString()));
|
|
|
|
|
}
|
2025-11-16 11:25:27 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
// Извлечь IDs подписок из продуктов
|
|
|
|
|
final subscriptionIds = <int>[];
|
|
|
|
|
if (payment.products != null) {
|
|
|
|
|
for (final product in payment.products!) {
|
|
|
|
|
final productMap = product as Map<String, dynamic>;
|
|
|
|
|
if (productMap['type'] == 'subscription' && productMap['id'] != null) {
|
|
|
|
|
subscriptionIds.add(int.parse(productMap['id'].toString()));
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-11-16 11:25:27 +00:00
|
|
|
}
|
2025-12-13 13:27:05 +00:00
|
|
|
|
|
|
|
|
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<dynamic>),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
log('Created subscription for user ${payment.userId}, plan: $subscriptionId');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Обновить статус платежа
|
|
|
|
|
await _db.paymentDao.updatePaymentStatus(payment.id, PaymentStatus.processed.name);
|
|
|
|
|
log('Payment ${payment.id} processed successfully');
|
|
|
|
|
});
|
2025-11-16 11:25:27 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
/// Проверить платеж Google Play
|
2025-11-16 11:25:27 +00:00
|
|
|
Future<bool> checkGooglePayment({
|
2025-12-13 13:27:05 +00:00
|
|
|
required String productId,
|
2025-11-16 11:25:27 +00:00
|
|
|
required String token,
|
|
|
|
|
required UserModel? user,
|
|
|
|
|
}) async {
|
2025-12-13 13:27:05 +00:00
|
|
|
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');
|
2025-11-16 11:25:27 +00:00
|
|
|
return false;
|
|
|
|
|
}
|
2025-12-13 13:27:05 +00:00
|
|
|
|
|
|
|
|
// Проверить статус в Google Play
|
|
|
|
|
final acknowledged = await googlePurchaseHandler.acknowledge(productId, token);
|
2025-11-16 11:25:27 +00:00
|
|
|
if (!acknowledged) {
|
2025-12-13 13:27:05 +00:00
|
|
|
await _db.paymentDao.updatePaymentStatus(payment.id, PaymentStatus.waiting.name);
|
|
|
|
|
return false;
|
2025-11-16 11:25:27 +00:00
|
|
|
}
|
2025-12-13 13:27:05 +00:00
|
|
|
|
|
|
|
|
// Обновить статус платежа
|
|
|
|
|
await _db.paymentDao.updatePaymentStatus(payment.id, PaymentStatus.succeeded.name);
|
|
|
|
|
|
|
|
|
|
// Обработать платеж
|
|
|
|
|
await processPayment(payment);
|
|
|
|
|
|
2025-11-16 11:25:27 +00:00
|
|
|
return true;
|
2025-12-13 13:27:05 +00:00
|
|
|
} catch (e) {
|
|
|
|
|
log('Error checking Google payment: $e');
|
|
|
|
|
return false;
|
2025-11-16 11:25:27 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
/// Проверить платеж RuStore
|
2025-11-16 11:25:27 +00:00
|
|
|
Future<bool> checkRustorePayment({
|
|
|
|
|
required String productId,
|
|
|
|
|
required String subscriptionToken,
|
|
|
|
|
required UserModel? user,
|
|
|
|
|
}) async {
|
2025-12-13 13:27:05 +00:00
|
|
|
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');
|
2025-11-16 11:25:27 +00:00
|
|
|
return false;
|
|
|
|
|
}
|
2025-12-13 13:27:05 +00:00
|
|
|
|
|
|
|
|
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;
|
2025-11-16 11:25:27 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
/// Проверить платеж YooKassa
|
|
|
|
|
Future<bool> 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);
|
2025-11-16 11:25:27 +00:00
|
|
|
}
|
2025-12-13 13:27:05 +00:00
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
log('Error checking YooKassa payment: $e');
|
|
|
|
|
return false;
|
2025-11-16 11:25:27 +00:00
|
|
|
}
|
2025-12-13 13:27:05 +00:00
|
|
|
}
|
2025-11-16 11:25:27 +00:00
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
/// Создать URL для оплаты через YooKassa
|
|
|
|
|
Future<String> createYookassaUrl({
|
|
|
|
|
required String amount,
|
|
|
|
|
required String description,
|
|
|
|
|
required String userId,
|
|
|
|
|
}) async {
|
|
|
|
|
final yookassaPayment = await _yooMoneyHandler.createPayment(
|
|
|
|
|
amount: amount,
|
|
|
|
|
description: description,
|
|
|
|
|
userId: userId,
|
2025-11-16 11:25:27 +00:00
|
|
|
);
|
|
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
if (yookassaPayment.confirmationUrl == null) {
|
|
|
|
|
throw Exception('Failed to create YooKassa payment URL');
|
2025-11-16 11:25:27 +00:00
|
|
|
}
|
2025-12-13 13:27:05 +00:00
|
|
|
|
|
|
|
|
// Создать запись платежа в БД
|
|
|
|
|
final paymentDto = PaymentDto(
|
|
|
|
|
amount: amount,
|
|
|
|
|
currency: 'RUB',
|
|
|
|
|
date: DateTime.now(),
|
|
|
|
|
status: PaymentStatus.created,
|
|
|
|
|
paymentSystem: PaymentSystem.yookassa,
|
|
|
|
|
packs: [],
|
|
|
|
|
subscription: false,
|
|
|
|
|
products: [], // TODO: add products
|
|
|
|
|
externalToken: yookassaPayment.id,
|
|
|
|
|
meta: null,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await createPayment(paymentDto, int.parse(userId));
|
|
|
|
|
|
|
|
|
|
return yookassaPayment.confirmationUrl!;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Получить платежи пользователя
|
|
|
|
|
Future<List<PaymentDto>> 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();
|
2025-11-16 11:25:27 +00:00
|
|
|
}
|
2025-12-13 13:27:05 +00:00
|
|
|
}
|