mnemo_cards/mnemo_cards_backend/lib/api/purchase/payment_manager.dart
Dmitry 306b2fdca7
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
Deploy Telegram Bot / Deploy Telegram Bot (push) Waiting to run
fixes
2025-12-13 17:48:00 +03:00

370 lines
No EOL
13 KiB
Dart

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<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();
}
/// Обновить платеж в базе данных
Future<void> updatePayment(int paymentId, PaymentDto paymentDto) async {
final companion = paymentDto.toUpdateCompanion(paymentId);
await _db.paymentDao.updatePaymentCompanion(companion);
}
/// Получить платеж по 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 {
return {};
}
/// Выдать продукт пользователю (для промокодов и других бесплатных активаций)
Future<void> 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<dynamic>),
),
);
log('Granted subscription $planId to user $userId via promo code');
}
}
}
});
}
/// Обработать платеж - дать доступ к купленным пакетам и подпискам
Future<void> 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 = <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()));
}
}
}
// Извлечь 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()));
}
}
}
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');
});
}
/// Проверить платеж Google Play
Future<bool> 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<bool> 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<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);
}
return false;
} catch (e) {
log('Error checking YooKassa payment: $e');
return false;
}
}
/// Создать 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,
);
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<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();
}
/// Проверить и обработать платеж (для cron задач)
Future<void> 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<String, dynamic>;
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);
}
}
}