yookassa
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
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
This commit is contained in:
parent
d6a6356254
commit
a6fd99559a
3 changed files with 128 additions and 121 deletions
|
|
@ -5,7 +5,6 @@ import 'dart:io';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:injectable/injectable.dart';
|
import 'package:injectable/injectable.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
import 'package:yookassa_client/yookassa_client.dart';
|
|
||||||
|
|
||||||
/// Wrapper for YooKassa payment response
|
/// Wrapper for YooKassa payment response
|
||||||
class YookassaPayment {
|
class YookassaPayment {
|
||||||
|
|
@ -28,8 +27,9 @@ class YooMoneyHandler {
|
||||||
final String _shopId;
|
final String _shopId;
|
||||||
final String _secretKey;
|
final String _secretKey;
|
||||||
final String? _returnUrlBase;
|
final String? _returnUrlBase;
|
||||||
late final YookassaClient? _yookassaClient;
|
late final Dio? _dio;
|
||||||
final _uuid = const Uuid();
|
final _uuid = const Uuid();
|
||||||
|
static const String _baseUrl = 'https://api.yookassa.ru/v3';
|
||||||
|
|
||||||
YooMoneyHandler({
|
YooMoneyHandler({
|
||||||
required String shopId,
|
required String shopId,
|
||||||
|
|
@ -43,22 +43,37 @@ class YooMoneyHandler {
|
||||||
'YooKassa credentials not configured. Payments will not work.',
|
'YooKassa credentials not configured. Payments will not work.',
|
||||||
name: 'YooMoneyHandler',
|
name: 'YooMoneyHandler',
|
||||||
);
|
);
|
||||||
_yookassaClient = null;
|
_dio = null;
|
||||||
} else {
|
} else {
|
||||||
// Initialize YooKassa client with credentials
|
// Initialize Dio with Basic Auth
|
||||||
// According to spec: Basic Auth with shopId:secretKey
|
// According to spec: Basic Auth with shopId:secretKey
|
||||||
_yookassaClient = YookassaClient(
|
_dio = Dio(
|
||||||
Dio(),
|
BaseOptions(
|
||||||
credentials: YookassaAuthCredentials(
|
baseUrl: _baseUrl,
|
||||||
shopId: _shopId,
|
headers: {
|
||||||
secretKey: _secretKey,
|
'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
|
/// Check if YooKassa is properly configured
|
||||||
bool get isConfigured => _yookassaClient != null;
|
bool get isConfigured => _dio != null;
|
||||||
|
|
||||||
/// Create a payment in YooKassa
|
/// Create a payment in YooKassa
|
||||||
/// According to spec: POST /v3/payments
|
/// According to spec: POST /v3/payments
|
||||||
|
|
@ -88,37 +103,35 @@ class YooMoneyHandler {
|
||||||
? '${amountValue.substring(0, amountValue.length - 2)}.${amountValue.substring(amountValue.length - 2)}'
|
? '${amountValue.substring(0, amountValue.length - 2)}.${amountValue.substring(amountValue.length - 2)}'
|
||||||
: '0.${amountValue.padLeft(2, '0')}';
|
: '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
|
// Build return URL for payment confirmation
|
||||||
// According to spec: ReturnUrl - URL where user returns after payment
|
// According to spec: ReturnUrl - URL where user returns after payment
|
||||||
// Max 2048 characters per spec
|
// Max 2048 characters per spec
|
||||||
final returnUrl = _buildReturnUrl(userId);
|
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
|
// Generate idempotence key for request
|
||||||
// According to spec: Idempotence-Key header (required)
|
// According to spec: Idempotence-Key header (required)
|
||||||
final idempotenceKey = _uuid.v4();
|
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(
|
log(
|
||||||
'Creating YooKassa payment',
|
'Creating YooKassa payment',
|
||||||
name: 'YooMoneyHandler',
|
name: 'YooMoneyHandler',
|
||||||
|
|
@ -130,71 +143,85 @@ class YooMoneyHandler {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create payment via YooKassa API
|
// Create payment via YooKassa API
|
||||||
// According to spec: Idempotence-Key header is required
|
// POST /v3/payments with Idempotence-Key header
|
||||||
final payment = await _yookassaClient!.createPayment(
|
final response = await _dio!.post<Map<String, dynamic>>(
|
||||||
paymentRequest: paymentRequest,
|
'/payments',
|
||||||
idempotenceKey: idempotenceKey,
|
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
|
// Extract confirmation URL from payment response
|
||||||
// According to spec: confirmation.confirmation_url for redirect type
|
// According to spec: confirmation.confirmation_url for redirect type
|
||||||
String? confirmationUrl;
|
String? confirmationUrl;
|
||||||
payment.confirmation?.maybeMap(
|
final confirmation = paymentData['confirmation'] as Map<String, dynamic>?;
|
||||||
redirect: (redirect) {
|
if (confirmation != null) {
|
||||||
confirmationUrl = redirect.confirmationUrl;
|
final confirmationType = confirmation['type'] as String?;
|
||||||
},
|
if (confirmationType == 'redirect') {
|
||||||
qr: (qr) {
|
confirmationUrl = confirmation['confirmation_url'] as String?;
|
||||||
// For QR payments, we might need to handle differently
|
} else {
|
||||||
log('QR payment created, no redirect URL', name: 'YooMoneyHandler');
|
log(
|
||||||
},
|
'Payment created with confirmation type: $confirmationType',
|
||||||
embedded: (_) {
|
name: 'YooMoneyHandler',
|
||||||
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');
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (confirmationUrl == null) {
|
if (confirmationUrl == null) {
|
||||||
log(
|
log(
|
||||||
'Warning: No confirmation URL in payment response',
|
'Warning: No confirmation URL in payment response',
|
||||||
name: 'YooMoneyHandler',
|
name: 'YooMoneyHandler',
|
||||||
error: jsonEncode(payment.toJson()),
|
error: jsonEncode(paymentData),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map YooKassa payment status to our status
|
|
||||||
final status = _mapPaymentStatus(payment.status);
|
|
||||||
|
|
||||||
log(
|
log(
|
||||||
'YooKassa payment created successfully',
|
'YooKassa payment created successfully',
|
||||||
name: 'YooMoneyHandler',
|
name: 'YooMoneyHandler',
|
||||||
error: {
|
error: {
|
||||||
'paymentId': payment.id,
|
'paymentId': paymentId,
|
||||||
'status': status,
|
'status': paymentStatus,
|
||||||
'hasConfirmationUrl': confirmationUrl != null,
|
'hasConfirmationUrl': confirmationUrl != null,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return YookassaPayment(
|
return YookassaPayment(
|
||||||
id: payment.id,
|
id: paymentId,
|
||||||
status: status,
|
status: paymentStatus,
|
||||||
confirmationUrl: confirmationUrl,
|
confirmationUrl: confirmationUrl,
|
||||||
);
|
);
|
||||||
} on YookassaException catch (e, stackTrace) {
|
} on DioException catch (e, stackTrace) {
|
||||||
log(
|
log(
|
||||||
'YooKassa API error when creating payment',
|
'YooKassa API error when creating payment',
|
||||||
name: 'YooMoneyHandler',
|
name: 'YooMoneyHandler',
|
||||||
error: e,
|
error: e,
|
||||||
stackTrace: stackTrace,
|
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) {
|
} on Exception catch (e, stackTrace) {
|
||||||
log(
|
log(
|
||||||
'Unexpected error when creating YooKassa payment',
|
'Unexpected error when creating YooKassa payment',
|
||||||
|
|
@ -224,45 +251,61 @@ class YooMoneyHandler {
|
||||||
|
|
||||||
// Get payment info from YooKassa API
|
// Get payment info from YooKassa API
|
||||||
// According to spec: GET /v3/payments/{payment_id}
|
// According to spec: GET /v3/payments/{payment_id}
|
||||||
final payment = await _yookassaClient!.getPaymentInfo(
|
final response = await _dio!.get<Map<String, dynamic>>(
|
||||||
paymentId: paymentId,
|
'/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
|
// Extract confirmation URL if available
|
||||||
String? confirmationUrl;
|
String? confirmationUrl;
|
||||||
payment.confirmation?.maybeMap(
|
final confirmation = paymentData['confirmation'] as Map<String, dynamic>?;
|
||||||
redirect: (redirect) {
|
if (confirmation != null) {
|
||||||
confirmationUrl = redirect.confirmationUrl;
|
final confirmationType = confirmation['type'] as String?;
|
||||||
},
|
if (confirmationType == 'redirect') {
|
||||||
orElse: () {},
|
confirmationUrl = confirmation['confirmation_url'] as String?;
|
||||||
);
|
}
|
||||||
|
}
|
||||||
// Map YooKassa payment status to our status
|
|
||||||
final status = _mapPaymentStatus(payment.status);
|
|
||||||
|
|
||||||
log(
|
log(
|
||||||
'YooKassa payment status retrieved',
|
'YooKassa payment status retrieved',
|
||||||
name: 'YooMoneyHandler',
|
name: 'YooMoneyHandler',
|
||||||
error: {
|
error: {
|
||||||
'paymentId': payment.id,
|
'paymentId': id,
|
||||||
'status': status,
|
'status': paymentStatus,
|
||||||
'paid': payment.paid,
|
'paid': paid,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return YookassaPayment(
|
return YookassaPayment(
|
||||||
id: payment.id,
|
id: id,
|
||||||
status: status,
|
status: paymentStatus,
|
||||||
confirmationUrl: confirmationUrl,
|
confirmationUrl: confirmationUrl,
|
||||||
);
|
);
|
||||||
} on YookassaException catch (e, stackTrace) {
|
} on DioException catch (e, stackTrace) {
|
||||||
log(
|
log(
|
||||||
'YooKassa API error when checking payment',
|
'YooKassa API error when checking payment',
|
||||||
name: 'YooMoneyHandler',
|
name: 'YooMoneyHandler',
|
||||||
error: e,
|
error: e,
|
||||||
stackTrace: stackTrace,
|
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) {
|
} on Exception catch (e, stackTrace) {
|
||||||
log(
|
log(
|
||||||
'Unexpected error when checking YooKassa payment',
|
'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
|
/// Build return URL for payment confirmation
|
||||||
/// Uses YOOKASSA_RETURN_URL env var if set, otherwise defaults to web app URL
|
/// Uses YOOKASSA_RETURN_URL env var if set, otherwise defaults to web app URL
|
||||||
String _buildReturnUrl(String userId) {
|
String _buildReturnUrl(String userId) {
|
||||||
|
|
|
||||||
|
|
@ -313,14 +313,6 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
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:
|
frontend_server_client:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -639,14 +631,6 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.0"
|
version: "4.1.0"
|
||||||
retrofit:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: retrofit
|
|
||||||
sha256: "13a2865c0d97da580ea4e3c64d412d81f365fd5b26be2a18fca9582e021da37a"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "4.1.0"
|
|
||||||
retry:
|
retry:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -983,13 +967,5 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.2"
|
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:
|
sdks:
|
||||||
dart: ">=3.9.0 <4.0.0"
|
dart: ">=3.9.0 <4.0.0"
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,6 @@ dependencies:
|
||||||
uuid: ^4.5.2
|
uuid: ^4.5.2
|
||||||
minio: ^3.5.8
|
minio: ^3.5.8
|
||||||
|
|
||||||
yookassa_client: ^1.0.5
|
|
||||||
neat_periodic_task: ^2.0.1
|
neat_periodic_task: ^2.0.1
|
||||||
jaguar_jwt: ^3.0.0
|
jaguar_jwt: ^3.0.0
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue