mnemo_cards/mnemo_cards_backend/lib/api/purchase/payment_manager.dart

444 lines
15 KiB
Dart
Raw Normal View History

2025-11-16 11:25:27 +00:00
import 'dart:developer';
2025-12-13 23:35:14 +00:00
import 'package:drift_postgres/drift_postgres.dart';
2025-11-16 11:25:27 +00:00
import 'package:injectable/injectable.dart';
import 'package:mnemo_cards_backend/api/purchase/google_play_purchase_handler.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';
2025-12-13 13:27:05 +00:00
import 'package:drift/drift.dart' as drift;
import 'payment_drift_extension.dart';
2025-11-16 11:25:27 +00:00
import 'rustore/rustore_purchase_handler.dart';
import 'yoo_money.dart';
2025-12-13 20:55:50 +00:00
import '../../user/user_drift_extension.dart';
2025-11-16 11:25:27 +00:00
2026-01-06 21:33:51 +00:00
/// Result of creating YooKassa payment URL
class YookassaPaymentResult {
final String confirmationUrl;
final String paymentId;
YookassaPaymentResult({
required this.confirmationUrl,
required this.paymentId,
});
}
2025-11-16 11:25:27 +00:00
@lazySingleton
class PaymentManager {
2025-12-13 13:27:05 +00:00
final AppDatabase _db;
2025-11-16 11:25:27 +00:00
late final GooglePlayPurchaseHandler googlePurchaseHandler;
final YooMoneyHandler _yooMoneyHandler;
final RustorePurchaseHandler _rustorePurchaseHandler;
2025-12-20 18:26:15 +00:00
PaymentManager(this._db, this._yooMoneyHandler, this._rustorePurchaseHandler);
2025-11-16 11:25:27 +00:00
2025-12-13 13:27:05 +00:00
/// Создать платеж в базе данных
2025-12-13 20:55:50 +00:00
Future<PaymentDto> createPayment(PaymentDto paymentDto, String userId) async {
2026-01-06 21:20:43 +00:00
print('🔍 PaymentManager.createPayment: Starting');
2026-01-08 13:02:47 +00:00
print(
'🔍 PaymentManager.createPayment: userId=$userId, externalToken=${paymentDto.externalToken}',
);
2025-12-13 13:27:05 +00:00
final companion = paymentDto.toCompanion(userId);
2026-01-06 21:20:43 +00:00
print('🔍 PaymentManager.createPayment: Calling paymentDao.createPayment');
2025-12-13 13:27:05 +00:00
final paymentId = await _db.paymentDao.createPayment(companion);
2026-01-06 21:20:43 +00:00
print('✅ PaymentManager.createPayment: Payment created, id=$paymentId');
print('🔍 PaymentManager.createPayment: Getting payment by id');
2025-12-13 13:27:05 +00:00
final payment = await _db.paymentDao.getPaymentById(paymentId);
if (payment == null) {
2026-01-08 13:02:47 +00:00
print(
'❌ PaymentManager.createPayment: Payment not found after creation, id=$paymentId',
);
2025-12-13 13:27:05 +00:00
throw Exception('Failed to create payment');
}
2026-01-06 21:20:43 +00:00
print('✅ PaymentManager.createPayment: Payment retrieved successfully');
2025-12-13 13:27:05 +00:00
return payment.toDto();
2025-11-16 11:25:27 +00:00
}
2025-12-13 13:27:05 +00:00
/// Обновить платеж в базе данных
2025-12-13 20:55:50 +00:00
Future<void> updatePayment(String paymentId, PaymentDto paymentDto) async {
final companion = paymentDto.toUpdateCompanion();
await _db.paymentDao.updatePaymentCompanion(paymentId, companion);
2025-11-16 11:25:27 +00:00
}
2025-12-13 13:27:05 +00:00
/// Получить платеж по ID
2025-12-13 20:55:50 +00:00
Future<PaymentDto?> getPaymentById(String id) async {
2025-12-13 13:27:05 +00:00
final payment = await _db.paymentDao.getPaymentById(id);
return payment?.toDto();
}
2025-12-13 14:48:00 +00:00
/// Выдать продукт пользователю (для промокодов и других бесплатных активаций)
2025-12-20 18:26:15 +00:00
Future<void> grantProductToUser(
String userId,
MnemoCardsProductDto product,
) async {
2025-12-13 14:48:00 +00:00
await _db.transaction(() async {
if (product.type == MnemoCardsProductType.pack && product.id != null) {
2025-12-13 20:55:50 +00:00
final packId = product.id!;
await _db.userDao.grantPackAccess(
userId: userId,
packId: packId,
grantType: 'promo_code',
);
log('Granted pack $packId to user $userId via promo code');
2025-12-20 18:26:15 +00:00
} else if (product.type == MnemoCardsProductType.subscription &&
product.id != null) {
2025-12-13 20:55:50 +00:00
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,
2025-12-13 23:35:14 +00:00
start: PgDateTime(now),
finish: PgDateTime(endDate),
2025-12-13 20:55:50 +00:00
features: drift.Value(plan.features as List<dynamic>),
),
);
log('Granted subscription $planId to user $userId via promo code');
2025-12-13 14:48:00 +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-20 18:26:15 +00:00
// Извлечь IDs пакетов из продуктов
2025-12-13 20:55:50 +00:00
final packIds = <String>[];
try {
if (payment.products.isNotEmpty && payment.products != '[]') {
for (final product in payment.products) {
final productMap = product as Map<String, dynamic>;
if (productMap['type'] == 'pack' && productMap['id'] != null) {
packIds.add(productMap['id'].toString());
}
2025-12-13 13:27:05 +00:00
}
2025-11-16 11:25:27 +00:00
}
2025-12-13 20:55:50 +00:00
} catch (e) {
log('Error parsing products: $e');
2025-11-16 11:25:27 +00:00
}
2025-12-13 13:27:05 +00:00
// Извлечь IDs подписок из продуктов
2025-12-13 20:55:50 +00:00
final subscriptionIds = <String>[];
try {
if (payment.products.isNotEmpty && payment.products != '[]') {
for (final product in payment.products) {
final productMap = product as Map<String, dynamic>;
2025-12-20 18:26:15 +00:00
if (productMap['type'] == 'subscription' &&
productMap['id'] != null) {
2025-12-13 20:55:50 +00:00
subscriptionIds.add(productMap['id'].toString());
}
2025-12-13 13:27:05 +00:00
}
}
2025-12-13 20:55:50 +00:00
} catch (e) {
log('Error parsing products: $e');
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,
2025-12-13 23:35:14 +00:00
start: PgDateTime(now),
finish: PgDateTime(endDate),
2025-12-13 20:55:50 +00:00
features: drift.Value(plan.features),
2025-12-13 13:27:05 +00:00
),
);
2025-12-20 18:26:15 +00:00
log(
'Created subscription for user ${payment.userId}, plan: $subscriptionId',
);
2025-12-13 13:27:05 +00:00
}
}
// Обновить статус платежа
2025-12-13 20:55:50 +00:00
// 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');
2025-12-20 18:26:15 +00:00
log(
'Payment processed for user ${payment.userId}, but status update failed. Database regeneration may be needed.',
);
2025-12-13 20:55:50 +00:00
}
} else {
2025-12-20 18:26:15 +00:00
log(
'Payment processed for user ${payment.userId}, but cannot update status without externalToken or payment.id',
);
2025-12-13 20:55:50 +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
/// Проверить платеж 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
2025-12-20 18:26:15 +00:00
final acknowledged = await googlePurchaseHandler.acknowledge(
productId,
token,
);
2025-11-16 11:25:27 +00:00
if (!acknowledged) {
2025-12-13 20:55:50 +00:00
// Use token as payment identifier since Payment.id might not be available
2025-12-20 18:26:15 +00:00
await _db.paymentDao.updatePaymentStatus(
token,
PaymentStatus.waiting.name,
);
2025-12-13 13:27:05 +00:00
return false;
2025-11-16 11:25:27 +00:00
}
2025-12-13 13:27:05 +00:00
// Обновить статус платежа
2025-12-13 20:55:50 +00:00
// Use token as payment identifier
2025-12-20 18:26:15 +00:00
await _db.paymentDao.updatePaymentStatus(
token,
PaymentStatus.succeeded.name,
);
2025-12-13 13:27:05 +00:00
// Обработать платеж
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 {
2025-12-20 18:26:15 +00:00
final rustorePurchaseResponse = await _rustorePurchaseHandler
.checkPayment(subscriptionToken);
2025-12-13 13:27:05 +00:00
// Найти платеж по продукту
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') {
2025-12-13 20:55:50 +00:00
// Find payment id by productId or use subscriptionToken
final paymentId = payment.externalToken ?? subscriptionToken;
if (paymentId.isNotEmpty) {
2025-12-20 18:26:15 +00:00
await _db.paymentDao.updatePaymentStatus(
paymentId,
PaymentStatus.succeeded.name,
);
2025-12-13 20:55:50 +00:00
}
2025-12-13 13:27:05 +00:00
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) {
2026-01-08 13:02:47 +00:00
print('Payment not found for token: $token');
2025-12-13 13:27:05 +00:00
return false;
}
// Проверить статус в YooKassa
final yookassaPayment = await _yooMoneyHandler.checkPayment(token);
if (yookassaPayment.status == 'succeeded') {
2025-12-13 20:55:50 +00:00
// Use token as payment identifier
2025-12-20 18:26:15 +00:00
await _db.paymentDao.updatePaymentStatus(
token,
PaymentStatus.succeeded.name,
);
2025-12-13 13:27:05 +00:00
await processPayment(payment);
return true;
} else if (yookassaPayment.status == 'canceled') {
2025-12-20 18:26:15 +00:00
await _db.paymentDao.updatePaymentStatus(
token,
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
2026-01-06 21:33:51 +00:00
Future<YookassaPaymentResult> createYookassaUrl({
2025-12-13 13:27:05 +00:00
required String amount,
required String description,
required String userId,
2026-01-03 13:14:27 +00:00
List<MnemoCardsProductDto> products = const [],
2025-12-13 13:27:05 +00:00
}) async {
2026-01-06 21:20:43 +00:00
print('🔍 PaymentManager.createYookassaUrl: Starting');
2026-01-08 13:02:47 +00:00
print(
'🔍 PaymentManager.createYookassaUrl: amount=$amount, userId=$userId, products=${products.length}',
);
print(
'🔍 PaymentManager.createYookassaUrl: Calling YooMoneyHandler.createPayment',
);
2025-12-13 13:27:05 +00:00
final yookassaPayment = await _yooMoneyHandler.createPayment(
amount: amount,
description: description,
userId: userId,
2025-11-16 11:25:27 +00:00
);
2026-01-08 13:02:47 +00:00
print(
'✅ PaymentManager.createYookassaUrl: YooKassa payment created, id=${yookassaPayment.id}',
);
2025-11-16 11:25:27 +00:00
2025-12-13 13:27:05 +00:00
if (yookassaPayment.confirmationUrl == null) {
2026-01-06 21:20:43 +00:00
print('❌ PaymentManager.createYookassaUrl: confirmationUrl is null');
2025-12-13 13:27:05 +00:00
throw Exception('Failed to create YooKassa payment URL');
2025-11-16 11:25:27 +00:00
}
2025-12-13 13:27:05 +00:00
// Создать запись платежа в БД
2026-01-06 21:20:43 +00:00
print('🔍 PaymentManager.createYookassaUrl: Creating payment in database');
2025-12-13 13:27:05 +00:00
final paymentDto = PaymentDto(
amount: amount,
currency: 'RUB',
date: DateTime.now(),
status: PaymentStatus.created,
paymentSystem: PaymentSystem.yookassa,
2026-01-03 13:14:27 +00:00
products: products,
2025-12-13 13:27:05 +00:00
externalToken: yookassaPayment.id,
meta: null,
);
2026-01-06 21:20:43 +00:00
print('🔍 PaymentManager.createYookassaUrl: Calling createPayment');
2025-12-13 20:55:50 +00:00
await createPayment(paymentDto, userId);
2026-01-06 21:20:43 +00:00
print('✅ PaymentManager.createYookassaUrl: Payment created in database');
2025-12-13 13:27:05 +00:00
2026-01-08 13:02:47 +00:00
print(
'✅ PaymentManager.createYookassaUrl: Returning confirmationUrl and paymentId',
);
2026-01-06 21:33:51 +00:00
return YookassaPaymentResult(
confirmationUrl: yookassaPayment.confirmationUrl!,
paymentId: yookassaPayment.id,
);
2025-12-13 13:27:05 +00:00
}
/// Получить платежи пользователя
Future<List<PaymentDto>> getUserPayments(String userIdString) async {
2025-12-13 20:55:50 +00:00
final payments = await _db.paymentDao.getPaymentsByUserId(userIdString);
2025-12-13 13:27:05 +00:00
return payments.map((p) => p.toDto()).toList();
2025-11-16 11:25:27 +00:00
}
2025-12-13 14:48:00 +00:00
/// Проверить и обработать платеж (для 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!);
2025-12-20 18:26:15 +00:00
} else if (system == PaymentSystem.rustore &&
payment.externalToken != null) {
2025-12-13 14:48:00 +00:00
// Для RuStore нужен productId - получаем из products
2025-12-13 20:55:50 +00:00
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 _db.userDao.getUserById(payment.userId);
await checkRustorePayment(
productId: productId,
subscriptionToken: payment.externalToken!,
user: user != null ? await user.toUserModel() : null,
);
}
}
} catch (e) {
log('Error parsing products for RuStore payment: $e');
2025-12-13 14:48:00 +00:00
}
}
}
} catch (e, s) {
2025-12-13 20:55:50 +00:00
log('Error checking payment for user ${payment.userId}: $e');
2025-12-13 14:48:00 +00:00
print(s);
}
}
2025-12-20 18:26:15 +00:00
}