From 4648346d8351a36875599159512363afa4f7914f Mon Sep 17 00:00:00 2001 From: Gabriel Odero Date: Sun, 6 Oct 2024 22:35:52 +0300 Subject: [PATCH] Added Mpesa Ratiba api functions --- MpesaSdk/Callbacks/StandingOrderCallback.cs | 43 ++++++++ MpesaSdk/Dtos/StandingOrderRequest.cs | 76 +++++++++++++ MpesaSdk/Enums/FrequencyTypes.cs | 14 +++ MpesaSdk/Interfaces/IMpesaClient.cs | 18 ++++ MpesaSdk/MpesaClient.cs | 34 ++++++ MpesaSdk/MpesaRequestEndpoint.cs | 2 + MpesaSdk/Response/StandingOrderResponse.cs | 37 +++++++ MpesaSdk/Transaction_Type.cs | 12 ++- MpesaSdk/Validators/StandingOrderValidator.cs | 101 ++++++++++++++++++ 9 files changed, 336 insertions(+), 1 deletion(-) create mode 100644 MpesaSdk/Callbacks/StandingOrderCallback.cs create mode 100644 MpesaSdk/Dtos/StandingOrderRequest.cs create mode 100644 MpesaSdk/Enums/FrequencyTypes.cs create mode 100644 MpesaSdk/Response/StandingOrderResponse.cs create mode 100644 MpesaSdk/Validators/StandingOrderValidator.cs diff --git a/MpesaSdk/Callbacks/StandingOrderCallback.cs b/MpesaSdk/Callbacks/StandingOrderCallback.cs new file mode 100644 index 0000000..c7e674c --- /dev/null +++ b/MpesaSdk/Callbacks/StandingOrderCallback.cs @@ -0,0 +1,43 @@ +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace MpesaSdk.Callbacks +{ + public class StandingOrderCallback + { + [JsonProperty("responseHeader")] + public ResponseHeader ResponseHeader { get; set; } + + [JsonProperty("ResponseBody")] + public ResponseBody ResponseBody { get; set; } + } + + public class ResponseHeader + { + [JsonProperty("responseRefID")] + public string ResponseRefId { get; set; } + + [JsonProperty("requestRefID")] + public string RequestRefId { get; set; } + + [JsonProperty("responseCode")] + public long ResponseCode { get; set; } + + [JsonProperty("responseDescription")] + public string ResponseDescription { get; set; } + } + + public class ResponseBody + { + [JsonProperty("responseData")] + public List ResponseData { get; set; } + } + + public class StandingOrderCallbackMetadataItem + { + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("value")] + public string Value { get; set; } + } +} \ No newline at end of file diff --git a/MpesaSdk/Dtos/StandingOrderRequest.cs b/MpesaSdk/Dtos/StandingOrderRequest.cs new file mode 100644 index 0000000..c94eab0 --- /dev/null +++ b/MpesaSdk/Dtos/StandingOrderRequest.cs @@ -0,0 +1,76 @@ +using MpesaSdk.Enums; +using Newtonsoft.Json; +using System; + +namespace MpesaSdk.Dtos +{ + public class StandingOrderRequest + { + [JsonProperty("StandingOrderName")] + public string StandingOrderName { get; set; } + + [JsonProperty("StartDate")] + public string StartDate { get; set; } + + [JsonProperty("EndDate")] + public string EndDate { get; set; } + + [JsonProperty("BusinessShortCode")] + public string BusinessShortCode { get; set; } + + [JsonProperty("TransactionType")] + public string TransactionType { get; set; } + + [JsonProperty("ReceiverPartyIdentifierType")] + public string ReceiverPartyIdentifierType { get; set; } + + [JsonProperty("Amount")] + public string Amount { get; set; } + + [JsonProperty("PartyA")] + public string PartyA { get; set; } + + [JsonProperty("CallBackURL")] + public string CallBackUrl { get; set; } + + [JsonProperty("AccountReference")] + public string AccountReference { get; set; } + + [JsonProperty("TransactionDesc")] + public string TransactionDesc { get; set; } + + [JsonProperty("Frequency")] + public string Frequency { get; set; } + + /// + /// The Standing Order APIs enable teams to integrate with the standing order solution by initiating a request to create a standing order on the customer profile. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public StandingOrderRequest(string standingOrderName, DateTime startDate, DateTime endDate, string businessShortCode, string transactionType, IdentifierTypes receiverPartyIdentifierType, string amount, string partyA, string callBackUrl, string accountReference, string transactionDesc, FrequencyTypes frequency) + { + StandingOrderName = standingOrderName; + StartDate = startDate.ToString("yyyyMMdd"); + EndDate = endDate.ToString("yyyyMMdd"); + BusinessShortCode = businessShortCode; + TransactionType = transactionType; + ReceiverPartyIdentifierType = ((int)receiverPartyIdentifierType).ToString(); + Amount = amount; + PartyA = partyA; + CallBackUrl = callBackUrl; + AccountReference = accountReference; + TransactionDesc = transactionDesc; + Frequency = ((int)frequency).ToString(); + } + } +} diff --git a/MpesaSdk/Enums/FrequencyTypes.cs b/MpesaSdk/Enums/FrequencyTypes.cs new file mode 100644 index 0000000..c4a6a15 --- /dev/null +++ b/MpesaSdk/Enums/FrequencyTypes.cs @@ -0,0 +1,14 @@ +namespace MpesaSdk.Enums +{ + public enum FrequencyTypes + { + One_Off = 1, + Daily = 2, + Weekly = 3, + Monthly = 4, + Bi_Monthly = 5, + Quarterly = 6, + Half_Year = 7, + Yearly = 8 + } +} diff --git a/MpesaSdk/Interfaces/IMpesaClient.cs b/MpesaSdk/Interfaces/IMpesaClient.cs index 42fdead..390fb9b 100644 --- a/MpesaSdk/Interfaces/IMpesaClient.cs +++ b/MpesaSdk/Interfaces/IMpesaClient.cs @@ -478,5 +478,23 @@ public interface IMpesaClient /// /// MpesaResponse B2CAccountTopUp(B2CAccountTopUpRequest b2CAccountTopUpRequest, string accesstoken, CancellationToken cancellationToken = default); + + /// + /// This API is intended for businesses who wish to integrate with standing orders for the automation of recurring revenue collection. + /// + /// + /// + /// + /// + Task MpesaRatibaAsync(StandingOrderRequest standingOrderRequest, string accesstoken, CancellationToken cancellationToken = default); + + /// + /// This API is intended for businesses who wish to integrate with standing orders for the automation of recurring revenue collection. + /// + /// + /// + /// + /// + StandingOrderResponse MpesaRatiba(StandingOrderRequest standingOrderRequest, string accesstoken, CancellationToken cancellationToken = default); } } diff --git a/MpesaSdk/MpesaClient.cs b/MpesaSdk/MpesaClient.cs index 5cfadff..ba89a1a 100644 --- a/MpesaSdk/MpesaClient.cs +++ b/MpesaSdk/MpesaClient.cs @@ -482,6 +482,40 @@ public async Task MakeLipaNaMpesaOnlinePayment : await MpesaPostRequestAsync(lipaNaMpesaOnline, accesstoken, MpesaRequestEndpoint.LipaNaMpesaOnline, cancellationToken); } + /// + /// This API is intended for businesses who wish to integrate with standing orders for the automation of recurring revenue collection. + /// + /// + /// + /// + /// + public StandingOrderResponse MpesaRatiba(StandingOrderRequest standingOrderRequest, string accesstoken, CancellationToken cancellationToken = default) + { + var validator = new StandingOrderValidator(); + var results = validator.Validate(standingOrderRequest); + + return !results.IsValid + ? throw new MpesaAPIException(HttpStatusCode.BadRequest, string.Join(Environment.NewLine, results.Errors.Select(x => x.ErrorMessage.ToString()))) + : MpesaPostRequestAsync(standingOrderRequest, accesstoken, MpesaRequestEndpoint.MpesaRatiba, cancellationToken).GetAwaiter().GetResult(); + } + + /// + /// This API is intended for businesses who wish to integrate with standing orders for the automation of recurring revenue collection. + /// + /// + /// + /// + /// + public async Task MpesaRatibaAsync(StandingOrderRequest standingOrderRequest, string accesstoken, CancellationToken cancellationToken = default) + { + var validator = new StandingOrderValidator(); + var results = await validator.ValidateAsync(standingOrderRequest, cancellationToken); + + return !results.IsValid + ? throw new MpesaAPIException(HttpStatusCode.BadRequest, string.Join(Environment.NewLine, results.Errors.Select(x => x.ErrorMessage.ToString()))) + : await MpesaPostRequestAsync(standingOrderRequest, accesstoken, MpesaRequestEndpoint.MpesaRatiba, cancellationToken); + } + /// /// Queries MPESA Paybill Account balance. /// diff --git a/MpesaSdk/MpesaRequestEndpoint.cs b/MpesaSdk/MpesaRequestEndpoint.cs index 4da230c..5acbe52 100644 --- a/MpesaSdk/MpesaRequestEndpoint.cs +++ b/MpesaSdk/MpesaRequestEndpoint.cs @@ -116,5 +116,7 @@ public static class MpesaRequestEndpoint public static string B2CAccountTopUp { get; set; } = "mpesa/b2b/v1/paymentrequest"; + public static string MpesaRatiba { get; set; } = "standingorder/v1/createStandingOrderExternal"; + } } diff --git a/MpesaSdk/Response/StandingOrderResponse.cs b/MpesaSdk/Response/StandingOrderResponse.cs new file mode 100644 index 0000000..a519737 --- /dev/null +++ b/MpesaSdk/Response/StandingOrderResponse.cs @@ -0,0 +1,37 @@ +using Newtonsoft.Json; + +namespace MpesaSdk.Response +{ + public class StandingOrderResponse + { + [JsonProperty("ResponseHeader")] + public ResponseHeader ResponseHeader { get; set; } + + [JsonProperty("ResponseBody")] + public ResponseBody ResponseBody { get; set; } + } + + public class ResponseBody + { + [JsonProperty("responseDescription")] + public string ResponseDescription { get; set; } + + [JsonProperty("responseCode")] + public string ResponseCode { get; set; } + } + + public class ResponseHeader + { + [JsonProperty("responseRefID")] + public string ResponseRefId { get; set; } + + [JsonProperty("responseCode")] + public string ResponseCode { get; set; } + + [JsonProperty("responseDescription")] + public string ResponseDescription { get; set; } + + [JsonProperty("ResultDesc")] + public string ResultDesc { get; set; } + } +} diff --git a/MpesaSdk/Transaction_Type.cs b/MpesaSdk/Transaction_Type.cs index e5b298a..8a12ad5 100644 --- a/MpesaSdk/Transaction_Type.cs +++ b/MpesaSdk/Transaction_Type.cs @@ -104,5 +104,15 @@ public static class Transaction_Type /// BusinessPayToBulk Command ID /// public const string BusinessPayToBulk = "BusinessPayToBulk"; - } + + /// + /// Standing Order Transaction Type + /// + public const string StandingOrderPaybill = "Standing Order Customer Pay Bill"; + + /// + /// Standing Order Transaction Type + /// + public const string StandingOrderTillNumber = "Standing Order Customer Pay Merchant"; + } } diff --git a/MpesaSdk/Validators/StandingOrderValidator.cs b/MpesaSdk/Validators/StandingOrderValidator.cs new file mode 100644 index 0000000..18a794d --- /dev/null +++ b/MpesaSdk/Validators/StandingOrderValidator.cs @@ -0,0 +1,101 @@ +using FluentValidation; +using MpesaSdk.Dtos; +using System; + +namespace MpesaSdk.Validators +{ + public class StandingOrderValidator : AbstractValidator + { + public StandingOrderValidator() + { + RuleFor(x => x.StandingOrderName) + .NotNull() + .WithMessage("{PropertyName} - The Standing order name is required") + .NotEmpty() + .WithMessage("{PropertyName} - The Standing order name must not be empty"); + + RuleFor(x => x.StartDate) + .NotNull() + .WithMessage("{PropertyName} - The start date is required") + .NotEmpty() + .WithMessage("{PropertyName} - The start date must not be empty"); + + RuleFor(x => x.EndDate) + .NotNull() + .WithMessage("{PropertyName} - The end date is required") + .NotEmpty() + .WithMessage("{PropertyName} - The end date must not be empty"); + + RuleFor(x => x.TransactionType) + .NotNull() + .WithMessage("{PropertyName} - The Transaction type is required") + .NotEmpty() + .WithMessage("{PropertyName} - The Transaction type must not be empty"); + + RuleFor(x => x.BusinessShortCode) + .NotNull() + .WithMessage("{PropertyName} - The paybill or till number shortcode should not be empty.") + .Must(x => int.TryParse(x, out int value)) + .WithMessage("{PropertyName} - The paybill or till number must be a numeric value.") + .Length(5, 7) + .WithMessage("{PropertyName} - The paybill or till number should be 5 to 7 account number digits."); + + 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 mobile number is required.") + .SetValidator(new PhoneNumberValidator()) + .WithMessage("{PropertyName} - The mobile number should start with 2547XXXX.") + .MaximumLength(12) + .WithMessage("{PropertyName} - The mobile number should be 12 digit."); + + RuleFor(x => x.CallBackUrl) + .NotNull() + .WithMessage("{PropertyName} - The callback url is required.") + .Must(x => LinkMustBeAUri(x)) + .WithMessage("{PropertyName} - The callback 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.TransactionDesc) + .NotNull() + .WithMessage("{PropertyName} - The transaction description should not be empty.") + .MaximumLength(13) + .WithMessage("{PropertyName} - The transaction description should not be more than 13 characters."); + + RuleFor(x => x.Frequency) + .NotNull() + .WithMessage("{PropertyName} - The Frequency is required") + .NotEmpty() + .WithMessage("{PropertyName} - The Frequency must not be empty"); + + RuleFor(x => x.ReceiverPartyIdentifierType) + .NotNull() + .WithMessage("{PropertyName} - The Receiver party identifier type is required") + .NotEmpty() + .WithMessage("{PropertyName} - The Receiver party identifier type must not be empty") + .NotEqual("1") + .WithMessage("{PropertyName} - The Receiver party does not support MSISDN"); + } + + private static bool LinkMustBeAUri(string link) + { + if (!Uri.IsWellFormedUriString(link, UriKind.Absolute)) + { + return false; + } + return true; + } + } +}