Skip to main content
Version: 4.0

Developing a gateway

This page builds a gateway for a made-up payment provider called Hero, as a package of its own. The gateway packages of Parbad, such as Parbad.Gateway.Zibal, are complete examples.

What a gateway does​

A gateway implements four methods. Parbad calls them and does everything around them: it stores the payments, gives tracking numbers, protects against double verification, and decides the result.

MethodWhenWhat the gateway does
RequestAsyncThe application requests a payment.Registers the payment with the bank and says how to send the customer there.
FetchAsyncThe customer comes back to the callback URL.Reads what the bank sent with the customer: did the customer pay?
VerifyAsyncThe application verifies the payment.Asks the bank to confirm the payment and reports what the bank answered.
RefundAsyncThe application refunds a payment.Asks the bank to refund, if the bank supports it.

A gateway reports what the bank said; Parbad decides what it means for the payment. For example, Parbad accepts a bank's reference for one invoice only, and compares the amount the bank reports with the invoice.

The project​

Create a class library for .NET Standard 2.0 or later, named for example Hero.Parbad.Gateway, and add Parbad:

dotnet add package Parbad

The account​

The settings an application gives for each of its accounts with the provider:

public class HeroGatewayAccount : GatewayAccount
{
public string MerchantId { get; set; } = null!;
}

The gateway​

using Microsoft.AspNetCore.Http;
using Parbad;
using Parbad.Abstraction;
using Parbad.GatewayBuilders;
using Parbad.Http;
using Parbad.Internal;
using Parbad.InvoiceBuilder;
using Parbad.Net;
using Parbad.Storage.Abstractions.Models;

[Gateway(Name)]
public class HeroGateway : GatewayBase<HeroGatewayAccount>
{
public const string Name = "Hero";

private readonly IHttpContextAccessor _httpContextAccessor;
private readonly HttpClient _httpClient;

public HeroGateway(IGatewayAccountProvider<HeroGatewayAccount> accountProvider,
IHttpContextAccessor httpContextAccessor,
IHttpClientFactory httpClientFactory) : base(accountProvider)
{
_httpContextAccessor = httpContextAccessor;

// The client of this gateway, with the timeout and proxy the application configured for it.
_httpClient = httpClientFactory.CreateClient(this);
}
}

The name in [Gateway(...)] is the name applications use with SetGateway("Hero").

Request​

public override async Task<IPaymentRequestResult> RequestAsync(Invoice invoice, CancellationToken cancellationToken = default)
{
var account = await GetAccountAsync(invoice);

// Register the payment with the bank, e.g. with _httpClient, and get its token.
var token = await RegisterPaymentAsync(account, invoice.TrackingNumber, invoice.Amount, invoice.CallbackUrl, cancellationToken);

if (token == null)
{
return PaymentRequestResult.Failed("The bank rejected the payment.", account.Name);
}

// Send the customer to the bank's page, with a redirect...
var result = PaymentRequestResult.SucceedWithRedirect(account.Name,
_httpContextAccessor.GetRequiredHttpContext(),
$"https://pay.hero.example/{token}");

// ...or by posting a form:
// PaymentRequestResult.SucceedWithPost(account.Name, httpContext, url, new Dictionary<string, string> { ... });

// Data for the later steps. Parbad stores it with the request transaction.
result.DatabaseAdditionalData.Add("HeroToken", token);

return result;
}

Fetch​

public override Task<IPaymentFetchResult> FetchAsync(InvoiceContext context, CancellationToken cancellationToken = default)
{
var request = _httpContextAccessor.GetRequiredHttpContext().Request;

// What the bank sent with the customer (in the query string or the form, depending on the bank).
var status = request.Query["status"];

var result = status == "OK"
? PaymentFetchResult.ReadyForVerifying()
: PaymentFetchResult.Failed("The customer cancelled the payment.");

return Task.FromResult<IPaymentFetchResult>(result);
}

Verify​

public override async Task<IPaymentVerifyResult> VerifyAsync(InvoiceContext context, CancellationToken cancellationToken = default)
{
var account = await GetAccountAsync(context.Payment);

// The data stored by RequestAsync.
var token = context.Transactions
.Single(transaction => transaction.Type == TransactionType.Request)
.ToDictionary()["HeroToken"];

var answer = await VerifyAtBankAsync(account, token, cancellationToken);

if (!answer.IsPaid)
{
return PaymentVerifyResult.Failed(answer.Message, answer.Code);
}

// transactionCode: the code the customer sees (e.g. the RRN). bankReference: what the bank verified (here the
// token). The amount and the order ID that the bank reports let Parbad check them against the invoice.
return PaymentVerifyResult.Paid(answer.Rrn, token, "The payment was successful.") with
{
BankReportedAmount = answer.Amount,
BankReportedOrderId = answer.OrderId
};
}

When the bank answers "already verified" without proving which payment it means, return PaymentVerifyResult.AlreadyVerifiedAtBank(...): Parbad then records the payment as paid only if no other payment has the same bank reference.

If the bank cannot be reached, let the exception propagate: Parbad leaves the payment open, so it can be verified again.

Refund​

public override Task<IPaymentRefundResult> RefundAsync(InvoiceContext context, Money amount, CancellationToken cancellationToken = default)
{
// A bank without refunds:
return Task.FromResult<IPaymentRefundResult>(PaymentRefundResult.Failed("Hero does not support refunds."));
}

Extension methods​

So that applications can write AddHero(), WithAccounts(...) and UseHero():

public static class HeroGatewayExtensions
{
public static IGatewayConfigurationBuilder<HeroGateway> AddHero(this IGatewayBuilder builder) =>
builder.AddGateway<HeroGateway>()
.WithHttpClient(client => { });

public static IGatewayConfigurationBuilder<HeroGateway> WithAccounts(
this IGatewayConfigurationBuilder<HeroGateway> builder,
Action<IGatewayAccountBuilder<HeroGatewayAccount>> configureAccounts) =>
builder.WithAccounts(configureAccounts);

public static IInvoiceBuilder UseHero(this IInvoiceBuilder builder) =>
builder.SetGateway(HeroGateway.Name);
}

Publishing​

Give the project its package metadata (id, description, authors, license, a README), then:

dotnet pack -c Release

and publish the package to nuget.org.