From a422fe6ddf6054fe9433f8e44d5ffc6f176ab3eb Mon Sep 17 00:00:00 2001 From: Maks Szokalski <42069493+illunix@users.noreply.github.com> Date: Sat, 16 Dec 2023 02:41:12 +0100 Subject: [PATCH] Complete create new transaction --- samples/Axepta.Sample/Program.cs | 15 +-- .../appsettings.Development.json | 9 +- samples/Axepta.Sample/appsettings.json | 12 +-- .../Attributes/RequiredIfAttribute.cs | 6 +- src/Axepta.SDK/Entities/Request/Payment.cs | 16 ++- src/Axepta.SDK/Entities/Response/Data.cs | 7 +- ...eatePaymentResponse.cs => ResponseRoot.cs} | 2 +- .../Entities/Response/ValidationError.cs | 21 ++++ src/Axepta.SDK/Enums/PaymentMethodChannel.cs | 34 ++++++ src/Axepta.SDK/Exceptions/AxeptaException.cs | 16 +++ src/Axepta.SDK/Extensions.cs | 101 ++++++++++-------- src/Axepta.SDK/JSON/JsonContext.cs | 2 +- .../Options/AxeptaPaywallOptions.cs | 10 ++ .../Services/Abstractions/IAxepta.cs | 6 +- src/Axepta.SDK/Services/Axepta.cs | 8 +- src/Axepta.SDK/Usings.cs | 5 +- 16 files changed, 187 insertions(+), 83 deletions(-) rename src/Axepta.SDK/Entities/Response/{CreatePaymentResponse.cs => ResponseRoot.cs} (82%) create mode 100644 src/Axepta.SDK/Entities/Response/ValidationError.cs create mode 100644 src/Axepta.SDK/Enums/PaymentMethodChannel.cs create mode 100644 src/Axepta.SDK/Options/AxeptaPaywallOptions.cs diff --git a/samples/Axepta.Sample/Program.cs b/samples/Axepta.Sample/Program.cs index e5a9cb1..efce237 100644 --- a/samples/Axepta.Sample/Program.cs +++ b/samples/Axepta.Sample/Program.cs @@ -7,32 +7,33 @@ builder.Services .AddEndpointsApiExplorer() .AddSwaggerGen() - .AddAxeptaPaywall(); + .AddAxeptaPaywall(builder.Configuration); var app = builder.Build(); -var payment = app.MapGroup("payments"); +var paymentEndpoints = app.MapGroup("payments"); -payment.MapPost( +paymentEndpoints.MapPost( "/", async ( IAxepta axepta, CancellationToken ct ) => { - await axepta.CreatePaymentAsync( + var payment = await axepta.CreatePaymentAsync( new() { Type = PaymentType.Sale, - ServiceId = "62f574ed-d4ad-4a7e-9981-89ed7284aaba", + ServiceId = "eff3207f-d2a0-4560-99ce-bba83267c90b", Amount = 100, Currency = "PLN", OrderId = "123456789", PaymentMethod = PaymentMethod.Pbl, - PaymentMethodChannel = "pbl", + PaymentMethodChannel = PaymentMethodChannel.Ipko, SuccessReturnUrl = "https://example.com/success", FailureReturnUrl = "https://example.com/failure", ReturnUrl = "https://example.com", + ClientIp = "192.168.10.2", Customer = new() { Id = "123", @@ -44,7 +45,7 @@ await axepta.CreatePaymentAsync( ct ); - return Results.Ok(); + return Results.Ok(payment); } ); diff --git a/samples/Axepta.Sample/appsettings.Development.json b/samples/Axepta.Sample/appsettings.Development.json index 0c208ae..5c52ed1 100644 --- a/samples/Axepta.Sample/appsettings.Development.json +++ b/samples/Axepta.Sample/appsettings.Development.json @@ -1,8 +1,7 @@ { - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } + "axepta-paywall": { + "merchantId": "ir49nkdgnuex458f6wnq", + "authToken": "ttfc9ve4zeseca4egs0pguk15c3yckkwf7d1n1ts8e55y5hs68886ujt76z5glbl", + "sandbox": true } } diff --git a/samples/Axepta.Sample/appsettings.json b/samples/Axepta.Sample/appsettings.json index 10f68b8..7d01821 100644 --- a/samples/Axepta.Sample/appsettings.json +++ b/samples/Axepta.Sample/appsettings.json @@ -1,9 +1,7 @@ { - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*" + "axepta-paywall": { + "merchantId": "ir49nkdgnuex458f6wnq", + "authToken": "ttfc9ve4zeseca4egs0pguk15c3yckkwf7d1n1ts8e55y5hs68886ujt76z5glbl", + "sandbox": false + } } diff --git a/src/Axepta.SDK/Attributes/RequiredIfAttribute.cs b/src/Axepta.SDK/Attributes/RequiredIfAttribute.cs index bdc118e..0b2c147 100644 --- a/src/Axepta.SDK/Attributes/RequiredIfAttribute.cs +++ b/src/Axepta.SDK/Attributes/RequiredIfAttribute.cs @@ -1,7 +1,7 @@ namespace Axepta.SDK.Attributes; -[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)] -internal class RequiredIfAttribute : ValidationAttribute +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal sealed class RequiredIfAttribute : ValidationAttribute { private readonly string _propertyName; private readonly object _desiredValue; @@ -16,7 +16,7 @@ object desiredValue } protected override ValidationResult? IsValid( - object value, + object? value, ValidationContext validationContext ) { diff --git a/src/Axepta.SDK/Entities/Request/Payment.cs b/src/Axepta.SDK/Entities/Request/Payment.cs index af044b3..8c7f1a6 100644 --- a/src/Axepta.SDK/Entities/Request/Payment.cs +++ b/src/Axepta.SDK/Entities/Request/Payment.cs @@ -5,6 +5,8 @@ /// public sealed record Payment { + private int _amount; + /// /// Gets or initializes the type of payment. /// @@ -21,7 +23,11 @@ public sealed record Payment /// Gets or initializes the amount of the payment. /// [JsonPropertyName("amount")] - public required int Amount { get; init; } + public required int Amount + { + get => _amount; + set => _amount = value * 100; + } /// /// Gets or initializes the currency code for the payment amount. @@ -48,7 +54,7 @@ public sealed record Payment /// Gets or initializes the payment method channel for the transaction. /// [JsonPropertyName("paymentMethodChannel")] - public required string PaymentMethodChannel { get; init; } + public required PaymentMethodChannel PaymentMethodChannel { get; init; } /// /// Gets or initializes the URL to redirect to after a successful payment. @@ -80,6 +86,12 @@ public sealed record Payment [JsonPropertyName("customer")] public required Customer Customer { get; init; } + /// + /// Gets or initializes client ip address associated with the user. + /// + [JsonPropertyName("clientIp")] + public required string ClientIp { get; init; } + /// /// Gets or initializes the title associated with the payment. Can be null. /// diff --git a/src/Axepta.SDK/Entities/Response/Data.cs b/src/Axepta.SDK/Entities/Response/Data.cs index a5ccc3c..baa260d 100644 --- a/src/Axepta.SDK/Entities/Response/Data.cs +++ b/src/Axepta.SDK/Entities/Response/Data.cs @@ -3,8 +3,11 @@ public sealed record Data { [JsonPropertyName("transaction")] - public required Transaction Transaction { get; init; } + public Transaction? Transaction { get; init; } [JsonPropertyName("action")] - public required Action Action { get; init; } + public Action? Action { get; init; } + + [JsonPropertyName("validatorErrors")] + public IReadOnlyList? ValidationErrors { get; init; } } \ No newline at end of file diff --git a/src/Axepta.SDK/Entities/Response/CreatePaymentResponse.cs b/src/Axepta.SDK/Entities/Response/ResponseRoot.cs similarity index 82% rename from src/Axepta.SDK/Entities/Response/CreatePaymentResponse.cs rename to src/Axepta.SDK/Entities/Response/ResponseRoot.cs index 8ce080a..3116eb0 100644 --- a/src/Axepta.SDK/Entities/Response/CreatePaymentResponse.cs +++ b/src/Axepta.SDK/Entities/Response/ResponseRoot.cs @@ -1,6 +1,6 @@ namespace Axepta.SDK.Entities.Response; -public sealed class CreatePaymentResponse +public sealed class ResponseRoot { [JsonPropertyName("status")] public required string Status { get; set; } diff --git a/src/Axepta.SDK/Entities/Response/ValidationError.cs b/src/Axepta.SDK/Entities/Response/ValidationError.cs new file mode 100644 index 0000000..22cc4a1 --- /dev/null +++ b/src/Axepta.SDK/Entities/Response/ValidationError.cs @@ -0,0 +1,21 @@ +namespace Axepta.SDK.Entities.Response; + +public sealed record ValidationError +{ + private readonly string? _property; + + [JsonPropertyName("property")] + public required string Property + { + get => _property!; + init => _property = value + .Replace( + "instance.", + string.Empty + ) + .FirstCharToUpper(); + } + + [JsonPropertyName("message")] + public required string Message { get; init; } +} \ No newline at end of file diff --git a/src/Axepta.SDK/Enums/PaymentMethodChannel.cs b/src/Axepta.SDK/Enums/PaymentMethodChannel.cs new file mode 100644 index 0000000..aa07abb --- /dev/null +++ b/src/Axepta.SDK/Enums/PaymentMethodChannel.cs @@ -0,0 +1,34 @@ +namespace Axepta.SDK.Enums; + +public enum PaymentMethodChannel +{ +#region pbs + Bnpparibas, + Mtransfer, + Bzwbk, + Pekao24, + Inteligo, + Ing, + Ipko, + Getin, + CreditAgricole, + Alior, + Pbs, + Millennium, + Citi, + Bos, + Pocztowy, + Plusbank, + Bs, + Bspb, + Nest, +#endregion +#region card + Ecom3ds, + Oneclick, + Recurring, +#endregion +#region Blik + Blik +#endregion +} \ No newline at end of file diff --git a/src/Axepta.SDK/Exceptions/AxeptaException.cs b/src/Axepta.SDK/Exceptions/AxeptaException.cs index f132dbf..4ff5c65 100644 --- a/src/Axepta.SDK/Exceptions/AxeptaException.cs +++ b/src/Axepta.SDK/Exceptions/AxeptaException.cs @@ -3,4 +3,20 @@ internal sealed class AxeptaException : Exception { public AxeptaException(string msg) : base(msg) { } + + public AxeptaException(IReadOnlyList? validationErrors) : base(CreateValidationExceptionMessage(validationErrors)) { } + + private static string CreateValidationExceptionMessage(IReadOnlyList? validationErrors) + { + if (validationErrors is null) + return string.Empty; + + var stringBuilder = new StringBuilder(); + stringBuilder.AppendLine("Validation errors occurred:"); + + foreach (var error in validationErrors) + stringBuilder.AppendLine($"- {error.Property}: {error.Message}"); + + return stringBuilder.ToString(); + } } diff --git a/src/Axepta.SDK/Extensions.cs b/src/Axepta.SDK/Extensions.cs index 060ff0d..8a7b6b7 100644 --- a/src/Axepta.SDK/Extensions.cs +++ b/src/Axepta.SDK/Extensions.cs @@ -2,20 +2,37 @@ public static class Extensions { - private static JsonSerializerOptions _sourceGenOptions = new JsonSerializerOptions + private static readonly JsonSerializerOptions _sourceGenOptions = new() { TypeInfoResolver = JsonContext.Default, Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) - } + }, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; - public static IServiceCollection AddAxeptaPaywall(this IServiceCollection services) + public static IServiceCollection AddAxeptaPaywall( + this IServiceCollection services, + IConfiguration cfg + ) { + var optionsSelection = cfg.GetSection(AxeptaPaywallOptions.SelectionName); + + services + .AddOptions() + .Bind(optionsSelection) + .ValidateOnStart(); + + var axeptaPaywallOptions = optionsSelection.Get(); + + var axeptaUrl = axeptaPaywallOptions!.Sandbox ? + "api.sandbox.axepta.pl" : + "api.axepta.pl"; + services.AddHttpClient(q => { - q.BaseAddress = new("https://api.axepta.pl/v1/merchant/ir49nkdgnuex458f6wnq"); + q.BaseAddress = new($"https://{axeptaUrl}/v1/merchant/{axeptaPaywallOptions.MerchantId}/"); q.DefaultRequestHeaders.Accept.Add(new("application/json")); q.DefaultRequestHeaders.TryAddWithoutValidation( "Content-Type", @@ -23,12 +40,12 @@ public static IServiceCollection AddAxeptaPaywall(this IServiceCollection servic ); q.DefaultRequestHeaders.Authorization = new( "Bearer", - "ttfc9ve4zeseca4egs0pguk15c3yckkwf7d1n1ts8e55y5hs68886ujt76z5glbl" + axeptaPaywallOptions.AuthToken ); }) .AddPolicyHandler(HttpPolicyExtensions .HandleTransientHttpError() - .OrResult(q => q.StatusCode == System.Net.HttpStatusCode.NotFound) + .OrResult(q => q.StatusCode == HttpStatusCode.NotFound) .WaitAndRetryAsync( 2, q => TimeSpan.FromSeconds(Math.Pow( @@ -40,40 +57,8 @@ public static IServiceCollection AddAxeptaPaywall(this IServiceCollection servic return services; } - - internal static async Task PostAsync( - this HttpClient http, - string url, - T body, - CancellationToken ct - ) - { - HttpResponseMessage? httpRes = null; - - try - { - httpRes = await http.PostAsync( - url, - new StringContent( - JsonSerializer.Serialize( - body, - _sourceGenOptions - ), - Encoding.UTF8, - "application/json" - ), - ct - ); - - httpRes.EnsureSuccessStatusCode(); - } - catch (HttpRequestException) - { - throw new AxeptaException(await httpRes!.Content.ReadAsStringAsync(ct)); - } - } - public static async Task PostAsync( + internal static async Task PostAsync( this HttpClient http, string url, T body, @@ -83,13 +68,15 @@ public static async Task PostAsync( { HttpResponseMessage? httpRes = null; + var elo = JsonSerializer.Serialize( + body, + _sourceGenOptions + ); + + Console.WriteLine(elo); + try { - var elo = JsonSerializer.Serialize( - body, - _sourceGenOptions - ); - httpRes = await http.PostAsync( url, new StringContent( @@ -112,9 +99,31 @@ await httpRes.Content.ReadAsStringAsync(ct), } catch (HttpRequestException) { - var elo = httpRes!.Content.ReadAsStringAsync(); + switch (httpRes?.StatusCode) + { + case HttpStatusCode.Unauthorized: + throw new AxeptaException("Authorization failed: The provided token is invalid, preventing authorized access to the requested resource."); + case HttpStatusCode.UnprocessableEntity: + { + var resBody = JsonSerializer.Deserialize( + await httpRes.Content.ReadAsStringAsync(ct), + typeof(ResponseRoot), + _sourceGenOptions + )! as ResponseRoot; - throw new AxeptaException(await httpRes!.Content.ReadAsStringAsync(ct)); + throw new AxeptaException(resBody?.Data.ValidationErrors); + } + default: + throw new AxeptaException(await httpRes!.Content.ReadAsStringAsync(ct)); + } } } + + internal static string FirstCharToUpper(this string input) => + input switch + { + null => throw new ArgumentNullException(nameof(input)), + "" => throw new ArgumentException($"{nameof(input)} cannot be empty", nameof(input)), + _ => string.Concat(input[0].ToString().ToUpper(), input.AsSpan(1)) + }; } \ No newline at end of file diff --git a/src/Axepta.SDK/JSON/JsonContext.cs b/src/Axepta.SDK/JSON/JsonContext.cs index b6061ba..cf0bcb7 100644 --- a/src/Axepta.SDK/JSON/JsonContext.cs +++ b/src/Axepta.SDK/JSON/JsonContext.cs @@ -1,5 +1,5 @@ namespace Axepta.SDK.JSON; [JsonSerializable(typeof(Payment))] -[JsonSerializable(typeof(CreatePaymentResponse))] +[JsonSerializable(typeof(ResponseRoot))] internal sealed partial class JsonContext : JsonSerializerContext { } \ No newline at end of file diff --git a/src/Axepta.SDK/Options/AxeptaPaywallOptions.cs b/src/Axepta.SDK/Options/AxeptaPaywallOptions.cs new file mode 100644 index 0000000..ff88e7a --- /dev/null +++ b/src/Axepta.SDK/Options/AxeptaPaywallOptions.cs @@ -0,0 +1,10 @@ +namespace Axepta.SDK.Options; + +internal sealed record AxeptaPaywallOptions +{ + public const string SelectionName = "axepta-paywall"; + + public required string MerchantId { get; init; } + public required string AuthToken { get; init; } + public required bool Sandbox { get; init; } +} \ No newline at end of file diff --git a/src/Axepta.SDK/Services/Abstractions/IAxepta.cs b/src/Axepta.SDK/Services/Abstractions/IAxepta.cs index e8dbc4f..859f956 100644 --- a/src/Axepta.SDK/Services/Abstractions/IAxepta.cs +++ b/src/Axepta.SDK/Services/Abstractions/IAxepta.cs @@ -1,10 +1,8 @@ -using Payment = Axepta.SDK.Entities.Request.Payment; - -namespace Axepta.SDK.Services.Abstractions; +namespace Axepta.SDK.Services.Abstractions; public interface IAxepta { - Task CreatePaymentAsync( + Task CreatePaymentAsync( Payment payment, CancellationToken ct = default ); diff --git a/src/Axepta.SDK/Services/Axepta.cs b/src/Axepta.SDK/Services/Axepta.cs index b45c185..f76eb07 100644 --- a/src/Axepta.SDK/Services/Axepta.cs +++ b/src/Axepta.SDK/Services/Axepta.cs @@ -2,14 +2,14 @@ internal sealed class Axepta(HttpClient http) : IAxepta { - public async Task CreatePaymentAsync( + public Task CreatePaymentAsync( Payment payment, CancellationToken ct = default ) - => await http.PostAsync( - $"transaction", + => http.PostAsync( + "transaction", payment, - JsonContext.Default.CreatePaymentResponse, + JsonContext.Default.ResponseRoot, ct ); } \ No newline at end of file diff --git a/src/Axepta.SDK/Usings.cs b/src/Axepta.SDK/Usings.cs index 5771ac9..a3a3ad4 100644 --- a/src/Axepta.SDK/Usings.cs +++ b/src/Axepta.SDK/Usings.cs @@ -1,10 +1,12 @@ global using System.Text.Json.Serialization.Metadata; global using System.Security.Cryptography; +global using System.Net; global using System.Text.Json; global using System.Text; global using System.Text.Json.Serialization; global using System.ComponentModel.DataAnnotations; global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Configuration; global using Polly; global using Polly.Extensions.Http; global using Axepta.SDK.Entities; @@ -16,4 +18,5 @@ global using Axepta.SDK.Enums; global using Axepta.SDK.Attributes; global using Axepta.SDK.Entities.Response; -global using Axepta.SDK.Entities.Request; \ No newline at end of file +global using Axepta.SDK.Entities.Request; +global using Axepta.SDK.Options;