From 7226da2fd8b20a981a8cec414d29ea02f7b19e42 Mon Sep 17 00:00:00 2001 From: Gabriel Odero Date: Sat, 18 Nov 2023 01:34:38 +0300 Subject: [PATCH] B2B Express checkout added --- .../Callbacks/B2BExpressCheckoutCallback.cs | 34 +++++++++ MpesaSdk/Dtos/B2BExpressCheckoutRequest.cs | 39 +++++++++++ MpesaSdk/Interfaces/IMpesaClient.cs | 18 +++++ MpesaSdk/MpesaClient.cs | 23 ++++++- MpesaSdk/MpesaRequestEndpoint.cs | 4 +- .../Response/B2BExpressCheckoutResponse.cs | 13 ++++ .../Validators/B2BExpressCheckoutValidator.cs | 69 +++++++++++++++++++ 7 files changed, 198 insertions(+), 2 deletions(-) create mode 100644 MpesaSdk/Callbacks/B2BExpressCheckoutCallback.cs create mode 100644 MpesaSdk/Dtos/B2BExpressCheckoutRequest.cs create mode 100644 MpesaSdk/Response/B2BExpressCheckoutResponse.cs create mode 100644 MpesaSdk/Validators/B2BExpressCheckoutValidator.cs diff --git a/MpesaSdk/Callbacks/B2BExpressCheckoutCallback.cs b/MpesaSdk/Callbacks/B2BExpressCheckoutCallback.cs new file mode 100644 index 0000000..acc8c71 --- /dev/null +++ b/MpesaSdk/Callbacks/B2BExpressCheckoutCallback.cs @@ -0,0 +1,34 @@ +using Newtonsoft.Json; + +namespace MpesaSdk.Callbacks +{ + public class B2BExpressCheckoutCallback + { + [JsonProperty("resultCode")] + public string ResultCode { get; set; } + + [JsonProperty("resultDesc")] + public string ResultDesc { get; set; } + + [JsonProperty("amount")] + public string Amount { get; set; } + + [JsonProperty("requestId")] + public string RequestId { get; set; } + + [JsonProperty("resultType")] + public string ResultType { get; set; } + + [JsonProperty("conversationID")] + public string ConversationId { get; set; } + + [JsonProperty("transactionId")] + public string TransactionId { get; set; } + + [JsonProperty("status")] + public string Status { get; set; } + + [JsonProperty("paymentReference")] + public string PaymentReference { get; set; } + } +} diff --git a/MpesaSdk/Dtos/B2BExpressCheckoutRequest.cs b/MpesaSdk/Dtos/B2BExpressCheckoutRequest.cs new file mode 100644 index 0000000..a466895 --- /dev/null +++ b/MpesaSdk/Dtos/B2BExpressCheckoutRequest.cs @@ -0,0 +1,39 @@ +using Newtonsoft.Json; + +namespace MpesaSdk.Dtos +{ + public class B2BExpressCheckoutRequest + { + [JsonProperty("primaryShortCode")] + public string PrimaryShortCode { get; set; } + + [JsonProperty("receiverShortCode")] + public string ReceiverShortCode { get; set; } + + [JsonProperty("amount")] + public string Amount { get; set; } + + [JsonProperty("paymentRef")] + public string PaymentRef { get; set; } + + [JsonProperty("callbackUrl")] + public string CallbackUrl { get; set; } + + [JsonProperty("partnerName")] + public string PartnerName { get; set; } + + [JsonProperty("RequestRefID")] + public string RequestRefId { get; set; } + + public B2BExpressCheckoutRequest(string primaryShortCode, string receiverShortCode, string amount, string paymentRef, string callbackUrl, string partnerName, string requestRefId) + { + PrimaryShortCode = primaryShortCode; + ReceiverShortCode = receiverShortCode; + Amount = amount; + PaymentRef = paymentRef; + CallbackUrl = callbackUrl; + PartnerName = partnerName; + RequestRefId = requestRefId; + } + } +} diff --git a/MpesaSdk/Interfaces/IMpesaClient.cs b/MpesaSdk/Interfaces/IMpesaClient.cs index 6065c86..e5ffceb 100644 --- a/MpesaSdk/Interfaces/IMpesaClient.cs +++ b/MpesaSdk/Interfaces/IMpesaClient.cs @@ -442,5 +442,23 @@ public interface IMpesaClient /// /// BillManagerResponse BillManagerUpdateInvoice(BillManagerInvoicingRequest billManagerInvoicingRequest, string accesstoken, CancellationToken cancellationToken = default); + + /// + /// B2B(UssdPush to Till) is a product for enabling merchants to initiate USSD Push to Till enabling their fellow merchants to pay from their owned till numbers to the vendor's paybill. + /// + /// + /// + /// + /// + Task B2BExpressCheckoutAsync(B2BExpressCheckoutRequest b2BExpressCheckoutRequest, string accesstoken, CancellationToken cancellationToken = default); + + /// + /// B2B(UssdPush to Till) is a product for enabling merchants to initiate USSD Push to Till enabling their fellow merchants to pay from their owned till numbers to the vendor's paybill. + /// + /// + /// + /// + /// + B2BExpressCheckoutResponse B2BExpressCheckout(B2BExpressCheckoutRequest b2BExpressCheckoutRequest, string accesstoken, CancellationToken cancellationToken = default); } } diff --git a/MpesaSdk/MpesaClient.cs b/MpesaSdk/MpesaClient.cs index 1fa2462..fce4a06 100644 --- a/MpesaSdk/MpesaClient.cs +++ b/MpesaSdk/MpesaClient.cs @@ -90,7 +90,28 @@ public MpesaClient(HttpClient client) _client = client; } - public async Task BillManagerOnboardingAsync(BillManagerOnboardingRequest billManagerOnboardingRequest, string accesstoken, CancellationToken cancellationToken = default) + public async Task B2BExpressCheckoutAsync(B2BExpressCheckoutRequest b2BExpressCheckoutRequest, string accesstoken, CancellationToken cancellationToken = default) + { + var validator = new B2BExpressCheckoutValidator(); + var results = await validator.ValidateAsync(b2BExpressCheckoutRequest, cancellationToken); + + return !results.IsValid + ? throw new MpesaAPIException(HttpStatusCode.BadRequest, string.Join(Environment.NewLine, results.Errors.Select(x => x.ErrorMessage.ToString()))) + : await MpesaPostRequestAsync(b2BExpressCheckoutRequest, accesstoken, MpesaRequestEndpoint.B2BExpressCheckout, cancellationToken); + } + + public B2BExpressCheckoutResponse B2BExpressCheckout(B2BExpressCheckoutRequest b2BExpressCheckoutRequest, string accesstoken, CancellationToken cancellationToken = default) + { + var validator = new B2BExpressCheckoutValidator(); + var results = validator.Validate(b2BExpressCheckoutRequest); + + return !results.IsValid + ? throw new MpesaAPIException(HttpStatusCode.BadRequest, string.Join(Environment.NewLine, results.Errors.Select(x => x.ErrorMessage.ToString()))) + : MpesaPostRequestAsync(b2BExpressCheckoutRequest, accesstoken, MpesaRequestEndpoint.B2BExpressCheckout, cancellationToken).GetAwaiter().GetResult(); + } + + + public async Task BillManagerOnboardingAsync(BillManagerOnboardingRequest billManagerOnboardingRequest, string accesstoken, CancellationToken cancellationToken = default) { var validator = new BillManagerOnboardingValidator(); var results = await validator.ValidateAsync(billManagerOnboardingRequest, cancellationToken); diff --git a/MpesaSdk/MpesaRequestEndpoint.cs b/MpesaSdk/MpesaRequestEndpoint.cs index 97bb36d..dbe2ee2 100644 --- a/MpesaSdk/MpesaRequestEndpoint.cs +++ b/MpesaSdk/MpesaRequestEndpoint.cs @@ -110,6 +110,8 @@ public static class MpesaRequestEndpoint /// /// Update invoice API allows you to alter invoice items by using the external reference previously used to create the invoice you want to update. Any other update on the invoice can be done by using the Cancel Invoice API which will recall the invoice, then a new invoice can be created. /// - public static string BusinessManagerSingleInvoicingUpdate { get; set; } = "/v1/billmanager-invoice/update/single-invoicing"; + public static string BusinessManagerSingleInvoicingUpdate { get; set; } = "v1/billmanager-invoice/update/single-invoicing"; + + public static string B2BExpressCheckout { get; set; } = "v1/ussdpush/get-msisdn"; } } diff --git a/MpesaSdk/Response/B2BExpressCheckoutResponse.cs b/MpesaSdk/Response/B2BExpressCheckoutResponse.cs new file mode 100644 index 0000000..41a1f0c --- /dev/null +++ b/MpesaSdk/Response/B2BExpressCheckoutResponse.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace MpesaSdk.Response +{ + public class B2BExpressCheckoutResponse + { + [JsonProperty("code")] + public string Code { get; set; } + + [JsonProperty("status")] + public string Status { get; set; } + } +} diff --git a/MpesaSdk/Validators/B2BExpressCheckoutValidator.cs b/MpesaSdk/Validators/B2BExpressCheckoutValidator.cs new file mode 100644 index 0000000..37b8e29 --- /dev/null +++ b/MpesaSdk/Validators/B2BExpressCheckoutValidator.cs @@ -0,0 +1,69 @@ +using FluentValidation; +using MpesaSdk.Dtos; +using System; + +namespace MpesaSdk.Validators +{ + public class B2BExpressCheckoutValidator : AbstractValidator + { + public B2BExpressCheckoutValidator() + { + RuleFor(x => x.PrimaryShortCode) + .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.ReceiverShortCode) + .NotNull() + .WithMessage("{PropertyName} - The receiver paybill or till number shortcode should not be empty.") + .Must(x => int.TryParse(x, out int value)) + .WithMessage("{PropertyName} - The receiver paybill or till number must be a numeric value.") + .Length(5, 7) + .WithMessage("{PropertyName} - The receiver 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.PaymentRef) + .NotNull() + .WithMessage("{PropertyName} - The payment reference should not be empty.") + .MaximumLength(12) + .WithMessage("{PropertyName} - The payment reference should not be more than 12 characters."); + + 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.PartnerName) + .NotNull() + .WithMessage("{PropertyName} - The partner name is required.") + .NotEmpty() + .WithMessage("{PropertyName} - This partner name should not be empty."); + + RuleFor(x => x.RequestRefId) + .NotNull() + .WithMessage("{PropertyName} - The unique identifier is required.") + .NotEmpty() + .WithMessage("{PropertyName} - The unique identifier should not be empty."); + } + + private static bool LinkMustBeAUri(string link) + { + if (!Uri.IsWellFormedUriString(link, UriKind.Absolute)) + { + return false; + } + return true; + } + } +}