Skip to content

Commit

Permalink
Added Mpesa Ratiba api functions
Browse files Browse the repository at this point in the history
  • Loading branch information
gabrieldwight committed Oct 6, 2024
1 parent a9d27bc commit 4648346
Show file tree
Hide file tree
Showing 9 changed files with 336 additions and 1 deletion.
43 changes: 43 additions & 0 deletions MpesaSdk/Callbacks/StandingOrderCallback.cs
Original file line number Diff line number Diff line change
@@ -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<StandingOrderCallbackMetadataItem> ResponseData { get; set; }
}

public class StandingOrderCallbackMetadataItem
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("value")]
public string Value { get; set; }
}
}
76 changes: 76 additions & 0 deletions MpesaSdk/Dtos/StandingOrderRequest.cs
Original file line number Diff line number Diff line change
@@ -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; }

/// <summary>
/// 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.
/// </summary>
/// <param name="standingOrderName"></param>
/// <param name="startDate"></param>
/// <param name="endDate"></param>
/// <param name="businessShortCode"></param>
/// <param name="transactionType"></param>
/// <param name="receiverPartyIdentifierType"></param>
/// <param name="amount"></param>
/// <param name="partyA"></param>
/// <param name="callBackUrl"></param>
/// <param name="accountReference"></param>
/// <param name="transactionDesc"></param>
/// <param name="frequency"></param>
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();
}
}
}
14 changes: 14 additions & 0 deletions MpesaSdk/Enums/FrequencyTypes.cs
Original file line number Diff line number Diff line change
@@ -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
}
}
18 changes: 18 additions & 0 deletions MpesaSdk/Interfaces/IMpesaClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -478,5 +478,23 @@ public interface IMpesaClient
/// <param name="cancellationToken"></param>
/// <returns></returns>
MpesaResponse B2CAccountTopUp(B2CAccountTopUpRequest b2CAccountTopUpRequest, string accesstoken, CancellationToken cancellationToken = default);

/// <summary>
/// This API is intended for businesses who wish to integrate with standing orders for the automation of recurring revenue collection.
/// </summary>
/// <param name="standingOrderRequest"></param>
/// <param name="accesstoken"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<StandingOrderResponse> MpesaRatibaAsync(StandingOrderRequest standingOrderRequest, string accesstoken, CancellationToken cancellationToken = default);

/// <summary>
/// This API is intended for businesses who wish to integrate with standing orders for the automation of recurring revenue collection.
/// </summary>
/// <param name="standingOrderRequest"></param>
/// <param name="accesstoken"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
StandingOrderResponse MpesaRatiba(StandingOrderRequest standingOrderRequest, string accesstoken, CancellationToken cancellationToken = default);
}
}
34 changes: 34 additions & 0 deletions MpesaSdk/MpesaClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,40 @@ public async Task<LipaNaMpesaOnlinePushStkResponse> MakeLipaNaMpesaOnlinePayment
: await MpesaPostRequestAsync<LipaNaMpesaOnlinePushStkResponse>(lipaNaMpesaOnline, accesstoken, MpesaRequestEndpoint.LipaNaMpesaOnline, cancellationToken);
}

/// <summary>
/// This API is intended for businesses who wish to integrate with standing orders for the automation of recurring revenue collection.
/// </summary>
/// <param name="standingOrderRequest"></param>
/// <param name="accesstoken"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
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<StandingOrderResponse>(standingOrderRequest, accesstoken, MpesaRequestEndpoint.MpesaRatiba, cancellationToken).GetAwaiter().GetResult();
}

/// <summary>
/// This API is intended for businesses who wish to integrate with standing orders for the automation of recurring revenue collection.
/// </summary>
/// <param name="standingOrderRequest"></param>
/// <param name="accesstoken"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<StandingOrderResponse> 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<StandingOrderResponse>(standingOrderRequest, accesstoken, MpesaRequestEndpoint.MpesaRatiba, cancellationToken);
}

/// <summary>
/// Queries MPESA Paybill Account balance.
/// </summary>
Expand Down
2 changes: 2 additions & 0 deletions MpesaSdk/MpesaRequestEndpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";

}
}
37 changes: 37 additions & 0 deletions MpesaSdk/Response/StandingOrderResponse.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
}
12 changes: 11 additions & 1 deletion MpesaSdk/Transaction_Type.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,5 +104,15 @@ public static class Transaction_Type
/// BusinessPayToBulk Command ID
/// </summary>
public const string BusinessPayToBulk = "BusinessPayToBulk";
}

/// <summary>
/// Standing Order Transaction Type
/// </summary>
public const string StandingOrderPaybill = "Standing Order Customer Pay Bill";

/// <summary>
/// Standing Order Transaction Type
/// </summary>
public const string StandingOrderTillNumber = "Standing Order Customer Pay Merchant";
}
}
101 changes: 101 additions & 0 deletions MpesaSdk/Validators/StandingOrderValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
using FluentValidation;
using MpesaSdk.Dtos;
using System;

namespace MpesaSdk.Validators
{
public class StandingOrderValidator : AbstractValidator<StandingOrderRequest>
{
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<StandingOrderRequest, string>())
.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;
}
}
}

0 comments on commit 4648346

Please sign in to comment.