Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ own version is independent of Monnify's API versioning.
## [Unreleased]

### Added
- `IMonnifyCollectionsClient`: refunds - `InitiateRefundAsync`, `GetRefundAsync`, `GetRefundsAsync`.
- `IMonnifyCollectionsClient`: limit profiles - `CreateLimitProfileAsync`, `GetLimitProfilesAsync`,
`UpdateLimitProfileAsync`; and `CreateReservedAccountWithLimitAsync`,
`UpdateReservedAccountLimitAsync` for attaching limit profiles to reserved accounts.
- `IMonnifyCollectionsClient`: sub-accounts - `CreateSubAccountsAsync`, `GetSubAccountsAsync`,
`UpdateSubAccountAsync`, `DeleteSubAccountAsync`. Create takes an array body (not a single object)
and the delete endpoint returns no `responseBody`. Requires relationship-manager approval for live.
- `IMonnifyCollectionsClient`: direct debit mandates - `CreateMandateAsync`, `GetMandatesAsync`,
`DebitMandateAsync`, `GetMandateDebitStatusAsync`, `CancelMandateAsync`, `ListMandatesAsync`.
Sandbox testing surfaced several real discrepancies with our own docs (wrong field name for the
Expand Down
13 changes: 12 additions & 1 deletion docs/COMPATIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,18 @@ Status legend:
| `IMonnifyCollectionsClient.SearchTransactionsAsync` | `GET /api/v1/transactions/search?page=&size=&...` | **Implemented** | Many optional filters (reference, amount range, customer, status, date range) |
| `IMonnifyCollectionsClient.GetTransactionAsync` | `GET /api/v2/transactions/{transactionReference}` | **Implemented** | Amount fields are quoted strings; `decimal` via `JsonNumberHandling.AllowReadingFromString` |
| `IMonnifyCollectionsClient.QueryTransactionAsync` | `GET /api/v2/merchant/transactions/query?transactionReference=&paymentReference=` | **Implemented** | Same response shape as `GetTransactionAsync`; requires at least one query parameter |
| *(Collections — sub-accounts/splitting)* | TBD | Planned | |
| `IMonnifyCollectionsClient.InitiateRefundAsync` | `POST /api/v1/refunds/initiate-refund` | **Implemented**
| `IMonnifyCollectionsClient.GetRefundAsync` | `GET /api/v1/refunds/{refundReference}` | **Implemented**
| `IMonnifyCollectionsClient.GetRefundsAsync` | `GET /api/v1/refunds?page=&size=` | **Implemented**
| `IMonnifyCollectionsClient.CreateLimitProfileAsync` | `POST /api/v1/limit-profile/` | **Implemented** | Sandbox-verified |
| `IMonnifyCollectionsClient.GetLimitProfilesAsync` | `GET /api/v1/limit-profile/` | **Implemented** | `pageable`/`sort` are `null` in the real response — not modelled |
| `IMonnifyCollectionsClient.UpdateLimitProfileAsync` | `PUT /api/v1/limit-profile/{limitProfileCode}` | **Implemented** | Profile code goes in the path; body has only the four editable fields |
| `IMonnifyCollectionsClient.CreateReservedAccountWithLimitAsync` | `POST /api/v1/bank-transfer/reserved-accounts/limit` | **Implemented** | Separate from `CreateReservedAccountAsync` (v2 path); requires `limitProfileCode` |
| `IMonnifyCollectionsClient.UpdateReservedAccountLimitAsync` | `PUT /api/v1/bank-transfer/reserved-accounts/limit` | **Implemented** | Sandbox-verified |
| `IMonnifyCollectionsClient.CreateSubAccountsAsync` | `POST /api/v1/sub-accounts` | **Implemented** | Body and response are both arrays; requires relationship-manager approval for live |
| `IMonnifyCollectionsClient.GetSubAccountsAsync` | `GET /api/v1/sub-accounts` | **Implemented** | Returns flat array (not paged) |
| `IMonnifyCollectionsClient.UpdateSubAccountAsync` | `PUT /api/v1/sub-accounts` | **Implemented** | `subAccountCode` included in the PUT body, not the path |
| `IMonnifyCollectionsClient.DeleteSubAccountAsync` | `DELETE /api/v1/sub-accounts/{subAccountCode}` | **Implemented** | Returns no `responseBody` — handled via `SendVoidAsync` |
| `IMonnifyCollectionsClient.CreateMandateAsync` | `POST /api/v1/direct-debit/mandate/create` | **Implemented** | Sandbox returned `mandateStatus: "PENDING"` and no `redirectUrl`, not the `"INITIATED"` + echoed `redirectUrl` our docs sample shows |
| `IMonnifyCollectionsClient.GetMandatesAsync` | `GET /api/v1/direct-debit/mandate/?mandateReferences=` | **Implemented** | The reference field in the response is `mandateReference`, not `externalMandateReference` as our docs sample shows. Two undocumented fields found: `responseMessage` and `schemeCode`. `mandateStatus` can be `PENDING_AUTHORIZATION`, not in our docs' status list |
| `IMonnifyCollectionsClient.DebitMandateAsync` | `POST /api/v1/direct-debit/mandate/debit` | **Implemented** | Not sandbox-verified end-to-end - needs an `ACTIVE` mandate. Automatic retry disabled - this directly debits a mandate |
Expand Down
48 changes: 48 additions & 0 deletions src/Monnify/Collections/IMonnifyCollectionsClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,54 @@ Task<MonnifyPagedResult<TransactionSummary>> SearchTransactionsAsync(
Task<Transaction> QueryTransactionAsync(
string? transactionReference = null, string? paymentReference = null, CancellationToken cancellationToken = default);

/// <summary>
/// Initiates a refund for a completed transaction. Requires prior approval from your
/// relationship manager for the live environment.
/// </summary>
Task<Refund> InitiateRefundAsync(InitiateRefundRequest request, CancellationToken cancellationToken = default);

/// <summary>Returns the current status of a refund by its merchant-supplied refund reference.</summary>
Task<Refund> GetRefundAsync(string refundReference, CancellationToken cancellationToken = default);

/// <summary>Returns all refunds on the integration, newest first.</summary>
Task<MonnifyPagedResult<Refund>> GetRefundsAsync(int page = 0, int size = 10, CancellationToken cancellationToken = default);

/// <summary>Creates a transaction limit profile that can be attached to reserved accounts.</summary>
Task<LimitProfile> CreateLimitProfileAsync(CreateLimitProfileRequest request, CancellationToken cancellationToken = default);

/// <summary>Returns all limit profiles created on the integration.</summary>
Task<MonnifyPagedResult<LimitProfile>> GetLimitProfilesAsync(int page = 0, int size = 10, CancellationToken cancellationToken = default);

/// <summary>Updates the name and limit values of an existing limit profile.</summary>
Task<LimitProfile> UpdateLimitProfileAsync(string limitProfileCode, UpdateLimitProfileRequest request, CancellationToken cancellationToken = default);

/// <summary>
/// Creates a reserved account with a transaction limit profile applied from the start.
/// Use <see cref="CreateReservedAccountAsync"/> when no limit profile is needed.
/// </summary>
Task<ReservedAccount> CreateReservedAccountWithLimitAsync(
CreateReservedAccountWithLimitRequest request, CancellationToken cancellationToken = default);

/// <summary>Attaches or replaces the limit profile on an existing reserved account.</summary>
Task<ReservedAccount> UpdateReservedAccountLimitAsync(
UpdateReservedAccountLimitRequest request, CancellationToken cancellationToken = default);

/// <summary>
/// Creates one or more sub accounts for splitting payments between different settlement accounts.
/// Requires prior approval from your relationship manager for the live environment.
/// </summary>
Task<IReadOnlyList<SubAccount>> CreateSubAccountsAsync(
IReadOnlyList<CreateSubAccountRequest> requests, CancellationToken cancellationToken = default);

/// <summary>Returns all sub accounts created on the integration.</summary>
Task<IReadOnlyList<SubAccount>> GetSubAccountsAsync(CancellationToken cancellationToken = default);

/// <summary>Updates the details of an existing sub account.</summary>
Task<SubAccount> UpdateSubAccountAsync(UpdateSubAccountRequest request, CancellationToken cancellationToken = default);

/// <summary>Deletes a sub account from the integration.</summary>
Task DeleteSubAccountAsync(string subAccountCode, CancellationToken cancellationToken = default);

/// <summary>
/// Initiates a direct debit mandate - a recurring authorization to debit a customer's bank
/// account. The mandate isn't active until the customer completes authorization (a token
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.Text.Json.Serialization;

namespace Monnify.Collections;

public sealed class CreateLimitProfileRequest
{
[JsonPropertyName("limitProfileName")]
public string LimitProfileName { get; set; } = string.Empty;

[JsonPropertyName("singleTransactionValue")]
public decimal SingleTransactionValue { get; set; }

[JsonPropertyName("dailyTransactionValue")]
public decimal DailyTransactionValue { get; set; }

[JsonPropertyName("dailyTransactionVolume")]
public int DailyTransactionVolume { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System.Text.Json.Serialization;

namespace Monnify.Collections;

public sealed class CreateReservedAccountWithLimitRequest
{
[JsonPropertyName("contractCode")]
public string ContractCode { get; set; } = string.Empty;

[JsonPropertyName("accountName")]
public string AccountName { get; set; } = string.Empty;

[JsonPropertyName("currencyCode")]
public string CurrencyCode { get; set; } = "NGN";

[JsonPropertyName("accountReference")]
public string AccountReference { get; set; } = string.Empty;

[JsonPropertyName("customerEmail")]
public string CustomerEmail { get; set; } = string.Empty;

[JsonPropertyName("customerName")]
public string CustomerName { get; set; } = string.Empty;

/// <summary>When <c>false</c>, <see cref="PreferredBanks"/> must be supplied.</summary>
[JsonPropertyName("getAllAvailableBanks")]
public bool GetAllAvailableBanks { get; set; } = true;

[JsonPropertyName("preferredBanks")]
public IReadOnlyList<string>? PreferredBanks { get; set; }

/// <summary>The limit profile to enforce on this reserved account.</summary>
[JsonPropertyName("limitProfileCode")]
public string LimitProfileCode { get; set; } = string.Empty;
}
27 changes: 27 additions & 0 deletions src/Monnify/Collections/Models/LimitProfiles/LimitProfile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System.Text.Json.Serialization;

namespace Monnify.Collections;

public sealed class LimitProfile
{
[JsonPropertyName("limitProfileCode")]
public string LimitProfileCode { get; set; } = string.Empty;

[JsonPropertyName("limitProfileName")]
public string LimitProfileName { get; set; } = string.Empty;

[JsonPropertyName("singleTransactionValue")]
public decimal SingleTransactionValue { get; set; }

[JsonPropertyName("dailyTransactionVolume")]
public int DailyTransactionVolume { get; set; }

[JsonPropertyName("dailyTransactionValue")]
public decimal DailyTransactionValue { get; set; }

[JsonPropertyName("dateCreated")]
public string DateCreated { get; set; } = string.Empty;

[JsonPropertyName("lastModified")]
public string LastModified { get; set; } = string.Empty;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.Text.Json.Serialization;

namespace Monnify.Collections;

public sealed class UpdateLimitProfileRequest
{
[JsonPropertyName("limitProfileName")]
public string LimitProfileName { get; set; } = string.Empty;

[JsonPropertyName("singleTransactionValue")]
public decimal SingleTransactionValue { get; set; }

[JsonPropertyName("dailyTransactionValue")]
public decimal DailyTransactionValue { get; set; }

[JsonPropertyName("dailyTransactionVolume")]
public int DailyTransactionVolume { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.Text.Json.Serialization;

namespace Monnify.Collections;

public sealed class UpdateReservedAccountLimitRequest
{
[JsonPropertyName("accountReference")]
public string AccountReference { get; set; } = string.Empty;

[JsonPropertyName("limitProfileCode")]
public string LimitProfileCode { get; set; } = string.Empty;
}
30 changes: 30 additions & 0 deletions src/Monnify/Collections/Models/Refunds/InitiateRefundRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System.Text.Json.Serialization;

namespace Monnify.Collections;

public sealed class InitiateRefundRequest
{
[JsonPropertyName("transactionReference")]
public string TransactionReference { get; set; } = string.Empty;

[JsonPropertyName("refundAmount")]
public decimal RefundAmount { get; set; }

[JsonPropertyName("refundReference")]
public string RefundReference { get; set; } = string.Empty;

[JsonPropertyName("refundReason")]
public string RefundReason { get; set; } = string.Empty;

[JsonPropertyName("customerNote")]
public string CustomerNote { get; set; } = string.Empty;

/// <summary>Account to credit for the refund. Defaults to the original payment source when omitted.</summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("destinationAccountNumber")]
public string? DestinationAccountNumber { get; set; }

[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("destinationAccountBankCode")]
public string? DestinationAccountBankCode { get; set; }
}
62 changes: 62 additions & 0 deletions src/Monnify/Collections/Models/Refunds/Refund.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using System.Text.Json.Serialization;

namespace Monnify.Collections;

public sealed class Refund
{
[JsonPropertyName("refundReference")]
public string RefundReference { get; set; } = string.Empty;

[JsonPropertyName("transactionReference")]
public string TransactionReference { get; set; } = string.Empty;

[JsonPropertyName("refundReason")]
public string RefundReason { get; set; } = string.Empty;

[JsonPropertyName("customerNote")]
public string CustomerNote { get; set; } = string.Empty;

[JsonPropertyName("refundAmount")]
public decimal RefundAmount { get; set; }

/// <summary>E.g. <c>PARTIAL_REFUND</c> or <c>FULL_REFUND</c>.</summary>
[JsonPropertyName("refundType")]
public string RefundType { get; set; } = string.Empty;

/// <summary>E.g. <c>PENDING</c>, <c>COMPLETED</c>, <c>FAILED</c>.</summary>
[JsonPropertyName("refundStatus")]
public string RefundStatus { get; set; } = string.Empty;

/// <summary>E.g. <c>MERCHANT_WALLET</c> or <c>ORIGINAL_PAYMENT_SOURCE</c>.</summary>
[JsonPropertyName("refundStrategy")]
public string RefundStrategy { get; set; } = string.Empty;

[JsonPropertyName("comment")]
public string? Comment { get; set; }

[JsonPropertyName("createdOn")]
public string CreatedOn { get; set; } = string.Empty;

/// <summary>Monnify's internal transfer reference (e.g. <c>TRFD|...</c>). Not in our published docs sample but returned by the real API.</summary>
[JsonPropertyName("reference")]
public string? Reference { get; set; }

/// <summary>Populated once the refund has settled; empty string when still pending.</summary>
[JsonPropertyName("completedOn")]
public string? CompletedOn { get; set; }

[JsonPropertyName("destinationAccountNumber")]
public string? DestinationAccountNumber { get; set; }

[JsonPropertyName("destinationAccountName")]
public string? DestinationAccountName { get; set; }

[JsonPropertyName("destinationAccountBankCode")]
public string? DestinationAccountBankCode { get; set; }

[JsonPropertyName("destinationBankName")]
public string? DestinationBankName { get; set; }

[JsonPropertyName("currencyCode")]
public string? CurrencyCode { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System.Text.Json.Serialization;

namespace Monnify.Collections;

public sealed class CreateSubAccountRequest
{
[JsonPropertyName("currencyCode")]
public string CurrencyCode { get; set; } = "NGN";

[JsonPropertyName("accountNumber")]
public string AccountNumber { get; set; } = string.Empty;

[JsonPropertyName("bankCode")]
public string BankCode { get; set; } = string.Empty;

[JsonPropertyName("email")]
public string Email { get; set; } = string.Empty;

[JsonPropertyName("defaultSplitPercentage")]
public decimal DefaultSplitPercentage { get; set; }
}
36 changes: 36 additions & 0 deletions src/Monnify/Collections/Models/SubAccounts/SubAccount.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System.Text.Json.Serialization;

namespace Monnify.Collections;

public sealed class SubAccount
{
[JsonPropertyName("subAccountCode")]
public string SubAccountCode { get; set; } = string.Empty;

[JsonPropertyName("accountNumber")]
public string AccountNumber { get; set; } = string.Empty;

[JsonPropertyName("accountName")]
public string AccountName { get; set; } = string.Empty;

[JsonPropertyName("currencyCode")]
public string CurrencyCode { get; set; } = string.Empty;

[JsonPropertyName("email")]
public string Email { get; set; } = string.Empty;

[JsonPropertyName("bankCode")]
public string BankCode { get; set; } = string.Empty;

[JsonPropertyName("bankName")]
public string BankName { get; set; } = string.Empty;

[JsonPropertyName("defaultSplitPercentage")]
public decimal DefaultSplitPercentage { get; set; }

[JsonPropertyName("settlementProfileCode")]
public string? SettlementProfileCode { get; set; }

[JsonPropertyName("settlementReportEmails")]
public IReadOnlyList<string>? SettlementReportEmails { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Text.Json.Serialization;

namespace Monnify.Collections;

public sealed class UpdateSubAccountRequest
{
[JsonPropertyName("subAccountCode")]
public string SubAccountCode { get; set; } = string.Empty;

[JsonPropertyName("currencyCode")]
public string CurrencyCode { get; set; } = "NGN";

[JsonPropertyName("accountNumber")]
public string AccountNumber { get; set; } = string.Empty;

[JsonPropertyName("bankCode")]
public string BankCode { get; set; } = string.Empty;

[JsonPropertyName("email")]
public string Email { get; set; } = string.Empty;

[JsonPropertyName("defaultSplitPercentage")]
public decimal DefaultSplitPercentage { get; set; }
}
Loading
Loading