Skip to content

Commit 25a62b3

Browse files
authored
Merge pull request #9 from Monnify/dev
feat: add card transactions and automated release tagging
2 parents e6616c7 + cd902ff commit 25a62b3

14 files changed

Lines changed: 444 additions & 2 deletions

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ own version is independent of Monnify's API versioning.
1111

1212
## [Unreleased]
1313

14+
### Added
15+
- `IMonnifyCollectionsClient`: `ChargeAsync`, `AuthorizeOtpAsync`, `Authorize3dsAsync` for direct
16+
card charges. The whole client now registers with automatic retry disabled, same reasoning as
17+
Disbursements/Bills, since `ChargeAsync` directly debits a card. See docs/COMPATIBILITY.md for
18+
sandbox-verification notes.
19+
- Corrected `InitiateBankTransferAsync`'s doc note: the real response field is `ussdPayment`, not
20+
`ussdCode` as our simplified doc sample shows (the SDK already had this right).
21+
1422
## [0.1.0] - 2026-06-29
1523

1624
### Fixed

docs/COMPATIBILITY.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ Status legend:
2020
| `IMonnifyCollectionsClient.GetInvoiceAsync` | `GET /api/v1/invoice/{invoiceReference}/details` | **Implemented** | |
2121
| `IMonnifyCollectionsClient.GetInvoicesAsync` | `GET /api/v1/invoice/all?page=&size=` | **Implemented** | |
2222
| `IMonnifyCollectionsClient.CancelInvoiceAsync` | `DELETE /api/v1/invoice/{invoiceReference}/cancel` | **Implemented** | |
23-
| `IMonnifyCollectionsClient.InitiateBankTransferAsync` | `POST /api/v1/merchant/bank-transfer/init-payment` | **Implemented** | Generates a one-time dynamic account for an already-initialized transaction |
23+
| `IMonnifyCollectionsClient.InitiateBankTransferAsync` | `POST /api/v1/merchant/bank-transfer/init-payment` | **Implemented** | Real response field is `ussdPayment`, not `ussdCode` as our simplified doc sample shows |
24+
| `IMonnifyCollectionsClient.ChargeAsync` | `POST /api/v1/merchant/cards/charge` | **Implemented** | Requires a `transactionReference` from `InitializeTransactionAsync` first. Automatic retry disabled - this directly debits a card |
25+
| `IMonnifyCollectionsClient.AuthorizeOtpAsync` | `POST /api/v1/merchant/cards/otp/authorize` | **Implemented** | Not sandbox-verified - blocked on the `ChargeAsync` bin issue above |
26+
| `IMonnifyCollectionsClient.Authorize3dsAsync` | `POST /api/v1/sdk/cards/secure-3d/authorize` | **Implemented** | Restricted to PCI DSS-certified merchants, requires separate activation |
2427
| `IMonnifyCollectionsClient.SearchTransactionsAsync` | `GET /api/v1/transactions/search?page=&size=&...` | **Implemented** | Many optional filters (reference, amount range, customer, status, date range) |
2528
| `IMonnifyCollectionsClient.GetTransactionAsync` | `GET /api/v2/transactions/{transactionReference}` | **Implemented** | Amount fields are quoted strings; `decimal` via `JsonNumberHandling.AllowReadingFromString` |
2629
| `IMonnifyCollectionsClient.QueryTransactionAsync` | `GET /api/v2/merchant/transactions/query?transactionReference=&paymentReference=` | **Implemented** | Same response shape as `GetTransactionAsync`; requires at least one query parameter |

src/Monnify/Collections/IMonnifyCollectionsClient.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@
22

33
namespace Monnify.Collections;
44

