pardakht 0.8.1 copy "pardakht: ^0.8.1" to clipboard
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.

example/pardakht_example.dart

// 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();
}
3
likes
160
points
42
downloads

Documentation

Documentation
API reference

Publisher

unverified uploader

Weekly Downloads

One type-safe API across Iranian payment gateways. Pure Dart, explicit currency units, a unified error taxonomy and server-safe verification.

Repository (GitHub)
View/report issues

Topics

#payment #iran #gateway #fintech #zarinpal

License

MIT (license)

Dependencies

http, meta

More

Packages that depend on pardakht