From c0cc72a88f5bbaf313e13567c67df111ac996831 Mon Sep 17 00:00:00 2001 From: Gabriel Odero Date: Sat, 15 Jun 2024 00:11:25 +0300 Subject: [PATCH] Added B2CAccountTopUp api --- MpesaSdk/Callbacks/B2CAccountTopUpCallback.cs | 6 ++ MpesaSdk/Callbacks/BaseCallback.cs | 4 +- MpesaSdk/Dtos/B2CAccountTopUpRequest.cs | 60 +++++++++++++++ MpesaSdk/Interfaces/IMpesaClient.cs | 18 +++++ MpesaSdk/MpesaClient.cs | 30 ++++++-- MpesaSdk/MpesaRequestEndpoint.cs | 5 +- MpesaSdk/MpesaSdk.csproj | 10 +-- MpesaSdk/Transaction_Type.cs | 5 ++ .../Validators/B2CAccountTopUpValidator.cs | 77 +++++++++++++++++++ 9 files changed, 201 insertions(+), 14 deletions(-) create mode 100644 MpesaSdk/Callbacks/B2CAccountTopUpCallback.cs create mode 100644 MpesaSdk/Dtos/B2CAccountTopUpRequest.cs create mode 100644 MpesaSdk/Validators/B2CAccountTopUpValidator.cs diff --git a/MpesaSdk/Callbacks/B2CAccountTopUpCallback.cs b/MpesaSdk/Callbacks/B2CAccountTopUpCallback.cs new file mode 100644 index 0000000..3503a15 --- /dev/null +++ b/MpesaSdk/Callbacks/B2CAccountTopUpCallback.cs @@ -0,0 +1,6 @@ +namespace MpesaSdk.Callbacks +{ + public class B2CAccountTopUpCallback : BaseCallback + { + } +} diff --git a/MpesaSdk/Callbacks/BaseCallback.cs b/MpesaSdk/Callbacks/BaseCallback.cs index 6cad476..98b1d1d 100644 --- a/MpesaSdk/Callbacks/BaseCallback.cs +++ b/MpesaSdk/Callbacks/BaseCallback.cs @@ -30,9 +30,9 @@ public class Result public string TransactionId { get; set; } [JsonProperty("ResultParameters")] - public BaseResultCallbackMetadata TaxRemittanceCallbackMetadata { get; set; } + public BaseResultCallbackMetadata ResultCallbackMetadata { get; set; } [JsonProperty("ReferenceData")] - public BaseReferenceCallbackMetadata TaxReferenceCallbackMetadata { get; set; } + public BaseReferenceCallbackMetadata ReferenceCallbackMetadata { get; set; } } } diff --git a/MpesaSdk/Dtos/B2CAccountTopUpRequest.cs b/MpesaSdk/Dtos/B2CAccountTopUpRequest.cs new file mode 100644 index 0000000..1a81176 --- /dev/null +++ b/MpesaSdk/Dtos/B2CAccountTopUpRequest.cs @@ -0,0 +1,60 @@ +using Newtonsoft.Json; + +namespace MpesaSdk.Dtos +{ + public class B2CAccountTopUpRequest + { + [JsonProperty("Initiator")] + public string Initiator { get; private set; } + + [JsonProperty("SecurityCredential")] + public string SecurityCredential { get; private set; } + + [JsonProperty("CommandID")] + public string CommandId { get; private set; } = Transaction_Type.BusinessPayToBulk; + + [JsonProperty("SenderIdentifierType")] + public string SenderIdentifierType { get; private set; } = "4"; + + [JsonProperty("RecieverIdentifierType")] + public string RecieverIdentifierType { get; private set; } = "4"; + + [JsonProperty("Amount")] + public string Amount { get; private set; } + + [JsonProperty("PartyA")] + public string PartyA { get; private set; } + + [JsonProperty("PartyB")] + public string PartyB { get; private set; } + + [JsonProperty("AccountReference")] + public string AccountReference { get; private set; } + + [JsonProperty("Requester")] + public string Requester { get; private set; } + + [JsonProperty("Remarks")] + public string Remarks { get; private set; } + + [JsonProperty("QueueTimeOutURL")] + public string QueueTimeOutUrl { get; private set; } + + [JsonProperty("ResultURL")] + public string ResultUrl { get; private set; } + + public B2CAccountTopUpRequest(string initiator, string securityCredential, string amount, string partyA, string partyB, string accountReference, string requester, string remarks, string queueTimeOutUrl, string resultUrl) + { + Initiator = initiator; + SecurityCredential = securityCredential; + Amount = amount; + PartyA = partyA; + PartyB = partyB; + AccountReference = accountReference; + Requester = requester; + Remarks = remarks; + QueueTimeOutUrl = queueTimeOutUrl; + ResultUrl = resultUrl; + } + } +} diff --git a/MpesaSdk/Interfaces/IMpesaClient.cs b/MpesaSdk/Interfaces/IMpesaClient.cs index e5ffceb..42fdead 100644 --- a/MpesaSdk/Interfaces/IMpesaClient.cs +++ b/MpesaSdk/Interfaces/IMpesaClient.cs @@ -460,5 +460,23 @@ public interface IMpesaClient /// /// B2BExpressCheckoutResponse B2BExpressCheckout(B2BExpressCheckoutRequest b2BExpressCheckoutRequest, string accesstoken, CancellationToken cancellationToken = default); + + /// + /// This API enables you to load funds to a B2C shortcode directly for disbursement. The transaction moves money from your MMF/Working account to the recipient’s utility account. + /// + /// + /// + /// + /// + Task B2CAccountTopUpAsync(B2CAccountTopUpRequest b2CAccountTopUpRequest, string accesstoken, CancellationToken cancellationToken = default); + + /// + /// This API enables you to load funds to a B2C shortcode directly for disbursement. The transaction moves money from your MMF/Working account to the recipient’s utility account. + /// + /// + /// + /// + /// + MpesaResponse B2CAccountTopUp(B2CAccountTopUpRequest b2CAccountTopUpRequest, string accesstoken, CancellationToken cancellationToken = default); } } diff --git a/MpesaSdk/MpesaClient.cs b/MpesaSdk/MpesaClient.cs index fce4a06..5cfadff 100644 --- a/MpesaSdk/MpesaClient.cs +++ b/MpesaSdk/MpesaClient.cs @@ -1,5 +1,4 @@ -using FluentValidation; -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using MpesaSdk.Dtos; using MpesaSdk.Exceptions; using MpesaSdk.Interfaces; @@ -21,10 +20,10 @@ namespace MpesaSdk { - /// - /// Mpesa client class provides all the implemented interface methods that make the different API calls to MPESA Server - /// - public class MpesaClient : IMpesaClient + /// + /// Mpesa client class provides all the implemented interface methods that make the different API calls to MPESA Server + /// + public class MpesaClient : IMpesaClient { private readonly HttpClient _client; readonly Random jitterer = new Random(); @@ -110,6 +109,25 @@ public B2BExpressCheckoutResponse B2BExpressCheckout(B2BExpressCheckoutRequest b : MpesaPostRequestAsync(b2BExpressCheckoutRequest, accesstoken, MpesaRequestEndpoint.B2BExpressCheckout, cancellationToken).GetAwaiter().GetResult(); } + public async Task B2CAccountTopUpAsync(B2CAccountTopUpRequest b2CAccountTopUpRequest, string accesstoken, CancellationToken cancellationToken = default) + { + var validator = new B2CAccountTopUpValidator(); + var results = await validator.ValidateAsync(b2CAccountTopUpRequest, cancellationToken); + + return !results.IsValid + ? throw new MpesaAPIException(HttpStatusCode.BadRequest, string.Join(Environment.NewLine, results.Errors.Select(x => x.ErrorMessage.ToString()))) + : await MpesaPostRequestAsync(b2CAccountTopUpRequest, accesstoken, MpesaRequestEndpoint.B2CAccountTopUp, cancellationToken); + } + + public MpesaResponse B2CAccountTopUp(B2CAccountTopUpRequest b2CAccountTopUpRequest, string accesstoken, CancellationToken cancellationToken = default) + { + var validator = new B2CAccountTopUpValidator(); + var results = validator.Validate(b2CAccountTopUpRequest); + + return !results.IsValid + ? throw new MpesaAPIException(HttpStatusCode.BadRequest, string.Join(Environment.NewLine, results.Errors.Select(x => x.ErrorMessage.ToString()))) + : MpesaPostRequestAsync(b2CAccountTopUpRequest, accesstoken, MpesaRequestEndpoint.B2CAccountTopUp, cancellationToken).GetAwaiter().GetResult(); + } public async Task BillManagerOnboardingAsync(BillManagerOnboardingRequest billManagerOnboardingRequest, string accesstoken, CancellationToken cancellationToken = default) { diff --git a/MpesaSdk/MpesaRequestEndpoint.cs b/MpesaSdk/MpesaRequestEndpoint.cs index dbe2ee2..4da230c 100644 --- a/MpesaSdk/MpesaRequestEndpoint.cs +++ b/MpesaSdk/MpesaRequestEndpoint.cs @@ -113,5 +113,8 @@ public static class MpesaRequestEndpoint public static string BusinessManagerSingleInvoicingUpdate { get; set; } = "v1/billmanager-invoice/update/single-invoicing"; public static string B2BExpressCheckout { get; set; } = "v1/ussdpush/get-msisdn"; - } + + public static string B2CAccountTopUp { get; set; } = "mpesa/b2b/v1/paymentrequest"; + + } } diff --git a/MpesaSdk/MpesaSdk.csproj b/MpesaSdk/MpesaSdk.csproj index 33e505d..cdd1070 100644 --- a/MpesaSdk/MpesaSdk.csproj +++ b/MpesaSdk/MpesaSdk.csproj @@ -14,29 +14,29 @@ latest - 1.0.24 + 1.0.25 - + - + - + - + diff --git a/MpesaSdk/Transaction_Type.cs b/MpesaSdk/Transaction_Type.cs index 8763497..e5b298a 100644 --- a/MpesaSdk/Transaction_Type.cs +++ b/MpesaSdk/Transaction_Type.cs @@ -99,5 +99,10 @@ public static class Transaction_Type /// PayTaxToKRA Command ID /// public const string PayTaxToKRA = "PayTaxToKRA"; + + /// + /// BusinessPayToBulk Command ID + /// + public const string BusinessPayToBulk = "BusinessPayToBulk"; } } diff --git a/MpesaSdk/Validators/B2CAccountTopUpValidator.cs b/MpesaSdk/Validators/B2CAccountTopUpValidator.cs new file mode 100644 index 0000000..b30315b --- /dev/null +++ b/MpesaSdk/Validators/B2CAccountTopUpValidator.cs @@ -0,0 +1,77 @@ +using FluentValidation; +using MpesaSdk.Dtos; +using System; + +namespace MpesaSdk.Validators +{ + public class B2CAccountTopUpValidator : AbstractValidator + { + public B2CAccountTopUpValidator() + { + RuleFor(x => x.Initiator) + .NotNull() + .WithMessage("{PropertyName} - The name of the initiator is required.") + .NotEmpty() + .WithMessage("{PropertyName} - The name of the intiator should not be empty."); + + RuleFor(x => x.SecurityCredential) + .NotNull() + .WithMessage("{PropertyName} - The encrypted credential is required.") + .NotEmpty() + .WithMessage("{PropertyName} - The encrypted credential should not be empty."); + + RuleFor(x => x.QueueTimeOutUrl) + .NotNull() + .WithMessage("{PropertyName} - The queuetimeout url is required.") + .Must(x => LinkMustBeAUri(x)) + .WithMessage("{PropertyName} - The queuetimeout url should be a valid secure url."); + + RuleFor(x => x.Amount) + .NotNull() + .WithMessage("{PropertyName} - Amount is required.") + .NotEmpty() + .WithMessage("{PropertyName} - Amount must not be empty") + .Must(x => int.TryParse(x, out int value)) + .WithMessage("{PropertyName} - The amount should be in numeric value."); + + RuleFor(x => x.PartyA) + .NotNull() + .WithMessage("{PropertyName} - The shortcode is required.") + .Length(5, 7) + .WithMessage("{PropertyName} - The shortcode should be between 5 and 7 digit."); + + RuleFor(x => x.PartyB) + .NotNull() + .WithMessage("{PropertyName} - The account number is required.") + .Must(x => int.TryParse(x, out int value)) + .WithMessage("{PropertyName} - The account must be a numeric value."); + + RuleFor(x => x.ResultUrl) + .NotNull() + .WithMessage("{PropertyName} - The result url is required.") + .Must(x => LinkMustBeAUri(x)) + .WithMessage("{PropertyName} - The result url should be a valid secure url."); + + RuleFor(x => x.AccountReference) + .NotNull() + .WithMessage("{PropertyName} - The account reference should not be empty.") + .MaximumLength(12) + .WithMessage("{PropertyName} - The account reference should not be more than 12 characters."); + + RuleFor(x => x.Remarks) + .NotNull() + .WithMessage("{PropertyName} - The remarks should not be empty.") + .MaximumLength(100) + .WithMessage("{PropertyName} - The remarks should not be more than 100 characters."); + } + + private static bool LinkMustBeAUri(string link) + { + if (!Uri.IsWellFormedUriString(link, UriKind.Absolute)) + { + return false; + } + return true; + } + } +}