pardakht 0.8.1
pardakht: ^0.8.1 copied to clipboard
One type-safe API across Iranian payment gateways. Pure Dart, explicit currency units, a unified error taxonomy and server-safe verification.
// A complete walkthrough of a payment, from opening a session to confirming
// it, plus a worked custom gateway.
//
// Run it with:
//
// dart run example/pardakht_example.dart
//
// It needs no credentials and makes no network call: the transport is stubbed
// with recorded responses, so the whole flow can be followed end to end
// without a merchant account.
//
// The gateway it drives, `DemoPayGateway`, lives in demo_pay_gateway.dart. It
// is a full adapter for a fictional provider, in the same shape every real
// adapter has, and the companion to doc/adding_a_gateway.md.
import 'dart:async';
import 'package:pardakht/pardakht.dart';
import 'demo_pay_gateway.dart';
Future<void> main() async {
final gateway = DemoPayGateway(
credentials: const DemoPayCredentials(merchantKey: 'demo-merchant-key-01'),
options: GatewayOptions(
httpClient: RecordedTransport(),
logger: CallbackPaymentLogger(
(level, message, error, stackTrace) =>
print(' log[${level.name}] $message'),
minimumLevel: LogLevel.debug,
),
),
);
// The merchant's own record of what this order costs. Everything later is
// checked against this, never against a value that arrives with the payer.
final order = (id: 'ORD-1042', amount: Money.toman(25000));
print('Gateway: ${gateway.displayName} (${gateway.id})');
print(
'Order ${order.id} for ${order.amount} '
'(${order.amount.inRial} rial)\n',
);
// ---------------------------------------------------------------- step 1
print('1. Opening a session');
final session = await gateway.createSession(
PaymentRequest(
amount: order.amount,
callbackUrl: Uri.parse('https://shop.example/pay/callback'),
orderId: order.id,
description: 'Order ${order.id}',
payer: PayerInfo(mobile: '09123456789'),
),
);
// Store this before redirecting. A payer who closes the browser mid-payment
// leaves no other way to find out what happened.
print(' reference : ${session.reference}');
print(' send payer: ${session.redirectUrl}\n');
// ---------------------------------------------------------------- step 2
// The payer pays and the provider returns them to the callback URL. In a
// real server this is `request.uri.queryParameters`.
print('2. Payer returns to the callback');
final returnedTo = Uri.parse(
'https://shop.example/pay/callback?token=${session.reference}&status=1',
);
final payload = gateway.parseCallback(returnedTo.queryParameters);
print(' cancelled : ${payload.userCancelled}');
print(' reference : ${payload.reference}\n');
if (payload.userCancelled) {
// Still worth verifying: a payer can cancel on a page that already took
// the money, and only the provider knows for certain.
print(' payer cancelled; returning them to the basket');
}
// ---------------------------------------------------------------- step 3
print('3. Verifying on the server');
final result = await gateway.verify(
VerificationRequest(
reference: payload.reference,
// From the merchant's own record. An amount that arrives with the payer
// is exactly the value an attacker controls.
amount: order.amount,
orderId: order.id,
),
);
print(' status : ${result.status.name}');
print(' successful: ${result.isSuccessful}');
print(' bank ref : ${result.referenceId}');
print(' card : ${result.cardPanMasked}');
print(' amount : ${result.amount}\n');
// isSuccessful, not `status == verified`: a repeat verification is still a
// payment that was collected.
if (result.isSuccessful && result.amount == order.amount) {
print('=> Fulfil order ${order.id}.');
} else {
print('=> Do not fulfil order ${order.id}.');
}
// ------------------------------------------------- what failures look like
print('\n4. Handling a failure');
const resolver = MapPaymentMessageResolver(persianPaymentMessages);
try {
await gateway.verify(
VerificationRequest(reference: 'unpaid-token', amount: order.amount),
);
} on PaymentException catch (error) {
// The switch is exhaustive: a new failure mode in a later release becomes
// a compile error here rather than a silent fallthrough.
final forDeveloper = switch (error) {
GatewayRejectedException(:final code, :final rawCode) =>
'rejected: ${code.name} (provider code $rawCode)',
NetworkPaymentException() => 'the provider could not be reached',
TimeoutPaymentException() => 'the provider did not answer in time',
MalformedResponseException() => 'the provider sent an unusable reply',
ConfigurationException() => 'this gateway is misconfigured',
};
print(' developer : $forDeveloper');
print(' payer : ${resolver.resolveException(error)}');
}
gateway.close();
}