From a6fd99559af23f38019cd7110ec15da858a4b1c3 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Sat, 3 Jan 2026 16:57:30 +0300 Subject: [PATCH] yookassa --- .../lib/api/purchase/yoo_money.dart | 224 ++++++++++-------- mnemo_cards_backend/pubspec.lock | 24 -- mnemo_cards_backend/pubspec.yaml | 1 - 3 files changed, 128 insertions(+), 121 deletions(-) diff --git a/mnemo_cards_backend/lib/api/purchase/yoo_money.dart b/mnemo_cards_backend/lib/api/purchase/yoo_money.dart index 80ecaab..13e5b99 100644 --- a/mnemo_cards_backend/lib/api/purchase/yoo_money.dart +++ b/mnemo_cards_backend/lib/api/purchase/yoo_money.dart @@ -5,7 +5,6 @@ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:injectable/injectable.dart'; import 'package:uuid/uuid.dart'; -import 'package:yookassa_client/yookassa_client.dart'; /// Wrapper for YooKassa payment response class YookassaPayment { @@ -28,8 +27,9 @@ class YooMoneyHandler { final String _shopId; final String _secretKey; final String? _returnUrlBase; - late final YookassaClient? _yookassaClient; + late final Dio? _dio; final _uuid = const Uuid(); + static const String _baseUrl = 'https://api.yookassa.ru/v3'; YooMoneyHandler({ required String shopId, @@ -43,22 +43,37 @@ class YooMoneyHandler { 'YooKassa credentials not configured. Payments will not work.', name: 'YooMoneyHandler', ); - _yookassaClient = null; + _dio = null; } else { - // Initialize YooKassa client with credentials + // Initialize Dio with Basic Auth // According to spec: Basic Auth with shopId:secretKey - _yookassaClient = YookassaClient( - Dio(), - credentials: YookassaAuthCredentials( - shopId: _shopId, - secretKey: _secretKey, + _dio = Dio( + BaseOptions( + baseUrl: _baseUrl, + headers: { + 'Content-Type': 'application/json', + }, + ), + ); + + // Set up Basic Auth interceptor + _dio!.interceptors.add( + InterceptorsWrapper( + onRequest: (options, handler) { + // Basic Auth: base64(shopId:secretKey) + final credentials = base64Encode( + utf8.encode('$_shopId:$_secretKey'), + ); + options.headers['Authorization'] = 'Basic $credentials'; + handler.next(options); + }, ), ); } } /// Check if YooKassa is properly configured - bool get isConfigured => _yookassaClient != null; + bool get isConfigured => _dio != null; /// Create a payment in YooKassa /// According to spec: POST /v3/payments @@ -88,37 +103,35 @@ class YooMoneyHandler { ? '${amountValue.substring(0, amountValue.length - 2)}.${amountValue.substring(amountValue.length - 2)}' : '0.${amountValue.padLeft(2, '0')}'; - // Create amount object according to MonetaryAmount schema - final yookassaAmount = Amount( - value: formattedAmount, - currency: 'RUB', // Default currency - ); - // Build return URL for payment confirmation // According to spec: ReturnUrl - URL where user returns after payment // Max 2048 characters per spec final returnUrl = _buildReturnUrl(userId); - // Create payment request according to CreatePaymentRequest schema - final paymentRequest = CreatePaymentRequest( - amount: yookassaAmount, - description: description.length > 128 - ? description.substring(0, 128) - : description, // Max 128 chars per spec - confirmation: YookassaConfirmation.redirect( - returnUrl: returnUrl, - ), - capture: true, // Auto-capture payment when succeeded - metadata: { - 'userId': userId, - 'createdAt': DateTime.now().toIso8601String(), - }, - ); - // Generate idempotence key for request // According to spec: Idempotence-Key header (required) final idempotenceKey = _uuid.v4(); + // Create payment request according to CreatePaymentRequest schema + final requestBody = { + 'amount': { + 'value': formattedAmount, + 'currency': 'RUB', + }, + 'description': description.length > 128 + ? description.substring(0, 128) + : description, // Max 128 chars per spec + 'confirmation': { + 'type': 'redirect', + 'return_url': returnUrl, + }, + 'capture': true, // Auto-capture payment when succeeded + 'metadata': { + 'userId': userId, + 'createdAt': DateTime.now().toIso8601String(), + }, + }; + log( 'Creating YooKassa payment', name: 'YooMoneyHandler', @@ -130,71 +143,85 @@ class YooMoneyHandler { ); // Create payment via YooKassa API - // According to spec: Idempotence-Key header is required - final payment = await _yookassaClient!.createPayment( - paymentRequest: paymentRequest, - idempotenceKey: idempotenceKey, + // POST /v3/payments with Idempotence-Key header + final response = await _dio!.post>( + '/payments', + data: requestBody, + options: Options( + headers: { + 'Idempotence-Key': idempotenceKey, + }, + ), ); + final paymentData = response.data; + if (paymentData == null) { + throw Exception('Empty response from YooKassa API'); + } + + // Extract payment ID and status + final paymentId = paymentData['id'] as String?; + if (paymentId == null) { + throw Exception('Payment ID not found in response'); + } + + final paymentStatus = paymentData['status'] as String? ?? 'pending'; + // Extract confirmation URL from payment response // According to spec: confirmation.confirmation_url for redirect type String? confirmationUrl; - payment.confirmation?.maybeMap( - redirect: (redirect) { - confirmationUrl = redirect.confirmationUrl; - }, - qr: (qr) { - // For QR payments, we might need to handle differently - log('QR payment created, no redirect URL', name: 'YooMoneyHandler'); - }, - embedded: (_) { - log('Embedded payment created', name: 'YooMoneyHandler'); - }, - external: (_) { - log('External payment created', name: 'YooMoneyHandler'); - }, - mobileApplication: (_) { - log('Mobile application payment created', name: 'YooMoneyHandler'); - }, - orElse: () { - log('Unknown confirmation type', name: 'YooMoneyHandler'); - }, - ); + final confirmation = paymentData['confirmation'] as Map?; + if (confirmation != null) { + final confirmationType = confirmation['type'] as String?; + if (confirmationType == 'redirect') { + confirmationUrl = confirmation['confirmation_url'] as String?; + } else { + log( + 'Payment created with confirmation type: $confirmationType', + name: 'YooMoneyHandler', + ); + } + } if (confirmationUrl == null) { log( 'Warning: No confirmation URL in payment response', name: 'YooMoneyHandler', - error: jsonEncode(payment.toJson()), + error: jsonEncode(paymentData), ); } - // Map YooKassa payment status to our status - final status = _mapPaymentStatus(payment.status); - log( 'YooKassa payment created successfully', name: 'YooMoneyHandler', error: { - 'paymentId': payment.id, - 'status': status, + 'paymentId': paymentId, + 'status': paymentStatus, 'hasConfirmationUrl': confirmationUrl != null, }, ); return YookassaPayment( - id: payment.id, - status: status, + id: paymentId, + status: paymentStatus, confirmationUrl: confirmationUrl, ); - } on YookassaException catch (e, stackTrace) { + } on DioException catch (e, stackTrace) { log( 'YooKassa API error when creating payment', name: 'YooMoneyHandler', error: e, stackTrace: stackTrace, ); - rethrow; + if (e.response != null) { + log( + 'YooKassa error response: ${e.response?.data}', + name: 'YooMoneyHandler', + ); + } + throw Exception( + 'Failed to create YooKassa payment: ${e.message ?? 'Unknown error'}', + ); } on Exception catch (e, stackTrace) { log( 'Unexpected error when creating YooKassa payment', @@ -224,45 +251,61 @@ class YooMoneyHandler { // Get payment info from YooKassa API // According to spec: GET /v3/payments/{payment_id} - final payment = await _yookassaClient!.getPaymentInfo( - paymentId: paymentId, + final response = await _dio!.get>( + '/payments/$paymentId', ); + final paymentData = response.data; + if (paymentData == null) { + throw Exception('Empty response from YooKassa API'); + } + + // Extract payment ID and status + final id = paymentData['id'] as String? ?? paymentId; + final paymentStatus = paymentData['status'] as String? ?? 'pending'; + final paid = paymentData['paid'] as bool? ?? false; + // Extract confirmation URL if available String? confirmationUrl; - payment.confirmation?.maybeMap( - redirect: (redirect) { - confirmationUrl = redirect.confirmationUrl; - }, - orElse: () {}, - ); - - // Map YooKassa payment status to our status - final status = _mapPaymentStatus(payment.status); + final confirmation = paymentData['confirmation'] as Map?; + if (confirmation != null) { + final confirmationType = confirmation['type'] as String?; + if (confirmationType == 'redirect') { + confirmationUrl = confirmation['confirmation_url'] as String?; + } + } log( 'YooKassa payment status retrieved', name: 'YooMoneyHandler', error: { - 'paymentId': payment.id, - 'status': status, - 'paid': payment.paid, + 'paymentId': id, + 'status': paymentStatus, + 'paid': paid, }, ); return YookassaPayment( - id: payment.id, - status: status, + id: id, + status: paymentStatus, confirmationUrl: confirmationUrl, ); - } on YookassaException catch (e, stackTrace) { + } on DioException catch (e, stackTrace) { log( 'YooKassa API error when checking payment', name: 'YooMoneyHandler', error: e, stackTrace: stackTrace, ); - rethrow; + if (e.response != null) { + log( + 'YooKassa error response: ${e.response?.data}', + name: 'YooMoneyHandler', + ); + } + throw Exception( + 'Failed to check YooKassa payment: ${e.message ?? 'Unknown error'}', + ); } on Exception catch (e, stackTrace) { log( 'Unexpected error when checking YooKassa payment', @@ -274,17 +317,6 @@ class YooMoneyHandler { } } - /// Map YooKassa payment status to string status - /// According to spec: pending, waiting_for_capture, succeeded, canceled - String _mapPaymentStatus(YookassaPaymentStatus status) { - return switch (status) { - YookassaPaymentStatus.pending => 'pending', - YookassaPaymentStatus.waitingForCapture => 'waiting_for_capture', - YookassaPaymentStatus.succeeded => 'succeeded', - YookassaPaymentStatus.canceled => 'canceled', - }; - } - /// Build return URL for payment confirmation /// Uses YOOKASSA_RETURN_URL env var if set, otherwise defaults to web app URL String _buildReturnUrl(String userId) { diff --git a/mnemo_cards_backend/pubspec.lock b/mnemo_cards_backend/pubspec.lock index 5af10b8..e43890a 100644 --- a/mnemo_cards_backend/pubspec.lock +++ b/mnemo_cards_backend/pubspec.lock @@ -313,14 +313,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" - freezed_annotation: - dependency: transitive - description: - name: freezed_annotation - sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 - url: "https://pub.dev" - source: hosted - version: "2.4.4" frontend_server_client: dependency: transitive description: @@ -639,14 +631,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" - retrofit: - dependency: transitive - description: - name: retrofit - sha256: "13a2865c0d97da580ea4e3c64d412d81f365fd5b26be2a18fca9582e021da37a" - url: "https://pub.dev" - source: hosted - version: "4.1.0" retry: dependency: transitive description: @@ -983,13 +967,5 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" - yookassa_client: - dependency: "direct main" - description: - name: yookassa_client - sha256: e801e1bb22f21f883adbee15645e2c9b21c4a640f8e096006a6295c335c588aa - url: "https://pub.dev" - source: hosted - version: "1.0.5" sdks: dart: ">=3.9.0 <4.0.0" diff --git a/mnemo_cards_backend/pubspec.yaml b/mnemo_cards_backend/pubspec.yaml index de14cf6..5f60ed9 100644 --- a/mnemo_cards_backend/pubspec.yaml +++ b/mnemo_cards_backend/pubspec.yaml @@ -52,7 +52,6 @@ dependencies: uuid: ^4.5.2 minio: ^3.5.8 - yookassa_client: ^1.0.5 neat_periodic_task: ^2.0.1 jaguar_jwt: ^3.0.0