diff --git a/FlutterWave.Core.Tests.Unit/Webhooks/WebhookEventDeserializationTests.cs b/FlutterWave.Core.Tests.Unit/Webhooks/WebhookEventDeserializationTests.cs new file mode 100644 index 0000000..933ac10 --- /dev/null +++ b/FlutterWave.Core.Tests.Unit/Webhooks/WebhookEventDeserializationTests.cs @@ -0,0 +1,304 @@ +using FluentAssertions; +using FlutterWave.Core.Webhooks; +using FlutterWave.Core.Webhooks.Events; +using Newtonsoft.Json; + +namespace FlutterWave.Core.Tests.Unit.Webhooks +{ + public class WebhookEventDeserializationTests + { + [Fact] + public void ShouldDeserializeChargeCompletedWebhookEvent() + { + // given + string json = @"{ + ""event"": ""charge.completed"", + ""data"": { + ""id"": 285959875, + ""tx_ref"": ""Links-616626414629"", + ""flw_ref"": ""PeterEkene/FLW270177170"", + ""device_fingerprint"": ""a42937f4a73ce8bb8b8df14e63a2df31"", + ""amount"": 100, + ""currency"": ""NGN"", + ""charged_amount"": 100, + ""app_fee"": 1.4, + ""merchant_fee"": 0, + ""processor_response"": ""Approved by Financial Institution"", + ""auth_model"": ""PIN"", + ""ip"": ""197.210.64.96"", + ""narration"": ""CARD Transaction "", + ""status"": ""successful"", + ""payment_type"": ""card"", + ""created_at"": ""2020-07-06T19:17:04.000Z"", + ""account_id"": 17321, + ""customer"": { + ""id"": 215604089, + ""name"": ""Yemi Desola"", + ""phone_number"": null, + ""email"": ""user@gmail.com"", + ""created_at"": ""2020-07-06T19:17:04.000Z"" + }, + ""card"": { + ""first_6digits"": ""123456"", + ""last_4digits"": ""7889"", + ""issuer"": ""VERVE FIRST CITY MONUMENT BANK PLC"", + ""country"": ""NG"", + ""type"": ""VERVE"", + ""expiry"": ""02/23"" + } + } + }"; + + // when + WebhookEvent actualEvent = + JsonConvert.DeserializeObject>(json); + + // then + actualEvent.Event.Should().Be("charge.completed"); + actualEvent.Data.Id.Should().Be(285959875); + actualEvent.Data.TxRef.Should().Be("Links-616626414629"); + actualEvent.Data.Status.Should().Be("successful"); + actualEvent.Data.Amount.Should().Be(100); + actualEvent.Data.Customer.Email.Should().Be("user@gmail.com"); + actualEvent.Data.Card.Last4digits.Should().Be("7889"); + } + + [Fact] + public void ShouldDeserializeTransferCompletedWebhookEvent() + { + // given + string json = @"{ + ""event"": ""transfer.completed"", + ""event.type"": ""Transfer"", + ""data"": { + ""id"": 33286, + ""account_number"": ""0690000033"", + ""bank_name"": ""ACCESS BANK NIGERIA"", + ""bank_code"": ""044"", + ""fullname"": ""Bale Gary"", + ""created_at"": ""2020-04-14T16:39:17.000Z"", + ""currency"": ""NGN"", + ""debit_currency"": ""NGN"", + ""amount"": 30020, + ""fee"": 26.875, + ""status"": ""SUCCESSFUL"", + ""reference"": ""a0a827b1eca65311_PMCKDU_5"", + ""meta"": null, + ""narration"": ""lolololo"", + ""approver"": null, + ""complete_message"": ""Successful"", + ""requires_approval"": 0, + ""is_approved"": 1 + } + }"; + + // when + WebhookEvent actualEvent = + JsonConvert.DeserializeObject>(json); + + // then + actualEvent.Event.Should().Be("transfer.completed"); + actualEvent.EventType.Should().Be("Transfer"); + actualEvent.Data.Status.Should().Be("SUCCESSFUL"); + actualEvent.Data.Amount.Should().Be("30020"); + actualEvent.Data.Reference.Should().Be("a0a827b1eca65311_PMCKDU_5"); + } + + [Fact] + public void ShouldDeserializeSubscriptionCancelledWebhookEvent() + { + // given + string json = @"{ + ""event"": ""subscription.cancelled"", + ""data"": { + ""status"": ""deactivated"", + ""currency"": ""NGN"", + ""amount"": 200, + ""customer"": { + ""email"": ""adeoyesamuel14@gmail.com"", + ""full_name"": ""Anonymous customer"" + }, + ""plan"": { + ""id"": 10944, + ""name"": ""month"", + ""amount"": 200, + ""currency"": ""NGN"", + ""interval"": ""monthly"", + ""duration"": 1, + ""status"": ""cancel"", + ""date_created"": ""2021-04-19T10:52:06.000Z"" + } + } + }"; + + // when + WebhookEvent actualEvent = + JsonConvert.DeserializeObject>(json); + + // then + actualEvent.Event.Should().Be("subscription.cancelled"); + actualEvent.Data.Status.Should().Be("deactivated"); + actualEvent.Data.Customer.Email.Should().Be("adeoyesamuel14@gmail.com"); + actualEvent.Data.Plan.Name.Should().Be("month"); + actualEvent.Data.Plan.Interval.Should().Be("monthly"); + } + + [Fact] + public void ShouldDeserializeVirtualCardDebitWebhookEvent() + { + // given + string json = @"{ + ""TransactionId"": ""9998d447-9159-4591-9661-aa7f1458d91b"", + ""MerchantName"": ""Flutterwave"", + ""Description"": ""CHARGE"", + ""Status"": ""Successful"", + ""Balance"": 4.7925, + ""Amount"": 0.2075, + ""Type"": ""Debit"", + ""CardId"": ""ccb40595-eaad-4a8f-bb9a-74ae245b12bb"", + ""MaskedPan"": ""428803******4329"" + }"; + + // when + VirtualCardDebitWebhookEvent actualEvent = + JsonConvert.DeserializeObject(json); + + // then + actualEvent.TransactionId.Should().Be("9998d447-9159-4591-9661-aa7f1458d91b"); + actualEvent.Status.Should().Be("Successful"); + actualEvent.Amount.Should().Be(0.2075m); + actualEvent.MaskedPan.Should().Be("428803******4329"); + } + + [Fact] + public void ShouldDeserializeVirtualCardOtpWebhookEvent() + { + // given + string json = @"{ + ""TransactionId"": null, + ""MerchantName"": null, + ""Description"": ""OTP"", + ""Status"": ""Pending Auth"", + ""Balance"": 0, + ""Amount"": 0, + ""Type"": ""Notification"", + ""CardId"": ""731a7d58-9293-4b50-b1f4-f2e0d56f3f4e"", + ""MaskedPan"": ""536898*******9526"", + ""Otp"": ""383290"" + }"; + + // when + VirtualCardOtpWebhookEvent actualEvent = + JsonConvert.DeserializeObject(json); + + // then + actualEvent.Description.Should().Be("OTP"); + actualEvent.Status.Should().Be("Pending Auth"); + actualEvent.Otp.Should().Be("383290"); + } + + [Fact] + public void ShouldDeserializeRefundWebhookEvent() + { + // given + string json = @"{ + ""id"": 99025, + ""AmountRefunded"": 100, + ""status"": ""completed"", + ""FlwRef"": ""4687213286"", + ""destination"": ""payment_source"", + ""comments"": null, + ""settlement_id"": ""142228"", + ""meta"": ""{\""source\"":\""ledgerbalance\""}"", + ""createdAt"": ""2026-01-30T08:35:45.000Z"", + ""updatedAt"": ""2026-01-30T08:35:46.000Z"", + ""deletedAt"": null, + ""walletId"": 684098, + ""AccountId"": 169295, + ""TransactionId"": 9231836 + }"; + + // when + RefundWebhookEvent actualEvent = + JsonConvert.DeserializeObject(json); + + // then + actualEvent.Id.Should().Be(99025); + actualEvent.AmountRefunded.Should().Be(100); + actualEvent.Status.Should().Be("completed"); + actualEvent.FlwRef.Should().Be("4687213286"); + actualEvent.TransactionId.Should().Be(9231836); + } + + [Fact] + public void ShouldDeserializeBvnCompletedWebhookEvent() + { + // given + string json = @"{ + ""event"": ""bvn.completed"", + ""event.type"": ""BVN"", + ""data"": { + ""id"": 18, + ""reference"": ""FLW441BD872AEBB28BD53B239"", + ""status"": ""COMPLETED"", + ""firstname"": ""LYRA"", + ""lastname"": ""Balacqua"", + ""callback_url"": ""https://webhook.site/939e641f-e477-4b1d-af3a-1ee9bbbd1181"", + ""AccountId"": 35308, + ""bvn_data"": { + ""dateOfBirth"": ""199x-05-xxT23:00:00Z"", + ""email"": ""test-user@example.com"", + ""gender"": ""Male"", + ""surname"": ""Balacqua"" + }, + ""createdAt"": ""2023-04-13T23:02:23.000Z"", + ""updatedAt"": ""2023-04-13T23:06:18.000Z"", + ""deletedAt"": null + } + }"; + + // when + WebhookEvent actualEvent = + JsonConvert.DeserializeObject>(json); + + // then + actualEvent.Event.Should().Be("bvn.completed"); + actualEvent.Data.Status.Should().Be("COMPLETED"); + actualEvent.Data.BvnData.Gender.Should().Be("Male"); + actualEvent.Data.BvnData.Surname.Should().Be("Balacqua"); + } + + [Fact] + public void ShouldDeserializeSingleBillPaymentStatusWebhookEvent() + { + // given + string json = @"{ + ""event"": ""singlebillpayment.status"", + ""event.type"": ""SingleBillPayment"", + ""data"": { + ""customer"": ""+2347065657658"", + ""amount"": 200, + ""network"": ""MTN"", + ""tx_ref"": ""CF-FLYAPI-20240604022555817834333"", + ""flw_ref"": ""BPUSSD17175111565077679855"", + ""batch_reference"": null, + ""customer_reference"": ""test-ref-kuf-01"", + ""status"": ""success"", + ""message"": ""Bill Payment was completed successfully"", + ""reference"": null + } + }"; + + // when + WebhookEvent actualEvent = + JsonConvert.DeserializeObject>(json); + + // then + actualEvent.Event.Should().Be("singlebillpayment.status"); + actualEvent.EventType.Should().Be("SingleBillPayment"); + actualEvent.Data.Status.Should().Be("success"); + actualEvent.Data.Network.Should().Be("MTN"); + actualEvent.Data.Customer.Should().Be("+2347065657658"); + } + } +} diff --git a/FlutterWave.Core.Tests.Unit/Webhooks/WebhookSignatureVerifierTests.cs b/FlutterWave.Core.Tests.Unit/Webhooks/WebhookSignatureVerifierTests.cs new file mode 100644 index 0000000..825d41c --- /dev/null +++ b/FlutterWave.Core.Tests.Unit/Webhooks/WebhookSignatureVerifierTests.cs @@ -0,0 +1,53 @@ +using FluentAssertions; +using FlutterWave.Core.Webhooks; + +namespace FlutterWave.Core.Tests.Unit.Webhooks +{ + public class WebhookSignatureVerifierTests + { + [Fact] + public void ShouldReturnTrueWhenSecretHashMatchesHeaderValue() + { + // given + string secretHash = "my-super-secret-hash"; + string verifHashHeaderValue = "my-super-secret-hash"; + + // when + bool actualResult = WebhookSignatureVerifier.IsValid(secretHash, verifHashHeaderValue); + + // then + actualResult.Should().BeTrue(); + } + + [Theory] + [InlineData("my-super-secret-hash", "a-different-hash")] + [InlineData("my-super-secret-hash", "my-super-secret-has")] + [InlineData("my-super-secret-hash", "MY-SUPER-SECRET-HASH")] + public void ShouldReturnFalseWhenSecretHashDoesNotMatchHeaderValue( + string secretHash, string verifHashHeaderValue) + { + // when + bool actualResult = WebhookSignatureVerifier.IsValid(secretHash, verifHashHeaderValue); + + // then + actualResult.Should().BeFalse(); + } + + [Theory] + [InlineData(null, "some-hash")] + [InlineData("", "some-hash")] + [InlineData("some-hash", null)] + [InlineData("some-hash", "")] + [InlineData(null, null)] + [InlineData("", "")] + public void ShouldReturnFalseWhenEitherValueIsNullOrEmpty( + string secretHash, string verifHashHeaderValue) + { + // when + bool actualResult = WebhookSignatureVerifier.IsValid(secretHash, verifHashHeaderValue); + + // then + actualResult.Should().BeFalse(); + } + } +} diff --git a/FlutterWave.Core/Webhooks/Events/BvnCompletedWebhookEventData.cs b/FlutterWave.Core/Webhooks/Events/BvnCompletedWebhookEventData.cs new file mode 100644 index 0000000..6f6ba9d --- /dev/null +++ b/FlutterWave.Core/Webhooks/Events/BvnCompletedWebhookEventData.cs @@ -0,0 +1,98 @@ +using Newtonsoft.Json; +using System; + +namespace FlutterWave.Core.Webhooks.Events +{ + public class BvnCompletedWebhookEventData + { + [JsonProperty("id")] + public long Id { get; set; } + + [JsonProperty("reference")] + public string Reference { get; set; } + + [JsonProperty("status")] + public string Status { get; set; } + + [JsonProperty("firstname")] + public string Firstname { get; set; } + + [JsonProperty("lastname")] + public string Lastname { get; set; } + + [JsonProperty("callback_url")] + public string CallbackUrl { get; set; } + + [JsonProperty("AccountId")] + public long AccountId { get; set; } + + [JsonProperty("bvn_data")] + public BvnCompletedBvnData BvnData { get; set; } + + [JsonProperty("createdAt")] + public DateTime CreatedAt { get; set; } + + [JsonProperty("updatedAt")] + public DateTime UpdatedAt { get; set; } + + [JsonProperty("deletedAt")] + public DateTime? DeletedAt { get; set; } + + public class BvnCompletedBvnData + { + [JsonProperty("dateOfBirth")] + public string DateOfBirth { get; set; } + + [JsonProperty("email")] + public string Email { get; set; } + + [JsonProperty("enrollUserName")] + public string EnrollUserName { get; set; } + + [JsonProperty("firstName")] + public string FirstName { get; set; } + + [JsonProperty("gender")] + public string Gender { get; set; } + + [JsonProperty("lgaOfOrigin")] + public string LgaOfOrigin { get; set; } + + [JsonProperty("lgaOfResidence")] + public string LgaOfResidence { get; set; } + + [JsonProperty("maritalStatus")] + public string MaritalStatus { get; set; } + + [JsonProperty("middleName")] + public string MiddleName { get; set; } + + [JsonProperty("nin")] + public string Nin { get; set; } + + [JsonProperty("phoneNumber1")] + public string PhoneNumber1 { get; set; } + + [JsonProperty("phoneNumber2")] + public string PhoneNumber2 { get; set; } + + [JsonProperty("productReference")] + public string ProductReference { get; set; } + + [JsonProperty("stateOfCapture")] + public string StateOfCapture { get; set; } + + [JsonProperty("stateOfOrigin")] + public string StateOfOrigin { get; set; } + + [JsonProperty("stateOfResidence")] + public string StateOfResidence { get; set; } + + [JsonProperty("surname")] + public string Surname { get; set; } + + [JsonProperty("watchlisted")] + public string Watchlisted { get; set; } + } + } +} diff --git a/FlutterWave.Core/Webhooks/Events/ChargeCompletedWebhookEventData.cs b/FlutterWave.Core/Webhooks/Events/ChargeCompletedWebhookEventData.cs new file mode 100644 index 0000000..2c1ad27 --- /dev/null +++ b/FlutterWave.Core/Webhooks/Events/ChargeCompletedWebhookEventData.cs @@ -0,0 +1,104 @@ +using Newtonsoft.Json; +using System; + +namespace FlutterWave.Core.Webhooks.Events +{ + public class ChargeCompletedWebhookEventData + { + [JsonProperty("id")] + public long Id { get; set; } + + [JsonProperty("tx_ref")] + public string TxRef { get; set; } + + [JsonProperty("flw_ref")] + public string FlwRef { get; set; } + + [JsonProperty("device_fingerprint")] + public string DeviceFingerprint { get; set; } + + [JsonProperty("amount")] + public decimal Amount { get; set; } + + [JsonProperty("currency")] + public string Currency { get; set; } + + [JsonProperty("charged_amount")] + public decimal ChargedAmount { get; set; } + + [JsonProperty("app_fee")] + public decimal AppFee { get; set; } + + [JsonProperty("merchant_fee")] + public decimal MerchantFee { get; set; } + + [JsonProperty("processor_response")] + public string ProcessorResponse { get; set; } + + [JsonProperty("auth_model")] + public string AuthModel { get; set; } + + [JsonProperty("ip")] + public string Ip { get; set; } + + [JsonProperty("narration")] + public string Narration { get; set; } + + [JsonProperty("status")] + public string Status { get; set; } + + [JsonProperty("payment_type")] + public string PaymentType { get; set; } + + [JsonProperty("created_at")] + public DateTime CreatedAt { get; set; } + + [JsonProperty("account_id")] + public long AccountId { get; set; } + + [JsonProperty("customer")] + public ChargeCompletedCustomer Customer { get; set; } + + [JsonProperty("card")] + public ChargeCompletedCard Card { get; set; } + + public class ChargeCompletedCustomer + { + [JsonProperty("id")] + public long Id { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("phone_number")] + public string PhoneNumber { get; set; } + + [JsonProperty("email")] + public string Email { get; set; } + + [JsonProperty("created_at")] + public DateTime CreatedAt { get; set; } + } + + public class ChargeCompletedCard + { + [JsonProperty("first_6digits")] + public string First6digits { get; set; } + + [JsonProperty("last_4digits")] + public string Last4digits { get; set; } + + [JsonProperty("issuer")] + public string Issuer { get; set; } + + [JsonProperty("country")] + public string Country { get; set; } + + [JsonProperty("type")] + public string Type { get; set; } + + [JsonProperty("expiry")] + public string Expiry { get; set; } + } + } +} diff --git a/FlutterWave.Core/Webhooks/Events/RefundWebhookEvent.cs b/FlutterWave.Core/Webhooks/Events/RefundWebhookEvent.cs new file mode 100644 index 0000000..6539aff --- /dev/null +++ b/FlutterWave.Core/Webhooks/Events/RefundWebhookEvent.cs @@ -0,0 +1,50 @@ +using Newtonsoft.Json; +using System; + +namespace FlutterWave.Core.Webhooks.Events +{ + public class RefundWebhookEvent + { + [JsonProperty("id")] + public long Id { get; set; } + + [JsonProperty("AmountRefunded")] + public decimal AmountRefunded { get; set; } + + [JsonProperty("status")] + public string Status { get; set; } + + [JsonProperty("FlwRef")] + public string FlwRef { get; set; } + + [JsonProperty("destination")] + public string Destination { get; set; } + + [JsonProperty("comments")] + public string Comments { get; set; } + + [JsonProperty("settlement_id")] + public string SettlementId { get; set; } + + [JsonProperty("meta")] + public string Meta { get; set; } + + [JsonProperty("createdAt")] + public DateTime CreatedAt { get; set; } + + [JsonProperty("updatedAt")] + public DateTime UpdatedAt { get; set; } + + [JsonProperty("deletedAt")] + public DateTime? DeletedAt { get; set; } + + [JsonProperty("walletId")] + public long WalletId { get; set; } + + [JsonProperty("AccountId")] + public long AccountId { get; set; } + + [JsonProperty("TransactionId")] + public long TransactionId { get; set; } + } +} diff --git a/FlutterWave.Core/Webhooks/Events/SingleBillPaymentStatusWebhookEventData.cs b/FlutterWave.Core/Webhooks/Events/SingleBillPaymentStatusWebhookEventData.cs new file mode 100644 index 0000000..3e883e0 --- /dev/null +++ b/FlutterWave.Core/Webhooks/Events/SingleBillPaymentStatusWebhookEventData.cs @@ -0,0 +1,37 @@ +using Newtonsoft.Json; + +namespace FlutterWave.Core.Webhooks.Events +{ + public class SingleBillPaymentStatusWebhookEventData + { + [JsonProperty("customer")] + public string Customer { get; set; } + + [JsonProperty("amount")] + public decimal Amount { get; set; } + + [JsonProperty("network")] + public string Network { get; set; } + + [JsonProperty("tx_ref")] + public string TxRef { get; set; } + + [JsonProperty("flw_ref")] + public string FlwRef { get; set; } + + [JsonProperty("batch_reference")] + public string BatchReference { get; set; } + + [JsonProperty("customer_reference")] + public string CustomerReference { get; set; } + + [JsonProperty("status")] + public string Status { get; set; } + + [JsonProperty("message")] + public string Message { get; set; } + + [JsonProperty("reference")] + public string Reference { get; set; } + } +} diff --git a/FlutterWave.Core/Webhooks/Events/SubscriptionCancelledWebhookEventData.cs b/FlutterWave.Core/Webhooks/Events/SubscriptionCancelledWebhookEventData.cs new file mode 100644 index 0000000..caa8d5b --- /dev/null +++ b/FlutterWave.Core/Webhooks/Events/SubscriptionCancelledWebhookEventData.cs @@ -0,0 +1,59 @@ +using Newtonsoft.Json; +using System; + +namespace FlutterWave.Core.Webhooks.Events +{ + public class SubscriptionCancelledWebhookEventData + { + [JsonProperty("status")] + public string Status { get; set; } + + [JsonProperty("currency")] + public string Currency { get; set; } + + [JsonProperty("amount")] + public decimal Amount { get; set; } + + [JsonProperty("customer")] + public SubscriptionCancelledCustomer Customer { get; set; } + + [JsonProperty("plan")] + public SubscriptionCancelledPlan Plan { get; set; } + + public class SubscriptionCancelledCustomer + { + [JsonProperty("email")] + public string Email { get; set; } + + [JsonProperty("full_name")] + public string FullName { get; set; } + } + + public class SubscriptionCancelledPlan + { + [JsonProperty("id")] + public long Id { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("amount")] + public decimal Amount { get; set; } + + [JsonProperty("currency")] + public string Currency { get; set; } + + [JsonProperty("interval")] + public string Interval { get; set; } + + [JsonProperty("duration")] + public int Duration { get; set; } + + [JsonProperty("status")] + public string Status { get; set; } + + [JsonProperty("date_created")] + public DateTime DateCreated { get; set; } + } + } +} diff --git a/FlutterWave.Core/Webhooks/Events/TransferCompletedWebhookEventData.cs b/FlutterWave.Core/Webhooks/Events/TransferCompletedWebhookEventData.cs new file mode 100644 index 0000000..27dc418 --- /dev/null +++ b/FlutterWave.Core/Webhooks/Events/TransferCompletedWebhookEventData.cs @@ -0,0 +1,62 @@ +using Newtonsoft.Json; +using System; + +namespace FlutterWave.Core.Webhooks.Events +{ + public class TransferCompletedWebhookEventData + { + [JsonProperty("id")] + public long Id { get; set; } + + [JsonProperty("account_number")] + public string AccountNumber { get; set; } + + [JsonProperty("bank_name")] + public string BankName { get; set; } + + [JsonProperty("bank_code")] + public string BankCode { get; set; } + + [JsonProperty("fullname")] + public string Fullname { get; set; } + + [JsonProperty("created_at")] + public DateTime CreatedAt { get; set; } + + [JsonProperty("currency")] + public string Currency { get; set; } + + [JsonProperty("debit_currency")] + public string DebitCurrency { get; set; } + + [JsonProperty("amount")] + public string Amount { get; set; } + + [JsonProperty("fee")] + public string Fee { get; set; } + + [JsonProperty("status")] + public string Status { get; set; } + + [JsonProperty("reference")] + public string Reference { get; set; } + + [JsonProperty("meta")] + public object Meta { get; set; } + + [JsonProperty("narration")] + public string Narration { get; set; } + + [JsonProperty("approver")] + public string Approver { get; set; } + + [JsonProperty("complete_message")] + public string CompleteMessage { get; set; } + + [JsonProperty("requires_approval")] + public int RequiresApproval { get; set; } + + [JsonProperty("is_approved")] + public int IsApproved { get; set; } + } +} diff --git a/FlutterWave.Core/Webhooks/Events/VirtualCardDebitWebhookEvent.cs b/FlutterWave.Core/Webhooks/Events/VirtualCardDebitWebhookEvent.cs new file mode 100644 index 0000000..683587f --- /dev/null +++ b/FlutterWave.Core/Webhooks/Events/VirtualCardDebitWebhookEvent.cs @@ -0,0 +1,34 @@ +using Newtonsoft.Json; + +namespace FlutterWave.Core.Webhooks.Events +{ + public class VirtualCardDebitWebhookEvent + { + [JsonProperty("TransactionId")] + public string TransactionId { get; set; } + + [JsonProperty("MerchantName")] + public string MerchantName { get; set; } + + [JsonProperty("Description")] + public string Description { get; set; } + + [JsonProperty("Status")] + public string Status { get; set; } + + [JsonProperty("Balance")] + public decimal Balance { get; set; } + + [JsonProperty("Amount")] + public decimal Amount { get; set; } + + [JsonProperty("Type")] + public string Type { get; set; } + + [JsonProperty("CardId")] + public string CardId { get; set; } + + [JsonProperty("MaskedPan")] + public string MaskedPan { get; set; } + } +} diff --git a/FlutterWave.Core/Webhooks/Events/VirtualCardOtpWebhookEvent.cs b/FlutterWave.Core/Webhooks/Events/VirtualCardOtpWebhookEvent.cs new file mode 100644 index 0000000..51af08a --- /dev/null +++ b/FlutterWave.Core/Webhooks/Events/VirtualCardOtpWebhookEvent.cs @@ -0,0 +1,37 @@ +using Newtonsoft.Json; + +namespace FlutterWave.Core.Webhooks.Events +{ + public class VirtualCardOtpWebhookEvent + { + [JsonProperty("TransactionId")] + public string TransactionId { get; set; } + + [JsonProperty("MerchantName")] + public string MerchantName { get; set; } + + [JsonProperty("Description")] + public string Description { get; set; } + + [JsonProperty("Status")] + public string Status { get; set; } + + [JsonProperty("Balance")] + public decimal Balance { get; set; } + + [JsonProperty("Amount")] + public decimal Amount { get; set; } + + [JsonProperty("Type")] + public string Type { get; set; } + + [JsonProperty("CardId")] + public string CardId { get; set; } + + [JsonProperty("MaskedPan")] + public string MaskedPan { get; set; } + + [JsonProperty("Otp")] + public string Otp { get; set; } + } +} diff --git a/FlutterWave.Core/Webhooks/WebhookEvent.cs b/FlutterWave.Core/Webhooks/WebhookEvent.cs new file mode 100644 index 0000000..1bb2817 --- /dev/null +++ b/FlutterWave.Core/Webhooks/WebhookEvent.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace FlutterWave.Core.Webhooks +{ + public class WebhookEvent + { + [JsonProperty("event")] + public string Event { get; set; } + + [JsonProperty("event.type")] + public string EventType { get; set; } + + [JsonProperty("data")] + public TData Data { get; set; } + } +} diff --git a/FlutterWave.Core/Webhooks/WebhookSignatureVerifier.cs b/FlutterWave.Core/Webhooks/WebhookSignatureVerifier.cs new file mode 100644 index 0000000..b7db5b2 --- /dev/null +++ b/FlutterWave.Core/Webhooks/WebhookSignatureVerifier.cs @@ -0,0 +1,26 @@ +using System.Security.Cryptography; +using System.Text; + +namespace FlutterWave.Core.Webhooks +{ + public static class WebhookSignatureVerifier + { + public static bool IsValid(string secretHash, string verifHashHeaderValue) + { + if (string.IsNullOrEmpty(secretHash) || string.IsNullOrEmpty(verifHashHeaderValue)) + { + return false; + } + + byte[] expectedBytes = Encoding.UTF8.GetBytes(secretHash); + byte[] actualBytes = Encoding.UTF8.GetBytes(verifHashHeaderValue); + + if (expectedBytes.Length != actualBytes.Length) + { + return false; + } + + return CryptographicOperations.FixedTimeEquals(expectedBytes, actualBytes); + } + } +}