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;