Skip to content

Commit

Permalink
Added B2CAccountTopUp api
Browse files Browse the repository at this point in the history
  • Loading branch information
gabrieldwight committed Jun 14, 2024
1 parent 9eed72e commit c0cc72a
Show file tree
Hide file tree
Showing 9 changed files with 201 additions and 14 deletions.
6 changes: 6 additions & 0 deletions MpesaSdk/Callbacks/B2CAccountTopUpCallback.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace MpesaSdk.Callbacks
{
public class B2CAccountTopUpCallback : BaseCallback
{
}
}
4 changes: 2 additions & 2 deletions MpesaSdk/Callbacks/BaseCallback.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}
}
60 changes: 60 additions & 0 deletions MpesaSdk/Dtos/B2CAccountTopUpRequest.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
18 changes: 18 additions & 0 deletions MpesaSdk/Interfaces/IMpesaClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -460,5 +460,23 @@ public interface IMpesaClient
/// <param name="cancellationToken"></param>
/// <returns></returns>
B2BExpressCheckoutResponse B2BExpressCheckout(B2BExpressCheckoutRequest b2BExpressCheckoutRequest, string accesstoken, CancellationToken cancellationToken = default);

/// <summary>
/// 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.
/// </summary>
/// <param name="b2CAccountTopUpRequest"></param>
/// <param name="accesstoken"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<MpesaResponse> B2CAccountTopUpAsync(B2CAccountTopUpRequest b2CAccountTopUpRequest, string accesstoken, CancellationToken cancellationToken = default);

/// <summary>
/// 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.
/// </summary>
/// <param name="b2CAccountTopUpRequest"></param>
/// <param name="accesstoken"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
MpesaResponse B2CAccountTopUp(B2CAccountTopUpRequest b2CAccountTopUpRequest, string accesstoken, CancellationToken cancellationToken = default);
}
}
30 changes: 24 additions & 6 deletions MpesaSdk/MpesaClient.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using FluentValidation;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using MpesaSdk.Dtos;
using MpesaSdk.Exceptions;
using MpesaSdk.Interfaces;
Expand All @@ -21,10 +20,10 @@

namespace MpesaSdk
{
/// <summary>
/// Mpesa client class provides all the implemented interface methods that make the different API calls to MPESA Server
/// </summary>
public class MpesaClient : IMpesaClient
/// <summary>
/// Mpesa client class provides all the implemented interface methods that make the different API calls to MPESA Server
/// </summary>
public class MpesaClient : IMpesaClient
{
private readonly HttpClient _client;
readonly Random jitterer = new Random();
Expand Down Expand Up @@ -110,6 +109,25 @@ public B2BExpressCheckoutResponse B2BExpressCheckout(B2BExpressCheckoutRequest b
: MpesaPostRequestAsync<B2BExpressCheckoutResponse>(b2BExpressCheckoutRequest, accesstoken, MpesaRequestEndpoint.B2BExpressCheckout, cancellationToken).GetAwaiter().GetResult();
}

public async Task<MpesaResponse> 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<MpesaResponse>(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<MpesaResponse>(b2CAccountTopUpRequest, accesstoken, MpesaRequestEndpoint.B2CAccountTopUp, cancellationToken).GetAwaiter().GetResult();
}

public async Task<BillManagerResponse> BillManagerOnboardingAsync(BillManagerOnboardingRequest billManagerOnboardingRequest, string accesstoken, CancellationToken cancellationToken = default)
{
Expand Down
5 changes: 4 additions & 1 deletion MpesaSdk/MpesaRequestEndpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";

}
}
10 changes: 5 additions & 5 deletions MpesaSdk/MpesaSdk.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,29 @@

<PropertyGroup>
<LangVersion>latest</LangVersion>
<Version>1.0.24</Version>
<Version>1.0.25</Version>
</PropertyGroup>

<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' or '$(TargetFramework)' == 'netstandard2.1'">
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="8.0.4" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="8.0.6" />
</ItemGroup>

<ItemGroup Condition=" '$(TargetFramework)' == 'net6.0' or '$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="6.0.29" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="6.0.31" />
</ItemGroup>

<ItemGroup Condition=" '$(TargetFramework)' == 'net7.0'">
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="7.0.18" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="7.0.20" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="FluentValidation" Version="11.9.1" />
<PackageReference Include="FluentValidation" Version="11.9.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
</Project>
5 changes: 5 additions & 0 deletions MpesaSdk/Transaction_Type.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,5 +99,10 @@ public static class Transaction_Type
/// PayTaxToKRA Command ID
/// </summary>
public const string PayTaxToKRA = "PayTaxToKRA";

/// <summary>
/// BusinessPayToBulk Command ID
/// </summary>
public const string BusinessPayToBulk = "BusinessPayToBulk";
}
}
77 changes: 77 additions & 0 deletions MpesaSdk/Validators/B2CAccountTopUpValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using FluentValidation;
using MpesaSdk.Dtos;
using System;

namespace MpesaSdk.Validators
{
public class B2CAccountTopUpValidator : AbstractValidator<B2CAccountTopUpRequest>
{
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;
}
}
}

0 comments on commit c0cc72a

Please sign in to comment.