5+
/// <remarks>
6+
/// Registered with automatic HTTP retry disabled: <see cref="ChargeAsync"/> directly attempts to
7+
/// debit a card, so an ambiguous failure (timeout, network error, 5xx) must not be blindly resent
8+
/// with the same details - that risks a double charge, same reasoning as
9+
/// <see cref="Disbursements.IMonnifyDisbursementsClient"/> and <see cref="Bills.IMonnifyBillsClient"/>.
10+
/// Query <see cref="GetTransactionAsync"/> with the same <c>transactionReference</c> instead. The
11+
/// other methods on this client don't move money directly (they set up a payment instrument the
12+
/// customer still has to complete), so this is a more conservative retry stance than they
13+
/// strictly need, traded for keeping every collection method - including card charges - on one
14+
/// client.
15+
/// </remarks>
516
public interface IMonnifyCollectionsClient
617
{
718
/// <summary>Starts a checkout, returning a hosted page URL to redirect the customer to.</summary>
@@ -37,6 +48,24 @@ Task<MonnifyPagedResult<ReservedAccountTransaction>> GetReservedAccountTransacti
3748
Task<BankTransferPaymentDetails> InitiateBankTransferAsync(
3849
InitiateBankTransferRequest request, CancellationToken cancellationToken = default);
3950

51+
/// <summary>
52+
/// Charges a card (raw PAN/CVV/PIN) against a transaction reference from
53+
/// <see cref="InitializeTransactionAsync"/>, as an alternative to the hosted checkout flow.
54+
/// Depending on the card, the result is either immediately final, or requires a follow-up call
55+
/// to <see cref="AuthorizeOtpAsync"/> (OTP) or <see cref="Authorize3dsAsync"/> (3DS) to complete.
56+
/// </summary>
57+
Task<ChargeCardResult> ChargeAsync(ChargeCardRequest request, CancellationToken cancellationToken = default);
58+
59+
/// <summary>Completes a charge that required an OTP, using the token ID from <see cref="ChargeAsync"/>'s response.</summary>
60+
Task<AuthorizeCardOtpResult> AuthorizeOtpAsync(AuthorizeCardOtpRequest request, CancellationToken cancellationToken = default);
61+
62+
/// <summary>
63+
/// Completes a charge on a card that uses 3D Secure authentication. This is not active by
64+
/// default - it's restricted to PCI DSS-certified merchants and requires it to be enabled for
65+
/// your account first.
66+
/// </summary>
67+
Task<AuthorizeCardOtpResult> Authorize3dsAsync(Authorize3dsCardRequest request, CancellationToken cancellationToken = default);
68+
4069
/// <summary>Searches transactions on the integration, optionally filtered by reference, amount range, customer, status, or date range.</summary>
4170
Task<MonnifyPagedResult<TransactionSummary>> SearchTransactionsAsync(
4271
SearchTransactionsRequest? filter = null, int page = 0, int size = 10, CancellationToken cancellationToken = default);
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace Monnify.Collections;
4+
5+
public sealed class Authorize3dsCardRequest
6+
{
7+
[JsonPropertyName("transactionReference")]
8+
public string TransactionReference { get; set; } = string.Empty;
9+
10+
/// <summary>Your Monnify API key. Unlike every other call in this SDK, this endpoint's contract
11+
/// puts it in the request body rather than relying solely on the bearer token.</summary>
12+
[JsonPropertyName("apiKey")]
13+
public string ApiKey { get; set; } = string.Empty;
14+
15+
[JsonPropertyName("collectionChannel")]
16+
public string CollectionChannel { get; set; } = "API_NOTIFICATION";
17+
18+
[JsonPropertyName("card")]
19+
public Card Card { get; set; } = new();
20+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace Monnify.Collections;
4+
5+
public sealed class AuthorizeCardOtpRequest
6+
{
7+
/// <summary>The transaction reference from the <c>ChargeAsync</c> call.</summary>
8+
[JsonPropertyName("transactionReference")]
9+
public string TransactionReference { get; set; } = string.Empty;
10+
11+
[JsonPropertyName("collectionChannel")]
12+
public string CollectionChannel { get; set; } = "API_NOTIFICATION";
13+
14+
/// <summary>The token ID returned by the <c>ChargeAsync</c> response for a card that requires OTP.</summary>
15+
[JsonPropertyName("tokenId")]
16+
public string TokenId { get; set; } = string.Empty;
17+
18+
/// <summary>The OTP sent to the cardholder's device.</summary>
19+
[JsonPropertyName("token")]
20+
public string Token { get; set; } = string.Empty;
21+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace Monnify.Collections;
4+
5+
/// <summary>Returned by both <c>AuthorizeOtpAsync</c> and <c>Authorize3dsAsync</c> - they share the same shape.</summary>
6+
public sealed class AuthorizeCardOtpResult
7+
{
8+
/// <summary>E.g. <c>SUCCESSFUL</c>.</summary>
9+
[JsonPropertyName("paymentStatus")]
10+
public string PaymentStatus { get; set; } = string.Empty;
11+
12+
[JsonPropertyName("paymentDescription")]
13+
public string PaymentDescription { get; set; } = string.Empty;
14+
15+
[JsonPropertyName("transactionReference")]
16+
public string TransactionReference { get; set; } = string.Empty;
17+
18+
[JsonPropertyName("paymentReference")]
19+
public string PaymentReference { get; set; } = string.Empty;
20+
21+
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
22+
[JsonPropertyName("amountPaid")]
23+
public decimal AmountPaid { get; set; }
24+
25+
[JsonPropertyName("currencyPaid")]
26+
public string CurrencyPaid { get; set; } = string.Empty;
27+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace Monnify.Collections;
4+
5+
/// <summary>
6+
/// Raw card details for a direct charge. All fields are modeled as strings (rather than the
7+
/// numeric types our docs show for some of these on the 3DS endpoint specifically) to avoid
8+
/// silently dropping a leading zero in <see cref="ExpiryMonth"/>, <see cref="Cvv"/>, or
9+
/// <see cref="Pin"/>.
10+
/// </summary>
11+
public sealed class Card
12+
{
13+
[JsonPropertyName("number")]
14+
public string Number { get; set; } = string.Empty;
15+
16+
/// <summary>Two digits, e.g. <c>"01"</c>-<c>"12"</c>.</summary>
17+
[JsonPropertyName("expiryMonth")]
18+
public string ExpiryMonth { get; set; } = string.Empty;
19+
20+
/// <summary>Four digits, e.g. <c>"2025"</c>.</summary>
21+
[JsonPropertyName("expiryYear")]
22+
public string ExpiryYear { get; set; } = string.Empty;
23+
24+
[JsonPropertyName("pin")]
25+
public string Pin { get; set; } = string.Empty;
26+
27+
[JsonPropertyName("cvv")]
28+
public string Cvv { get; set; } = string.Empty;
29+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace Monnify.Collections;
4+
5+
public sealed class ChargeCardRequest
6+
{
7+
/// <summary>A transaction reference from a prior <c>InitializeTransactionAsync</c> call.</summary>
8+
[JsonPropertyName("transactionReference")]
9+
public string TransactionReference { get; set; } = string.Empty;
10+
11+
[JsonPropertyName("collectionChannel")]
12+
public string CollectionChannel { get; set; } = "API_NOTIFICATION";
13+
14+
[JsonPropertyName("card")]
15+
public Card Card { get; set; } = new();
16+
17+
[JsonPropertyName("deviceInformation")]
18+
public DeviceInformation DeviceInformation { get; set; } = new();
19+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace Monnify.Collections;
4+
5+
public sealed class ChargeCardResult
6+
{
7+
/// <summary>E.g. <c>SUCCESS</c> for a card that doesn't require OTP/3DS.</summary>
8+
[JsonPropertyName("status")]
9+
public string Status { get; set; } = string.Empty;
10+
11+
[JsonPropertyName("message")]
12+
public string Message { get; set; } = string.Empty;
13+
14+
[JsonPropertyName("transactionReference")]
15+
public string TransactionReference { get; set; } = string.Empty;
16+
17+
[JsonPropertyName("paymentReference")]
18+
public string PaymentReference { get; set; } = string.Empty;
19+
20+
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
21+
[JsonPropertyName("authorizedAmount")]
22+
public decimal AuthorizedAmount { get; set; }
23+
24+
/// <summary>
25+
/// Present when the card requires OTP - pass it to <c>AuthorizeOtpAsync</c>. Our only documented
26+
/// sample response is for a card that doesn't need OTP, so this field doesn't appear there; its
27+
/// existence and name come from the OTP-authorize endpoint's own request docs ("the token ID
28+
/// from the charge card endpoint response"), not from a captured OTP-required sample.
29+
/// </summary>
30+
[JsonPropertyName("tokenId")]
31+
public string? TokenId { get; set; }
32+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace Monnify.Collections;
4+
5+
/// <summary>
6+
/// Browser/device fingerprint sent alongside a card charge, used for fraud screening and 3DS.
7+
/// Field names mirror the EMV 3-D Secure browser-data fields our docs use verbatim.
8+
/// </summary>
9+
public sealed class DeviceInformation
10+
{
11+
[JsonPropertyName("httpBrowserLanguage")]
12+
public string HttpBrowserLanguage { get; set; } = string.Empty;
13+
14+
[JsonPropertyName("httpBrowserJavaEnabled")]
15+
public bool HttpBrowserJavaEnabled { get; set; }
16+
17+
[JsonPropertyName("httpBrowserJavaScriptEnabled")]
18+
public bool HttpBrowserJavaScriptEnabled { get; set; }
19+
20+
[JsonPropertyName("httpBrowserColorDepth")]
21+
public int HttpBrowserColorDepth { get; set; }
22+
23+
[JsonPropertyName("httpBrowserScreenHeight")]
24+
public int HttpBrowserScreenHeight { get; set; }
25+
26+
[JsonPropertyName("httpBrowserScreenWidth")]
27+
public int HttpBrowserScreenWidth { get; set; }
28+
29+
/// <summary>The browser's UTC offset in minutes, as a string. Our own documented sample leaves this empty.</summary>
30+
[JsonPropertyName("httpBrowserTimeDifference")]
31+
public string HttpBrowserTimeDifference { get; set; } = string.Empty;
32+
33+
[JsonPropertyName("userAgentBrowserValue")]
34+
public string UserAgentBrowserValue { get; set; } = string.Empty;
35+
}

0 commit comments

Comments
 (0)