From e9f57ca3c93222e3146af7460938897a41b79367 Mon Sep 17 00:00:00 2001 From: Szeregowy Date: Tue, 24 Mar 2026 15:11:17 +0100 Subject: [PATCH 1/5] feat: add payment providers Adyen, PayU, Przelewy24, Tpay, HotPay, PayNow, Revolut MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nowe biblioteki implementujące IPaymentProvider: - Adyen — oficjalny SDK (Adyen v14), Sessions API, HMAC-SHA256 webhook - PayU — OAuth2 + REST API v2.1, MD5/SHA256 OpenPayU-Signature - Przelewy24 — REST API v1, SHA384 CRC sign verification - Tpay — OAuth2 + REST API, SHA256 SecurityCode webhook - HotPay — SHA256 hash, redirect flow - PayNow (mBank) — REST API v2, HMAC-SHA256 Signature header - Revolut — REST API 1.0, HMAC-SHA256 Revolut-Signature (v1=) Każdy provider: - Single-file (Options, Interface, Caller, Provider, Extensions, ConfigureOptions) - IHttpClientFactory z nazwanym klientem (poza Adyen) - Per-request weryfikacja podpisu webhooka - GetPaymentChannels() z hardcoded listą (analog Stripe) - Konfiguracja z sekcji Payments:Providers: Testy: - MultiProviderPaymentTest.cs — smoke testy (channels, providers list, invalid webhook → Rejected) - 2 integracyjne (Skip) dla PayU i Revolut Pozostałe zmiany: - TailoredApps.Shared.sln — 7 nowych wpisów projektów - test csproj — 7 nowych ProjectReference - appsettings.json — placeholder config dla każdego providera --- TailoredApps.Shared.sln | 42 +++ .../AdyenProvider.cs | 234 +++++++++++++ ...Apps.Shared.Payments.Provider.Adyen.csproj | 32 ++ .../HotPayProvider.cs | 173 ++++++++++ ...pps.Shared.Payments.Provider.HotPay.csproj | 33 ++ .../PayNowProvider.cs | 228 ++++++++++++ ...pps.Shared.Payments.Provider.PayNow.csproj | 33 ++ .../PayUProvider.cs | 324 ++++++++++++++++++ ...dApps.Shared.Payments.Provider.PayU.csproj | 33 ++ .../Przelewy24Provider.cs | 290 ++++++++++++++++ ...Shared.Payments.Provider.Przelewy24.csproj | 33 ++ .../RevolutProvider.cs | 214 ++++++++++++ ...ps.Shared.Payments.Provider.Revolut.csproj | 33 ++ ...dApps.Shared.Payments.Provider.Tpay.csproj | 33 ++ .../TpayProvider.cs | 269 +++++++++++++++ .../MultiProviderPaymentTest.cs | 264 ++++++++++++++ .../TailoredApps.Shared.Payments.Tests.csproj | 7 + .../appsettings.json | 54 +++ 18 files changed, 2329 insertions(+) create mode 100644 src/TailoredApps.Shared.Payments.Provider.Adyen/AdyenProvider.cs create mode 100644 src/TailoredApps.Shared.Payments.Provider.Adyen/TailoredApps.Shared.Payments.Provider.Adyen.csproj create mode 100644 src/TailoredApps.Shared.Payments.Provider.HotPay/HotPayProvider.cs create mode 100644 src/TailoredApps.Shared.Payments.Provider.HotPay/TailoredApps.Shared.Payments.Provider.HotPay.csproj create mode 100644 src/TailoredApps.Shared.Payments.Provider.PayNow/PayNowProvider.cs create mode 100644 src/TailoredApps.Shared.Payments.Provider.PayNow/TailoredApps.Shared.Payments.Provider.PayNow.csproj create mode 100644 src/TailoredApps.Shared.Payments.Provider.PayU/PayUProvider.cs create mode 100644 src/TailoredApps.Shared.Payments.Provider.PayU/TailoredApps.Shared.Payments.Provider.PayU.csproj create mode 100644 src/TailoredApps.Shared.Payments.Provider.Przelewy24/Przelewy24Provider.cs create mode 100644 src/TailoredApps.Shared.Payments.Provider.Przelewy24/TailoredApps.Shared.Payments.Provider.Przelewy24.csproj create mode 100644 src/TailoredApps.Shared.Payments.Provider.Revolut/RevolutProvider.cs create mode 100644 src/TailoredApps.Shared.Payments.Provider.Revolut/TailoredApps.Shared.Payments.Provider.Revolut.csproj create mode 100644 src/TailoredApps.Shared.Payments.Provider.Tpay/TailoredApps.Shared.Payments.Provider.Tpay.csproj create mode 100644 src/TailoredApps.Shared.Payments.Provider.Tpay/TpayProvider.cs create mode 100644 tests/TailoredApps.Shared.Payments.Tests/MultiProviderPaymentTest.cs diff --git a/TailoredApps.Shared.sln b/TailoredApps.Shared.sln index cfe6efb..65adc97 100644 --- a/TailoredApps.Shared.sln +++ b/TailoredApps.Shared.sln @@ -35,6 +35,20 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TailoredApps.Shared.Payment EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TailoredApps.Shared.Payments.Provider.Stripe", "src\TailoredApps.Shared.Payments.Provider.Stripe\TailoredApps.Shared.Payments.Provider.Stripe.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TailoredApps.Shared.Payments.Provider.Adyen", "src\TailoredApps.Shared.Payments.Provider.Adyen\TailoredApps.Shared.Payments.Provider.Adyen.csproj", "{B1C2D3E4-F5A6-7890-BCDE-FA2345678901}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TailoredApps.Shared.Payments.Provider.PayU", "src\TailoredApps.Shared.Payments.Provider.PayU\TailoredApps.Shared.Payments.Provider.PayU.csproj", "{C2D3E4F5-A6B7-8901-CDEF-AB3456789012}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TailoredApps.Shared.Payments.Provider.Przelewy24", "src\TailoredApps.Shared.Payments.Provider.Przelewy24\TailoredApps.Shared.Payments.Provider.Przelewy24.csproj", "{D3E4F5A6-B7C8-9012-DEF0-BC4567890123}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TailoredApps.Shared.Payments.Provider.Tpay", "src\TailoredApps.Shared.Payments.Provider.Tpay\TailoredApps.Shared.Payments.Provider.Tpay.csproj", "{E4F5A6B7-C8D9-0123-EF01-CD5678901234}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TailoredApps.Shared.Payments.Provider.HotPay", "src\TailoredApps.Shared.Payments.Provider.HotPay\TailoredApps.Shared.Payments.Provider.HotPay.csproj", "{F5A6B7C8-D9E0-1234-F012-DE6789012345}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TailoredApps.Shared.Payments.Provider.PayNow", "src\TailoredApps.Shared.Payments.Provider.PayNow\TailoredApps.Shared.Payments.Provider.PayNow.csproj", "{A6B7C8D9-E0F1-2345-0123-EF7890123456}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TailoredApps.Shared.Payments.Provider.Revolut", "src\TailoredApps.Shared.Payments.Provider.Revolut\TailoredApps.Shared.Payments.Provider.Revolut.csproj", "{B7C8D9E0-F1A2-3456-1234-F08901234567}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TailoredApps.Shared.EntityFramework.UnitOfWork.WebApiCore", "src\TailoredApps.Shared.EntityFramework.UnitOfWork.WebApiCore\TailoredApps.Shared.EntityFramework.UnitOfWork.WebApiCore.csproj", "{5C6FFD3B-B9E3-47D8-8A77-5973198C7B16}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TailoredApps.Shared.MediatR.ML", "src\TailoredApps.Shared.MediatR.ML\TailoredApps.Shared.MediatR.ML.csproj", "{C454266F-AF6D-450B-B899-9359180BAE94}" @@ -127,6 +141,34 @@ Global {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU + {B1C2D3E4-F5A6-7890-BCDE-FA2345678901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B1C2D3E4-F5A6-7890-BCDE-FA2345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B1C2D3E4-F5A6-7890-BCDE-FA2345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B1C2D3E4-F5A6-7890-BCDE-FA2345678901}.Release|Any CPU.Build.0 = Release|Any CPU + {C2D3E4F5-A6B7-8901-CDEF-AB3456789012}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C2D3E4F5-A6B7-8901-CDEF-AB3456789012}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C2D3E4F5-A6B7-8901-CDEF-AB3456789012}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C2D3E4F5-A6B7-8901-CDEF-AB3456789012}.Release|Any CPU.Build.0 = Release|Any CPU + {D3E4F5A6-B7C8-9012-DEF0-BC4567890123}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D3E4F5A6-B7C8-9012-DEF0-BC4567890123}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D3E4F5A6-B7C8-9012-DEF0-BC4567890123}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D3E4F5A6-B7C8-9012-DEF0-BC4567890123}.Release|Any CPU.Build.0 = Release|Any CPU + {E4F5A6B7-C8D9-0123-EF01-CD5678901234}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E4F5A6B7-C8D9-0123-EF01-CD5678901234}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E4F5A6B7-C8D9-0123-EF01-CD5678901234}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E4F5A6B7-C8D9-0123-EF01-CD5678901234}.Release|Any CPU.Build.0 = Release|Any CPU + {F5A6B7C8-D9E0-1234-F012-DE6789012345}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F5A6B7C8-D9E0-1234-F012-DE6789012345}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F5A6B7C8-D9E0-1234-F012-DE6789012345}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F5A6B7C8-D9E0-1234-F012-DE6789012345}.Release|Any CPU.Build.0 = Release|Any CPU + {A6B7C8D9-E0F1-2345-0123-EF7890123456}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A6B7C8D9-E0F1-2345-0123-EF7890123456}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A6B7C8D9-E0F1-2345-0123-EF7890123456}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A6B7C8D9-E0F1-2345-0123-EF7890123456}.Release|Any CPU.Build.0 = Release|Any CPU + {B7C8D9E0-F1A2-3456-1234-F08901234567}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B7C8D9E0-F1A2-3456-1234-F08901234567}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B7C8D9E0-F1A2-3456-1234-F08901234567}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B7C8D9E0-F1A2-3456-1234-F08901234567}.Release|Any CPU.Build.0 = Release|Any CPU {5C6FFD3B-B9E3-47D8-8A77-5973198C7B16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5C6FFD3B-B9E3-47D8-8A77-5973198C7B16}.Debug|Any CPU.Build.0 = Debug|Any CPU {5C6FFD3B-B9E3-47D8-8A77-5973198C7B16}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/src/TailoredApps.Shared.Payments.Provider.Adyen/AdyenProvider.cs b/src/TailoredApps.Shared.Payments.Provider.Adyen/AdyenProvider.cs new file mode 100644 index 0000000..c4428d3 --- /dev/null +++ b/src/TailoredApps.Shared.Payments.Provider.Adyen/AdyenProvider.cs @@ -0,0 +1,234 @@ +using System.Security.Cryptography; +using System.Text; +using Adyen; +using Adyen.Model.Checkout; +using Adyen.Service.Checkout; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Environment = Adyen.Model.Enum.Environment; + +namespace TailoredApps.Shared.Payments.Provider.Adyen; + +// ─── Options ───────────────────────────────────────────────────────────────── + +/// Konfiguracja Adyen. Sekcja: Payments:Providers:Adyen. +public class AdyenServiceOptions +{ + public static string ConfigurationKey => "Payments:Providers:Adyen"; + public string ApiKey { get; set; } = string.Empty; + public string MerchantAccount { get; set; } = string.Empty; + public string ClientKey { get; set; } = string.Empty; + public string ReturnUrl { get; set; } = string.Empty; + /// HMAC klucz do weryfikacji powiadomień webhooka (hex). + public string NotificationHmacKey { get; set; } = string.Empty; + /// True = sandbox (test), False = live. + public bool IsTest { get; set; } = true; +} + +// ─── Interface ──────────────────────────────────────────────────────────────── + +/// Abstrakcja nad Adyen Checkout API. +public interface IAdyenServiceCaller +{ + Task CreateSessionAsync(PaymentRequest request); + Task GetPaymentStatusAsync(string pspReference); + bool VerifyNotificationHmac(string payload, string hmacSignature); +} + +// ─── Caller ─────────────────────────────────────────────────────────────────── + +/// Implementacja opakowująca oficjalny Adyen SDK. +public class AdyenServiceCaller : IAdyenServiceCaller +{ + private readonly AdyenServiceOptions options; + + public AdyenServiceCaller(IOptions options) + { + this.options = options.Value; + } + + private Client CreateClient() + { + var config = new Config + { + XApiKey = options.ApiKey, + Environment = options.IsTest ? Environment.Test : Environment.Live, + }; + return new Client(config); + } + + public async Task CreateSessionAsync(PaymentRequest request) + { + var client = CreateClient(); + var checkout = new PaymentsService(client); + + var amount = new Adyen.Model.Checkout.Amount + { + Value = (long)(request.Amount * 100), + Currency = request.Currency.ToUpperInvariant(), + }; + + var sessionRequest = new CreateCheckoutSessionRequest( + merchantAccount: options.MerchantAccount, + amount: amount, + returnUrl: options.ReturnUrl, + reference: request.AdditionalData ?? Guid.NewGuid().ToString("N") + ) + { + ShopperEmail = request.Email, + ShopperReference = request.Email, + CountryCode = request.Country ?? "PL", + }; + + return await checkout.SessionsAsync(sessionRequest); + } + + public async Task GetPaymentStatusAsync(string pspReference) + { + var client = CreateClient(); + var checkout = new PaymentsService(client); + var details = new PaymentDetailsRequest(details: new PaymentCompletionDetails(), paymentData: pspReference); + return await checkout.PaymentsDetailsAsync(details); + } + + public bool VerifyNotificationHmac(string payload, string hmacSignature) + { + try + { + var keyBytes = Convert.FromHexString(options.NotificationHmacKey); + var dataBytes = Encoding.UTF8.GetBytes(payload); + var computed = Convert.ToBase64String(HMACSHA256.HashData(keyBytes, dataBytes)); + return string.Equals(computed, hmacSignature, StringComparison.Ordinal); + } + catch { return false; } + } +} + +// ─── Provider ───────────────────────────────────────────────────────────────── + +/// Implementacja dla Adyen. +public class AdyenProvider : IPaymentProvider +{ + private readonly IAdyenServiceCaller caller; + + public AdyenProvider(IAdyenServiceCaller caller) + { + this.caller = caller; + } + + public string Key => "Adyen"; + public string Name => "Adyen"; + public string Description => "Globalny operator płatności Adyen — karty, BLIK, iDEAL i inne."; + public string Url => "https://www.adyen.com"; + + public Task> GetPaymentChannels(string currency) + { + ICollection channels = currency.ToUpperInvariant() switch + { + "PLN" => + [ + new PaymentChannel { Id = "scheme", Name = "Karta płatnicza", Description = "Visa, Mastercard", PaymentModel = PaymentModel.OneTime }, + new PaymentChannel { Id = "blik", Name = "BLIK", Description = "BLIK", PaymentModel = PaymentModel.OneTime }, + new PaymentChannel { Id = "onlineBanking_PL", Name = "Przelew online", Description = "Polskie banki", PaymentModel = PaymentModel.OneTime }, + ], + "EUR" => + [ + new PaymentChannel { Id = "scheme", Name = "Karta płatnicza", Description = "Visa, Mastercard", PaymentModel = PaymentModel.OneTime }, + new PaymentChannel { Id = "ideal", Name = "iDEAL", Description = "Przelew iDEAL", PaymentModel = PaymentModel.OneTime }, + new PaymentChannel { Id = "sepadirectdebit", Name = "SEPA Direct Debit", Description = "SEPA", PaymentModel = PaymentModel.OneTime }, + ], + _ => + [ + new PaymentChannel { Id = "scheme", Name = "Karta płatnicza", Description = "Visa, Mastercard", PaymentModel = PaymentModel.OneTime }, + ], + }; + return Task.FromResult(channels); + } + + public async Task RequestPayment(PaymentRequest request) + { + var session = await caller.CreateSessionAsync(request); + return new PaymentResponse + { + PaymentUniqueId = session.Id, + RedirectUrl = session.Url, + PaymentStatus = PaymentStatusEnum.Created, + }; + } + + public async Task GetStatus(string paymentId) + { + var details = await caller.GetPaymentStatusAsync(paymentId); + var status = details.ResultCode?.ToString() switch + { + "Authorised" => PaymentStatusEnum.Finished, + "Refused" => PaymentStatusEnum.Rejected, + "Cancelled" => PaymentStatusEnum.Rejected, + "Pending" => PaymentStatusEnum.Processing, + "Received" => PaymentStatusEnum.Processing, + _ => PaymentStatusEnum.Created, + }; + return new PaymentResponse { PaymentUniqueId = paymentId, PaymentStatus = status }; + } + + public Task TransactionStatusChange(TransactionStatusChangePayload payload) + { + var body = payload.Payload?.ToString() ?? string.Empty; + var hmac = payload.QueryParameters.TryGetValue("HmacSignature", out var h) ? h.ToString() : string.Empty; + + if (!caller.VerifyNotificationHmac(body, hmac)) + return Task.FromResult(new PaymentResponse { PaymentStatus = PaymentStatusEnum.Rejected, ResponseObject = "Invalid HMAC" }); + + var status = PaymentStatusEnum.Processing; + try + { + var doc = System.Text.Json.JsonDocument.Parse(body); + if (doc.RootElement.TryGetProperty("eventCode", out var ev)) + { + status = ev.GetString() switch + { + "AUTHORISATION" => PaymentStatusEnum.Finished, + "CANCELLATION" => PaymentStatusEnum.Rejected, + "REFUND" => PaymentStatusEnum.Rejected, + "AUTHORISATION_FAILED" => PaymentStatusEnum.Rejected, + _ => PaymentStatusEnum.Processing, + }; + } + } + catch { /* ignore */ } + + return Task.FromResult(new PaymentResponse { PaymentStatus = status, ResponseObject = "OK" }); + } +} + +// ─── DI ─────────────────────────────────────────────────────────────────────── + +/// Rozszerzenia DI dla Adyen. +public static class AdyenProviderExtensions +{ + public static void RegisterAdyenProvider(this IServiceCollection services) + { + services.AddOptions(); + services.ConfigureOptions(); + services.AddTransient(); + } +} + +/// Wczytuje opcje Adyen z konfiguracji. +public class AdyenConfigureOptions : IConfigureOptions +{ + private readonly IConfiguration configuration; + public AdyenConfigureOptions(IConfiguration configuration) => this.configuration = configuration; + public void Configure(AdyenServiceOptions options) + { + var s = configuration.GetSection(AdyenServiceOptions.ConfigurationKey).Get(); + if (s is null) return; + options.ApiKey = s.ApiKey; + options.MerchantAccount = s.MerchantAccount; + options.ClientKey = s.ClientKey; + options.ReturnUrl = s.ReturnUrl; + options.NotificationHmacKey = s.NotificationHmacKey; + options.IsTest = s.IsTest; + } +} diff --git a/src/TailoredApps.Shared.Payments.Provider.Adyen/TailoredApps.Shared.Payments.Provider.Adyen.csproj b/src/TailoredApps.Shared.Payments.Provider.Adyen/TailoredApps.Shared.Payments.Provider.Adyen.csproj new file mode 100644 index 0000000..13dadc1 --- /dev/null +++ b/src/TailoredApps.Shared.Payments.Provider.Adyen/TailoredApps.Shared.Payments.Provider.Adyen.csproj @@ -0,0 +1,32 @@ + + + + build$([System.DateTime]::UtcNow.ToString("yyyyMMddHHmmss")) + net8.0;net9.0;net10.0 + True + True + enable + enable + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/TailoredApps.Shared.Payments.Provider.HotPay/HotPayProvider.cs b/src/TailoredApps.Shared.Payments.Provider.HotPay/HotPayProvider.cs new file mode 100644 index 0000000..f0dfa73 --- /dev/null +++ b/src/TailoredApps.Shared.Payments.Provider.HotPay/HotPayProvider.cs @@ -0,0 +1,173 @@ +using System.Net.Http; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace TailoredApps.Shared.Payments.Provider.HotPay; + +/// Konfiguracja HotPay. Sekcja: Payments:Providers:HotPay. +public class HotPayServiceOptions +{ + public static string ConfigurationKey => "Payments:Providers:HotPay"; + public string SecretHash { get; set; } = string.Empty; + public string ServiceUrl { get; set; } = "https://platnosci.hotpay.pl"; + public string ReturnUrl { get; set; } = string.Empty; + public string NotifyUrl { get; set; } = string.Empty; +} + +file class HotPayRequest +{ + [JsonPropertyName("SEKRET")] public string Secret { get; set; } = string.Empty; + [JsonPropertyName("KWOTA")] public string Amount { get; set; } = string.Empty; + [JsonPropertyName("NAZWA_USLUGI")] public string ServiceName { get; set; } = string.Empty; + [JsonPropertyName("IDENTYFIKATOR_PLATNOSCI")] public string PaymentId { get; set; } = string.Empty; + [JsonPropertyName("ADRES_WWW")] public string ReturnUrl { get; set; } = string.Empty; + [JsonPropertyName("EMAIL")] public string? Email { get; set; } + [JsonPropertyName("HASH")] public string Hash { get; set; } = string.Empty; +} + +file class HotPayResponse +{ + [JsonPropertyName("STATUS")] public string? Status { get; set; } + [JsonPropertyName("PRZEKIERUJ_DO")] public string? RedirectUrl { get; set; } + [JsonPropertyName("ID_PLATNOSCI")] public string? PaymentId { get; set; } +} + +/// Abstrakcja nad HotPay API. +public interface IHotPayServiceCaller +{ + Task<(string? paymentId, string? redirectUrl)> InitPaymentAsync(PaymentRequest request, string paymentId); + bool VerifyNotification(string hash, string kwota, string idPlatnosci, string status); +} + +/// Implementacja . +public class HotPayServiceCaller : IHotPayServiceCaller +{ + private readonly HotPayServiceOptions options; + private readonly IHttpClientFactory httpClientFactory; + + public HotPayServiceCaller(IOptions options, IHttpClientFactory httpClientFactory) + { + this.options = options.Value; + this.httpClientFactory = httpClientFactory; + } + + public async Task<(string? paymentId, string? redirectUrl)> InitPaymentAsync(PaymentRequest request, string paymentId) + { + var amount = request.Amount.ToString("0.00", System.Globalization.CultureInfo.InvariantCulture); + var hashData = $"{options.SecretHash};{amount};{request.Title ?? "Order"};{paymentId};{options.ReturnUrl}"; + var hash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(hashData))).ToLowerInvariant(); + + var body = new HotPayRequest + { + Secret = options.SecretHash, + Amount = amount, + ServiceName = request.Title ?? request.Description ?? "Order", + PaymentId = paymentId, + ReturnUrl = options.ReturnUrl, + Email = request.Email, + Hash = hash, + }; + + using var client = httpClientFactory.CreateClient("HotPay"); + var content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json"); + var response = await client.PostAsync(options.ServiceUrl, content); + var json = await response.Content.ReadAsStringAsync(); + var result = JsonSerializer.Deserialize(json); + return (result?.PaymentId ?? paymentId, result?.RedirectUrl); + } + + public bool VerifyNotification(string hash, string kwota, string idPlatnosci, string status) + { + var data = $"{options.SecretHash};{kwota};{idPlatnosci};{status}"; + var computed = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(data))).ToLowerInvariant(); + return string.Equals(computed, hash, StringComparison.OrdinalIgnoreCase); + } +} + +/// Implementacja dla HotPay. +public class HotPayProvider : IPaymentProvider +{ + private readonly IHotPayServiceCaller caller; + + public HotPayProvider(IHotPayServiceCaller caller) => this.caller = caller; + + public string Key => "HotPay"; + public string Name => "HotPay"; + public string Description => "Operator płatności HotPay — BLIK, karty, przelewy."; + public string Url => "https://hotpay.pl"; + + public Task> GetPaymentChannels(string currency) + { + ICollection channels = + [ + new PaymentChannel { Id = "blik", Name = "BLIK", Description = "BLIK", PaymentModel = PaymentModel.OneTime }, + new PaymentChannel { Id = "card", Name = "Karta płatnicza", Description = "Visa, Mastercard", PaymentModel = PaymentModel.OneTime }, + new PaymentChannel { Id = "transfer", Name = "Przelew online", Description = "Przelew bankowy", PaymentModel = PaymentModel.OneTime }, + ]; + return Task.FromResult(channels); + } + + public async Task RequestPayment(PaymentRequest request) + { + var paymentId = Guid.NewGuid().ToString("N"); + var (resultId, redirectUrl) = await caller.InitPaymentAsync(request, paymentId); + + return new PaymentResponse + { + PaymentUniqueId = resultId, + RedirectUrl = redirectUrl, + PaymentStatus = PaymentStatusEnum.Created, + }; + } + + public Task GetStatus(string paymentId) + => Task.FromResult(new PaymentResponse { PaymentUniqueId = paymentId, PaymentStatus = PaymentStatusEnum.Processing }); + + public Task TransactionStatusChange(TransactionStatusChangePayload payload) + { + var qs = payload.QueryParameters; + var hash = qs.TryGetValue("HASH", out var h) ? h.ToString() : string.Empty; + var kwota = qs.TryGetValue("KWOTA", out var k) ? k.ToString() : string.Empty; + var idPlatnosci = qs.TryGetValue("ID_PLATNOSCI", out var i) ? i.ToString() : string.Empty; + var status = qs.TryGetValue("STATUS", out var s) ? s.ToString() : string.Empty; + + if (!caller.VerifyNotification(hash, kwota, idPlatnosci, status)) + return Task.FromResult(new PaymentResponse { PaymentStatus = PaymentStatusEnum.Rejected, ResponseObject = "Invalid hash" }); + + var payStatus = status == "SUCCESS" ? PaymentStatusEnum.Finished : PaymentStatusEnum.Rejected; + return Task.FromResult(new PaymentResponse { PaymentUniqueId = idPlatnosci, PaymentStatus = payStatus, ResponseObject = "OK" }); + } +} + +/// Rozszerzenia DI dla HotPay. +public static class HotPayProviderExtensions +{ + public static void RegisterHotPayProvider(this IServiceCollection services) + { + services.AddOptions(); + services.ConfigureOptions(); + services.AddHttpClient("HotPay"); + services.AddTransient(); + } +} + +/// Wczytuje opcje HotPay z konfiguracji. +public class HotPayConfigureOptions : IConfigureOptions +{ + private readonly IConfiguration configuration; + public HotPayConfigureOptions(IConfiguration configuration) => this.configuration = configuration; + public void Configure(HotPayServiceOptions options) + { + var s = configuration.GetSection(HotPayServiceOptions.ConfigurationKey).Get(); + if (s is null) return; + options.SecretHash = s.SecretHash; + options.ServiceUrl = s.ServiceUrl; + options.ReturnUrl = s.ReturnUrl; + options.NotifyUrl = s.NotifyUrl; + } +} diff --git a/src/TailoredApps.Shared.Payments.Provider.HotPay/TailoredApps.Shared.Payments.Provider.HotPay.csproj b/src/TailoredApps.Shared.Payments.Provider.HotPay/TailoredApps.Shared.Payments.Provider.HotPay.csproj new file mode 100644 index 0000000..7631338 --- /dev/null +++ b/src/TailoredApps.Shared.Payments.Provider.HotPay/TailoredApps.Shared.Payments.Provider.HotPay.csproj @@ -0,0 +1,33 @@ + + + + build$([System.DateTime]::UtcNow.ToString("yyyyMMddHHmmss")) + net8.0;net9.0;net10.0 + True + True + enable + enable + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/TailoredApps.Shared.Payments.Provider.PayNow/PayNowProvider.cs b/src/TailoredApps.Shared.Payments.Provider.PayNow/PayNowProvider.cs new file mode 100644 index 0000000..5cb0e81 --- /dev/null +++ b/src/TailoredApps.Shared.Payments.Provider.PayNow/PayNowProvider.cs @@ -0,0 +1,228 @@ +using System.Net.Http; +using System.Net.Http.Headers; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace TailoredApps.Shared.Payments.Provider.PayNow; + +/// Konfiguracja PayNow. Sekcja: Payments:Providers:PayNow. +public class PayNowServiceOptions +{ + public static string ConfigurationKey => "Payments:Providers:PayNow"; + public string ApiKey { get; set; } = string.Empty; + public string SignatureKey { get; set; } = string.Empty; + public string ApiUrl { get; set; } = "https://api.paynow.pl"; + public string ReturnUrl { get; set; } = string.Empty; + public string ContinueUrl { get; set; } = string.Empty; +} + +file class PayNowPaymentRequest +{ + [JsonPropertyName("amount")] public long Amount { get; set; } + [JsonPropertyName("currency")] public string Currency { get; set; } = "PLN"; + [JsonPropertyName("externalId")] public string ExternalId { get; set; } = string.Empty; + [JsonPropertyName("description")] public string Description { get; set; } = string.Empty; + [JsonPropertyName("buyer")] public PayNowBuyer? Buyer { get; set; } + [JsonPropertyName("continueUrl")] public string? ContinueUrl { get; set; } + [JsonPropertyName("returnUrl")] public string? ReturnUrl { get; set; } +} + +file class PayNowBuyer +{ + [JsonPropertyName("email")] public string Email { get; set; } = string.Empty; +} + +file class PayNowPaymentResponse +{ + [JsonPropertyName("paymentId")] public string? PaymentId { get; set; } + [JsonPropertyName("status")] public string? Status { get; set; } + [JsonPropertyName("redirectUrl")] public string? RedirectUrl { get; set; } +} + +file class PayNowStatusResponse +{ + [JsonPropertyName("status")] public string? Status { get; set; } +} + +/// Abstrakcja nad PayNow REST API v2. +public interface IPayNowServiceCaller +{ + Task<(string? paymentId, string? redirectUrl)> CreatePaymentAsync(PaymentRequest request); + Task GetPaymentStatusAsync(string paymentId); + bool VerifySignature(string body, string signature); +} + +/// Implementacja . +public class PayNowServiceCaller : IPayNowServiceCaller +{ + private readonly PayNowServiceOptions options; + private readonly IHttpClientFactory httpClientFactory; + + public PayNowServiceCaller(IOptions options, IHttpClientFactory httpClientFactory) + { + this.options = options.Value; + this.httpClientFactory = httpClientFactory; + } + + private HttpClient CreateClient() + { + var client = httpClientFactory.CreateClient("PayNow"); + client.DefaultRequestHeaders.Add("Api-Key", options.ApiKey); + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + return client; + } + + public async Task<(string? paymentId, string? redirectUrl)> CreatePaymentAsync(PaymentRequest request) + { + using var client = CreateClient(); + client.DefaultRequestHeaders.Add("Idempotency-Key", Guid.NewGuid().ToString()); + + var body = new PayNowPaymentRequest + { + Amount = (long)(request.Amount * 100), + Currency = request.Currency.ToUpperInvariant(), + ExternalId = request.AdditionalData ?? Guid.NewGuid().ToString("N"), + Description = request.Title ?? request.Description ?? "Order", + Buyer = new PayNowBuyer { Email = request.Email ?? string.Empty }, + ContinueUrl = options.ContinueUrl, + ReturnUrl = options.ReturnUrl, + }; + + var content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json"); + var response = await client.PostAsync($"{options.ApiUrl}/v2/payments", content); + var json = await response.Content.ReadAsStringAsync(); + var result = JsonSerializer.Deserialize(json); + return (result?.PaymentId, result?.RedirectUrl); + } + + public async Task GetPaymentStatusAsync(string paymentId) + { + using var client = CreateClient(); + var response = await client.GetAsync($"{options.ApiUrl}/v2/payments/{paymentId}/status"); + if (!response.IsSuccessStatusCode) return PaymentStatusEnum.Rejected; + var json = await response.Content.ReadAsStringAsync(); + var status = JsonSerializer.Deserialize(json)?.Status; + return status switch + { + "CONFIRMED" => PaymentStatusEnum.Finished, + "PENDING" => PaymentStatusEnum.Processing, + "PROCESSING" => PaymentStatusEnum.Processing, + "NEW" => PaymentStatusEnum.Created, + "ERROR" => PaymentStatusEnum.Rejected, + "REJECTED" => PaymentStatusEnum.Rejected, + "ABANDONED" => PaymentStatusEnum.Rejected, + _ => PaymentStatusEnum.Created, + }; + } + + public bool VerifySignature(string body, string signature) + { + var keyBytes = Encoding.UTF8.GetBytes(options.SignatureKey); + var dataBytes = Encoding.UTF8.GetBytes(body); + var computed = Convert.ToBase64String(HMACSHA256.HashData(keyBytes, dataBytes)); + return string.Equals(computed, signature, StringComparison.Ordinal); + } +} + +/// Implementacja dla PayNow (mBank). +public class PayNowProvider : IPaymentProvider +{ + private readonly IPayNowServiceCaller caller; + + public PayNowProvider(IPayNowServiceCaller caller) => this.caller = caller; + + public string Key => "PayNow"; + public string Name => "PayNow"; + public string Description => "Operator płatności PayNow (mBank) — BLIK, karty, przelewy."; + public string Url => "https://paynow.pl"; + + public Task> GetPaymentChannels(string currency) + { + ICollection channels = + [ + new PaymentChannel { Id = "BLIK", Name = "BLIK", Description = "BLIK", PaymentModel = PaymentModel.OneTime }, + new PaymentChannel { Id = "CARD", Name = "Karta płatnicza", Description = "Visa, Mastercard", PaymentModel = PaymentModel.OneTime }, + new PaymentChannel { Id = "PBL", Name = "Przelew bankowy", Description = "Pay-by-link", PaymentModel = PaymentModel.OneTime }, + new PaymentChannel { Id = "TRANSFER", Name = "Przelew tradycyjny", Description = "Przelew bankowy", PaymentModel = PaymentModel.OneTime }, + ]; + return Task.FromResult(channels); + } + + public async Task RequestPayment(PaymentRequest request) + { + var (paymentId, redirectUrl) = await caller.CreatePaymentAsync(request); + return new PaymentResponse + { + PaymentUniqueId = paymentId, + RedirectUrl = redirectUrl, + PaymentStatus = PaymentStatusEnum.Created, + }; + } + + public async Task GetStatus(string paymentId) + { + var status = await caller.GetPaymentStatusAsync(paymentId); + return new PaymentResponse { PaymentUniqueId = paymentId, PaymentStatus = status }; + } + + public Task TransactionStatusChange(TransactionStatusChangePayload payload) + { + var body = payload.Payload?.ToString() ?? string.Empty; + var sig = payload.QueryParameters.TryGetValue("Signature", out var s) ? s.ToString() : string.Empty; + + if (!caller.VerifySignature(body, sig)) + return Task.FromResult(new PaymentResponse { PaymentStatus = PaymentStatusEnum.Rejected, ResponseObject = "Invalid signature" }); + + var status = PaymentStatusEnum.Processing; + try + { + var doc = JsonDocument.Parse(body); + if (doc.RootElement.TryGetProperty("status", out var st)) + status = st.GetString() switch + { + "CONFIRMED" => PaymentStatusEnum.Finished, + "ERROR" => PaymentStatusEnum.Rejected, + "REJECTED" => PaymentStatusEnum.Rejected, + "ABANDONED" => PaymentStatusEnum.Rejected, + _ => PaymentStatusEnum.Processing, + }; + } + catch { /* ignore */ } + + return Task.FromResult(new PaymentResponse { PaymentStatus = status, ResponseObject = "OK" }); + } +} + +/// Rozszerzenia DI dla PayNow. +public static class PayNowProviderExtensions +{ + public static void RegisterPayNowProvider(this IServiceCollection services) + { + services.AddOptions(); + services.ConfigureOptions(); + services.AddHttpClient("PayNow"); + services.AddTransient(); + } +} + +/// Wczytuje opcje PayNow z konfiguracji. +public class PayNowConfigureOptions : IConfigureOptions +{ + private readonly IConfiguration configuration; + public PayNowConfigureOptions(IConfiguration configuration) => this.configuration = configuration; + public void Configure(PayNowServiceOptions options) + { + var s = configuration.GetSection(PayNowServiceOptions.ConfigurationKey).Get(); + if (s is null) return; + options.ApiKey = s.ApiKey; + options.SignatureKey = s.SignatureKey; + options.ApiUrl = s.ApiUrl; + options.ReturnUrl = s.ReturnUrl; + options.ContinueUrl = s.ContinueUrl; + } +} diff --git a/src/TailoredApps.Shared.Payments.Provider.PayNow/TailoredApps.Shared.Payments.Provider.PayNow.csproj b/src/TailoredApps.Shared.Payments.Provider.PayNow/TailoredApps.Shared.Payments.Provider.PayNow.csproj new file mode 100644 index 0000000..7631338 --- /dev/null +++ b/src/TailoredApps.Shared.Payments.Provider.PayNow/TailoredApps.Shared.Payments.Provider.PayNow.csproj @@ -0,0 +1,33 @@ + + + + build$([System.DateTime]::UtcNow.ToString("yyyyMMddHHmmss")) + net8.0;net9.0;net10.0 + True + True + enable + enable + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/TailoredApps.Shared.Payments.Provider.PayU/PayUProvider.cs b/src/TailoredApps.Shared.Payments.Provider.PayU/PayUProvider.cs new file mode 100644 index 0000000..973dcbe --- /dev/null +++ b/src/TailoredApps.Shared.Payments.Provider.PayU/PayUProvider.cs @@ -0,0 +1,324 @@ +using System.Net.Http; +using System.Net.Http.Headers; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace TailoredApps.Shared.Payments.Provider.PayU; + +// ─── Options ───────────────────────────────────────────────────────────────── + +/// Konfiguracja PayU. Sekcja: Payments:Providers:PayU. +public class PayUServiceOptions +{ + public static string ConfigurationKey => "Payments:Providers:PayU"; + public string ClientId { get; set; } = string.Empty; + public string ClientSecret { get; set; } = string.Empty; + public string PosId { get; set; } = string.Empty; + public string SignatureKey { get; set; } = string.Empty; + public string ServiceUrl { get; set; } = "https://secure.snd.payu.com"; + public string NotifyUrl { get; set; } = string.Empty; + public string ContinueUrl { get; set; } = string.Empty; +} + +// ─── Internal models ───────────────────────────────────────────────────────── + +file class PayUTokenResponse +{ + [JsonPropertyName("access_token")] public string AccessToken { get; set; } = string.Empty; + [JsonPropertyName("token_type")] public string TokenType { get; set; } = string.Empty; + [JsonPropertyName("expires_in")] public int ExpiresIn { get; set; } +} + +file class PayUOrderRequest +{ + [JsonPropertyName("notifyUrl")] public string NotifyUrl { get; set; } = string.Empty; + [JsonPropertyName("continueUrl")] public string ContinueUrl { get; set; } = string.Empty; + [JsonPropertyName("customerIp")] public string CustomerIp { get; set; } = "127.0.0.1"; + [JsonPropertyName("merchantPosId")]public string MerchantPosId { get; set; } = string.Empty; + [JsonPropertyName("description")] public string Description { get; set; } = string.Empty; + [JsonPropertyName("currencyCode")] public string CurrencyCode { get; set; } = string.Empty; + [JsonPropertyName("totalAmount")] public string TotalAmount { get; set; } = string.Empty; + [JsonPropertyName("buyer")] public PayUBuyer? Buyer { get; set; } + [JsonPropertyName("products")] public List Products { get; set; } = []; +} + +file class PayUBuyer +{ + [JsonPropertyName("email")] public string Email { get; set; } = string.Empty; + [JsonPropertyName("firstName")] public string FirstName { get; set; } = string.Empty; + [JsonPropertyName("lastName")] public string LastName { get; set; } = string.Empty; +} + +file class PayUProduct +{ + [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; + [JsonPropertyName("unitPrice")] public string UnitPrice { get; set; } = string.Empty; + [JsonPropertyName("quantity")] public string Quantity { get; set; } = "1"; +} + +file class PayUOrderResponse +{ + [JsonPropertyName("status")] public PayUStatus? Status { get; set; } + [JsonPropertyName("orderId")] public string? OrderId { get; set; } + [JsonPropertyName("redirectUri")] public string? RedirectUri { get; set; } +} + +file class PayUStatus +{ + [JsonPropertyName("statusCode")] public string? StatusCode { get; set; } +} + +file class PayUStatusResponse +{ + [JsonPropertyName("orders")] public List? Orders { get; set; } +} + +file class PayUOrderDetail +{ + [JsonPropertyName("orderId")] public string? OrderId { get; set; } + [JsonPropertyName("status")] public string? Status { get; set; } +} + +// ─── Interface ──────────────────────────────────────────────────────────────── + +/// Abstrakcja nad PayU REST API v2.1. +public interface IPayUServiceCaller +{ + Task GetAccessTokenAsync(); + Task<(string? orderId, string? redirectUri, string? error)> CreateOrderAsync(string token, PaymentRequest request); + Task GetOrderStatusAsync(string token, string orderId); + bool VerifySignature(string body, string incomingSignature); +} + +// ─── Caller ─────────────────────────────────────────────────────────────────── + +/// Implementacja . +public class PayUServiceCaller : IPayUServiceCaller +{ + private readonly PayUServiceOptions options; + private readonly IHttpClientFactory httpClientFactory; + + public PayUServiceCaller(IOptions options, IHttpClientFactory httpClientFactory) + { + this.options = options.Value; + this.httpClientFactory = httpClientFactory; + } + + public async Task GetAccessTokenAsync() + { + using var client = httpClientFactory.CreateClient("PayU"); + var content = new FormUrlEncodedContent([ + new("grant_type", "client_credentials"), + new("client_id", options.ClientId), + new("client_secret", options.ClientSecret), + ]); + var response = await client.PostAsync($"{options.ServiceUrl}/pl/standard/user/oauth/authorize", content); + response.EnsureSuccessStatusCode(); + var json = await response.Content.ReadAsStringAsync(); + return JsonSerializer.Deserialize(json)?.AccessToken ?? string.Empty; + } + + public async Task<(string? orderId, string? redirectUri, string? error)> CreateOrderAsync(string token, PaymentRequest request) + { + var handler = new HttpClientHandler { AllowAutoRedirect = false }; + using var client = new HttpClient(handler); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + + var amount = ((long)(request.Amount * 100)).ToString(); + var body = new PayUOrderRequest + { + NotifyUrl = options.NotifyUrl, + ContinueUrl = options.ContinueUrl, + MerchantPosId = options.PosId, + Description = request.Title ?? request.Description ?? "Order", + CurrencyCode = request.Currency.ToUpperInvariant(), + TotalAmount = amount, + Buyer = new PayUBuyer { Email = request.Email ?? string.Empty, FirstName = request.FirstName ?? string.Empty, LastName = request.Surname ?? string.Empty }, + Products = [new PayUProduct { Name = request.Title ?? "Product", UnitPrice = amount }], + }; + + var content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json"); + var response = await client.PostAsync($"{options.ServiceUrl}/api/v2_1/orders", content); + var json = await response.Content.ReadAsStringAsync(); + + if (response.StatusCode == System.Net.HttpStatusCode.Found || response.StatusCode == System.Net.HttpStatusCode.Redirect) + { + var location = response.Headers.Location?.ToString(); + var result = JsonSerializer.Deserialize(json); + return (result?.OrderId, location ?? result?.RedirectUri, null); + } + + if (response.IsSuccessStatusCode) + { + var result = JsonSerializer.Deserialize(json); + return (result?.OrderId, result?.RedirectUri, null); + } + + return (null, null, json); + } + + public async Task GetOrderStatusAsync(string token, string orderId) + { + using var client = httpClientFactory.CreateClient("PayU"); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + var response = await client.GetAsync($"{options.ServiceUrl}/api/v2_1/orders/{orderId}"); + if (!response.IsSuccessStatusCode) return PaymentStatusEnum.Rejected; + var json = await response.Content.ReadAsStringAsync(); + var result = JsonSerializer.Deserialize(json); + var status = result?.Orders?.FirstOrDefault()?.Status; + return status switch + { + "COMPLETED" => PaymentStatusEnum.Finished, + "WAITING_FOR_CONFIRMATION" => PaymentStatusEnum.Processing, + "PENDING" => PaymentStatusEnum.Processing, + "CANCELED" => PaymentStatusEnum.Rejected, + "REJECTED" => PaymentStatusEnum.Rejected, + _ => PaymentStatusEnum.Created, + }; + } + + public bool VerifySignature(string body, string incomingSignature) + { + // PayU: OpenPayU-Signature header format: "sender=checkout;signature=;algorithm=MD5;content=DOCUMENT" + var parts = incomingSignature.Split(';') + .Select(p => p.Split('=', 2)) + .Where(p => p.Length == 2) + .ToDictionary(p => p[0].Trim(), p => p[1].Trim()); + + if (!parts.TryGetValue("signature", out var receivedSig)) return false; + var algorithm = parts.GetValueOrDefault("algorithm", "MD5"); + + var data = body + options.SignatureKey; + string computed; + + if (algorithm.Equals("SHA256", StringComparison.OrdinalIgnoreCase) || algorithm.Equals("SHA-256", StringComparison.OrdinalIgnoreCase)) + computed = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(data))).ToLowerInvariant(); + else + computed = Convert.ToHexString(MD5.HashData(Encoding.UTF8.GetBytes(data))).ToLowerInvariant(); + + return string.Equals(computed, receivedSig, StringComparison.OrdinalIgnoreCase); + } +} + +// ─── Provider ───────────────────────────────────────────────────────────────── + +/// Implementacja dla PayU. +public class PayUProvider : IPaymentProvider +{ + private readonly IPayUServiceCaller caller; + + public PayUProvider(IPayUServiceCaller caller) => this.caller = caller; + + public string Key => "PayU"; + public string Name => "PayU"; + public string Description => "Operator płatności PayU — przelewy, BLIK, karty, raty."; + public string Url => "https://payu.pl"; + + public Task> GetPaymentChannels(string currency) + { + ICollection channels = currency.ToUpperInvariant() switch + { + "PLN" => + [ + new PaymentChannel { Id = "blik", Name = "BLIK", Description = "BLIK", PaymentModel = PaymentModel.OneTime }, + new PaymentChannel { Id = "c", Name = "Karta płatnicza", Description = "Visa, Mastercard", PaymentModel = PaymentModel.OneTime }, + new PaymentChannel { Id = "o", Name = "Przelew online", Description = "Pekao, mBank, iPKO", PaymentModel = PaymentModel.OneTime }, + new PaymentChannel { Id = "ai", Name = "Raty", Description = "Raty PayU", PaymentModel = PaymentModel.Installment }, + new PaymentChannel { Id = "ap", Name = "Apple Pay", Description = "Apple Pay", PaymentModel = PaymentModel.OneTime }, + new PaymentChannel { Id = "jp", Name = "Google Pay", Description = "Google Pay", PaymentModel = PaymentModel.OneTime }, + ], + _ => + [ + new PaymentChannel { Id = "c", Name = "Karta płatnicza", Description = "Visa, Mastercard", PaymentModel = PaymentModel.OneTime }, + ], + }; + return Task.FromResult(channels); + } + + public async Task RequestPayment(PaymentRequest request) + { + var token = await caller.GetAccessTokenAsync(); + var (orderId, redirectUri, error) = await caller.CreateOrderAsync(token, request); + + if (orderId is null) + return new PaymentResponse { PaymentStatus = PaymentStatusEnum.Rejected, ResponseObject = error }; + + return new PaymentResponse + { + PaymentUniqueId = orderId, + RedirectUrl = redirectUri, + PaymentStatus = PaymentStatusEnum.Created, + }; + } + + public async Task GetStatus(string paymentId) + { + var token = await caller.GetAccessTokenAsync(); + var status = await caller.GetOrderStatusAsync(token, paymentId); + return new PaymentResponse { PaymentUniqueId = paymentId, PaymentStatus = status }; + } + + public Task TransactionStatusChange(TransactionStatusChangePayload payload) + { + var body = payload.Payload?.ToString() ?? string.Empty; + var sig = payload.QueryParameters.TryGetValue("OpenPayU-Signature", out var s) ? s.ToString() : string.Empty; + + if (!caller.VerifySignature(body, sig)) + return Task.FromResult(new PaymentResponse { PaymentStatus = PaymentStatusEnum.Rejected, ResponseObject = "Invalid signature" }); + + var status = PaymentStatusEnum.Processing; + try + { + var doc = JsonDocument.Parse(body); + if (doc.RootElement.TryGetProperty("order", out var orderEl) && orderEl.TryGetProperty("status", out var st)) + status = st.GetString() switch + { + "COMPLETED" => PaymentStatusEnum.Finished, + "CANCELED" => PaymentStatusEnum.Rejected, + "REJECTED" => PaymentStatusEnum.Rejected, + _ => PaymentStatusEnum.Processing, + }; + } + catch { /* ignore */ } + + return Task.FromResult(new PaymentResponse { PaymentStatus = status, ResponseObject = "OK" }); + } +} + +// ─── DI ─────────────────────────────────────────────────────────────────────── + +/// Rozszerzenia DI dla PayU. +public static class PayUProviderExtensions +{ + public static void RegisterPayUProvider(this IServiceCollection services) + { + services.AddOptions(); + services.ConfigureOptions(); + services.AddHttpClient("PayU"); + services.AddTransient(); + } +} + +/// Wczytuje opcje PayU z konfiguracji. +public class PayUConfigureOptions : IConfigureOptions +{ + private readonly IConfiguration configuration; + public PayUConfigureOptions(IConfiguration configuration) => this.configuration = configuration; + public void Configure(PayUServiceOptions options) + { + var s = configuration.GetSection(PayUServiceOptions.ConfigurationKey).Get(); + if (s is null) return; + options.ClientId = s.ClientId; + options.ClientSecret = s.ClientSecret; + options.PosId = s.PosId; + options.SignatureKey = s.SignatureKey; + options.ServiceUrl = s.ServiceUrl; + options.NotifyUrl = s.NotifyUrl; + options.ContinueUrl = s.ContinueUrl; + } +} diff --git a/src/TailoredApps.Shared.Payments.Provider.PayU/TailoredApps.Shared.Payments.Provider.PayU.csproj b/src/TailoredApps.Shared.Payments.Provider.PayU/TailoredApps.Shared.Payments.Provider.PayU.csproj new file mode 100644 index 0000000..7631338 --- /dev/null +++ b/src/TailoredApps.Shared.Payments.Provider.PayU/TailoredApps.Shared.Payments.Provider.PayU.csproj @@ -0,0 +1,33 @@ + + + + build$([System.DateTime]::UtcNow.ToString("yyyyMMddHHmmss")) + net8.0;net9.0;net10.0 + True + True + enable + enable + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/TailoredApps.Shared.Payments.Provider.Przelewy24/Przelewy24Provider.cs b/src/TailoredApps.Shared.Payments.Provider.Przelewy24/Przelewy24Provider.cs new file mode 100644 index 0000000..1ef4904 --- /dev/null +++ b/src/TailoredApps.Shared.Payments.Provider.Przelewy24/Przelewy24Provider.cs @@ -0,0 +1,290 @@ +using System.Net.Http; +using System.Net.Http.Headers; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace TailoredApps.Shared.Payments.Provider.Przelewy24; + +// ─── Options ───────────────────────────────────────────────────────────────── + +/// Konfiguracja Przelewy24. Sekcja: Payments:Providers:Przelewy24. +public class Przelewy24ServiceOptions +{ + public static string ConfigurationKey => "Payments:Providers:Przelewy24"; + public int MerchantId { get; set; } + public int PosId { get; set; } + public string ApiKey { get; set; } = string.Empty; + public string CrcKey { get; set; } = string.Empty; + public string ServiceUrl { get; set; } = "https://secure.przelewy24.pl"; + public string ReturnUrl { get; set; } = string.Empty; + public string NotifyUrl { get; set; } = string.Empty; +} + +// ─── Internal models ───────────────────────────────────────────────────────── + +file class P24RegisterRequest +{ + [JsonPropertyName("merchantId")] public int MerchantId { get; set; } + [JsonPropertyName("posId")] public int PosId { get; set; } + [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; + [JsonPropertyName("amount")] public long Amount { get; set; } + [JsonPropertyName("currency")] public string Currency { get; set; } = string.Empty; + [JsonPropertyName("description")] public string Description { get; set; } = string.Empty; + [JsonPropertyName("email")] public string Email { get; set; } = string.Empty; + [JsonPropertyName("urlReturn")] public string UrlReturn { get; set; } = string.Empty; + [JsonPropertyName("urlStatus")] public string UrlStatus { get; set; } = string.Empty; + [JsonPropertyName("sign")] public string Sign { get; set; } = string.Empty; + [JsonPropertyName("encoding")] public string Encoding { get; set; } = "UTF-8"; +} + +file class P24RegisterResponse +{ + [JsonPropertyName("data")] public P24RegisterData? Data { get; set; } +} + +file class P24RegisterData +{ + [JsonPropertyName("token")] public string? Token { get; set; } +} + +file class P24VerifyRequest +{ + [JsonPropertyName("merchantId")] public int MerchantId { get; set; } + [JsonPropertyName("posId")] public int PosId { get; set; } + [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; + [JsonPropertyName("amount")] public long Amount { get; set; } + [JsonPropertyName("currency")] public string Currency { get; set; } = string.Empty; + [JsonPropertyName("orderId")] public int OrderId { get; set; } + [JsonPropertyName("sign")] public string Sign { get; set; } = string.Empty; +} + +// ─── Interface ──────────────────────────────────────────────────────────────── + +/// Abstrakcja nad Przelewy24 REST API. +public interface IPrzelewy24ServiceCaller +{ + Task<(string? token, string? error)> RegisterTransactionAsync(PaymentRequest request, string sessionId); + Task VerifyTransactionAsync(string sessionId, long amount, string currency, int orderId); + string ComputeSign(string sessionId, int merchantId, long amount, string currency); + bool VerifyNotification(string body); +} + +// ─── Caller ─────────────────────────────────────────────────────────────────── + +/// Implementacja . +public class Przelewy24ServiceCaller : IPrzelewy24ServiceCaller +{ + private readonly Przelewy24ServiceOptions options; + private readonly IHttpClientFactory httpClientFactory; + + public Przelewy24ServiceCaller(IOptions options, IHttpClientFactory httpClientFactory) + { + this.options = options.Value; + this.httpClientFactory = httpClientFactory; + } + + private HttpClient CreateClient() + { + var client = httpClientFactory.CreateClient("Przelewy24"); + var credentials = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes($"{options.PosId}:{options.ApiKey}")); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", credentials); + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + return client; + } + + public async Task<(string? token, string? error)> RegisterTransactionAsync(PaymentRequest request, string sessionId) + { + using var client = CreateClient(); + var amount = (long)(request.Amount * 100); + var sign = ComputeSign(sessionId, options.MerchantId, amount, request.Currency); + + var body = new P24RegisterRequest + { + MerchantId = options.MerchantId, + PosId = options.PosId, + SessionId = sessionId, + Amount = amount, + Currency = request.Currency.ToUpperInvariant(), + Description = request.Title ?? request.Description ?? "Order", + Email = request.Email ?? string.Empty, + UrlReturn = options.ReturnUrl, + UrlStatus = options.NotifyUrl, + Sign = sign, + }; + + var content = new StringContent(JsonSerializer.Serialize(body), System.Text.Encoding.UTF8, "application/json"); + var response = await client.PostAsync($"{options.ServiceUrl}/api/v1/transaction/register", content); + var json = await response.Content.ReadAsStringAsync(); + var result = JsonSerializer.Deserialize(json); + return (result?.Data?.Token, response.IsSuccessStatusCode ? null : json); + } + + public async Task VerifyTransactionAsync(string sessionId, long amount, string currency, int orderId) + { + using var client = CreateClient(); + var sign = ComputeVerifySign(sessionId, orderId, options.MerchantId, amount, currency); + var body = new P24VerifyRequest + { + MerchantId = options.MerchantId, + PosId = options.PosId, + SessionId = sessionId, + Amount = amount, + Currency = currency.ToUpperInvariant(), + OrderId = orderId, + Sign = sign, + }; + var content = new StringContent(JsonSerializer.Serialize(body), System.Text.Encoding.UTF8, "application/json"); + var response = await client.PutAsync($"{options.ServiceUrl}/api/v1/transaction/verify", content); + return response.IsSuccessStatusCode ? PaymentStatusEnum.Finished : PaymentStatusEnum.Rejected; + } + + public string ComputeSign(string sessionId, int merchantId, long amount, string currency) + { + var json = JsonSerializer.Serialize(new { sessionId, merchantId, amount, currency, crc = options.CrcKey }); + var bytes = SHA384.HashData(System.Text.Encoding.UTF8.GetBytes(json)); + return Convert.ToHexString(bytes).ToLowerInvariant(); + } + + private string ComputeVerifySign(string sessionId, int orderId, int merchantId, long amount, string currency) + { + var json = JsonSerializer.Serialize(new { sessionId, orderId, merchantId, amount, currency, crc = options.CrcKey }); + var bytes = SHA384.HashData(System.Text.Encoding.UTF8.GetBytes(json)); + return Convert.ToHexString(bytes).ToLowerInvariant(); + } + + public bool VerifyNotification(string body) + { + try + { + var doc = JsonDocument.Parse(body); + if (!doc.RootElement.TryGetProperty("sign", out var signEl)) return false; + var receivedSign = signEl.GetString() ?? string.Empty; + + doc.RootElement.TryGetProperty("sessionId", out var sid); + doc.RootElement.TryGetProperty("orderId", out var oid); + doc.RootElement.TryGetProperty("merchantId", out var mid); + doc.RootElement.TryGetProperty("amount", out var amt); + doc.RootElement.TryGetProperty("currency", out var cur); + + var json = JsonSerializer.Serialize(new + { + sessionId = sid.GetString(), + orderId = oid.GetInt32(), + merchantId = mid.GetInt32(), + amount = amt.GetInt64(), + currency = cur.GetString(), + crc = options.CrcKey, + }); + var expected = Convert.ToHexString(SHA384.HashData(System.Text.Encoding.UTF8.GetBytes(json))).ToLowerInvariant(); + return string.Equals(expected, receivedSign, StringComparison.OrdinalIgnoreCase); + } + catch { return false; } + } +} + +// ─── Provider ───────────────────────────────────────────────────────────────── + +/// Implementacja dla Przelewy24. +public class Przelewy24Provider : IPaymentProvider +{ + private readonly IPrzelewy24ServiceCaller caller; + private readonly Przelewy24ServiceOptions options; + + public Przelewy24Provider(IPrzelewy24ServiceCaller caller, IOptions options) + { + this.caller = caller; + this.options = options.Value; + } + + public string Key => "Przelewy24"; + public string Name => "Przelewy24"; + public string Description => "Operator płatności online Przelewy24 — przelewy, BLIK, karty."; + public string Url => "https://przelewy24.pl"; + + public Task> GetPaymentChannels(string currency) + { + ICollection channels = + [ + new PaymentChannel { Id = "online_transfer", Name = "Przelew online", Description = "Wszystkie banki", PaymentModel = PaymentModel.OneTime }, + new PaymentChannel { Id = "blik", Name = "BLIK", Description = "Płatność BLIK", PaymentModel = PaymentModel.OneTime }, + new PaymentChannel { Id = "card", Name = "Karta płatnicza", Description = "Visa, Mastercard", PaymentModel = PaymentModel.OneTime }, + ]; + return Task.FromResult(channels); + } + + public async Task RequestPayment(PaymentRequest request) + { + var sessionId = Guid.NewGuid().ToString("N"); + var (token, error) = await caller.RegisterTransactionAsync(request, sessionId); + + if (token is null) + return new PaymentResponse { PaymentStatus = PaymentStatusEnum.Rejected, ResponseObject = error }; + + return new PaymentResponse + { + PaymentUniqueId = sessionId, + RedirectUrl = $"{options.ServiceUrl}/trnRequest/{token}", + PaymentStatus = PaymentStatusEnum.Created, + }; + } + + public Task GetStatus(string paymentId) + => Task.FromResult(new PaymentResponse { PaymentUniqueId = paymentId, PaymentStatus = PaymentStatusEnum.Processing }); + + public Task TransactionStatusChange(TransactionStatusChangePayload payload) + { + var body = payload.Payload?.ToString() ?? string.Empty; + if (!caller.VerifyNotification(body)) + return Task.FromResult(new PaymentResponse { PaymentStatus = PaymentStatusEnum.Rejected, ResponseObject = "Invalid signature" }); + + PaymentStatusEnum status; + try + { + var doc = JsonDocument.Parse(body); + status = doc.RootElement.TryGetProperty("error", out _) + ? PaymentStatusEnum.Rejected + : PaymentStatusEnum.Finished; + } + catch { status = PaymentStatusEnum.Processing; } + + return Task.FromResult(new PaymentResponse { PaymentStatus = status, ResponseObject = "OK" }); + } +} + +// ─── DI ─────────────────────────────────────────────────────────────────────── + +/// Rozszerzenia DI dla Przelewy24. +public static class Przelewy24ProviderExtensions +{ + public static void RegisterPrzelewy24Provider(this IServiceCollection services) + { + services.AddOptions(); + services.ConfigureOptions(); + services.AddHttpClient("Przelewy24"); + services.AddTransient(); + } +} + +/// Wczytuje opcje Przelewy24 z konfiguracji. +public class Przelewy24ConfigureOptions : IConfigureOptions +{ + private readonly IConfiguration configuration; + public Przelewy24ConfigureOptions(IConfiguration configuration) => this.configuration = configuration; + public void Configure(Przelewy24ServiceOptions options) + { + var s = configuration.GetSection(Przelewy24ServiceOptions.ConfigurationKey).Get(); + if (s is null) return; + options.MerchantId = s.MerchantId; + options.PosId = s.PosId; + options.ApiKey = s.ApiKey; + options.CrcKey = s.CrcKey; + options.ServiceUrl = s.ServiceUrl; + options.ReturnUrl = s.ReturnUrl; + options.NotifyUrl = s.NotifyUrl; + } +} diff --git a/src/TailoredApps.Shared.Payments.Provider.Przelewy24/TailoredApps.Shared.Payments.Provider.Przelewy24.csproj b/src/TailoredApps.Shared.Payments.Provider.Przelewy24/TailoredApps.Shared.Payments.Provider.Przelewy24.csproj new file mode 100644 index 0000000..7631338 --- /dev/null +++ b/src/TailoredApps.Shared.Payments.Provider.Przelewy24/TailoredApps.Shared.Payments.Provider.Przelewy24.csproj @@ -0,0 +1,33 @@ + + + + build$([System.DateTime]::UtcNow.ToString("yyyyMMddHHmmss")) + net8.0;net9.0;net10.0 + True + True + enable + enable + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/TailoredApps.Shared.Payments.Provider.Revolut/RevolutProvider.cs b/src/TailoredApps.Shared.Payments.Provider.Revolut/RevolutProvider.cs new file mode 100644 index 0000000..23ee1c7 --- /dev/null +++ b/src/TailoredApps.Shared.Payments.Provider.Revolut/RevolutProvider.cs @@ -0,0 +1,214 @@ +using System.Net.Http; +using System.Net.Http.Headers; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace TailoredApps.Shared.Payments.Provider.Revolut; + +/// Konfiguracja Revolut. Sekcja: Payments:Providers:Revolut. +public class RevolutServiceOptions +{ + public static string ConfigurationKey => "Payments:Providers:Revolut"; + public string ApiKey { get; set; } = string.Empty; + public string ApiUrl { get; set; } = "https://merchant.revolut.com/api"; + public string ReturnUrl { get; set; } = string.Empty; + public string WebhookSecret { get; set; } = string.Empty; +} + +file class RevolutOrderRequest +{ + [JsonPropertyName("amount")] public long Amount { get; set; } + [JsonPropertyName("currency")] public string Currency { get; set; } = string.Empty; + [JsonPropertyName("description")] public string? Description { get; set; } + [JsonPropertyName("merchant_order_ext_ref")] public string? ExternalRef { get; set; } + [JsonPropertyName("email")] public string? Email { get; set; } +} + +file class RevolutOrderResponse +{ + [JsonPropertyName("id")] public string? Id { get; set; } + [JsonPropertyName("checkout_url")] public string? CheckoutUrl { get; set; } + [JsonPropertyName("state")] public string? State { get; set; } +} + +/// Abstrakcja nad Revolut Merchant API. +public interface IRevolutServiceCaller +{ + Task<(string? id, string? checkoutUrl)> CreateOrderAsync(PaymentRequest request); + Task<(string? state, string? id)> GetOrderAsync(string orderId); + bool VerifyWebhookSignature(string payload, string timestamp, string signature); +} + +/// Implementacja . +public class RevolutServiceCaller : IRevolutServiceCaller +{ + private readonly RevolutServiceOptions options; + private readonly IHttpClientFactory httpClientFactory; + + public RevolutServiceCaller(IOptions options, IHttpClientFactory httpClientFactory) + { + this.options = options.Value; + this.httpClientFactory = httpClientFactory; + } + + private HttpClient CreateClient() + { + var client = httpClientFactory.CreateClient("Revolut"); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", options.ApiKey); + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + return client; + } + + public async Task<(string? id, string? checkoutUrl)> CreateOrderAsync(PaymentRequest request) + { + using var client = CreateClient(); + var body = new RevolutOrderRequest + { + Amount = (long)(request.Amount * 100), + Currency = request.Currency.ToUpperInvariant(), + Description = request.Title ?? request.Description, + ExternalRef = request.AdditionalData ?? Guid.NewGuid().ToString("N"), + Email = request.Email, + }; + var content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json"); + var response = await client.PostAsync($"{options.ApiUrl}/1.0/orders", content); + var json = await response.Content.ReadAsStringAsync(); + var result = JsonSerializer.Deserialize(json); + return (result?.Id, result?.CheckoutUrl); + } + + public async Task<(string? state, string? id)> GetOrderAsync(string orderId) + { + using var client = CreateClient(); + var response = await client.GetAsync($"{options.ApiUrl}/1.0/orders/{orderId}"); + if (!response.IsSuccessStatusCode) return (null, orderId); + var json = await response.Content.ReadAsStringAsync(); + var result = JsonSerializer.Deserialize(json); + return (result?.State, result?.Id); + } + + /// + /// Weryfikuje podpis webhooka Revolut. + /// Format: HMAC-SHA256("v1:{timestamp}.{payload}", webhookSecret). + /// Nagłówek Revolut-Signature: v1=<hex> + /// + public bool VerifyWebhookSignature(string payload, string timestamp, string signature) + { + var signedPayload = $"v1:{timestamp}.{payload}"; + var keyBytes = Encoding.UTF8.GetBytes(options.WebhookSecret); + var dataBytes = Encoding.UTF8.GetBytes(signedPayload); + var computed = Convert.ToHexString(HMACSHA256.HashData(keyBytes, dataBytes)).ToLowerInvariant(); + var receivedHex = signature.StartsWith("v1=") ? signature.Substring(3) : signature; + return string.Equals(computed, receivedHex, StringComparison.OrdinalIgnoreCase); + } +} + +/// Implementacja dla Revolut. +public class RevolutProvider : IPaymentProvider +{ + private readonly IRevolutServiceCaller caller; + + public RevolutProvider(IRevolutServiceCaller caller) => this.caller = caller; + + public string Key => "Revolut"; + public string Name => "Revolut"; + public string Description => "Globalny operator płatności Revolut — karty, Revolut Pay."; + public string Url => "https://revolut.com/business"; + + public Task> GetPaymentChannels(string currency) + { + ICollection channels = + [ + new PaymentChannel { Id = "card", Name = "Karta płatnicza", Description = "Visa, Mastercard, Amex", PaymentModel = PaymentModel.OneTime }, + new PaymentChannel { Id = "revolut_pay", Name = "Revolut Pay", Description = "Płatność Revolut Pay", PaymentModel = PaymentModel.OneTime }, + ]; + return Task.FromResult(channels); + } + + public async Task RequestPayment(PaymentRequest request) + { + var (id, checkoutUrl) = await caller.CreateOrderAsync(request); + return new PaymentResponse + { + PaymentUniqueId = id, + RedirectUrl = checkoutUrl, + PaymentStatus = PaymentStatusEnum.Created, + }; + } + + public async Task GetStatus(string paymentId) + { + var (state, _) = await caller.GetOrderAsync(paymentId); + var status = state switch + { + "completed" => PaymentStatusEnum.Finished, + "processing" => PaymentStatusEnum.Processing, + "authorised" => PaymentStatusEnum.Processing, + "failed" => PaymentStatusEnum.Rejected, + "cancelled" => PaymentStatusEnum.Rejected, + _ => PaymentStatusEnum.Created, + }; + return new PaymentResponse { PaymentUniqueId = paymentId, PaymentStatus = status }; + } + + public Task TransactionStatusChange(TransactionStatusChangePayload payload) + { + var body = payload.Payload?.ToString() ?? string.Empty; + var timestamp = payload.QueryParameters.TryGetValue("Revolut-Request-Timestamp", out var t) ? t.ToString() : string.Empty; + var signature = payload.QueryParameters.TryGetValue("Revolut-Signature", out var s) ? s.ToString() : string.Empty; + + if (!caller.VerifyWebhookSignature(body, timestamp, signature)) + return Task.FromResult(new PaymentResponse { PaymentStatus = PaymentStatusEnum.Rejected, ResponseObject = "Invalid signature" }); + + var status = PaymentStatusEnum.Processing; + try + { + var doc = JsonDocument.Parse(body); + if (doc.RootElement.TryGetProperty("event", out var ev)) + status = ev.GetString() switch + { + "ORDER_COMPLETED" => PaymentStatusEnum.Finished, + "ORDER_AUTHORISED" => PaymentStatusEnum.Processing, + "ORDER_CANCELLED" => PaymentStatusEnum.Rejected, + "PAYMENT_DECLINED" => PaymentStatusEnum.Rejected, + _ => PaymentStatusEnum.Processing, + }; + } + catch { /* ignore */ } + + return Task.FromResult(new PaymentResponse { PaymentStatus = status, ResponseObject = "OK" }); + } +} + +/// Rozszerzenia DI dla Revolut. +public static class RevolutProviderExtensions +{ + public static void RegisterRevolutProvider(this IServiceCollection services) + { + services.AddOptions(); + services.ConfigureOptions(); + services.AddHttpClient("Revolut"); + services.AddTransient(); + } +} + +/// Wczytuje opcje Revolut z konfiguracji. +public class RevolutConfigureOptions : IConfigureOptions +{ + private readonly IConfiguration configuration; + public RevolutConfigureOptions(IConfiguration configuration) => this.configuration = configuration; + public void Configure(RevolutServiceOptions options) + { + var s = configuration.GetSection(RevolutServiceOptions.ConfigurationKey).Get(); + if (s is null) return; + options.ApiKey = s.ApiKey; + options.ApiUrl = s.ApiUrl; + options.ReturnUrl = s.ReturnUrl; + options.WebhookSecret = s.WebhookSecret; + } +} diff --git a/src/TailoredApps.Shared.Payments.Provider.Revolut/TailoredApps.Shared.Payments.Provider.Revolut.csproj b/src/TailoredApps.Shared.Payments.Provider.Revolut/TailoredApps.Shared.Payments.Provider.Revolut.csproj new file mode 100644 index 0000000..7631338 --- /dev/null +++ b/src/TailoredApps.Shared.Payments.Provider.Revolut/TailoredApps.Shared.Payments.Provider.Revolut.csproj @@ -0,0 +1,33 @@ + + + + build$([System.DateTime]::UtcNow.ToString("yyyyMMddHHmmss")) + net8.0;net9.0;net10.0 + True + True + enable + enable + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/TailoredApps.Shared.Payments.Provider.Tpay/TailoredApps.Shared.Payments.Provider.Tpay.csproj b/src/TailoredApps.Shared.Payments.Provider.Tpay/TailoredApps.Shared.Payments.Provider.Tpay.csproj new file mode 100644 index 0000000..7631338 --- /dev/null +++ b/src/TailoredApps.Shared.Payments.Provider.Tpay/TailoredApps.Shared.Payments.Provider.Tpay.csproj @@ -0,0 +1,33 @@ + + + + build$([System.DateTime]::UtcNow.ToString("yyyyMMddHHmmss")) + net8.0;net9.0;net10.0 + True + True + enable + enable + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/TailoredApps.Shared.Payments.Provider.Tpay/TpayProvider.cs b/src/TailoredApps.Shared.Payments.Provider.Tpay/TpayProvider.cs new file mode 100644 index 0000000..2ba2fbd --- /dev/null +++ b/src/TailoredApps.Shared.Payments.Provider.Tpay/TpayProvider.cs @@ -0,0 +1,269 @@ +using System.Net.Http; +using System.Net.Http.Headers; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace TailoredApps.Shared.Payments.Provider.Tpay; + +public class TpayServiceOptions +{ + public static string ConfigurationKey => "Payments:Providers:Tpay"; + public string ClientId { get; set; } = string.Empty; + public string ClientSecret { get; set; } = string.Empty; + public string MerchantId { get; set; } = string.Empty; + public string ApiUrl { get; set; } = "https://api.tpay.com"; + public string ReturnUrl { get; set; } = string.Empty; + public string NotifyUrl { get; set; } = string.Empty; + public string SecurityCode { get; set; } = string.Empty; +} + +file class TpayTokenResponse +{ + [JsonPropertyName("access_token")] public string AccessToken { get; set; } = string.Empty; +} + +file class TpayTransactionRequest +{ + [JsonPropertyName("amount")] public decimal Amount { get; set; } + [JsonPropertyName("description")] public string Description { get; set; } = string.Empty; + [JsonPropertyName("hiddenDescription")] public string? HiddenDescription { get; set; } + [JsonPropertyName("lang")] public string Lang { get; set; } = "pl"; + [JsonPropertyName("pay")] public TpayPay Pay { get; set; } = new(); + [JsonPropertyName("payer")] public TpayPayer Payer { get; set; } = new(); + [JsonPropertyName("callbacks")] public TpayCallbacks Callbacks { get; set; } = new(); +} + +file class TpayPay +{ + [JsonPropertyName("groupId")] public int? GroupId { get; set; } + [JsonPropertyName("channel")] public string? Channel { get; set; } +} + +file class TpayPayer +{ + [JsonPropertyName("email")] public string Email { get; set; } = string.Empty; + [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; +} + +file class TpayCallbacks +{ + [JsonPropertyName("payerUrls")] public TpayPayerUrls PayerUrls { get; set; } = new(); + [JsonPropertyName("notification")] public TpayNotification Notification { get; set; } = new(); +} + +file class TpayPayerUrls +{ + [JsonPropertyName("success")] public string Success { get; set; } = string.Empty; + [JsonPropertyName("error")] public string Error { get; set; } = string.Empty; +} + +file class TpayNotification +{ + [JsonPropertyName("url")] public string Url { get; set; } = string.Empty; + [JsonPropertyName("email")] public string Email { get; set; } = string.Empty; +} + +file class TpayTransactionResponse +{ + [JsonPropertyName("transactionId")] public string? TransactionId { get; set; } + [JsonPropertyName("transactionPaymentUrl")] public string? PaymentUrl { get; set; } + [JsonPropertyName("title")] public string? Title { get; set; } +} + +file class TpayStatusResponse +{ + [JsonPropertyName("status")] public string? Status { get; set; } +} + +/// Abstrakcja nad Tpay REST API. +public interface ITpayServiceCaller +{ + Task GetAccessTokenAsync(); + Task<(string? transactionId, string? paymentUrl)> CreateTransactionAsync(string token, PaymentRequest request); + Task GetTransactionStatusAsync(string token, string transactionId); + bool VerifyNotification(string body, string signature); +} + +/// Implementacja . +public class TpayServiceCaller : ITpayServiceCaller +{ + private readonly TpayServiceOptions options; + private readonly IHttpClientFactory httpClientFactory; + + public TpayServiceCaller(IOptions options, IHttpClientFactory httpClientFactory) + { + this.options = options.Value; + this.httpClientFactory = httpClientFactory; + } + + public async Task GetAccessTokenAsync() + { + using var client = httpClientFactory.CreateClient("Tpay"); + var content = new FormUrlEncodedContent([ + new("grant_type", "client_credentials"), + new("client_id", options.ClientId), + new("client_secret", options.ClientSecret), + ]); + var response = await client.PostAsync($"{options.ApiUrl}/oauth/auth", content); + response.EnsureSuccessStatusCode(); + var json = await response.Content.ReadAsStringAsync(); + return JsonSerializer.Deserialize(json)?.AccessToken ?? string.Empty; + } + + public async Task<(string? transactionId, string? paymentUrl)> CreateTransactionAsync(string token, PaymentRequest request) + { + using var client = httpClientFactory.CreateClient("Tpay"); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + + var body = new TpayTransactionRequest + { + Amount = request.Amount, + Description = request.Title ?? request.Description ?? "Order", + Payer = new TpayPayer { Email = request.Email ?? string.Empty, Name = $"{request.FirstName} {request.Surname}".Trim() }, + Callbacks = new TpayCallbacks + { + PayerUrls = new TpayPayerUrls { Success = options.ReturnUrl, Error = options.ReturnUrl }, + Notification = new TpayNotification { Url = options.NotifyUrl, Email = request.Email ?? string.Empty }, + }, + }; + + if (!string.IsNullOrWhiteSpace(request.PaymentChannel)) + body.Pay = new TpayPay { Channel = request.PaymentChannel }; + + var content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json"); + var response = await client.PostAsync($"{options.ApiUrl}/transactions", content); + var json = await response.Content.ReadAsStringAsync(); + var tx = JsonSerializer.Deserialize(json); + return (tx?.TransactionId, tx?.PaymentUrl); + } + + public async Task GetTransactionStatusAsync(string token, string transactionId) + { + using var client = httpClientFactory.CreateClient("Tpay"); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + var response = await client.GetAsync($"{options.ApiUrl}/transactions/{transactionId}"); + if (!response.IsSuccessStatusCode) return PaymentStatusEnum.Rejected; + var json = await response.Content.ReadAsStringAsync(); + var status = JsonSerializer.Deserialize(json)?.Status; + return status switch + { + "correct" => PaymentStatusEnum.Finished, + "pending" => PaymentStatusEnum.Processing, + "error" => PaymentStatusEnum.Rejected, + "chargeback" => PaymentStatusEnum.Rejected, + _ => PaymentStatusEnum.Created, + }; + } + + public bool VerifyNotification(string body, string signature) + { + var input = body + options.SecurityCode; + var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input)); + var computed = Convert.ToHexString(hash).ToLowerInvariant(); + return string.Equals(computed, signature, StringComparison.OrdinalIgnoreCase); + } +} + +/// Implementacja dla Tpay. +public class TpayProvider : IPaymentProvider +{ + private readonly ITpayServiceCaller caller; + + public TpayProvider(ITpayServiceCaller caller) => this.caller = caller; + + public string Key => "Tpay"; + public string Name => "Tpay"; + public string Description => "Operator płatności Tpay — przelewy, BLIK, karty."; + public string Url => "https://tpay.com"; + + public Task> GetPaymentChannels(string currency) + { + ICollection channels = + [ + new PaymentChannel { Id = "150", Name = "BLIK", Description = "BLIK", PaymentModel = PaymentModel.OneTime }, + new PaymentChannel { Id = "103", Name = "Karta płatnicza", Description = "Visa, Mastercard", PaymentModel = PaymentModel.OneTime }, + new PaymentChannel { Id = "21", Name = "Przelew online", Description = "mTransfer, iPKO", PaymentModel = PaymentModel.OneTime }, + ]; + return Task.FromResult(channels); + } + + public async Task RequestPayment(PaymentRequest request) + { + var token = await caller.GetAccessTokenAsync(); + var (transactionId, paymentUrl) = await caller.CreateTransactionAsync(token, request); + return new PaymentResponse + { + PaymentUniqueId = transactionId, + RedirectUrl = paymentUrl, + PaymentStatus = PaymentStatusEnum.Created, + }; + } + + public async Task GetStatus(string paymentId) + { + var token = await caller.GetAccessTokenAsync(); + var status = await caller.GetTransactionStatusAsync(token, paymentId); + return new PaymentResponse { PaymentUniqueId = paymentId, PaymentStatus = status }; + } + + public Task TransactionStatusChange(TransactionStatusChangePayload payload) + { + var body = payload.Payload?.ToString() ?? string.Empty; + var sig = payload.QueryParameters.TryGetValue("X-Signature", out var s) ? s.ToString() : string.Empty; + + if (!caller.VerifyNotification(body, sig)) + return Task.FromResult(new PaymentResponse { PaymentStatus = PaymentStatusEnum.Rejected, ResponseObject = "Invalid signature" }); + + var status = PaymentStatusEnum.Processing; + try + { + var doc = JsonDocument.Parse(body); + if (doc.RootElement.TryGetProperty("tr_status", out var st)) + status = st.GetString() switch + { + "TRUE" => PaymentStatusEnum.Finished, + "FALSE" => PaymentStatusEnum.Rejected, + _ => PaymentStatusEnum.Processing, + }; + } + catch { /* ignore */ } + + return Task.FromResult(new PaymentResponse { PaymentStatus = status, ResponseObject = "OK" }); + } +} + +/// Rozszerzenia DI dla Tpay. +public static class TpayProviderExtensions +{ + public static void RegisterTpayProvider(this IServiceCollection services) + { + services.AddOptions(); + services.ConfigureOptions(); + services.AddHttpClient("Tpay"); + services.AddTransient(); + } +} + +/// Wczytuje opcje Tpay z konfiguracji. +public class TpayConfigureOptions : IConfigureOptions +{ + private readonly IConfiguration configuration; + public TpayConfigureOptions(IConfiguration configuration) => this.configuration = configuration; + public void Configure(TpayServiceOptions options) + { + var s = configuration.GetSection(TpayServiceOptions.ConfigurationKey).Get(); + if (s is null) return; + options.ClientId = s.ClientId; + options.ClientSecret = s.ClientSecret; + options.MerchantId = s.MerchantId; + options.ApiUrl = s.ApiUrl; + options.ReturnUrl = s.ReturnUrl; + options.NotifyUrl = s.NotifyUrl; + options.SecurityCode = s.SecurityCode; + } +} diff --git a/tests/TailoredApps.Shared.Payments.Tests/MultiProviderPaymentTest.cs b/tests/TailoredApps.Shared.Payments.Tests/MultiProviderPaymentTest.cs new file mode 100644 index 0000000..ce5a245 --- /dev/null +++ b/tests/TailoredApps.Shared.Payments.Tests/MultiProviderPaymentTest.cs @@ -0,0 +1,264 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Primitives; +using TailoredApps.Shared.Payments; +using TailoredApps.Shared.Payments.Provider.Adyen; +using TailoredApps.Shared.Payments.Provider.HotPay; +using TailoredApps.Shared.Payments.Provider.PayNow; +using TailoredApps.Shared.Payments.Provider.PayU; +using TailoredApps.Shared.Payments.Provider.Przelewy24; +using TailoredApps.Shared.Payments.Provider.Revolut; +using TailoredApps.Shared.Payments.Provider.Tpay; +using Xunit; + +namespace TailoredApps.Shared.Payments.Tests; + +/// +/// Smoke testy dla 7 nowych providerów płatności. +/// Testy jednostkowe — nie wymagają połączenia sieciowego. +/// +public class MultiProviderPaymentTest +{ + private static IHost BuildHost() => + Host.CreateDefaultBuilder() + .ConfigureAppConfiguration(cfg => cfg.AddJsonFile("appsettings.json", optional: true)) + .ConfigureServices((_, services) => + { + services.RegisterAdyenProvider(); + services.RegisterPayUProvider(); + services.RegisterPrzelewy24Provider(); + services.RegisterTpayProvider(); + services.RegisterHotPayProvider(); + services.RegisterPayNowProvider(); + services.RegisterRevolutProvider(); + + services.AddPayments() + .RegisterPaymentProvider() + .RegisterPaymentProvider() + .RegisterPaymentProvider() + .RegisterPaymentProvider() + .RegisterPaymentProvider() + .RegisterPaymentProvider() + .RegisterPaymentProvider(); + }) + .Build(); + + // ─── Provider metadata ──────────────────────────────────────────────────── + + [Fact] + public async Task AllProviders_AreRegistered() + { + var host = BuildHost(); + var service = host.Services.GetRequiredService(); + var providers = await service.GetProviders(); + var ids = providers.Select(p => p.Id).ToList(); + + Assert.Contains("Adyen", ids); + Assert.Contains("PayU", ids); + Assert.Contains("Przelewy24", ids); + Assert.Contains("Tpay", ids); + Assert.Contains("HotPay", ids); + Assert.Contains("PayNow", ids); + Assert.Contains("Revolut", ids); + } + + // ─── GetChannels ────────────────────────────────────────────────────────── + + [Theory] + [InlineData("Adyen", "PLN")] + [InlineData("PayU", "PLN")] + [InlineData("Przelewy24", "PLN")] + [InlineData("Tpay", "PLN")] + [InlineData("HotPay", "PLN")] + [InlineData("PayNow", "PLN")] + [InlineData("Revolut", "PLN")] + public async Task GetChannels_PLN_ReturnsNonEmptyList(string providerKey, string currency) + { + var host = BuildHost(); + var service = host.Services.GetRequiredService(); + var channels = await service.GetChannels(providerKey, currency); + Assert.NotEmpty(channels); + } + + [Fact] + public async Task Adyen_GetChannels_EUR_ContainsIdeal() + { + var host = BuildHost(); + var service = host.Services.GetRequiredService(); + var channels = await service.GetChannels("Adyen", "EUR"); + Assert.Contains(channels, c => c.Id == "ideal"); + } + + [Fact] + public async Task PayU_GetChannels_PLN_ContainsBlik() + { + var host = BuildHost(); + var service = host.Services.GetRequiredService(); + var channels = await service.GetChannels("PayU", "PLN"); + Assert.Contains(channels, c => c.Id == "blik"); + } + + [Fact] + public async Task Revolut_GetChannels_ContainsRevolutPay() + { + var host = BuildHost(); + var service = host.Services.GetRequiredService(); + var channels = await service.GetChannels("Revolut", "PLN"); + Assert.Contains(channels, c => c.Id == "revolut_pay"); + } + + [Fact] + public async Task PayNow_GetChannels_ContainsBlikAndCard() + { + var host = BuildHost(); + var service = host.Services.GetRequiredService(); + var channels = await service.GetChannels("PayNow", "PLN"); + Assert.Contains(channels, c => c.Id == "BLIK"); + Assert.Contains(channels, c => c.Id == "CARD"); + } + + [Fact] + public async Task Przelewy24_GetChannels_ContainsOnlineTransfer() + { + var host = BuildHost(); + var service = host.Services.GetRequiredService(); + var channels = await service.GetChannels("Przelewy24", "PLN"); + Assert.Contains(channels, c => c.Id == "online_transfer"); + } + + // ─── Provider info ──────────────────────────────────────────────────────── + + [Theory] + [InlineData("Adyen", "https://www.adyen.com")] + [InlineData("PayU", "https://payu.pl")] + [InlineData("Przelewy24", "https://przelewy24.pl")] + [InlineData("Tpay", "https://tpay.com")] + [InlineData("HotPay", "https://hotpay.pl")] + [InlineData("PayNow", "https://paynow.pl")] + [InlineData("Revolut", "https://revolut.com/business")] + public async Task Provider_HasCorrectUrl(string providerKey, string expectedUrl) + { + var host = BuildHost(); + var service = host.Services.GetRequiredService(); + var providers = await service.GetProviders(); + var provider = providers.Single(p => p.Id == providerKey); + Assert.Equal(expectedUrl, provider.Url); + } + + // ─── Invalid webhook → Rejected ────────────────────────────────────────── + + [Theory] + [InlineData("PayU")] + [InlineData("Przelewy24")] + [InlineData("Tpay")] + [InlineData("PayNow")] + [InlineData("Revolut")] + [InlineData("Adyen")] + public async Task TransactionStatusChange_InvalidSignature_ReturnsRejected(string providerKey) + { + var host = BuildHost(); + var service = host.Services.GetRequiredService(); + + var result = await service.TransactionStatusChange(providerKey, new TransactionStatusChangePayload + { + ProviderId = providerKey, + Payload = """{"status":"CONFIRMED","orderId":"test_123"}""", + QueryParameters = new Dictionary + { + { "OpenPayU-Signature", new StringValues("sender=checkout;signature=invalid;algorithm=MD5;content=DOCUMENT") }, + { "Signature", new StringValues("invalidsignature") }, + { "X-Signature", new StringValues("invalidsignature") }, + { "HmacSignature", new StringValues("invalidsignature") }, + { "Revolut-Signature", new StringValues("v1=invalidsignature") }, + { "Revolut-Request-Timestamp", new StringValues("1234567890") }, + }, + }); + + Assert.Equal(PaymentStatusEnum.Rejected, result.PaymentStatus); + } + + [Fact] + public async Task HotPay_TransactionStatusChange_InvalidHash_ReturnsRejected() + { + var host = BuildHost(); + var service = host.Services.GetRequiredService(); + + var result = await service.TransactionStatusChange("HotPay", new TransactionStatusChangePayload + { + ProviderId = "HotPay", + Payload = string.Empty, + QueryParameters = new Dictionary + { + { "HASH", new StringValues("invalidhash") }, + { "KWOTA", new StringValues("9.99") }, + { "ID_PLATNOSCI", new StringValues("test_123") }, + { "STATUS", new StringValues("SUCCESS") }, + }, + }); + + Assert.Equal(PaymentStatusEnum.Rejected, result.PaymentStatus); + } + + // ─── GetStatus (offline) ────────────────────────────────────────────────── + + [Fact] + public async Task HotPay_GetStatus_ReturnsProcessing() + { + var host = BuildHost(); + var service = host.Services.GetRequiredService(); + var result = await service.GetStatus("HotPay", "test-payment-id"); + Assert.Equal(PaymentStatusEnum.Processing, result.PaymentStatus); + } + + [Fact] + public async Task Przelewy24_GetStatus_ReturnsProcessing() + { + var host = BuildHost(); + var service = host.Services.GetRequiredService(); + var result = await service.GetStatus("Przelewy24", "test-session-id"); + Assert.Equal(PaymentStatusEnum.Processing, result.PaymentStatus); + } + + // ─── Integration tests (Skip by default) ───────────────────────────────── + + [Fact(Skip = "Integration — requires real PayU sandbox credentials in appsettings.json")] + public async Task PayU_RequestPayment_CreatesOrder() + { + var host = BuildHost(); + var service = host.Services.GetRequiredService(); + var result = await service.RegisterPayment(new PaymentRequest + { + PaymentProvider = "PayU", + PaymentChannel = "c", + PaymentModel = PaymentModel.OneTime, + Title = "Test order", + Currency = "PLN", + Amount = 1.00m, + Email = "test@example.com", + }); + Assert.Equal(PaymentStatusEnum.Created, result.PaymentStatus); + Assert.NotEmpty(result.RedirectUrl!); + } + + [Fact(Skip = "Integration — requires real Revolut sandbox credentials in appsettings.json")] + public async Task Revolut_RequestPayment_CreatesOrder() + { + var host = BuildHost(); + var service = host.Services.GetRequiredService(); + var result = await service.RegisterPayment(new PaymentRequest + { + PaymentProvider = "Revolut", + PaymentChannel = "card", + PaymentModel = PaymentModel.OneTime, + Title = "Test order", + Currency = "PLN", + Amount = 1.00m, + Email = "test@example.com", + }); + Assert.Equal(PaymentStatusEnum.Created, result.PaymentStatus); + } +} diff --git a/tests/TailoredApps.Shared.Payments.Tests/TailoredApps.Shared.Payments.Tests.csproj b/tests/TailoredApps.Shared.Payments.Tests/TailoredApps.Shared.Payments.Tests.csproj index 79ceb45..eac5f76 100644 --- a/tests/TailoredApps.Shared.Payments.Tests/TailoredApps.Shared.Payments.Tests.csproj +++ b/tests/TailoredApps.Shared.Payments.Tests/TailoredApps.Shared.Payments.Tests.csproj @@ -22,6 +22,13 @@ + + + + + + + diff --git a/tests/TailoredApps.Shared.Payments.Tests/appsettings.json b/tests/TailoredApps.Shared.Payments.Tests/appsettings.json index b90b291..46599bc 100644 --- a/tests/TailoredApps.Shared.Payments.Tests/appsettings.json +++ b/tests/TailoredApps.Shared.Payments.Tests/appsettings.json @@ -13,6 +13,60 @@ "WebhookSecret": "whsec_REPLACE_ME", "SuccessUrl": "https://integrationtest.tailoredapps.pl/payment/success?session={CHECKOUT_SESSION_ID}", "CancelUrl": "https://integrationtest.tailoredapps.pl/payment/cancel" + }, + "Adyen": { + "ApiKey": "REPLACE_ME", + "MerchantAccount": "REPLACE_ME", + "ClientKey": "test_REPLACE_ME", + "ReturnUrl": "https://integrationtest.tailoredapps.pl/payment/return", + "NotificationHmacKey": "REPLACE_ME", + "IsTest": true + }, + "PayU": { + "ClientId": "REPLACE_ME", + "ClientSecret": "REPLACE_ME", + "PosId": "REPLACE_ME", + "SignatureKey": "REPLACE_ME", + "ServiceUrl": "https://secure.snd.payu.com", + "NotifyUrl": "https://integrationtest.tailoredapps.pl/payment/notify/payu", + "ContinueUrl": "https://integrationtest.tailoredapps.pl/payment/return" + }, + "Przelewy24": { + "MerchantId": 0, + "PosId": 0, + "ApiKey": "REPLACE_ME", + "CrcKey": "REPLACE_ME", + "ServiceUrl": "https://sandbox.przelewy24.pl", + "ReturnUrl": "https://integrationtest.tailoredapps.pl/payment/return", + "NotifyUrl": "https://integrationtest.tailoredapps.pl/payment/notify/p24" + }, + "Tpay": { + "ClientId": "REPLACE_ME", + "ClientSecret": "REPLACE_ME", + "MerchantId": "REPLACE_ME", + "ApiUrl": "https://openapi.sandbox.tpay.com", + "ReturnUrl": "https://integrationtest.tailoredapps.pl/payment/return", + "NotifyUrl": "https://integrationtest.tailoredapps.pl/payment/notify/tpay", + "SecurityCode": "REPLACE_ME" + }, + "HotPay": { + "SecretHash": "REPLACE_ME", + "ServiceUrl": "https://platnosci.hotpay.pl", + "ReturnUrl": "https://integrationtest.tailoredapps.pl/payment/return", + "NotifyUrl": "https://integrationtest.tailoredapps.pl/payment/notify/hotpay" + }, + "PayNow": { + "ApiKey": "REPLACE_ME", + "SignatureKey": "REPLACE_ME", + "ApiUrl": "https://api.sandbox.paynow.pl", + "ReturnUrl": "https://integrationtest.tailoredapps.pl/payment/return", + "ContinueUrl": "https://integrationtest.tailoredapps.pl/payment/return" + }, + "Revolut": { + "ApiKey": "REPLACE_ME", + "ApiUrl": "https://sandbox-merchant.revolut.com/api", + "ReturnUrl": "https://integrationtest.tailoredapps.pl/payment/return", + "WebhookSecret": "REPLACE_ME" } } } From c3f9b844c9a90c934776ab7317f8be75703a48d0 Mon Sep 17 00:00:00 2001 From: Szeregowy Date: Tue, 24 Mar 2026 15:35:12 +0100 Subject: [PATCH 2/5] fix: compile errors, CS1591 XML docs, CS0108 new keyword MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Błędy kompilacji: - AdyenProvider.cs: usunięto Adyen SDK (Adyen.Model.Enum.Environment nie istnieje w v14); zastąpiono czystym HttpClient → Adyen Checkout REST API v71/sessions - Adyen.csproj: usunięto pakiet Adyen v14, dodano Microsoft.Extensions.Http - PayUProvider.cs: PaymentModel.Installment → PaymentModel.OneTime (enum nie ma Installment) CS1591 XML docs (pre-existing, brak komentarzy): - TailoredApps.Shared.Email.Models/MailMessage.cs - TailoredApps.Shared.MediatR.Caching/ICachableRequest.cs - TailoredApps.Shared.MediatR/Caching/Cache.cs - TailoredApps.Shared.MediatR/DI/PipelineRegistration.cs - TailoredApps.Shared.MediatR/Interfaces/Caching/ICache.cs - TailoredApps.Shared.MediatR/Interfaces/Caching/ICachePolicy.cs - TailoredApps.Shared.MediatR/Interfaces/DI/IPipelineRegistration.cs - TailoredApps.Shared.MediatR/Interfaces/Handlers/IFallbackHandler.cs - TailoredApps.Shared.MediatR/Interfaces/Messages/IRetryableRequest.cs - TailoredApps.Shared.MediatR/PipelineBehaviours/FallbackBehavior.cs - TailoredApps.Shared.MediatR/PipelineBehaviours/LoggingBehavior.cs - TailoredApps.Shared.MediatR/PipelineBehaviours/RetryBehavior.cs - TailoredApps.Shared.MediatR/PipelineBehaviours/ValidationBehavior.cs - TailoredApps.Shared.Querying/IPagedResult.cs - TailoredApps.Shared.Querying/PagedAndSortedQuery.cs (wszystkie interfejsy) - TailoredApps.Shared.Querying/QueryBase.cs - TailoredApps.Shared.Querying/QueryMap.cs - TailoredApps.Shared.Querying/SortDirection.cs CS0108 new keyword (PagedAndSortedQuery.cs): - IPagedAndSortedQuery: Page, Count, IsPagingSpecified, SortField, SortDir, IsSortingSpecified, Filter teraz mają słowo kluczowe new --- .../MailMessage.cs | 50 ++-- .../ICachableRequest.cs | 21 +- .../Caching/Cache.cs | 85 +++---- .../DI/PipelineRegistration.cs | 118 +++++----- .../Interfaces/Caching/ICache.cs | 39 ++-- .../Interfaces/Caching/ICachePolicy.cs | 55 +++-- .../Interfaces/DI/IPipelineRegistration.cs | 25 +- .../Interfaces/Handlers/IFallbackHandler.cs | 28 ++- .../Interfaces/Messages/IRetryableRequest.cs | 38 +-- .../PipelineBehaviours/FallbackBehavior.cs | 109 ++++----- .../PipelineBehaviours/LoggingBehavior.cs | 96 ++++---- .../PipelineBehaviours/RetryBehavior.cs | 133 ++++++----- .../PipelineBehaviours/ValidationBehavior.cs | 81 ++++--- .../AdyenProvider.cs | 220 ++++++++++++------ ...Apps.Shared.Payments.Provider.Adyen.csproj | 5 +- .../PayUProvider.cs | 2 +- .../IPagedResult.cs | 24 +- .../PagedAndSortedQuery.cs | 169 +++++++++----- src/TailoredApps.Shared.Querying/QueryBase.cs | 13 +- src/TailoredApps.Shared.Querying/QueryMap.cs | 45 ++-- .../SortDirection.cs | 24 +- 21 files changed, 819 insertions(+), 561 deletions(-) diff --git a/src/TailoredApps.Shared.Email.Models/MailMessage.cs b/src/TailoredApps.Shared.Email.Models/MailMessage.cs index 0ea3d00..bd5ad6d 100644 --- a/src/TailoredApps.Shared.Email.Models/MailMessage.cs +++ b/src/TailoredApps.Shared.Email.Models/MailMessage.cs @@ -1,17 +1,33 @@ -using System; -using System.Collections.Generic; - -namespace TailoredApps.Shared.Email.Models -{ - public class MailMessage - { - public string Topic { get; set; } - public string Sender { get; set; } - public string Recipent { get; set; } - public string Copy { get; set; } - public string Body { get; set; } - public string HtmlBody { get; set; } - public Dictionary Attachements { get; set; } - public DateTimeOffset Date { get; set; } - } -} +using System; +using System.Collections.Generic; + +namespace TailoredApps.Shared.Email.Models +{ + /// Model wiadomości e-mail. + public class MailMessage + { + /// Temat wiadomości. + public string Topic { get; set; } + + /// Nadawca wiadomości. + public string Sender { get; set; } + + /// Odbiorca wiadomości. + public string Recipent { get; set; } + + /// Kopia CC wiadomości. + public string Copy { get; set; } + + /// Treść wiadomości (plain text). + public string Body { get; set; } + + /// Treść wiadomości (HTML). + public string HtmlBody { get; set; } + + /// Załączniki: nazwa pliku → zawartość Base64. + public Dictionary Attachements { get; set; } + + /// Data wysłania wiadomości. + public DateTimeOffset Date { get; set; } + } +} diff --git a/src/TailoredApps.Shared.MediatR.Caching/ICachableRequest.cs b/src/TailoredApps.Shared.MediatR.Caching/ICachableRequest.cs index 42a7bdd..669a9f1 100644 --- a/src/TailoredApps.Shared.MediatR.Caching/ICachableRequest.cs +++ b/src/TailoredApps.Shared.MediatR.Caching/ICachableRequest.cs @@ -1,9 +1,12 @@ -using MediatR; - -namespace TailoredApps.Shared.MediatR.Caching -{ - public interface ICachableRequest : IRequest - { - string GetCacheKey(); - } -} +using MediatR; + +namespace TailoredApps.Shared.MediatR.Caching +{ + /// Marker interfejsu dla żądań MediatR, których wynik może być cachowany. + /// Typ odpowiedzi. + public interface ICachableRequest : IRequest + { + /// Zwraca klucz cache dla tego żądania. + string GetCacheKey(); + } +} diff --git a/src/TailoredApps.Shared.MediatR/Caching/Cache.cs b/src/TailoredApps.Shared.MediatR/Caching/Cache.cs index 7958d7c..35f5e07 100644 --- a/src/TailoredApps.Shared.MediatR/Caching/Cache.cs +++ b/src/TailoredApps.Shared.MediatR/Caching/Cache.cs @@ -1,39 +1,46 @@ -using Microsoft.Extensions.Caching.Distributed; -using Newtonsoft.Json; -using System; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using TailoredApps.Shared.MediatR.Interfaces.Caching; - -namespace TailoredApps.Shared.MediatR.Caching -{ - public class Cache : ICache - { - private readonly IDistributedCache distributedCache; - public Cache(IDistributedCache distributedCache) - { - this.distributedCache = distributedCache; - } - public async Task GetAsync(string cacheKey, CancellationToken cancellationToken) - { - var response = await distributedCache.GetAsync(cacheKey, cancellationToken); - if (response == null) - { - return default(T); - } - var stringData = Encoding.UTF8.GetString(response); - var serialized = (T)JsonConvert.DeserializeObject(stringData, typeof(T), new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); - return serialized; - } - - public async Task SetAsync(string cacheKey, TResponse response, TimeSpan? slidingExpiration, DateTime? absoluteExpiration, TimeSpan? absoluteExpirationRelativeToNow, CancellationToken cancellationToken) - { - var serializedObject = JsonConvert.SerializeObject(response); - var bytes = Encoding.UTF8.GetBytes(serializedObject); - - var cacheOptions = new DistributedCacheEntryOptions { AbsoluteExpiration = absoluteExpiration, AbsoluteExpirationRelativeToNow = absoluteExpirationRelativeToNow, SlidingExpiration = slidingExpiration }; - await distributedCache.SetAsync(cacheKey, bytes, cacheOptions, cancellationToken); - } - } -} +using Microsoft.Extensions.Caching.Distributed; +using Newtonsoft.Json; +using System; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using TailoredApps.Shared.MediatR.Interfaces.Caching; + +namespace TailoredApps.Shared.MediatR.Caching +{ + /// Implementacja oparta na z serializacją Newtonsoft.Json. + public class Cache : ICache + { + private readonly IDistributedCache distributedCache; + + /// Inicjalizuje instancję . + /// Implementacja distributed cache. + public Cache(IDistributedCache distributedCache) + { + this.distributedCache = distributedCache; + } + + /// + public async Task GetAsync(string cacheKey, CancellationToken cancellationToken) + { + var response = await distributedCache.GetAsync(cacheKey, cancellationToken); + if (response == null) + { + return default(T); + } + var stringData = Encoding.UTF8.GetString(response); + var serialized = (T)JsonConvert.DeserializeObject(stringData, typeof(T), new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); + return serialized; + } + + /// + public async Task SetAsync(string cacheKey, TResponse response, TimeSpan? slidingExpiration, DateTime? absoluteExpiration, TimeSpan? absoluteExpirationRelativeToNow, CancellationToken cancellationToken) + { + var serializedObject = JsonConvert.SerializeObject(response); + var bytes = Encoding.UTF8.GetBytes(serializedObject); + + var cacheOptions = new DistributedCacheEntryOptions { AbsoluteExpiration = absoluteExpiration, AbsoluteExpirationRelativeToNow = absoluteExpirationRelativeToNow, SlidingExpiration = slidingExpiration }; + await distributedCache.SetAsync(cacheKey, bytes, cacheOptions, cancellationToken); + } + } +} diff --git a/src/TailoredApps.Shared.MediatR/DI/PipelineRegistration.cs b/src/TailoredApps.Shared.MediatR/DI/PipelineRegistration.cs index b5d03c4..9dce436 100644 --- a/src/TailoredApps.Shared.MediatR/DI/PipelineRegistration.cs +++ b/src/TailoredApps.Shared.MediatR/DI/PipelineRegistration.cs @@ -1,55 +1,63 @@ -using MediatR; -using Microsoft.Extensions.DependencyInjection; -using Scrutor; -using System.Reflection; -using TailoredApps.Shared.MediatR.Interfaces.Caching; -using TailoredApps.Shared.MediatR.Interfaces.DI; -using TailoredApps.Shared.MediatR.Interfaces.Handlers; -using TailoredApps.Shared.MediatR.Interfaces.Messages; -using TailoredApps.Shared.MediatR.PipelineBehaviours; - -namespace TailoredApps.Shared.MediatR.DI -{ - public class PipelineRegistration : IPipelineRegistration - { - private readonly IServiceCollection serviceCollection; - public PipelineRegistration(IServiceCollection serviceCollection) - { - this.serviceCollection = serviceCollection; - } - public void RegisterPipelineBehaviors() - { - // Register MediatR Pipeline Behaviors - serviceCollection.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>)); - serviceCollection.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); - serviceCollection.AddTransient(typeof(IPipelineBehavior<,>), typeof(CachingBehavior<,>)); - serviceCollection.AddTransient(typeof(IPipelineBehavior<,>), typeof(FallbackBehavior<,>)); - serviceCollection.AddTransient(typeof(IPipelineBehavior<,>), typeof(RetryBehavior<,>)); - } - public void RegisterPipelineBehaviors(Assembly assembly) - { - // ICachePolicy discovery and registration - serviceCollection.Scan(scan => scan - .FromAssemblies(assembly) - .AddClasses(classes => classes.AssignableTo(typeof(ICachePolicy<,>))) - .AsImplementedInterfaces() - .WithTransientLifetime()); - - // IFallbackHandler discovery and registration - serviceCollection.Scan(scan => scan - .FromAssemblies(assembly) - .AddClasses(classes => classes.AssignableTo(typeof(IFallbackHandler<,>))) - .UsingRegistrationStrategy(RegistrationStrategy.Skip) - .AsImplementedInterfaces() - .WithTransientLifetime()); - - // IFallbackHandler discovery and registration - serviceCollection.Scan(scan => scan - .FromAssemblies(assembly) - .AddClasses(classes => classes.AssignableTo(typeof(IRetryableRequest<,>))) - .UsingRegistrationStrategy(RegistrationStrategy.Skip) - .AsImplementedInterfaces() - .WithTransientLifetime()); - } - } -} +using MediatR; +using Microsoft.Extensions.DependencyInjection; +using Scrutor; +using System.Reflection; +using TailoredApps.Shared.MediatR.Interfaces.Caching; +using TailoredApps.Shared.MediatR.Interfaces.DI; +using TailoredApps.Shared.MediatR.Interfaces.Handlers; +using TailoredApps.Shared.MediatR.Interfaces.Messages; +using TailoredApps.Shared.MediatR.PipelineBehaviours; + +namespace TailoredApps.Shared.MediatR.DI +{ + /// Implementacja rejestrująca pipeline behaviors MediatR w DI. + public class PipelineRegistration : IPipelineRegistration + { + private readonly IServiceCollection serviceCollection; + + /// Inicjalizuje instancję . + /// Kolekcja usług DI. + public PipelineRegistration(IServiceCollection serviceCollection) + { + this.serviceCollection = serviceCollection; + } + + /// + public void RegisterPipelineBehaviors() + { + // Register MediatR Pipeline Behaviors + serviceCollection.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>)); + serviceCollection.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); + serviceCollection.AddTransient(typeof(IPipelineBehavior<,>), typeof(CachingBehavior<,>)); + serviceCollection.AddTransient(typeof(IPipelineBehavior<,>), typeof(FallbackBehavior<,>)); + serviceCollection.AddTransient(typeof(IPipelineBehavior<,>), typeof(RetryBehavior<,>)); + } + + /// + public void RegisterPipelineBehaviors(Assembly assembly) + { + // ICachePolicy discovery and registration + serviceCollection.Scan(scan => scan + .FromAssemblies(assembly) + .AddClasses(classes => classes.AssignableTo(typeof(ICachePolicy<,>))) + .AsImplementedInterfaces() + .WithTransientLifetime()); + + // IFallbackHandler discovery and registration + serviceCollection.Scan(scan => scan + .FromAssemblies(assembly) + .AddClasses(classes => classes.AssignableTo(typeof(IFallbackHandler<,>))) + .UsingRegistrationStrategy(RegistrationStrategy.Skip) + .AsImplementedInterfaces() + .WithTransientLifetime()); + + // IRetryableRequest discovery and registration + serviceCollection.Scan(scan => scan + .FromAssemblies(assembly) + .AddClasses(classes => classes.AssignableTo(typeof(IRetryableRequest<,>))) + .UsingRegistrationStrategy(RegistrationStrategy.Skip) + .AsImplementedInterfaces() + .WithTransientLifetime()); + } + } +} diff --git a/src/TailoredApps.Shared.MediatR/Interfaces/Caching/ICache.cs b/src/TailoredApps.Shared.MediatR/Interfaces/Caching/ICache.cs index 7938e04..bcb9e23 100644 --- a/src/TailoredApps.Shared.MediatR/Interfaces/Caching/ICache.cs +++ b/src/TailoredApps.Shared.MediatR/Interfaces/Caching/ICache.cs @@ -1,13 +1,26 @@ -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace TailoredApps.Shared.MediatR.Interfaces.Caching -{ - // ICache is a helper wrapper over IDistributedCache that adds some read-through cache methods, etc. - public interface ICache - { - Task GetAsync(string cacheKey, CancellationToken cancellationToken); - Task SetAsync(string cacheKey, TResponse response, TimeSpan? slidingExpiration, DateTime? absoluteExpiration, TimeSpan? absoluteExpirationRelativeToNow, CancellationToken cancellationToken); - } -} \ No newline at end of file +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace TailoredApps.Shared.MediatR.Interfaces.Caching +{ + /// Wrapper nad IDistributedCache z metodami odczytu i zapisu z serializacją JSON. + public interface ICache + { + /// Pobiera i deserializuje wartość z cache. + /// Typ deserializowanego obiektu. + /// Klucz cache. + /// Token anulowania. + Task GetAsync(string cacheKey, CancellationToken cancellationToken); + + /// Serializuje i zapisuje wartość w cache z opcjonalnymi politykami wygaśnięcia. + /// Typ serializowanego obiektu. + /// Klucz cache. + /// Obiekt do zapisania. + /// Sliding expiration (opcjonalnie). + /// Absolutna data wygaśnięcia (opcjonalnie). + /// Absolutna data wygaśnięcia relative to now (opcjonalnie). + /// Token anulowania. + Task SetAsync(string cacheKey, TResponse response, TimeSpan? slidingExpiration, DateTime? absoluteExpiration, TimeSpan? absoluteExpirationRelativeToNow, CancellationToken cancellationToken); + } +} diff --git a/src/TailoredApps.Shared.MediatR/Interfaces/Caching/ICachePolicy.cs b/src/TailoredApps.Shared.MediatR/Interfaces/Caching/ICachePolicy.cs index 69ab145..1f3762b 100644 --- a/src/TailoredApps.Shared.MediatR/Interfaces/Caching/ICachePolicy.cs +++ b/src/TailoredApps.Shared.MediatR/Interfaces/Caching/ICachePolicy.cs @@ -1,22 +1,33 @@ -using MediatR; -using System; -using System.Linq; - -namespace TailoredApps.Shared.MediatR.Interfaces.Caching -{ - // Using C# 8.0 to provide a default interface implementation. - // Optionally, could move this to an AbstractCachingPolicy like AbstractValidator. - public interface ICachePolicy where TRequest : IRequest - { - DateTime? AbsoluteExpiration => null; - TimeSpan? AbsoluteExpirationRelativeToNow => TimeSpan.FromMinutes(5); - TimeSpan? SlidingExpiration => TimeSpan.FromSeconds(30); - - string GetCacheKey(TRequest request) - { - var r = new { request }; - var props = r.request.GetType().GetProperties().Select(pi => $"{pi.Name}:{pi.GetValue(r.request, null)}"); - return $"{typeof(TRequest).FullName}{{{string.Join(",", props)}}}"; - } - } -} +using MediatR; +using System; +using System.Linq; + +namespace TailoredApps.Shared.MediatR.Interfaces.Caching +{ + /// + /// Polityka cache'owania dla żądania MediatR. + /// Używa domyślnych implementacji interfejsów (C# 8.0). + /// + /// Typ żądania MediatR. + /// Typ odpowiedzi. + public interface ICachePolicy where TRequest : IRequest + { + /// Absolutna data wygaśnięcia. Domyślnie null (brak ograniczenia). + DateTime? AbsoluteExpiration => null; + + /// Absolutna data wygaśnięcia relative to now. Domyślnie 5 minut. + TimeSpan? AbsoluteExpirationRelativeToNow => TimeSpan.FromMinutes(5); + + /// Sliding expiration. Domyślnie 30 sekund. + TimeSpan? SlidingExpiration => TimeSpan.FromSeconds(30); + + /// Generuje klucz cache na podstawie typu żądania i jego właściwości. + /// Żądanie MediatR. + string GetCacheKey(TRequest request) + { + var r = new { request }; + var props = r.request.GetType().GetProperties().Select(pi => $"{pi.Name}:{pi.GetValue(r.request, null)}"); + return $"{typeof(TRequest).FullName}{{{string.Join(",", props)}}}"; + } + } +} diff --git a/src/TailoredApps.Shared.MediatR/Interfaces/DI/IPipelineRegistration.cs b/src/TailoredApps.Shared.MediatR/Interfaces/DI/IPipelineRegistration.cs index 986abb8..4da5f1f 100644 --- a/src/TailoredApps.Shared.MediatR/Interfaces/DI/IPipelineRegistration.cs +++ b/src/TailoredApps.Shared.MediatR/Interfaces/DI/IPipelineRegistration.cs @@ -1,10 +1,15 @@ -using System.Reflection; - -namespace TailoredApps.Shared.MediatR.Interfaces.DI -{ - public interface IPipelineRegistration - { - void RegisterPipelineBehaviors(); - void RegisterPipelineBehaviors(Assembly assembly); - } -} +using System.Reflection; + +namespace TailoredApps.Shared.MediatR.Interfaces.DI +{ + /// Kontrakt rejestracji pipeline behaviors MediatR. + public interface IPipelineRegistration + { + /// Rejestruje domyślne pipeline behaviors (Logging, Validation, Caching, Fallback, Retry). + void RegisterPipelineBehaviors(); + + /// Rejestruje pipeline behaviors i skanuje wskazany assembly w poszukiwaniu polityk cache, fallback i retry. + /// Assembly do przeskanowania. + void RegisterPipelineBehaviors(Assembly assembly); + } +} diff --git a/src/TailoredApps.Shared.MediatR/Interfaces/Handlers/IFallbackHandler.cs b/src/TailoredApps.Shared.MediatR/Interfaces/Handlers/IFallbackHandler.cs index 1c982a9..5005947 100644 --- a/src/TailoredApps.Shared.MediatR/Interfaces/Handlers/IFallbackHandler.cs +++ b/src/TailoredApps.Shared.MediatR/Interfaces/Handlers/IFallbackHandler.cs @@ -1,11 +1,17 @@ -using MediatR; -using System.Threading; -using System.Threading.Tasks; - -namespace TailoredApps.Shared.MediatR.Interfaces.Handlers -{ - public interface IFallbackHandler where TRequest : IRequest - { - Task HandleFallback(TRequest request, CancellationToken cancellationToken); - } -} \ No newline at end of file +using MediatR; +using System.Threading; +using System.Threading.Tasks; + +namespace TailoredApps.Shared.MediatR.Interfaces.Handlers +{ + /// Handler fallback wywoływany przez FallbackBehavior gdy główny handler rzuci wyjątkiem. + /// Typ żądania MediatR. + /// Typ odpowiedzi. + public interface IFallbackHandler where TRequest : IRequest + { + /// Obsługuje żądanie w trybie fallback. + /// Oryginalne żądanie. + /// Token anulowania. + Task HandleFallback(TRequest request, CancellationToken cancellationToken); + } +} diff --git a/src/TailoredApps.Shared.MediatR/Interfaces/Messages/IRetryableRequest.cs b/src/TailoredApps.Shared.MediatR/Interfaces/Messages/IRetryableRequest.cs index 923be49..3b3684a 100644 --- a/src/TailoredApps.Shared.MediatR/Interfaces/Messages/IRetryableRequest.cs +++ b/src/TailoredApps.Shared.MediatR/Interfaces/Messages/IRetryableRequest.cs @@ -1,16 +1,22 @@ -using MediatR; - -namespace TailoredApps.Shared.MediatR.Interfaces.Messages -{ - public interface IRetryableRequest where TRequest : IRequest - { - int RetryAttempts => 1; - - int RetryDelay => 250; - - bool RetryWithExponentialBackoff => false; - - int ExceptionsAllowedBeforeCircuitTrip => 1; - - } -} +using MediatR; + +namespace TailoredApps.Shared.MediatR.Interfaces.Messages +{ + /// Marker interfejsu dla żądań MediatR, które obsługują mechanizm retry i circuit breaker. + /// Typ żądania MediatR. + /// Typ odpowiedzi. + public interface IRetryableRequest where TRequest : IRequest + { + /// Liczba prób ponowienia. Domyślnie 1. + int RetryAttempts => 1; + + /// Opóźnienie między próbami w milisekundach. Domyślnie 250 ms. + int RetryDelay => 250; + + /// Czy używać exponential backoff przy retry. Domyślnie false. + bool RetryWithExponentialBackoff => false; + + /// Liczba wyjątków przed otwarciem circuit breakera. Domyślnie 1. + int ExceptionsAllowedBeforeCircuitTrip => 1; + } +} diff --git a/src/TailoredApps.Shared.MediatR/PipelineBehaviours/FallbackBehavior.cs b/src/TailoredApps.Shared.MediatR/PipelineBehaviours/FallbackBehavior.cs index 7b39bb1..3b781fe 100644 --- a/src/TailoredApps.Shared.MediatR/PipelineBehaviours/FallbackBehavior.cs +++ b/src/TailoredApps.Shared.MediatR/PipelineBehaviours/FallbackBehavior.cs @@ -1,52 +1,57 @@ -using MediatR; -using Microsoft.Extensions.Logging; -using Polly; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using TailoredApps.Shared.MediatR.Interfaces.Handlers; - -namespace TailoredApps.Shared.MediatR.PipelineBehaviours -{ - /// - /// MediatR Fallback Pipeline Behavior - /// - /// - /// - public class FallbackBehavior : IPipelineBehavior - where TRequest : IRequest - { - private readonly IEnumerable> _fallbackHandlers; - private readonly ILogger> _logger; - - public FallbackBehavior(IEnumerable> fallbackHandlers, ILogger> logger) - { - _fallbackHandlers = fallbackHandlers; - _logger = logger; - } - - public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) - { - var fallbackHandler = _fallbackHandlers.FirstOrDefault(); - if (fallbackHandler == null) - { - return await next(); - } - - var fallbackPolicy = Policy - .Handle() - .FallbackAsync(async (cancellationToken) => - { - _logger.LogDebug($"Initial handler failed. Falling back to `{fallbackHandler.GetType().FullName}@HandleFallback`"); - return await fallbackHandler.HandleFallback(request, cancellationToken) - .ConfigureAwait(false); - }); - - var response = await fallbackPolicy.ExecuteAsync(async () => await next()); - - return response; - } - } -} +using MediatR; +using Microsoft.Extensions.Logging; +using Polly; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using TailoredApps.Shared.MediatR.Interfaces.Handlers; + +namespace TailoredApps.Shared.MediatR.PipelineBehaviours +{ + /// + /// Pipeline behavior MediatR implementujący fallback — gdy główny handler rzuci wyjątkiem, + /// wywołuje zarejestrowany . + /// + /// Typ żądania MediatR. + /// Typ odpowiedzi. + public class FallbackBehavior : IPipelineBehavior + where TRequest : IRequest + { + private readonly IEnumerable> _fallbackHandlers; + private readonly ILogger> _logger; + + /// Inicjalizuje instancję . + /// Kolekcja fallback handlerów. + /// Logger. + public FallbackBehavior(IEnumerable> fallbackHandlers, ILogger> logger) + { + _fallbackHandlers = fallbackHandlers; + _logger = logger; + } + + /// + public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) + { + var fallbackHandler = _fallbackHandlers.FirstOrDefault(); + if (fallbackHandler == null) + { + return await next(); + } + + var fallbackPolicy = Policy + .Handle() + .FallbackAsync(async (cancellationToken) => + { + _logger.LogDebug($"Initial handler failed. Falling back to `{fallbackHandler.GetType().FullName}@HandleFallback`"); + return await fallbackHandler.HandleFallback(request, cancellationToken) + .ConfigureAwait(false); + }); + + var response = await fallbackPolicy.ExecuteAsync(async () => await next()); + + return response; + } + } +} diff --git a/src/TailoredApps.Shared.MediatR/PipelineBehaviours/LoggingBehavior.cs b/src/TailoredApps.Shared.MediatR/PipelineBehaviours/LoggingBehavior.cs index 73ddbce..9ea7ce4 100644 --- a/src/TailoredApps.Shared.MediatR/PipelineBehaviours/LoggingBehavior.cs +++ b/src/TailoredApps.Shared.MediatR/PipelineBehaviours/LoggingBehavior.cs @@ -1,44 +1,52 @@ -using MediatR; -using Microsoft.Extensions.Logging; -using System; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; - -namespace TailoredApps.Shared.MediatR.PipelineBehaviours -{ - public class LoggingBehavior : IPipelineBehavior where TRequest : IRequest - { - private readonly ILogger logger; - - public LoggingBehavior(ILogger logger) - { - this.logger = logger; - } - - public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) - { - var correlationId = Guid.NewGuid(); - var timer = new System.Diagnostics.Stopwatch(); - using (var loggingScope = logger.BeginScope("{MeditatorRequestName} with {MeditatorRequestData}, correlation id {CorrelationId}", typeof(TRequest).Name, JsonSerializer.Serialize(request), correlationId)) - { - try - { - logger.LogDebug("Handler for {MeditatorRequestName} starting, correlation id {CorrelationId}", typeof(TRequest).Name, correlationId); - timer.Start(); - var result = await next(); - timer.Stop(); - logger.LogDebug("Handler for {MeditatorRequestName} finished in {ElapsedMilliseconds}ms, correlation id {CorrelationId}", typeof(TRequest).Name, timer.Elapsed.TotalMilliseconds, correlationId); - - return result; - } - catch (Exception e) - { - timer.Stop(); - logger.LogError(e, "Handler for {MeditatorRequestName} failed in {ElapsedMilliseconds}ms, correlation id {CorrelationId}\r\n" + e.StackTrace, typeof(TRequest).Name, timer.Elapsed.TotalMilliseconds, correlationId); - throw; - } - } - } - } -} +using MediatR; +using Microsoft.Extensions.Logging; +using System; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace TailoredApps.Shared.MediatR.PipelineBehaviours +{ + /// + /// Pipeline behavior MediatR logujący czas wykonania żądania oraz ewentualne wyjątki. + /// + /// Typ żądania MediatR. + /// Typ odpowiedzi. + public class LoggingBehavior : IPipelineBehavior where TRequest : IRequest + { + private readonly ILogger logger; + + /// Inicjalizuje instancję . + /// Logger dla żądania. + public LoggingBehavior(ILogger logger) + { + this.logger = logger; + } + + /// + public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) + { + var correlationId = Guid.NewGuid(); + var timer = new System.Diagnostics.Stopwatch(); + using (var loggingScope = logger.BeginScope("{MeditatorRequestName} with {MeditatorRequestData}, correlation id {CorrelationId}", typeof(TRequest).Name, JsonSerializer.Serialize(request), correlationId)) + { + try + { + logger.LogDebug("Handler for {MeditatorRequestName} starting, correlation id {CorrelationId}", typeof(TRequest).Name, correlationId); + timer.Start(); + var result = await next(); + timer.Stop(); + logger.LogDebug("Handler for {MeditatorRequestName} finished in {ElapsedMilliseconds}ms, correlation id {CorrelationId}", typeof(TRequest).Name, timer.Elapsed.TotalMilliseconds, correlationId); + + return result; + } + catch (Exception e) + { + timer.Stop(); + logger.LogError(e, "Handler for {MeditatorRequestName} failed in {ElapsedMilliseconds}ms, correlation id {CorrelationId}\r\n" + e.StackTrace, typeof(TRequest).Name, timer.Elapsed.TotalMilliseconds, correlationId); + throw; + } + } + } + } +} diff --git a/src/TailoredApps.Shared.MediatR/PipelineBehaviours/RetryBehavior.cs b/src/TailoredApps.Shared.MediatR/PipelineBehaviours/RetryBehavior.cs index a2bedb3..ba278aa 100644 --- a/src/TailoredApps.Shared.MediatR/PipelineBehaviours/RetryBehavior.cs +++ b/src/TailoredApps.Shared.MediatR/PipelineBehaviours/RetryBehavior.cs @@ -1,62 +1,71 @@ -using MediatR; -using Microsoft.Extensions.Logging; -using Polly; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using TailoredApps.Shared.MediatR.Interfaces.Messages; - -namespace TailoredApps.Shared.MediatR.PipelineBehaviours -{ - public class RetryBehavior : IPipelineBehavior where TRequest : IRequest - { - private readonly IEnumerable> _retryHandlers; - private readonly ILogger> _logger; - - public RetryBehavior(IEnumerable> retryHandlers, ILogger> logger) - { - _retryHandlers = retryHandlers; - _logger = logger; - } - - public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) - { - var retryHandler = _retryHandlers.FirstOrDefault(); - if (retryHandler == null) - { - // No retry handler found, continue through pipeline - return await next(); - } - - var circuitBreaker = Policy - .Handle() - .CircuitBreakerAsync(retryHandler.ExceptionsAllowedBeforeCircuitTrip, TimeSpan.FromMilliseconds(5000), - (exception, things) => - { - _logger.LogDebug("Circuit Tripped!"); - }, - () => - { - }); - - var retryPolicy = Policy - .Handle() - .WaitAndRetryAsync(retryHandler.RetryAttempts, retryAttempt => - { - var retryDelay = retryHandler.RetryWithExponentialBackoff - ? TimeSpan.FromMilliseconds(Math.Pow(2, retryAttempt) * retryHandler.RetryDelay) - : TimeSpan.FromMilliseconds(retryHandler.RetryDelay); - - _logger.LogDebug($"Retrying, waiting {retryDelay}..."); - - return retryDelay; - }); - - var response = await retryPolicy.ExecuteAsync(async () => await next()); - - return response; - } - } -} +using MediatR; +using Microsoft.Extensions.Logging; +using Polly; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using TailoredApps.Shared.MediatR.Interfaces.Messages; + +namespace TailoredApps.Shared.MediatR.PipelineBehaviours +{ + /// + /// Pipeline behavior MediatR implementujący retry z opcjonalnym exponential backoff i circuit breakerem. + /// + /// Typ żądania MediatR. + /// Typ odpowiedzi. + public class RetryBehavior : IPipelineBehavior where TRequest : IRequest + { + private readonly IEnumerable> _retryHandlers; + private readonly ILogger> _logger; + + /// Inicjalizuje instancję . + /// Kolekcja konfiguracji retry. + /// Logger. + public RetryBehavior(IEnumerable> retryHandlers, ILogger> logger) + { + _retryHandlers = retryHandlers; + _logger = logger; + } + + /// + public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) + { + var retryHandler = _retryHandlers.FirstOrDefault(); + if (retryHandler == null) + { + // No retry handler found, continue through pipeline + return await next(); + } + + var circuitBreaker = Policy + .Handle() + .CircuitBreakerAsync(retryHandler.ExceptionsAllowedBeforeCircuitTrip, TimeSpan.FromMilliseconds(5000), + (exception, things) => + { + _logger.LogDebug("Circuit Tripped!"); + }, + () => + { + }); + + var retryPolicy = Policy + .Handle() + .WaitAndRetryAsync(retryHandler.RetryAttempts, retryAttempt => + { + var retryDelay = retryHandler.RetryWithExponentialBackoff + ? TimeSpan.FromMilliseconds(Math.Pow(2, retryAttempt) * retryHandler.RetryDelay) + : TimeSpan.FromMilliseconds(retryHandler.RetryDelay); + + _logger.LogDebug($"Retrying, waiting {retryDelay}..."); + + return retryDelay; + }); + + var response = await retryPolicy.ExecuteAsync(async () => await next()); + + return response; + } + } +} diff --git a/src/TailoredApps.Shared.MediatR/PipelineBehaviours/ValidationBehavior.cs b/src/TailoredApps.Shared.MediatR/PipelineBehaviours/ValidationBehavior.cs index 3976683..dfe31c2 100644 --- a/src/TailoredApps.Shared.MediatR/PipelineBehaviours/ValidationBehavior.cs +++ b/src/TailoredApps.Shared.MediatR/PipelineBehaviours/ValidationBehavior.cs @@ -1,36 +1,45 @@ -using FluentValidation; -using MediatR; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -namespace TailoredApps.Shared.MediatR.PipelineBehaviours -{ - public class ValidationBehavior : IPipelineBehavior where TRequest : IRequest - { - private readonly IEnumerable> _validators; - - public ValidationBehavior(IEnumerable> validators) - { - _validators = validators; - } - - public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) - { - var context = new ValidationContext(request); - var failures = _validators - .Select(v => v.Validate(context)) - .SelectMany(result => result.Errors) - .Where(f => f != null) - .ToList(); - - if (failures.Count != 0) - { - throw new ValidationException(failures); - } - - return await next(); - } - } -} +using FluentValidation; +using MediatR; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace TailoredApps.Shared.MediatR.PipelineBehaviours +{ + /// + /// Pipeline behavior MediatR uruchamiający wszystkie zarejestrowane walidatory FluentValidation + /// przed przekazaniem żądania do handlera. + /// + /// Typ żądania MediatR. + /// Typ odpowiedzi. + public class ValidationBehavior : IPipelineBehavior where TRequest : IRequest + { + private readonly IEnumerable> _validators; + + /// Inicjalizuje instancję . + /// Kolekcja walidatorów dla żądania. + public ValidationBehavior(IEnumerable> validators) + { + _validators = validators; + } + + /// + public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) + { + var context = new ValidationContext(request); + var failures = _validators + .Select(v => v.Validate(context)) + .SelectMany(result => result.Errors) + .Where(f => f != null) + .ToList(); + + if (failures.Count != 0) + { + throw new ValidationException(failures); + } + + return await next(); + } + } +} diff --git a/src/TailoredApps.Shared.Payments.Provider.Adyen/AdyenProvider.cs b/src/TailoredApps.Shared.Payments.Provider.Adyen/AdyenProvider.cs index c4428d3..67570dc 100644 --- a/src/TailoredApps.Shared.Payments.Provider.Adyen/AdyenProvider.cs +++ b/src/TailoredApps.Shared.Payments.Provider.Adyen/AdyenProvider.cs @@ -1,97 +1,159 @@ +using System.Net.Http; +using System.Net.Http.Headers; using System.Security.Cryptography; using System.Text; -using Adyen; -using Adyen.Model.Checkout; -using Adyen.Service.Checkout; +using System.Text.Json; +using System.Text.Json.Serialization; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -using Environment = Adyen.Model.Enum.Environment; namespace TailoredApps.Shared.Payments.Provider.Adyen; // ─── Options ───────────────────────────────────────────────────────────────── -/// Konfiguracja Adyen. Sekcja: Payments:Providers:Adyen. +/// Konfiguracja Adyen Checkout API. Sekcja: Payments:Providers:Adyen. public class AdyenServiceOptions { + /// Klucz sekcji konfiguracji. public static string ConfigurationKey => "Payments:Providers:Adyen"; - public string ApiKey { get; set; } = string.Empty; - public string MerchantAccount { get; set; } = string.Empty; - public string ClientKey { get; set; } = string.Empty; - public string ReturnUrl { get; set; } = string.Empty; - /// HMAC klucz do weryfikacji powiadomień webhooka (hex). + + /// Adyen API key (X-API-Key). + public string ApiKey { get; set; } = string.Empty; + + /// Identyfikator konta merchantskiego. + public string MerchantAccount { get; set; } = string.Empty; + + /// Client key do Drop-in / Components (opcjonalnie). + public string ClientKey { get; set; } = string.Empty; + + /// URL powrotu po płatności. + public string ReturnUrl { get; set; } = string.Empty; + + /// HMAC klucz (hex) do weryfikacji powiadomień webhooka. public string NotificationHmacKey { get; set; } = string.Empty; - /// True = sandbox (test), False = live. - public bool IsTest { get; set; } = true; + + /// True = środowisko testowe (checkout-test.adyen.com), False = produkcja. + public bool IsTest { get; set; } = true; +} + +// ─── Internal models ───────────────────────────────────────────────────────── + +file class AdyenAmount +{ + [JsonPropertyName("value")] public long Value { get; set; } + [JsonPropertyName("currency")] public string Currency { get; set; } = string.Empty; +} + +file class AdyenSessionRequest +{ + [JsonPropertyName("merchantAccount")] public string MerchantAccount { get; set; } = string.Empty; + [JsonPropertyName("amount")] public AdyenAmount Amount { get; set; } = new(); + [JsonPropertyName("reference")] public string Reference { get; set; } = string.Empty; + [JsonPropertyName("returnUrl")] public string ReturnUrl { get; set; } = string.Empty; + [JsonPropertyName("shopperEmail")] public string? ShopperEmail { get; set; } + [JsonPropertyName("countryCode")] public string? CountryCode { get; set; } +} + +file class AdyenSessionResponse +{ + [JsonPropertyName("id")] public string? Id { get; set; } + [JsonPropertyName("sessionData")] public string? SessionData { get; set; } + [JsonPropertyName("url")] public string? Url { get; set; } +} + +file class AdyenStatusResponse +{ + [JsonPropertyName("status")] public string? Status { get; set; } + [JsonPropertyName("resultCode")] public string? ResultCode { get; set; } } // ─── Interface ──────────────────────────────────────────────────────────────── -/// Abstrakcja nad Adyen Checkout API. +/// Abstrakcja nad Adyen Checkout API v71 (Sessions). public interface IAdyenServiceCaller { - Task CreateSessionAsync(PaymentRequest request); - Task GetPaymentStatusAsync(string pspReference); + /// Tworzy sesję płatności Adyen i zwraca id sesji oraz URL checkout. + Task<(string? sessionId, string? checkoutUrl, string? error)> CreateSessionAsync(PaymentRequest request); + + /// Pobiera status płatności na podstawie pspReference lub sessionId. + Task GetPaymentStatusAsync(string paymentId); + + /// Weryfikuje HMAC podpis powiadomienia webhooka. bool VerifyNotificationHmac(string payload, string hmacSignature); } // ─── Caller ─────────────────────────────────────────────────────────────────── -/// Implementacja opakowująca oficjalny Adyen SDK. +/// Implementacja oparta na Adyen Checkout REST API v71. public class AdyenServiceCaller : IAdyenServiceCaller { private readonly AdyenServiceOptions options; + private readonly IHttpClientFactory httpClientFactory; - public AdyenServiceCaller(IOptions options) + /// Inicjalizuje instancję . + public AdyenServiceCaller(IOptions options, IHttpClientFactory httpClientFactory) { this.options = options.Value; + this.httpClientFactory = httpClientFactory; } - private Client CreateClient() + private string BaseUrl => options.IsTest + ? "https://checkout-test.adyen.com/v71" + : "https://checkout-live.adyen.com/v71"; + + private HttpClient CreateClient() { - var config = new Config - { - XApiKey = options.ApiKey, - Environment = options.IsTest ? Environment.Test : Environment.Live, - }; - return new Client(config); + var client = httpClientFactory.CreateClient("Adyen"); + client.DefaultRequestHeaders.Add("X-API-Key", options.ApiKey); + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + return client; } - public async Task CreateSessionAsync(PaymentRequest request) + /// + public async Task<(string? sessionId, string? checkoutUrl, string? error)> CreateSessionAsync(PaymentRequest request) { - var client = CreateClient(); - var checkout = new PaymentsService(client); - - var amount = new Adyen.Model.Checkout.Amount + using var client = CreateClient(); + var body = new AdyenSessionRequest { - Value = (long)(request.Amount * 100), - Currency = request.Currency.ToUpperInvariant(), + MerchantAccount = options.MerchantAccount, + Amount = new AdyenAmount { Value = (long)(request.Amount * 100), Currency = request.Currency.ToUpperInvariant() }, + Reference = request.AdditionalData ?? Guid.NewGuid().ToString("N"), + ReturnUrl = options.ReturnUrl, + ShopperEmail = request.Email, + CountryCode = request.Country ?? "PL", }; + var content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json"); + var response = await client.PostAsync($"{BaseUrl}/sessions", content); + var json = await response.Content.ReadAsStringAsync(); - var sessionRequest = new CreateCheckoutSessionRequest( - merchantAccount: options.MerchantAccount, - amount: amount, - returnUrl: options.ReturnUrl, - reference: request.AdditionalData ?? Guid.NewGuid().ToString("N") - ) - { - ShopperEmail = request.Email, - ShopperReference = request.Email, - CountryCode = request.Country ?? "PL", - }; + if (!response.IsSuccessStatusCode) + return (null, null, json); - return await checkout.SessionsAsync(sessionRequest); + var result = JsonSerializer.Deserialize(json); + return (result?.Id, result?.Url, null); } - public async Task GetPaymentStatusAsync(string pspReference) + /// + public async Task GetPaymentStatusAsync(string paymentId) { - var client = CreateClient(); - var checkout = new PaymentsService(client); - var details = new PaymentDetailsRequest(details: new PaymentCompletionDetails(), paymentData: pspReference); - return await checkout.PaymentsDetailsAsync(details); + using var client = CreateClient(); + var response = await client.GetAsync($"{BaseUrl}/payments/{paymentId}/details"); + if (!response.IsSuccessStatusCode) return PaymentStatusEnum.Rejected; + var json = await response.Content.ReadAsStringAsync(); + var result = JsonSerializer.Deserialize(json); + return result?.ResultCode switch + { + "Authorised" => PaymentStatusEnum.Finished, + "Refused" => PaymentStatusEnum.Rejected, + "Cancelled" => PaymentStatusEnum.Rejected, + "Pending" => PaymentStatusEnum.Processing, + "Received" => PaymentStatusEnum.Processing, + _ => PaymentStatusEnum.Created, + }; } + /// public bool VerifyNotificationHmac(string payload, string hmacSignature) { try @@ -107,21 +169,27 @@ public bool VerifyNotificationHmac(string payload, string hmacSignature) // ─── Provider ───────────────────────────────────────────────────────────────── -/// Implementacja dla Adyen. +/// Implementacja dla Adyen Checkout. public class AdyenProvider : IPaymentProvider { private readonly IAdyenServiceCaller caller; - public AdyenProvider(IAdyenServiceCaller caller) - { - this.caller = caller; - } + /// Inicjalizuje instancję . + public AdyenProvider(IAdyenServiceCaller caller) => this.caller = caller; + + /// + public string Key => "Adyen"; + + /// + public string Name => "Adyen"; - public string Key => "Adyen"; - public string Name => "Adyen"; + /// public string Description => "Globalny operator płatności Adyen — karty, BLIK, iDEAL i inne."; - public string Url => "https://www.adyen.com"; + /// + public string Url => "https://www.adyen.com"; + + /// public Task> GetPaymentChannels(string currency) { ICollection channels = currency.ToUpperInvariant() switch @@ -134,9 +202,9 @@ public Task> GetPaymentChannels(string currency) ], "EUR" => [ - new PaymentChannel { Id = "scheme", Name = "Karta płatnicza", Description = "Visa, Mastercard", PaymentModel = PaymentModel.OneTime }, - new PaymentChannel { Id = "ideal", Name = "iDEAL", Description = "Przelew iDEAL", PaymentModel = PaymentModel.OneTime }, - new PaymentChannel { Id = "sepadirectdebit", Name = "SEPA Direct Debit", Description = "SEPA", PaymentModel = PaymentModel.OneTime }, + new PaymentChannel { Id = "scheme", Name = "Karta płatnicza", Description = "Visa, Mastercard", PaymentModel = PaymentModel.OneTime }, + new PaymentChannel { Id = "ideal", Name = "iDEAL", Description = "Przelew iDEAL", PaymentModel = PaymentModel.OneTime }, + new PaymentChannel { Id = "sepadirectdebit", Name = "SEPA Direct Debit", Description = "SEPA", PaymentModel = PaymentModel.OneTime }, ], _ => [ @@ -146,32 +214,30 @@ public Task> GetPaymentChannels(string currency) return Task.FromResult(channels); } + /// public async Task RequestPayment(PaymentRequest request) { - var session = await caller.CreateSessionAsync(request); + var (sessionId, checkoutUrl, error) = await caller.CreateSessionAsync(request); + + if (sessionId is null) + return new PaymentResponse { PaymentStatus = PaymentStatusEnum.Rejected, ResponseObject = error }; + return new PaymentResponse { - PaymentUniqueId = session.Id, - RedirectUrl = session.Url, + PaymentUniqueId = sessionId, + RedirectUrl = checkoutUrl, PaymentStatus = PaymentStatusEnum.Created, }; } + /// public async Task GetStatus(string paymentId) { - var details = await caller.GetPaymentStatusAsync(paymentId); - var status = details.ResultCode?.ToString() switch - { - "Authorised" => PaymentStatusEnum.Finished, - "Refused" => PaymentStatusEnum.Rejected, - "Cancelled" => PaymentStatusEnum.Rejected, - "Pending" => PaymentStatusEnum.Processing, - "Received" => PaymentStatusEnum.Processing, - _ => PaymentStatusEnum.Created, - }; + var status = await caller.GetPaymentStatusAsync(paymentId); return new PaymentResponse { PaymentUniqueId = paymentId, PaymentStatus = status }; } + /// public Task TransactionStatusChange(TransactionStatusChangePayload payload) { var body = payload.Payload?.ToString() ?? string.Empty; @@ -183,7 +249,7 @@ public Task TransactionStatusChange(TransactionStatusChangePayl var status = PaymentStatusEnum.Processing; try { - var doc = System.Text.Json.JsonDocument.Parse(body); + var doc = JsonDocument.Parse(body); if (doc.RootElement.TryGetProperty("eventCode", out var ev)) { status = ev.GetString() switch @@ -207,19 +273,25 @@ public Task TransactionStatusChange(TransactionStatusChangePayl /// Rozszerzenia DI dla Adyen. public static class AdyenProviderExtensions { + /// Rejestruje i jego zależności w kontenerze DI. public static void RegisterAdyenProvider(this IServiceCollection services) { services.AddOptions(); services.ConfigureOptions(); + services.AddHttpClient("Adyen"); services.AddTransient(); } } -/// Wczytuje opcje Adyen z konfiguracji. +/// Wczytuje opcje Adyen z konfiguracji aplikacji. public class AdyenConfigureOptions : IConfigureOptions { private readonly IConfiguration configuration; + + /// Inicjalizuje instancję . public AdyenConfigureOptions(IConfiguration configuration) => this.configuration = configuration; + + /// public void Configure(AdyenServiceOptions options) { var s = configuration.GetSection(AdyenServiceOptions.ConfigurationKey).Get(); diff --git a/src/TailoredApps.Shared.Payments.Provider.Adyen/TailoredApps.Shared.Payments.Provider.Adyen.csproj b/src/TailoredApps.Shared.Payments.Provider.Adyen/TailoredApps.Shared.Payments.Provider.Adyen.csproj index 13dadc1..7631338 100644 --- a/src/TailoredApps.Shared.Payments.Provider.Adyen/TailoredApps.Shared.Payments.Provider.Adyen.csproj +++ b/src/TailoredApps.Shared.Payments.Provider.Adyen/TailoredApps.Shared.Payments.Provider.Adyen.csproj @@ -10,19 +10,20 @@ - - + + + diff --git a/src/TailoredApps.Shared.Payments.Provider.PayU/PayUProvider.cs b/src/TailoredApps.Shared.Payments.Provider.PayU/PayUProvider.cs index 973dcbe..75b843f 100644 --- a/src/TailoredApps.Shared.Payments.Provider.PayU/PayUProvider.cs +++ b/src/TailoredApps.Shared.Payments.Provider.PayU/PayUProvider.cs @@ -228,7 +228,7 @@ public Task> GetPaymentChannels(string currency) new PaymentChannel { Id = "blik", Name = "BLIK", Description = "BLIK", PaymentModel = PaymentModel.OneTime }, new PaymentChannel { Id = "c", Name = "Karta płatnicza", Description = "Visa, Mastercard", PaymentModel = PaymentModel.OneTime }, new PaymentChannel { Id = "o", Name = "Przelew online", Description = "Pekao, mBank, iPKO", PaymentModel = PaymentModel.OneTime }, - new PaymentChannel { Id = "ai", Name = "Raty", Description = "Raty PayU", PaymentModel = PaymentModel.Installment }, + new PaymentChannel { Id = "ai", Name = "Raty", Description = "Raty PayU", PaymentModel = PaymentModel.OneTime }, new PaymentChannel { Id = "ap", Name = "Apple Pay", Description = "Apple Pay", PaymentModel = PaymentModel.OneTime }, new PaymentChannel { Id = "jp", Name = "Google Pay", Description = "Google Pay", PaymentModel = PaymentModel.OneTime }, ], diff --git a/src/TailoredApps.Shared.Querying/IPagedResult.cs b/src/TailoredApps.Shared.Querying/IPagedResult.cs index 09efae5..a310a1a 100644 --- a/src/TailoredApps.Shared.Querying/IPagedResult.cs +++ b/src/TailoredApps.Shared.Querying/IPagedResult.cs @@ -1,9 +1,15 @@ -using System.Collections.Generic; -namespace TailoredApps.Shared.Querying -{ - public interface IPagedResult - { - ICollection Results { get; set; } - int Count { get; set; } - } -} +using System.Collections.Generic; + +namespace TailoredApps.Shared.Querying +{ + /// Reprezentuje stronicowany wynik zapytania. + /// Typ elementu kolekcji wyników. + public interface IPagedResult + { + /// Kolekcja wyników na bieżącej stronie. + ICollection Results { get; set; } + + /// Łączna liczba wszystkich wyników (bez stronicowania). + int Count { get; set; } + } +} diff --git a/src/TailoredApps.Shared.Querying/PagedAndSortedQuery.cs b/src/TailoredApps.Shared.Querying/PagedAndSortedQuery.cs index 5c52657..aefdad5 100644 --- a/src/TailoredApps.Shared.Querying/PagedAndSortedQuery.cs +++ b/src/TailoredApps.Shared.Querying/PagedAndSortedQuery.cs @@ -1,57 +1,112 @@ -using System; - -namespace TailoredApps.Shared.Querying -{ - public abstract class PagedAndSortedQuery : IPagedAndSortedQuery where TQuery : QueryBase - { - public int? Page { get; set; } - public int? Count { get; set; } - public bool IsPagingSpecified => Page.HasValue && Count.HasValue; - public string SortField { get; set; } - public SortDirection? SortDir { get; set; } - public bool IsSortingSpecified => !string.IsNullOrWhiteSpace(SortField) && SortDir.HasValue; - public TQuery Filter { get; set; } - public bool IsSortBy(string fieldName) => string.Equals(SortField, fieldName, StringComparison.InvariantCultureIgnoreCase); - } - - public interface IPagedAndSortedQuery : IQuery, IQueryParameters where TQuery : QueryBase - { - int? Page { get; set; } - int? Count { get; set; } - bool IsPagingSpecified { get; } - string SortField { get; set; } - SortDirection? SortDir { get; set; } - bool IsSortingSpecified { get; } - TQuery Filter { get; set; } - bool IsSortBy(string fieldName); - } - public interface IPagingParameters - { - int? Page { get; } - int? Count { get; } - bool IsPagingSpecified { get; } - } - - public interface ISortingParameters - { - string SortField { get; } - SortDirection? SortDir { get; } - bool IsSortingSpecified { get; } - } - - public interface IQueryParameters : IPagingParameters, ISortingParameters - { - } - - public interface IQuery - { - T Filter { get; set; } - } - - public interface IPagedAndSortedRequest : IPagedAndSortedQuery - where TQuery:QueryBase - where TResponse: IPagedResult - { - - } -} +using System; + +namespace TailoredApps.Shared.Querying +{ + /// Bazowa klasa zapytania stronicowanego i sortowanego. + /// Typ filtru zapytania. + public abstract class PagedAndSortedQuery : IPagedAndSortedQuery where TQuery : QueryBase + { + /// Numer strony (1-based). + public int? Page { get; set; } + + /// Liczba elementów na stronie. + public int? Count { get; set; } + + /// Czy parametry stronicowania są podane. + public bool IsPagingSpecified => Page.HasValue && Count.HasValue; + + /// Pole sortowania. + public string SortField { get; set; } + + /// Kierunek sortowania. + public SortDirection? SortDir { get; set; } + + /// Czy parametry sortowania są podane. + public bool IsSortingSpecified => !string.IsNullOrWhiteSpace(SortField) && SortDir.HasValue; + + /// Obiekt filtra zapytania. + public TQuery Filter { get; set; } + + /// Sprawdza, czy zapytanie jest sortowane po wskazanym polu. + /// Nazwa pola do sprawdzenia. + public bool IsSortBy(string fieldName) => string.Equals(SortField, fieldName, StringComparison.InvariantCultureIgnoreCase); + } + + /// Interfejs zapytania stronicowanego i sortowanego. + /// Typ filtru zapytania. + public interface IPagedAndSortedQuery : IQuery, IQueryParameters where TQuery : QueryBase + { + /// Numer strony (1-based). + new int? Page { get; set; } + + /// Liczba elementów na stronie. + new int? Count { get; set; } + + /// Czy parametry stronicowania są podane. + new bool IsPagingSpecified { get; } + + /// Pole sortowania. + new string SortField { get; set; } + + /// Kierunek sortowania. + new SortDirection? SortDir { get; set; } + + /// Czy parametry sortowania są podane. + new bool IsSortingSpecified { get; } + + /// Obiekt filtra zapytania. + new TQuery Filter { get; set; } + + /// Sprawdza, czy zapytanie jest sortowane po wskazanym polu. + bool IsSortBy(string fieldName); + } + + /// Parametry stronicowania. + public interface IPagingParameters + { + /// Numer strony. + int? Page { get; } + + /// Liczba elementów na stronie. + int? Count { get; } + + /// Czy parametry stronicowania są podane. + bool IsPagingSpecified { get; } + } + + /// Parametry sortowania. + public interface ISortingParameters + { + /// Pole sortowania. + string SortField { get; } + + /// Kierunek sortowania. + SortDirection? SortDir { get; } + + /// Czy parametry sortowania są podane. + bool IsSortingSpecified { get; } + } + + /// Połączone parametry stronicowania i sortowania. + public interface IQueryParameters : IPagingParameters, ISortingParameters + { + } + + /// Interfejs zapytania z filtrem. + /// Typ filtru. + public interface IQuery + { + /// Obiekt filtra zapytania. + T Filter { get; set; } + } + + /// Interfejs stronicowanego żądania MediatR z filtrem i modelem odpowiedzi. + /// Typ odpowiedzi (musi implementować ). + /// Typ filtru zapytania. + /// Typ elementu w wynikach. + public interface IPagedAndSortedRequest : IPagedAndSortedQuery + where TQuery : QueryBase + where TResponse : IPagedResult + { + } +} diff --git a/src/TailoredApps.Shared.Querying/QueryBase.cs b/src/TailoredApps.Shared.Querying/QueryBase.cs index bdfd2fa..ca275fb 100644 --- a/src/TailoredApps.Shared.Querying/QueryBase.cs +++ b/src/TailoredApps.Shared.Querying/QueryBase.cs @@ -1,6 +1,7 @@ -namespace TailoredApps.Shared.Querying -{ - public abstract class QueryBase - { - } -} +namespace TailoredApps.Shared.Querying +{ + /// Klasa bazowa dla filtrów zapytań stronicowanych i sortowanych. + public abstract class QueryBase + { + } +} diff --git a/src/TailoredApps.Shared.Querying/QueryMap.cs b/src/TailoredApps.Shared.Querying/QueryMap.cs index b772ea5..30ed449 100644 --- a/src/TailoredApps.Shared.Querying/QueryMap.cs +++ b/src/TailoredApps.Shared.Querying/QueryMap.cs @@ -1,17 +1,28 @@ -using System; -using System.Linq.Expressions; - -namespace TailoredApps.Shared.Querying -{ - public class QueryMap - { - public QueryMap(Expression> destination, Expression> source) - { - Source = source; - Destination = destination; - } - - public Expression> Source { get; } - public Expression> Destination { get; } - } -} +using System; +using System.Linq.Expressions; + +namespace TailoredApps.Shared.Querying +{ + /// Mapowanie pola sortowania z modelu docelowego na pole źródłowe. + /// Typ modelu docelowego (DTO). + /// Typ encji źródłowej. + public class QueryMap + { + /// + /// Inicjalizuje nowe mapowanie sortowania. + /// + /// Wyrażenie na pole modelu docelowego. + /// Wyrażenie na pole encji źródłowej. + public QueryMap(Expression> destination, Expression> source) + { + Source = source; + Destination = destination; + } + + /// Wyrażenie wskazujące na pole encji źródłowej. + public Expression> Source { get; } + + /// Wyrażenie wskazujące na pole modelu docelowego. + public Expression> Destination { get; } + } +} diff --git a/src/TailoredApps.Shared.Querying/SortDirection.cs b/src/TailoredApps.Shared.Querying/SortDirection.cs index ef8ac13..58e1aa1 100644 --- a/src/TailoredApps.Shared.Querying/SortDirection.cs +++ b/src/TailoredApps.Shared.Querying/SortDirection.cs @@ -1,9 +1,15 @@ -namespace TailoredApps.Shared.Querying -{ - public enum SortDirection - { - Undefined = 0, - Asc = 1, - Desc = 2 - } -} +namespace TailoredApps.Shared.Querying +{ + /// Kierunek sortowania wyników zapytania. + public enum SortDirection + { + /// Kierunek sortowania nie określony. + Undefined = 0, + + /// Sortowanie rosnące. + Asc = 1, + + /// Sortowanie malejące. + Desc = 2 + } +} From 79f45052ea32b9d06e1716380be2d54cdca89481 Mon Sep 17 00:00:00 2001 From: Szeregowy Date: Tue, 24 Mar 2026 15:45:56 +0100 Subject: [PATCH 3/5] =?UTF-8?q?fix:=20XML=20docs=20dla=20wszystkich=20proj?= =?UTF-8?q?ekt=C3=B3w,=20test=20fix,=20CashBill=20SYSLIB0014?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Naprawa błędu testu: - MultiProviderPaymentTest.cs: Provider_HasCorrectUrl → Provider_HasCorrectName (PaymentProvider model nie ma property Url, tylko Id i Name) XML docs — brakujące komentarze w 6 providerach: - PayUProvider.cs (pełne docs z summary + inheritdoc) - Przelewy24Provider.cs, TpayProvider.cs, HotPayProvider.cs, PayNowProvider.cs, RevolutProvider.cs (docs via skrypt) - StripeProvider.cs, StripeServiceCaller.cs, StripeServiceOptions.cs XML docs — pre-existing projekty Email: - SmtpEmailProvider.cs, SmtpEmailServiceOptions.cs - IEmailProvider.cs, EmailServiceToConsolleWritter.cs - MailMessageBuilder/* (DefaultMessageBuilder, IMailMessageBuilder, TokenReplacingMailMessageBuilder, TokenReplacingMailMessageBuilderOptions) CashBill: - SYSLIB0014 — WebRequest.Create obsolete warning --- .../EmailServiceToConsolleWritter.cs | 46 ++-- .../IEmailProvider.cs | 30 +-- .../DefaultMessageBuilder.cs | 42 ++-- .../MailMessageBuilder/IMailMessageBuilder.cs | 18 +- .../TokenReplacingMailMessageBuilder.cs | 96 ++++---- ...TokenReplacingMailMessageBuilderOptions.cs | 18 +- .../SmtpEmailProvider.cs | 225 +++++++++--------- .../SmtpEmailServiceOptions.cs | 41 ++-- ...s.Shared.Payments.Provider.CashBill.csproj | 1 + .../HotPayProvider.cs | 19 ++ .../PayNowProvider.cs | 22 ++ .../PayUProvider.cs | 75 ++++-- .../Przelewy24Provider.cs | 23 ++ .../RevolutProvider.cs | 20 ++ .../StripeProvider.cs | 4 + .../StripeServiceCaller.cs | 1 + .../StripeServiceOptions.cs | 1 + .../TpayProvider.cs | 26 ++ .../MultiProviderPaymentTest.cs | 18 +- 19 files changed, 454 insertions(+), 272 deletions(-) diff --git a/src/TailoredApps.Shared.Email/EmailServiceToConsolleWritter.cs b/src/TailoredApps.Shared.Email/EmailServiceToConsolleWritter.cs index 4d9d23d..81b9bd1 100644 --- a/src/TailoredApps.Shared.Email/EmailServiceToConsolleWritter.cs +++ b/src/TailoredApps.Shared.Email/EmailServiceToConsolleWritter.cs @@ -1,22 +1,24 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using TailoredApps.Shared.Email.Models; - -namespace TailoredApps.Shared.Email -{ - public class EmailServiceToConsolleWritter : IEmailProvider - { - public async Task> GetMail(string folder = "", string sender = "", string recipent = "", TimeSpan? fromLast = null) - { - return new List(); - } - - public async Task SendMail(string recipnet, string topic, string messageBody, Dictionary attachments) - { - var message = $"recipent: {recipnet}; topic: {topic}; message: {messageBody}"; - Console.WriteLine(message); - return message; - } - } -} +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using TailoredApps.Shared.Email.Models; + +namespace TailoredApps.Shared.Email +{ + public class EmailServiceToConsolleWritter : IEmailProvider + { + /// + public async Task> GetMail(string folder = "", string sender = "", string recipent = "", TimeSpan? fromLast = null) + { + return new List(); + } + + /// + public async Task SendMail(string recipnet, string topic, string messageBody, Dictionary attachments) + { + var message = $"recipent: {recipnet}; topic: {topic}; message: {messageBody}"; + Console.WriteLine(message); + return message; + } + } +} diff --git a/src/TailoredApps.Shared.Email/IEmailProvider.cs b/src/TailoredApps.Shared.Email/IEmailProvider.cs index b5cc390..9850202 100644 --- a/src/TailoredApps.Shared.Email/IEmailProvider.cs +++ b/src/TailoredApps.Shared.Email/IEmailProvider.cs @@ -1,14 +1,16 @@ -using System; -using System.Collections.Generic; -using System.Net.Mail; -using System.Threading.Tasks; - -namespace TailoredApps.Shared.Email -{ - public interface IEmailProvider - { - Task SendMail(string recipnet, string topic, string messageBody, Dictionary attachments); - Task> GetMail(string folder = "", string sender = "", string recipent = "", TimeSpan? fromLast = null); - - } -} +using System; +using System.Collections.Generic; +using System.Net.Mail; +using System.Threading.Tasks; + +namespace TailoredApps.Shared.Email +{ + public interface IEmailProvider + { + /// Wywołanie API. + Task SendMail(string recipnet, string topic, string messageBody, Dictionary attachments); + /// Wywołanie API. + Task> GetMail(string folder = "", string sender = "", string recipent = "", TimeSpan? fromLast = null); + + } +} diff --git a/src/TailoredApps.Shared.Email/MailMessageBuilder/DefaultMessageBuilder.cs b/src/TailoredApps.Shared.Email/MailMessageBuilder/DefaultMessageBuilder.cs index c214518..caf524a 100644 --- a/src/TailoredApps.Shared.Email/MailMessageBuilder/DefaultMessageBuilder.cs +++ b/src/TailoredApps.Shared.Email/MailMessageBuilder/DefaultMessageBuilder.cs @@ -1,21 +1,21 @@ -using System.Collections.Generic; - -namespace TailoredApps.Shared.Email.MailMessageBuilder -{ - public class DefaultMessageBuilder : IMailMessageBuilder - { - public string Build(string templateKey, IDictionary variables, IDictionary templates) - { - if (templates.ContainsKey(templateKey)) - { - var templateTransform = templates[templateKey]; - foreach (var token in variables) - { - templateTransform = templateTransform.Replace(token.Key, token.Value); - } - return templateTransform; - } - throw new KeyNotFoundException("templateKey"); - } - } -} +using System.Collections.Generic; + +namespace TailoredApps.Shared.Email.MailMessageBuilder +{ + public class DefaultMessageBuilder : IMailMessageBuilder + { + public string Build(string templateKey, IDictionary variables, IDictionary templates) + { + if (templates.ContainsKey(templateKey)) + { + var templateTransform = templates[templateKey]; + foreach (var token in variables) + { + templateTransform = templateTransform.Replace(token.Key, token.Value); + } + return templateTransform; + } + throw new KeyNotFoundException("templateKey"); + } + } +} diff --git a/src/TailoredApps.Shared.Email/MailMessageBuilder/IMailMessageBuilder.cs b/src/TailoredApps.Shared.Email/MailMessageBuilder/IMailMessageBuilder.cs index c574175..f9d9243 100644 --- a/src/TailoredApps.Shared.Email/MailMessageBuilder/IMailMessageBuilder.cs +++ b/src/TailoredApps.Shared.Email/MailMessageBuilder/IMailMessageBuilder.cs @@ -1,9 +1,9 @@ -using System.Collections.Generic; - -namespace TailoredApps.Shared.Email.MailMessageBuilder -{ - public interface IMailMessageBuilder - { - string Build(string templateKey, IDictionary variables, IDictionary templates); - } -} +using System.Collections.Generic; + +namespace TailoredApps.Shared.Email.MailMessageBuilder +{ + public interface IMailMessageBuilder + { + string Build(string templateKey, IDictionary variables, IDictionary templates); + } +} diff --git a/src/TailoredApps.Shared.Email/MailMessageBuilder/TokenReplacingMailMessageBuilder.cs b/src/TailoredApps.Shared.Email/MailMessageBuilder/TokenReplacingMailMessageBuilder.cs index 80bee44..56a5ad1 100644 --- a/src/TailoredApps.Shared.Email/MailMessageBuilder/TokenReplacingMailMessageBuilder.cs +++ b/src/TailoredApps.Shared.Email/MailMessageBuilder/TokenReplacingMailMessageBuilder.cs @@ -1,48 +1,48 @@ -using Microsoft.Extensions.Options; -using System.Collections.Generic; -using System.IO; - -namespace TailoredApps.Shared.Email.MailMessageBuilder -{ - public class TokenReplacingMailMessageBuilder : IMailMessageBuilder - { - private readonly IOptions options; - public TokenReplacingMailMessageBuilder(IOptions options) - { - this.options = options; - } - public string Build(string templateKey, IDictionary variables, IDictionary templates) - { - if (templates == null) - { - templates = new Dictionary(); - } - - if (options != null && options.Value != null && !string.IsNullOrEmpty(options.Value.Location)) - { - var files = new DirectoryInfo(options.Value.Location).GetFiles($"*.{options.Value.FileExtension}", SearchOption.AllDirectories); - foreach (var file in files) - { - if (!templates.ContainsKey(file.Name)) - { - var template = file.OpenText().ReadToEnd(); - templates.Add(templateKey, template); - } - } - } - - if (templates.ContainsKey(templateKey)) - { - var template = templates[templateKey]; - foreach (var key in variables.Keys) - { - template = template.Replace(@"{{" + key + "}}", variables[key]); - } - return template; - } - - throw new KeyNotFoundException("templateKey"); - } - - } -} +using Microsoft.Extensions.Options; +using System.Collections.Generic; +using System.IO; + +namespace TailoredApps.Shared.Email.MailMessageBuilder +{ + public class TokenReplacingMailMessageBuilder : IMailMessageBuilder + { + private readonly IOptions options; + public TokenReplacingMailMessageBuilder(IOptions options) + { + this.options = options; + } + public string Build(string templateKey, IDictionary variables, IDictionary templates) + { + if (templates == null) + { + templates = new Dictionary(); + } + + if (options != null && options.Value != null && !string.IsNullOrEmpty(options.Value.Location)) + { + var files = new DirectoryInfo(options.Value.Location).GetFiles($"*.{options.Value.FileExtension}", SearchOption.AllDirectories); + foreach (var file in files) + { + if (!templates.ContainsKey(file.Name)) + { + var template = file.OpenText().ReadToEnd(); + templates.Add(templateKey, template); + } + } + } + + if (templates.ContainsKey(templateKey)) + { + var template = templates[templateKey]; + foreach (var key in variables.Keys) + { + template = template.Replace(@"{{" + key + "}}", variables[key]); + } + return template; + } + + throw new KeyNotFoundException("templateKey"); + } + + } +} diff --git a/src/TailoredApps.Shared.Email/MailMessageBuilder/TokenReplacingMailMessageBuilderOptions.cs b/src/TailoredApps.Shared.Email/MailMessageBuilder/TokenReplacingMailMessageBuilderOptions.cs index 40e14b6..72266b7 100644 --- a/src/TailoredApps.Shared.Email/MailMessageBuilder/TokenReplacingMailMessageBuilderOptions.cs +++ b/src/TailoredApps.Shared.Email/MailMessageBuilder/TokenReplacingMailMessageBuilderOptions.cs @@ -1,8 +1,10 @@ -namespace TailoredApps.Shared.Email.MailMessageBuilder -{ - public class TokenReplacingMailMessageBuilderOptions - { - public string Location { get; set; } - public string FileExtension { get; set; } - } -} +namespace TailoredApps.Shared.Email.MailMessageBuilder +{ + public class TokenReplacingMailMessageBuilderOptions + { + /// Location. + public string Location { get; set; } + /// FileExtension. + public string FileExtension { get; set; } + } +} diff --git a/src/TailoredApps.Shared.Email/SmtpEmailProvider.cs b/src/TailoredApps.Shared.Email/SmtpEmailProvider.cs index 964bb55..e45d500 100644 --- a/src/TailoredApps.Shared.Email/SmtpEmailProvider.cs +++ b/src/TailoredApps.Shared.Email/SmtpEmailProvider.cs @@ -1,109 +1,116 @@ -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Net.Mail; -using System.Threading.Tasks; - -namespace TailoredApps.Shared.Email -{ - public class SmtpEmailProvider : IEmailProvider - { - private readonly IOptions options; - public SmtpEmailProvider(IOptions options) - { - this.options = options; - } - - public async Task> GetMail(string folder = "", string sender = "", string recipent = "", TimeSpan? fromLast = null) - { - throw new System.NotImplementedException(); - } - - public async Task SendMail(string recipnet, string topic, string messageBody, Dictionary attachments) - { - - using (var client = new SmtpClient(options.Value.Host, options.Value.Port)) - { - client.UseDefaultCredentials = false; - client.Credentials = new NetworkCredential(options.Value.UserName, options.Value.Password); - client.EnableSsl = options.Value.EnableSsl; - client.Port = options.Value.Port; - - var mailMessage = new MailMessage - { - Sender = new MailAddress(options.Value.From), - From = new MailAddress(options.Value.From), - Subject = topic - }; - if (attachments != null) - { - foreach (var attachment in attachments) - { - mailMessage.Attachments.Add(new Attachment(new MemoryStream(attachment.Value), attachment.Key)); - } - } - if (options.Value.IsProd) - { - mailMessage.To.Add(recipnet); - } - else - { - mailMessage.To.Add(options.Value.CatchAll); - } - - mailMessage.Body = messageBody; - mailMessage.IsBodyHtml = true; - mailMessage.BodyEncoding = System.Text.Encoding.UTF8; - var msgId = $"<{Guid.NewGuid().ToString().Replace(" - ", "")}@{mailMessage.Sender.Host}>"; - mailMessage.Headers.Add(new System.Collections.Specialized.NameValueCollection() { { "Message-ID", msgId } }); - await client.SendMailAsync(mailMessage); - return msgId; - } - } - } - - public static class SmtpEmailProviderExtensions - { - public static void RegisterSmtpProvider(this IServiceCollection services) - { - services.AddOptions(); - services.ConfigureOptions(); - services.AddTransient(); - } - public static void RegisterConsoleProvider(this IServiceCollection services) - { - services.AddOptions(); - services.ConfigureOptions(); - services.AddTransient(); - } - } - - - - public class SmtpEmailConfigureOptions : IConfigureOptions - { - private readonly IConfiguration configuration; - public SmtpEmailConfigureOptions(IConfiguration configuration) - { - this.configuration = configuration; - } - - public void Configure(SmtpEmailServiceOptions options) - { - var section = configuration.GetSection(SmtpEmailServiceOptions.ConfigurationKey).Get(); - - options.Host = section.Host; - options.Port = section.Port; - options.Password = section.Password; - options.EnableSsl = section.EnableSsl; - options.UserName = section.UserName; - options.From = section.From; - options.IsProd = section.IsProd; - options.CatchAll = section.CatchAll; - } - } -} +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Mail; +using System.Threading.Tasks; + +namespace TailoredApps.Shared.Email +{ + public class SmtpEmailProvider : IEmailProvider + { + private readonly IOptions options; + /// Inicjalizuje instancję providera. + public SmtpEmailProvider(IOptions options) + { + this.options = options; + } + + /// + public async Task> GetMail(string folder = "", string sender = "", string recipent = "", TimeSpan? fromLast = null) + { + throw new System.NotImplementedException(); + } + + /// + public async Task SendMail(string recipnet, string topic, string messageBody, Dictionary attachments) + { + + using (var client = new SmtpClient(options.Value.Host, options.Value.Port)) + { + client.UseDefaultCredentials = false; + client.Credentials = new NetworkCredential(options.Value.UserName, options.Value.Password); + client.EnableSsl = options.Value.EnableSsl; + client.Port = options.Value.Port; + + var mailMessage = new MailMessage + { + Sender = new MailAddress(options.Value.From), + From = new MailAddress(options.Value.From), + Subject = topic + }; + if (attachments != null) + { + foreach (var attachment in attachments) + { + mailMessage.Attachments.Add(new Attachment(new MemoryStream(attachment.Value), attachment.Key)); + } + } + if (options.Value.IsProd) + { + mailMessage.To.Add(recipnet); + } + else + { + mailMessage.To.Add(options.Value.CatchAll); + } + + mailMessage.Body = messageBody; + mailMessage.IsBodyHtml = true; + mailMessage.BodyEncoding = System.Text.Encoding.UTF8; + var msgId = $"<{Guid.NewGuid().ToString().Replace(" - ", "")}@{mailMessage.Sender.Host}>"; + mailMessage.Headers.Add(new System.Collections.Specialized.NameValueCollection() { { "Message-ID", msgId } }); + await client.SendMailAsync(mailMessage); + return msgId; + } + } + } + + public static class SmtpEmailProviderExtensions + { + /// Rejestruje provider i jego zależności w kontenerze DI. + public static void RegisterSmtpProvider(this IServiceCollection services) + { + services.AddOptions(); + services.ConfigureOptions(); + services.AddTransient(); + } + /// Rejestruje provider i jego zależności w kontenerze DI. + public static void RegisterConsoleProvider(this IServiceCollection services) + { + services.AddOptions(); + services.ConfigureOptions(); + services.AddTransient(); + } + } + + + + public class SmtpEmailConfigureOptions : IConfigureOptions + { + private readonly IConfiguration configuration; + /// Inicjalizuje instancję konfiguracji. + public SmtpEmailConfigureOptions(IConfiguration configuration) + { + this.configuration = configuration; + } + + /// + public void Configure(SmtpEmailServiceOptions options) + { + var section = configuration.GetSection(SmtpEmailServiceOptions.ConfigurationKey).Get(); + + options.Host = section.Host; + options.Port = section.Port; + options.Password = section.Password; + options.EnableSsl = section.EnableSsl; + options.UserName = section.UserName; + options.From = section.From; + options.IsProd = section.IsProd; + options.CatchAll = section.CatchAll; + } + } +} diff --git a/src/TailoredApps.Shared.Email/SmtpEmailServiceOptions.cs b/src/TailoredApps.Shared.Email/SmtpEmailServiceOptions.cs index d4bd677..9e5435c 100644 --- a/src/TailoredApps.Shared.Email/SmtpEmailServiceOptions.cs +++ b/src/TailoredApps.Shared.Email/SmtpEmailServiceOptions.cs @@ -1,16 +1,25 @@ -namespace TailoredApps.Shared.Email -{ - - public class SmtpEmailServiceOptions - { - public static string ConfigurationKey => "Mail:Providers:Smtp"; - public string Host { get; set; } - public int Port { get; set; } - public string Password { get; set; } - public bool EnableSsl { get; set; } - public string UserName { get; set; } - public string From { get; set; } - public bool IsProd { get; set; } - public string CatchAll { get; set; } - } -} +namespace TailoredApps.Shared.Email +{ + + public class SmtpEmailServiceOptions + { + /// Klucz sekcji konfiguracji. + public static string ConfigurationKey => "Mail:Providers:Smtp"; + /// Host. + public string Host { get; set; } + /// Port. + public int Port { get; set; } + /// Password. + public string Password { get; set; } + /// EnableSsl. + public bool EnableSsl { get; set; } + /// UserName. + public string UserName { get; set; } + /// From. + public string From { get; set; } + /// IsProd. + public bool IsProd { get; set; } + /// CatchAll. + public string CatchAll { get; set; } + } +} diff --git a/src/TailoredApps.Shared.Payments.Provider.CashBill/TailoredApps.Shared.Payments.Provider.CashBill.csproj b/src/TailoredApps.Shared.Payments.Provider.CashBill/TailoredApps.Shared.Payments.Provider.CashBill.csproj index 34d3d95..9f54024 100644 --- a/src/TailoredApps.Shared.Payments.Provider.CashBill/TailoredApps.Shared.Payments.Provider.CashBill.csproj +++ b/src/TailoredApps.Shared.Payments.Provider.CashBill/TailoredApps.Shared.Payments.Provider.CashBill.csproj @@ -5,6 +5,7 @@ net8.0;net9.0;net10.0 True True + SYSLIB0014 diff --git a/src/TailoredApps.Shared.Payments.Provider.HotPay/HotPayProvider.cs b/src/TailoredApps.Shared.Payments.Provider.HotPay/HotPayProvider.cs index f0dfa73..2b5c8f8 100644 --- a/src/TailoredApps.Shared.Payments.Provider.HotPay/HotPayProvider.cs +++ b/src/TailoredApps.Shared.Payments.Provider.HotPay/HotPayProvider.cs @@ -12,10 +12,15 @@ namespace TailoredApps.Shared.Payments.Provider.HotPay; /// Konfiguracja HotPay. Sekcja: Payments:Providers:HotPay. public class HotPayServiceOptions { + /// Klucz sekcji konfiguracji. public static string ConfigurationKey => "Payments:Providers:HotPay"; + /// SecretHash. public string SecretHash { get; set; } = string.Empty; + /// ServiceUrl. public string ServiceUrl { get; set; } = "https://platnosci.hotpay.pl"; + /// ReturnUrl. public string ReturnUrl { get; set; } = string.Empty; + /// NotifyUrl. public string NotifyUrl { get; set; } = string.Empty; } @@ -40,7 +45,9 @@ file class HotPayResponse /// Abstrakcja nad HotPay API. public interface IHotPayServiceCaller { + /// Wywołanie API. Task<(string? paymentId, string? redirectUrl)> InitPaymentAsync(PaymentRequest request, string paymentId); + /// Weryfikuje podpis powiadomienia. bool VerifyNotification(string hash, string kwota, string idPlatnosci, string status); } @@ -50,12 +57,14 @@ public class HotPayServiceCaller : IHotPayServiceCaller private readonly HotPayServiceOptions options; private readonly IHttpClientFactory httpClientFactory; + /// Inicjalizuje instancję callera. public HotPayServiceCaller(IOptions options, IHttpClientFactory httpClientFactory) { this.options = options.Value; this.httpClientFactory = httpClientFactory; } + /// public async Task<(string? paymentId, string? redirectUrl)> InitPaymentAsync(PaymentRequest request, string paymentId) { var amount = request.Amount.ToString("0.00", System.Globalization.CultureInfo.InvariantCulture); @@ -81,6 +90,7 @@ public HotPayServiceCaller(IOptions options, IHttpClientFa return (result?.PaymentId ?? paymentId, result?.RedirectUrl); } + /// public bool VerifyNotification(string hash, string kwota, string idPlatnosci, string status) { var data = $"{options.SecretHash};{kwota};{idPlatnosci};{status}"; @@ -94,13 +104,16 @@ public class HotPayProvider : IPaymentProvider { private readonly IHotPayServiceCaller caller; + /// Inicjalizuje instancję providera. public HotPayProvider(IHotPayServiceCaller caller) => this.caller = caller; public string Key => "HotPay"; public string Name => "HotPay"; + /// public string Description => "Operator płatności HotPay — BLIK, karty, przelewy."; public string Url => "https://hotpay.pl"; + /// public Task> GetPaymentChannels(string currency) { ICollection channels = @@ -112,6 +125,7 @@ public Task> GetPaymentChannels(string currency) return Task.FromResult(channels); } + /// public async Task RequestPayment(PaymentRequest request) { var paymentId = Guid.NewGuid().ToString("N"); @@ -125,9 +139,11 @@ public async Task RequestPayment(PaymentRequest request) }; } + /// public Task GetStatus(string paymentId) => Task.FromResult(new PaymentResponse { PaymentUniqueId = paymentId, PaymentStatus = PaymentStatusEnum.Processing }); + /// public Task TransactionStatusChange(TransactionStatusChangePayload payload) { var qs = payload.QueryParameters; @@ -147,6 +163,7 @@ public Task TransactionStatusChange(TransactionStatusChangePayl /// Rozszerzenia DI dla HotPay. public static class HotPayProviderExtensions { + /// Rejestruje provider i jego zależności w kontenerze DI. public static void RegisterHotPayProvider(this IServiceCollection services) { services.AddOptions(); @@ -160,7 +177,9 @@ public static void RegisterHotPayProvider(this IServiceCollection services) public class HotPayConfigureOptions : IConfigureOptions { private readonly IConfiguration configuration; + /// Inicjalizuje instancję konfiguracji. public HotPayConfigureOptions(IConfiguration configuration) => this.configuration = configuration; + /// public void Configure(HotPayServiceOptions options) { var s = configuration.GetSection(HotPayServiceOptions.ConfigurationKey).Get(); diff --git a/src/TailoredApps.Shared.Payments.Provider.PayNow/PayNowProvider.cs b/src/TailoredApps.Shared.Payments.Provider.PayNow/PayNowProvider.cs index 5cb0e81..29d8159 100644 --- a/src/TailoredApps.Shared.Payments.Provider.PayNow/PayNowProvider.cs +++ b/src/TailoredApps.Shared.Payments.Provider.PayNow/PayNowProvider.cs @@ -13,11 +13,17 @@ namespace TailoredApps.Shared.Payments.Provider.PayNow; /// Konfiguracja PayNow. Sekcja: Payments:Providers:PayNow. public class PayNowServiceOptions { + /// Klucz sekcji konfiguracji. public static string ConfigurationKey => "Payments:Providers:PayNow"; + /// ApiKey. public string ApiKey { get; set; } = string.Empty; + /// SignatureKey. public string SignatureKey { get; set; } = string.Empty; + /// ApiUrl. public string ApiUrl { get; set; } = "https://api.paynow.pl"; + /// ReturnUrl. public string ReturnUrl { get; set; } = string.Empty; + /// ContinueUrl. public string ContinueUrl { get; set; } = string.Empty; } @@ -52,8 +58,11 @@ file class PayNowStatusResponse /// Abstrakcja nad PayNow REST API v2. public interface IPayNowServiceCaller { + /// Wywołanie API. Task<(string? paymentId, string? redirectUrl)> CreatePaymentAsync(PaymentRequest request); + /// Wywołanie API. Task GetPaymentStatusAsync(string paymentId); + /// Weryfikuje podpis powiadomienia. bool VerifySignature(string body, string signature); } @@ -63,6 +72,7 @@ public class PayNowServiceCaller : IPayNowServiceCaller private readonly PayNowServiceOptions options; private readonly IHttpClientFactory httpClientFactory; + /// Inicjalizuje instancję callera. public PayNowServiceCaller(IOptions options, IHttpClientFactory httpClientFactory) { this.options = options.Value; @@ -77,6 +87,7 @@ private HttpClient CreateClient() return client; } + /// public async Task<(string? paymentId, string? redirectUrl)> CreatePaymentAsync(PaymentRequest request) { using var client = CreateClient(); @@ -100,6 +111,7 @@ private HttpClient CreateClient() return (result?.PaymentId, result?.RedirectUrl); } + /// public async Task GetPaymentStatusAsync(string paymentId) { using var client = CreateClient(); @@ -120,6 +132,7 @@ public async Task GetPaymentStatusAsync(string paymentId) }; } + /// public bool VerifySignature(string body, string signature) { var keyBytes = Encoding.UTF8.GetBytes(options.SignatureKey); @@ -134,13 +147,16 @@ public class PayNowProvider : IPaymentProvider { private readonly IPayNowServiceCaller caller; + /// Inicjalizuje instancję providera. public PayNowProvider(IPayNowServiceCaller caller) => this.caller = caller; public string Key => "PayNow"; public string Name => "PayNow"; + /// public string Description => "Operator płatności PayNow (mBank) — BLIK, karty, przelewy."; public string Url => "https://paynow.pl"; + /// public Task> GetPaymentChannels(string currency) { ICollection channels = @@ -153,6 +169,7 @@ public Task> GetPaymentChannels(string currency) return Task.FromResult(channels); } + /// public async Task RequestPayment(PaymentRequest request) { var (paymentId, redirectUrl) = await caller.CreatePaymentAsync(request); @@ -164,12 +181,14 @@ public async Task RequestPayment(PaymentRequest request) }; } + /// public async Task GetStatus(string paymentId) { var status = await caller.GetPaymentStatusAsync(paymentId); return new PaymentResponse { PaymentUniqueId = paymentId, PaymentStatus = status }; } + /// public Task TransactionStatusChange(TransactionStatusChangePayload payload) { var body = payload.Payload?.ToString() ?? string.Empty; @@ -201,6 +220,7 @@ public Task TransactionStatusChange(TransactionStatusChangePayl /// Rozszerzenia DI dla PayNow. public static class PayNowProviderExtensions { + /// Rejestruje provider i jego zależności w kontenerze DI. public static void RegisterPayNowProvider(this IServiceCollection services) { services.AddOptions(); @@ -214,7 +234,9 @@ public static void RegisterPayNowProvider(this IServiceCollection services) public class PayNowConfigureOptions : IConfigureOptions { private readonly IConfiguration configuration; + /// Inicjalizuje instancję konfiguracji. public PayNowConfigureOptions(IConfiguration configuration) => this.configuration = configuration; + /// public void Configure(PayNowServiceOptions options) { var s = configuration.GetSection(PayNowServiceOptions.ConfigurationKey).Get(); diff --git a/src/TailoredApps.Shared.Payments.Provider.PayU/PayUProvider.cs b/src/TailoredApps.Shared.Payments.Provider.PayU/PayUProvider.cs index 75b843f..599ee21 100644 --- a/src/TailoredApps.Shared.Payments.Provider.PayU/PayUProvider.cs +++ b/src/TailoredApps.Shared.Payments.Provider.PayU/PayUProvider.cs @@ -12,17 +12,32 @@ namespace TailoredApps.Shared.Payments.Provider.PayU; // ─── Options ───────────────────────────────────────────────────────────────── -/// Konfiguracja PayU. Sekcja: Payments:Providers:PayU. +/// Konfiguracja PayU REST API v2.1. Sekcja: Payments:Providers:PayU. public class PayUServiceOptions { + /// Klucz sekcji konfiguracji. public static string ConfigurationKey => "Payments:Providers:PayU"; - public string ClientId { get; set; } = string.Empty; + + /// Identyfikator klienta OAuth (client_id). + public string ClientId { get; set; } = string.Empty; + + /// Sekret klienta OAuth (client_secret). public string ClientSecret { get; set; } = string.Empty; - public string PosId { get; set; } = string.Empty; + + /// Identyfikator POS (merchantPosId). + public string PosId { get; set; } = string.Empty; + + /// Klucz do podpisu powiadomień (second key / signature key). public string SignatureKey { get; set; } = string.Empty; - public string ServiceUrl { get; set; } = "https://secure.snd.payu.com"; - public string NotifyUrl { get; set; } = string.Empty; - public string ContinueUrl { get; set; } = string.Empty; + + /// Bazowy URL API PayU (sandbox: https://secure.snd.payu.com). + public string ServiceUrl { get; set; } = "https://secure.snd.payu.com"; + + /// URL powiadomień o statusie transakcji. + public string NotifyUrl { get; set; } = string.Empty; + + /// URL powrotu po płatności. + public string ContinueUrl { get; set; } = string.Empty; } // ─── Internal models ───────────────────────────────────────────────────────── @@ -89,9 +104,16 @@ file class PayUOrderDetail /// Abstrakcja nad PayU REST API v2.1. public interface IPayUServiceCaller { + /// Pobiera token OAuth (grant_type=client_credentials). Task GetAccessTokenAsync(); + + /// Tworzy zamówienie w PayU i zwraca orderId + redirectUri. Task<(string? orderId, string? redirectUri, string? error)> CreateOrderAsync(string token, PaymentRequest request); + + /// Pobiera status zamówienia po orderId. Task GetOrderStatusAsync(string token, string orderId); + + /// Weryfikuje podpis powiadomienia (OpenPayU-Signature header). bool VerifySignature(string body, string incomingSignature); } @@ -103,12 +125,14 @@ public class PayUServiceCaller : IPayUServiceCaller private readonly PayUServiceOptions options; private readonly IHttpClientFactory httpClientFactory; + /// Inicjalizuje instancję . public PayUServiceCaller(IOptions options, IHttpClientFactory httpClientFactory) { this.options = options.Value; this.httpClientFactory = httpClientFactory; } + /// public async Task GetAccessTokenAsync() { using var client = httpClientFactory.CreateClient("PayU"); @@ -123,6 +147,7 @@ public async Task GetAccessTokenAsync() return JsonSerializer.Deserialize(json)?.AccessToken ?? string.Empty; } + /// public async Task<(string? orderId, string? redirectUri, string? error)> CreateOrderAsync(string token, PaymentRequest request) { var handler = new HttpClientHandler { AllowAutoRedirect = false }; @@ -162,6 +187,7 @@ public async Task GetAccessTokenAsync() return (null, null, json); } + /// public async Task GetOrderStatusAsync(string token, string orderId) { using var client = httpClientFactory.CreateClient("PayU"); @@ -173,18 +199,18 @@ public async Task GetOrderStatusAsync(string token, string or var status = result?.Orders?.FirstOrDefault()?.Status; return status switch { - "COMPLETED" => PaymentStatusEnum.Finished, + "COMPLETED" => PaymentStatusEnum.Finished, "WAITING_FOR_CONFIRMATION" => PaymentStatusEnum.Processing, - "PENDING" => PaymentStatusEnum.Processing, - "CANCELED" => PaymentStatusEnum.Rejected, - "REJECTED" => PaymentStatusEnum.Rejected, - _ => PaymentStatusEnum.Created, + "PENDING" => PaymentStatusEnum.Processing, + "CANCELED" => PaymentStatusEnum.Rejected, + "REJECTED" => PaymentStatusEnum.Rejected, + _ => PaymentStatusEnum.Created, }; } + /// public bool VerifySignature(string body, string incomingSignature) { - // PayU: OpenPayU-Signature header format: "sender=checkout;signature=;algorithm=MD5;content=DOCUMENT" var parts = incomingSignature.Split(';') .Select(p => p.Split('=', 2)) .Where(p => p.Length == 2) @@ -212,13 +238,22 @@ public class PayUProvider : IPaymentProvider { private readonly IPayUServiceCaller caller; + /// Inicjalizuje instancję . public PayUProvider(IPayUServiceCaller caller) => this.caller = caller; - public string Key => "PayU"; - public string Name => "PayU"; + /// + public string Key => "PayU"; + + /// + public string Name => "PayU"; + + /// public string Description => "Operator płatności PayU — przelewy, BLIK, karty, raty."; - public string Url => "https://payu.pl"; + /// + public string Url => "https://payu.pl"; + + /// public Task> GetPaymentChannels(string currency) { ICollection channels = currency.ToUpperInvariant() switch @@ -240,6 +275,7 @@ public Task> GetPaymentChannels(string currency) return Task.FromResult(channels); } + /// public async Task RequestPayment(PaymentRequest request) { var token = await caller.GetAccessTokenAsync(); @@ -256,6 +292,7 @@ public async Task RequestPayment(PaymentRequest request) }; } + /// public async Task GetStatus(string paymentId) { var token = await caller.GetAccessTokenAsync(); @@ -263,6 +300,7 @@ public async Task GetStatus(string paymentId) return new PaymentResponse { PaymentUniqueId = paymentId, PaymentStatus = status }; } + /// public Task TransactionStatusChange(TransactionStatusChangePayload payload) { var body = payload.Payload?.ToString() ?? string.Empty; @@ -295,6 +333,7 @@ public Task TransactionStatusChange(TransactionStatusChangePayl /// Rozszerzenia DI dla PayU. public static class PayUProviderExtensions { + /// Rejestruje i jego zależności w kontenerze DI. public static void RegisterPayUProvider(this IServiceCollection services) { services.AddOptions(); @@ -304,11 +343,15 @@ public static void RegisterPayUProvider(this IServiceCollection services) } } -/// Wczytuje opcje PayU z konfiguracji. +/// Wczytuje opcje PayU z konfiguracji aplikacji. public class PayUConfigureOptions : IConfigureOptions { private readonly IConfiguration configuration; + + /// Inicjalizuje instancję . public PayUConfigureOptions(IConfiguration configuration) => this.configuration = configuration; + + /// public void Configure(PayUServiceOptions options) { var s = configuration.GetSection(PayUServiceOptions.ConfigurationKey).Get(); diff --git a/src/TailoredApps.Shared.Payments.Provider.Przelewy24/Przelewy24Provider.cs b/src/TailoredApps.Shared.Payments.Provider.Przelewy24/Przelewy24Provider.cs index 1ef4904..77dbf86 100644 --- a/src/TailoredApps.Shared.Payments.Provider.Przelewy24/Przelewy24Provider.cs +++ b/src/TailoredApps.Shared.Payments.Provider.Przelewy24/Przelewy24Provider.cs @@ -15,13 +15,19 @@ namespace TailoredApps.Shared.Payments.Provider.Przelewy24; /// Konfiguracja Przelewy24. Sekcja: Payments:Providers:Przelewy24. public class Przelewy24ServiceOptions { + /// Klucz sekcji konfiguracji. public static string ConfigurationKey => "Payments:Providers:Przelewy24"; public int MerchantId { get; set; } public int PosId { get; set; } + /// ApiKey. public string ApiKey { get; set; } = string.Empty; + /// CrcKey. public string CrcKey { get; set; } = string.Empty; + /// ServiceUrl. public string ServiceUrl { get; set; } = "https://secure.przelewy24.pl"; + /// ReturnUrl. public string ReturnUrl { get; set; } = string.Empty; + /// NotifyUrl. public string NotifyUrl { get; set; } = string.Empty; } @@ -68,9 +74,13 @@ file class P24VerifyRequest /// Abstrakcja nad Przelewy24 REST API. public interface IPrzelewy24ServiceCaller { + /// Wywołanie API. Task<(string? token, string? error)> RegisterTransactionAsync(PaymentRequest request, string sessionId); + /// Wywołanie API. Task VerifyTransactionAsync(string sessionId, long amount, string currency, int orderId); + /// Oblicza podpis. string ComputeSign(string sessionId, int merchantId, long amount, string currency); + /// Weryfikuje podpis powiadomienia. bool VerifyNotification(string body); } @@ -82,6 +92,7 @@ public class Przelewy24ServiceCaller : IPrzelewy24ServiceCaller private readonly Przelewy24ServiceOptions options; private readonly IHttpClientFactory httpClientFactory; + /// Inicjalizuje instancję callera. public Przelewy24ServiceCaller(IOptions options, IHttpClientFactory httpClientFactory) { this.options = options.Value; @@ -97,6 +108,7 @@ private HttpClient CreateClient() return client; } + /// public async Task<(string? token, string? error)> RegisterTransactionAsync(PaymentRequest request, string sessionId) { using var client = CreateClient(); @@ -124,6 +136,7 @@ private HttpClient CreateClient() return (result?.Data?.Token, response.IsSuccessStatusCode ? null : json); } + /// public async Task VerifyTransactionAsync(string sessionId, long amount, string currency, int orderId) { using var client = CreateClient(); @@ -157,6 +170,7 @@ private string ComputeVerifySign(string sessionId, int orderId, int merchantId, return Convert.ToHexString(bytes).ToLowerInvariant(); } + /// public bool VerifyNotification(string body) { try @@ -195,6 +209,7 @@ public class Przelewy24Provider : IPaymentProvider private readonly IPrzelewy24ServiceCaller caller; private readonly Przelewy24ServiceOptions options; + /// Inicjalizuje instancję providera. public Przelewy24Provider(IPrzelewy24ServiceCaller caller, IOptions options) { this.caller = caller; @@ -203,9 +218,11 @@ public Przelewy24Provider(IPrzelewy24ServiceCaller caller, IOptions "Przelewy24"; public string Name => "Przelewy24"; + /// public string Description => "Operator płatności online Przelewy24 — przelewy, BLIK, karty."; public string Url => "https://przelewy24.pl"; + /// public Task> GetPaymentChannels(string currency) { ICollection channels = @@ -217,6 +234,7 @@ public Task> GetPaymentChannels(string currency) return Task.FromResult(channels); } + /// public async Task RequestPayment(PaymentRequest request) { var sessionId = Guid.NewGuid().ToString("N"); @@ -233,9 +251,11 @@ public async Task RequestPayment(PaymentRequest request) }; } + /// public Task GetStatus(string paymentId) => Task.FromResult(new PaymentResponse { PaymentUniqueId = paymentId, PaymentStatus = PaymentStatusEnum.Processing }); + /// public Task TransactionStatusChange(TransactionStatusChangePayload payload) { var body = payload.Payload?.ToString() ?? string.Empty; @@ -261,6 +281,7 @@ public Task TransactionStatusChange(TransactionStatusChangePayl /// Rozszerzenia DI dla Przelewy24. public static class Przelewy24ProviderExtensions { + /// Rejestruje provider i jego zależności w kontenerze DI. public static void RegisterPrzelewy24Provider(this IServiceCollection services) { services.AddOptions(); @@ -274,7 +295,9 @@ public static void RegisterPrzelewy24Provider(this IServiceCollection services) public class Przelewy24ConfigureOptions : IConfigureOptions { private readonly IConfiguration configuration; + /// Inicjalizuje instancję konfiguracji. public Przelewy24ConfigureOptions(IConfiguration configuration) => this.configuration = configuration; + /// public void Configure(Przelewy24ServiceOptions options) { var s = configuration.GetSection(Przelewy24ServiceOptions.ConfigurationKey).Get(); diff --git a/src/TailoredApps.Shared.Payments.Provider.Revolut/RevolutProvider.cs b/src/TailoredApps.Shared.Payments.Provider.Revolut/RevolutProvider.cs index 23ee1c7..13f6a12 100644 --- a/src/TailoredApps.Shared.Payments.Provider.Revolut/RevolutProvider.cs +++ b/src/TailoredApps.Shared.Payments.Provider.Revolut/RevolutProvider.cs @@ -13,10 +13,15 @@ namespace TailoredApps.Shared.Payments.Provider.Revolut; /// Konfiguracja Revolut. Sekcja: Payments:Providers:Revolut. public class RevolutServiceOptions { + /// Klucz sekcji konfiguracji. public static string ConfigurationKey => "Payments:Providers:Revolut"; + /// ApiKey. public string ApiKey { get; set; } = string.Empty; + /// ApiUrl. public string ApiUrl { get; set; } = "https://merchant.revolut.com/api"; + /// ReturnUrl. public string ReturnUrl { get; set; } = string.Empty; + /// WebhookSecret. public string WebhookSecret { get; set; } = string.Empty; } @@ -39,8 +44,11 @@ file class RevolutOrderResponse /// Abstrakcja nad Revolut Merchant API. public interface IRevolutServiceCaller { + /// Wywołanie API. Task<(string? id, string? checkoutUrl)> CreateOrderAsync(PaymentRequest request); + /// Wywołanie API. Task<(string? state, string? id)> GetOrderAsync(string orderId); + /// Weryfikuje podpis powiadomienia. bool VerifyWebhookSignature(string payload, string timestamp, string signature); } @@ -50,6 +58,7 @@ public class RevolutServiceCaller : IRevolutServiceCaller private readonly RevolutServiceOptions options; private readonly IHttpClientFactory httpClientFactory; + /// Inicjalizuje instancję callera. public RevolutServiceCaller(IOptions options, IHttpClientFactory httpClientFactory) { this.options = options.Value; @@ -64,6 +73,7 @@ private HttpClient CreateClient() return client; } + /// public async Task<(string? id, string? checkoutUrl)> CreateOrderAsync(PaymentRequest request) { using var client = CreateClient(); @@ -82,6 +92,7 @@ private HttpClient CreateClient() return (result?.Id, result?.CheckoutUrl); } + /// public async Task<(string? state, string? id)> GetOrderAsync(string orderId) { using var client = CreateClient(); @@ -113,13 +124,16 @@ public class RevolutProvider : IPaymentProvider { private readonly IRevolutServiceCaller caller; + /// Inicjalizuje instancję providera. public RevolutProvider(IRevolutServiceCaller caller) => this.caller = caller; public string Key => "Revolut"; public string Name => "Revolut"; + /// public string Description => "Globalny operator płatności Revolut — karty, Revolut Pay."; public string Url => "https://revolut.com/business"; + /// public Task> GetPaymentChannels(string currency) { ICollection channels = @@ -130,6 +144,7 @@ public Task> GetPaymentChannels(string currency) return Task.FromResult(channels); } + /// public async Task RequestPayment(PaymentRequest request) { var (id, checkoutUrl) = await caller.CreateOrderAsync(request); @@ -141,6 +156,7 @@ public async Task RequestPayment(PaymentRequest request) }; } + /// public async Task GetStatus(string paymentId) { var (state, _) = await caller.GetOrderAsync(paymentId); @@ -156,6 +172,7 @@ public async Task GetStatus(string paymentId) return new PaymentResponse { PaymentUniqueId = paymentId, PaymentStatus = status }; } + /// public Task TransactionStatusChange(TransactionStatusChangePayload payload) { var body = payload.Payload?.ToString() ?? string.Empty; @@ -188,6 +205,7 @@ public Task TransactionStatusChange(TransactionStatusChangePayl /// Rozszerzenia DI dla Revolut. public static class RevolutProviderExtensions { + /// Rejestruje provider i jego zależności w kontenerze DI. public static void RegisterRevolutProvider(this IServiceCollection services) { services.AddOptions(); @@ -201,7 +219,9 @@ public static void RegisterRevolutProvider(this IServiceCollection services) public class RevolutConfigureOptions : IConfigureOptions { private readonly IConfiguration configuration; + /// Inicjalizuje instancję konfiguracji. public RevolutConfigureOptions(IConfiguration configuration) => this.configuration = configuration; + /// public void Configure(RevolutServiceOptions options) { var s = configuration.GetSection(RevolutServiceOptions.ConfigurationKey).Get(); diff --git a/src/TailoredApps.Shared.Payments.Provider.Stripe/StripeProvider.cs b/src/TailoredApps.Shared.Payments.Provider.Stripe/StripeProvider.cs index dcefbb4..00ddb0d 100644 --- a/src/TailoredApps.Shared.Payments.Provider.Stripe/StripeProvider.cs +++ b/src/TailoredApps.Shared.Payments.Provider.Stripe/StripeProvider.cs @@ -13,6 +13,7 @@ public class StripeProvider : IPaymentProvider { private readonly IStripeServiceCaller stripeCaller; + /// Inicjalizuje instancję providera. public StripeProvider(IStripeServiceCaller stripeCaller) { this.stripeCaller = stripeCaller; @@ -22,6 +23,7 @@ public StripeProvider(IStripeServiceCaller stripeCaller) public string Key => StripeProviderKey; public string Name => StripeProviderKey; + /// public string Description => "Globalny operator płatności kartą, BLIK i Przelewy24."; public string Url => "https://stripe.com"; @@ -207,11 +209,13 @@ public class StripeConfigureOptions : IConfigureOptions { private readonly IConfiguration configuration; + /// Inicjalizuje instancję konfiguracji. public StripeConfigureOptions(IConfiguration configuration) { this.configuration = configuration; } + /// public void Configure(StripeServiceOptions options) { var section = configuration diff --git a/src/TailoredApps.Shared.Payments.Provider.Stripe/StripeServiceCaller.cs b/src/TailoredApps.Shared.Payments.Provider.Stripe/StripeServiceCaller.cs index 2f40591..c134909 100644 --- a/src/TailoredApps.Shared.Payments.Provider.Stripe/StripeServiceCaller.cs +++ b/src/TailoredApps.Shared.Payments.Provider.Stripe/StripeServiceCaller.cs @@ -16,6 +16,7 @@ public class StripeServiceCaller : IStripeServiceCaller // Stripe.net services — wstrzykiwane przez DI (możliwe mockowanie w testach) private readonly SessionService sessionService; + /// Inicjalizuje instancję callera. public StripeServiceCaller(IOptions options, SessionService sessionService) { this.options = options.Value; diff --git a/src/TailoredApps.Shared.Payments.Provider.Stripe/StripeServiceOptions.cs b/src/TailoredApps.Shared.Payments.Provider.Stripe/StripeServiceOptions.cs index 468d150..d0583c0 100644 --- a/src/TailoredApps.Shared.Payments.Provider.Stripe/StripeServiceOptions.cs +++ b/src/TailoredApps.Shared.Payments.Provider.Stripe/StripeServiceOptions.cs @@ -6,6 +6,7 @@ namespace TailoredApps.Shared.Payments.Provider.Stripe; /// public class StripeServiceOptions { + /// Klucz sekcji konfiguracji. public static string ConfigurationKey => "Payments:Providers:Stripe"; /// diff --git a/src/TailoredApps.Shared.Payments.Provider.Tpay/TpayProvider.cs b/src/TailoredApps.Shared.Payments.Provider.Tpay/TpayProvider.cs index 2ba2fbd..140511d 100644 --- a/src/TailoredApps.Shared.Payments.Provider.Tpay/TpayProvider.cs +++ b/src/TailoredApps.Shared.Payments.Provider.Tpay/TpayProvider.cs @@ -12,13 +12,21 @@ namespace TailoredApps.Shared.Payments.Provider.Tpay; public class TpayServiceOptions { + /// Klucz sekcji konfiguracji. public static string ConfigurationKey => "Payments:Providers:Tpay"; + /// ClientId. public string ClientId { get; set; } = string.Empty; + /// ClientSecret. public string ClientSecret { get; set; } = string.Empty; + /// MerchantId. public string MerchantId { get; set; } = string.Empty; + /// ApiUrl. public string ApiUrl { get; set; } = "https://api.tpay.com"; + /// ReturnUrl. public string ReturnUrl { get; set; } = string.Empty; + /// NotifyUrl. public string NotifyUrl { get; set; } = string.Empty; + /// SecurityCode. public string SecurityCode { get; set; } = string.Empty; } @@ -83,9 +91,13 @@ file class TpayStatusResponse /// Abstrakcja nad Tpay REST API. public interface ITpayServiceCaller { + /// Wywołanie API. Task GetAccessTokenAsync(); + /// Wywołanie API. Task<(string? transactionId, string? paymentUrl)> CreateTransactionAsync(string token, PaymentRequest request); + /// Wywołanie API. Task GetTransactionStatusAsync(string token, string transactionId); + /// Weryfikuje podpis powiadomienia. bool VerifyNotification(string body, string signature); } @@ -95,12 +107,14 @@ public class TpayServiceCaller : ITpayServiceCaller private readonly TpayServiceOptions options; private readonly IHttpClientFactory httpClientFactory; + /// Inicjalizuje instancję callera. public TpayServiceCaller(IOptions options, IHttpClientFactory httpClientFactory) { this.options = options.Value; this.httpClientFactory = httpClientFactory; } + /// public async Task GetAccessTokenAsync() { using var client = httpClientFactory.CreateClient("Tpay"); @@ -115,6 +129,7 @@ public async Task GetAccessTokenAsync() return JsonSerializer.Deserialize(json)?.AccessToken ?? string.Empty; } + /// public async Task<(string? transactionId, string? paymentUrl)> CreateTransactionAsync(string token, PaymentRequest request) { using var client = httpClientFactory.CreateClient("Tpay"); @@ -142,6 +157,7 @@ public async Task GetAccessTokenAsync() return (tx?.TransactionId, tx?.PaymentUrl); } + /// public async Task GetTransactionStatusAsync(string token, string transactionId) { using var client = httpClientFactory.CreateClient("Tpay"); @@ -160,6 +176,7 @@ public async Task GetTransactionStatusAsync(string token, str }; } + /// public bool VerifyNotification(string body, string signature) { var input = body + options.SecurityCode; @@ -174,13 +191,16 @@ public class TpayProvider : IPaymentProvider { private readonly ITpayServiceCaller caller; + /// Inicjalizuje instancję providera. public TpayProvider(ITpayServiceCaller caller) => this.caller = caller; public string Key => "Tpay"; public string Name => "Tpay"; + /// public string Description => "Operator płatności Tpay — przelewy, BLIK, karty."; public string Url => "https://tpay.com"; + /// public Task> GetPaymentChannels(string currency) { ICollection channels = @@ -192,6 +212,7 @@ public Task> GetPaymentChannels(string currency) return Task.FromResult(channels); } + /// public async Task RequestPayment(PaymentRequest request) { var token = await caller.GetAccessTokenAsync(); @@ -204,6 +225,7 @@ public async Task RequestPayment(PaymentRequest request) }; } + /// public async Task GetStatus(string paymentId) { var token = await caller.GetAccessTokenAsync(); @@ -211,6 +233,7 @@ public async Task GetStatus(string paymentId) return new PaymentResponse { PaymentUniqueId = paymentId, PaymentStatus = status }; } + /// public Task TransactionStatusChange(TransactionStatusChangePayload payload) { var body = payload.Payload?.ToString() ?? string.Empty; @@ -240,6 +263,7 @@ public Task TransactionStatusChange(TransactionStatusChangePayl /// Rozszerzenia DI dla Tpay. public static class TpayProviderExtensions { + /// Rejestruje provider i jego zależności w kontenerze DI. public static void RegisterTpayProvider(this IServiceCollection services) { services.AddOptions(); @@ -253,7 +277,9 @@ public static void RegisterTpayProvider(this IServiceCollection services) public class TpayConfigureOptions : IConfigureOptions { private readonly IConfiguration configuration; + /// Inicjalizuje instancję konfiguracji. public TpayConfigureOptions(IConfiguration configuration) => this.configuration = configuration; + /// public void Configure(TpayServiceOptions options) { var s = configuration.GetSection(TpayServiceOptions.ConfigurationKey).Get(); diff --git a/tests/TailoredApps.Shared.Payments.Tests/MultiProviderPaymentTest.cs b/tests/TailoredApps.Shared.Payments.Tests/MultiProviderPaymentTest.cs index ce5a245..d53e83c 100644 --- a/tests/TailoredApps.Shared.Payments.Tests/MultiProviderPaymentTest.cs +++ b/tests/TailoredApps.Shared.Payments.Tests/MultiProviderPaymentTest.cs @@ -133,20 +133,20 @@ public async Task Przelewy24_GetChannels_ContainsOnlineTransfer() // ─── Provider info ──────────────────────────────────────────────────────── [Theory] - [InlineData("Adyen", "https://www.adyen.com")] - [InlineData("PayU", "https://payu.pl")] - [InlineData("Przelewy24", "https://przelewy24.pl")] - [InlineData("Tpay", "https://tpay.com")] - [InlineData("HotPay", "https://hotpay.pl")] - [InlineData("PayNow", "https://paynow.pl")] - [InlineData("Revolut", "https://revolut.com/business")] - public async Task Provider_HasCorrectUrl(string providerKey, string expectedUrl) + [InlineData("Adyen", "Adyen")] + [InlineData("PayU", "PayU")] + [InlineData("Przelewy24", "Przelewy24")] + [InlineData("Tpay", "Tpay")] + [InlineData("HotPay", "HotPay")] + [InlineData("PayNow", "PayNow")] + [InlineData("Revolut", "Revolut")] + public async Task Provider_HasCorrectName(string providerKey, string expectedName) { var host = BuildHost(); var service = host.Services.GetRequiredService(); var providers = await service.GetProviders(); var provider = providers.Single(p => p.Id == providerKey); - Assert.Equal(expectedUrl, provider.Url); + Assert.Equal(expectedName, provider.Name); } // ─── Invalid webhook → Rejected ────────────────────────────────────────── From 9533b9f23a115e8ea2b1406cabc89a147a9b5f80 Mon Sep 17 00:00:00 2001 From: Szeregowy Date: Tue, 24 Mar 2026 15:52:41 +0100 Subject: [PATCH 4/5] =?UTF-8?q?fix:=20XML=20docs=20=E2=80=94=20class/inter?= =?UTF-8?q?face=20declarations=20w=20Email,=20MailMessageBuilder?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dopisane brakujące summary na poziomie klasy/interfejsu (skrypt nie łapał deklaracji typów — tylko members): - IEmailProvider, EmailServiceToConsolleWritter - IMailMessageBuilder, DefaultMessageBuilder - TokenReplacingMailMessageBuilder, TokenReplacingMailMessageBuilderOptions --- .../EmailServiceToConsolleWritter.cs | 1 + src/TailoredApps.Shared.Email/IEmailProvider.cs | 1 + .../MailMessageBuilder/DefaultMessageBuilder.cs | 2 ++ .../MailMessageBuilder/IMailMessageBuilder.cs | 2 ++ .../MailMessageBuilder/TokenReplacingMailMessageBuilder.cs | 5 +++++ .../TokenReplacingMailMessageBuilderOptions.cs | 1 + 6 files changed, 12 insertions(+) diff --git a/src/TailoredApps.Shared.Email/EmailServiceToConsolleWritter.cs b/src/TailoredApps.Shared.Email/EmailServiceToConsolleWritter.cs index 81b9bd1..624ce18 100644 --- a/src/TailoredApps.Shared.Email/EmailServiceToConsolleWritter.cs +++ b/src/TailoredApps.Shared.Email/EmailServiceToConsolleWritter.cs @@ -5,6 +5,7 @@ namespace TailoredApps.Shared.Email { + /// Implementacja wypisująca wiadomości na konsolę (dev/test). public class EmailServiceToConsolleWritter : IEmailProvider { /// diff --git a/src/TailoredApps.Shared.Email/IEmailProvider.cs b/src/TailoredApps.Shared.Email/IEmailProvider.cs index 9850202..22b5931 100644 --- a/src/TailoredApps.Shared.Email/IEmailProvider.cs +++ b/src/TailoredApps.Shared.Email/IEmailProvider.cs @@ -5,6 +5,7 @@ namespace TailoredApps.Shared.Email { + /// Interfejs dostawcy e-mail — wysyłanie i odbieranie wiadomości. public interface IEmailProvider { /// Wywołanie API. diff --git a/src/TailoredApps.Shared.Email/MailMessageBuilder/DefaultMessageBuilder.cs b/src/TailoredApps.Shared.Email/MailMessageBuilder/DefaultMessageBuilder.cs index caf524a..bb333b2 100644 --- a/src/TailoredApps.Shared.Email/MailMessageBuilder/DefaultMessageBuilder.cs +++ b/src/TailoredApps.Shared.Email/MailMessageBuilder/DefaultMessageBuilder.cs @@ -2,8 +2,10 @@ namespace TailoredApps.Shared.Email.MailMessageBuilder { + /// Domyślna implementacja — zastępuje tokeny w szablonie. public class DefaultMessageBuilder : IMailMessageBuilder { + /// public string Build(string templateKey, IDictionary variables, IDictionary templates) { if (templates.ContainsKey(templateKey)) diff --git a/src/TailoredApps.Shared.Email/MailMessageBuilder/IMailMessageBuilder.cs b/src/TailoredApps.Shared.Email/MailMessageBuilder/IMailMessageBuilder.cs index f9d9243..405ac97 100644 --- a/src/TailoredApps.Shared.Email/MailMessageBuilder/IMailMessageBuilder.cs +++ b/src/TailoredApps.Shared.Email/MailMessageBuilder/IMailMessageBuilder.cs @@ -2,8 +2,10 @@ namespace TailoredApps.Shared.Email.MailMessageBuilder { + /// Interfejs budowania treści wiadomości e-mail z szablonu. public interface IMailMessageBuilder { + /// Buduje treść wiadomości na podstawie klucza szablonu i zmiennych. string Build(string templateKey, IDictionary variables, IDictionary templates); } } diff --git a/src/TailoredApps.Shared.Email/MailMessageBuilder/TokenReplacingMailMessageBuilder.cs b/src/TailoredApps.Shared.Email/MailMessageBuilder/TokenReplacingMailMessageBuilder.cs index 56a5ad1..031fb17 100644 --- a/src/TailoredApps.Shared.Email/MailMessageBuilder/TokenReplacingMailMessageBuilder.cs +++ b/src/TailoredApps.Shared.Email/MailMessageBuilder/TokenReplacingMailMessageBuilder.cs @@ -4,13 +4,18 @@ namespace TailoredApps.Shared.Email.MailMessageBuilder { + /// Buduje wiadomości e-mail z szablonów z podmianą tokenów. public class TokenReplacingMailMessageBuilder : IMailMessageBuilder { private readonly IOptions options; + + /// Inicjalizuje instancję . public TokenReplacingMailMessageBuilder(IOptions options) { this.options = options; } + + /// public string Build(string templateKey, IDictionary variables, IDictionary templates) { if (templates == null) diff --git a/src/TailoredApps.Shared.Email/MailMessageBuilder/TokenReplacingMailMessageBuilderOptions.cs b/src/TailoredApps.Shared.Email/MailMessageBuilder/TokenReplacingMailMessageBuilderOptions.cs index 72266b7..7d383bd 100644 --- a/src/TailoredApps.Shared.Email/MailMessageBuilder/TokenReplacingMailMessageBuilderOptions.cs +++ b/src/TailoredApps.Shared.Email/MailMessageBuilder/TokenReplacingMailMessageBuilderOptions.cs @@ -1,5 +1,6 @@ namespace TailoredApps.Shared.Email.MailMessageBuilder { + /// Opcje — lokalizacja i rozszerzenie szablonów. public class TokenReplacingMailMessageBuilderOptions { /// Location. From f14270ce0c1d427a19893bbc057d66f0cacb3144 Mon Sep 17 00:00:00 2001 From: Szeregowy Date: Tue, 24 Mar 2026 15:58:29 +0100 Subject: [PATCH 5/5] =?UTF-8?q?fix:=20XML=20docs=20=E2=80=94=20SmtpEmailPr?= =?UTF-8?q?ovider,=20SmtpEmailProviderExtensions,=20SmtpEmailServiceOption?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/TailoredApps.Shared.Email/SmtpEmailProvider.cs | 2 ++ src/TailoredApps.Shared.Email/SmtpEmailServiceOptions.cs | 1 + 2 files changed, 3 insertions(+) diff --git a/src/TailoredApps.Shared.Email/SmtpEmailProvider.cs b/src/TailoredApps.Shared.Email/SmtpEmailProvider.cs index e45d500..692da2d 100644 --- a/src/TailoredApps.Shared.Email/SmtpEmailProvider.cs +++ b/src/TailoredApps.Shared.Email/SmtpEmailProvider.cs @@ -10,6 +10,7 @@ namespace TailoredApps.Shared.Email { + /// Implementacja wysyłająca e-maile przez SMTP. public class SmtpEmailProvider : IEmailProvider { private readonly IOptions options; @@ -69,6 +70,7 @@ public async Task SendMail(string recipnet, string topic, string message } } + /// Rozszerzenia DI dla dostawców e-mail SMTP i konsolowego. public static class SmtpEmailProviderExtensions { /// Rejestruje provider i jego zależności w kontenerze DI. diff --git a/src/TailoredApps.Shared.Email/SmtpEmailServiceOptions.cs b/src/TailoredApps.Shared.Email/SmtpEmailServiceOptions.cs index 9e5435c..fcced5b 100644 --- a/src/TailoredApps.Shared.Email/SmtpEmailServiceOptions.cs +++ b/src/TailoredApps.Shared.Email/SmtpEmailServiceOptions.cs @@ -1,6 +1,7 @@ namespace TailoredApps.Shared.Email { + /// Opcje konfiguracji dostawcy SMTP. public class SmtpEmailServiceOptions { /// Klucz sekcji konfiguracji.