diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6af9f25 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,24 @@ +name: 'CI' + +on: + push: + branches: + - main + - dev + pull_request: {} + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: actions/setup-dotnet@v4 + with: + global-json-file: './global.json' + - run: dotnet --version + - name: Build + run: dotnet build + - name: Test + run: dotnet test --no-build diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml deleted file mode 100644 index 8b4fbc3..0000000 --- a/.github/workflows/dotnet.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: CI - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - name: Setup .NET - uses: actions/setup-dotnet@v3 - with: - dotnet-version: 6.0.x - - name: Restore dependencies - run: dotnet restore - - name: Build - run: dotnet build --no-restore - - name: Test - run: dotnet test --no-build --verbosity normal diff --git a/global.json b/global.json new file mode 100644 index 0000000..c0e3ec5 --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "8.0.400", + "rollForward": "latestFeature" + } + } + \ No newline at end of file diff --git a/samples/Axepta.Sample/Program.cs b/samples/Axepta.Sample/Program.cs index 14b267d..9b4c6d3 100644 --- a/samples/Axepta.Sample/Program.cs +++ b/samples/Axepta.Sample/Program.cs @@ -46,6 +46,37 @@ CancellationToken ct } ); +paymentEndpoints.MapPost( + "/payment-url", + async ( + IAxepta axepta, + CancellationToken ct + ) => + { + var payment = await axepta.CreatePaymentUrlAsync( + new() + { + Amount = 100, + Currency = Currency.PLN, + OrderId = "123456789", + ReturnUrl = "https://example.com", + SuccessReturnUrl = "https://example.com/success", + FailureReturnUrl = "https://example.com/failure", + Customer = new() + { + Id = "123", + FirstName = "Jan", + LastName = "Kowalski", + Email = "jan.kowalski@example.com" + } + }, + ct + ); + + return Results.Ok(payment); + } +); + paymentEndpoints.MapPost( "/webhook", async ( diff --git a/samples/Axepta.Sample/appsettings.Development.json b/samples/Axepta.Sample/appsettings.Development.json index e3af801..f7094b9 100644 --- a/samples/Axepta.Sample/appsettings.Development.json +++ b/samples/Axepta.Sample/appsettings.Development.json @@ -1,11 +1,11 @@ { "axepta-paywall": { - "merchantId": "5n3bxvzoxife15djkmpa", + "merchantId": "ir49nkdgnuex458f6wnq", "service": { - "id": "bf1efa70-a6d8-4c08-a512-52e8aa025952", - "key": "XrvZlCclg45zOOBtANK5reKo4onNlMrKAwgM" + "id": "eff3207f-d2a0-4560-99ce-bba83267c90b", + "key": "67ieYPyhrqMIpx7UZCLfSimWT0vUm0I_lH0Z" }, - "authToken": "rc9wg0rosqzfx4dcdh1epzloemn5fhuis62gy37faxhd7iojhhop6eb341jbqfpw", + "authToken": "ttfc9ve4zeseca4egs0pguk15c3yckkwf7d1n1ts8e55y5hs68886ujt76z5glbl", "sandbox": true } } diff --git a/src/Axepta.SDK/Axepta.SDK.csproj b/src/Axepta.SDK/Axepta.SDK.csproj index 231db80..e55150f 100644 --- a/src/Axepta.SDK/Axepta.SDK.csproj +++ b/src/Axepta.SDK/Axepta.SDK.csproj @@ -5,8 +5,7 @@ enable enable Axepta.SDK - 1.0.4 - alpha + 1.0.5 Maksymilian Szokalski Axepta.SDK is a class library for BNP Paribas Axepta Paywall BNP Paribas;Axepta;Paywall;Payments;Transactions diff --git a/src/Axepta.SDK/Entities/Request/GeneratePaymentLink.cs b/src/Axepta.SDK/Entities/Request/GeneratePaymentLink.cs new file mode 100644 index 0000000..97ac1a9 --- /dev/null +++ b/src/Axepta.SDK/Entities/Request/GeneratePaymentLink.cs @@ -0,0 +1,90 @@ +namespace Axepta.SDK; + +/// +/// Represents a payment transaction, encapsulating details such as service ID, amount, currency, +/// order information, customer details, and redirection URLs for successful, failed, or general outcomes. +/// This record focuses on capturing essential information required to generate a payment link. +/// +public sealed record GeneratePaymentLink +{ + private int _amount; + + /// + /// Gets or sets the service ID associated with the payment. This is a unique identifier for the service involved in the payment. + /// + [JsonPropertyName("serviceId")] + public string? ServiceId { get; private set; } + + /// + /// Gets or sets the payment amount. The setter converts the input amount from a major currency unit (e.g., dollars) to a minor unit (e.g., cents). + /// + [JsonPropertyName("amount")] + public required int Amount + { + get => _amount; + set => _amount = value * 100; + } + + /// + /// Gets or sets the currency code for the payment, following the ISO 4217 standard. + /// + [StringLength(3)] + [JsonPropertyName("currency")] + public required Currency Currency { get; init; } + + /// + /// Gets or sets the unique identifier for the payment order, which can be used for tracking and referencing the transaction. + /// + [StringLength(100)] + [RegularExpression(AllowedCharactersPatterns.ADDITIONAL_ALLOWED_CHARACTERS_PATTERN)] + [JsonPropertyName("orderId")] + public required string OrderId { get; init; } + + /// + /// Gets or sets the URL to which the user is redirected after a successful payment. This URL is used for post-transaction navigation. + /// + [StringLength(300)] + [Url] + [JsonPropertyName("successReturnUrl")] + public required string SuccessReturnUrl { get; init; } + + /// + /// Gets or sets the URL to which the user is redirected after a failed payment. This provides a means to handle unsuccessful transactions. + /// + [StringLength(300)] + [Url] + [JsonPropertyName("failureReturnUrl")] + public required string FailureReturnUrl { get; init; } + + /// + /// Gets or sets a general return URL for the payment, used for redirecting the user after the transaction is processed, regardless of the outcome. + /// + [StringLength(300)] + [Url] + [JsonPropertyName("returnUrl")] + public required string ReturnUrl { get; init; } + + /// + /// Gets or sets the customer's information, essential for processing the payment and for record-keeping purposes. + /// + [JsonPropertyName("customer")] + public required Customer Customer { get; init; } + + /// + /// Gets or sets the title associated with the payment. This can be a description or label for the payment, and can be null. + /// + [StringLength(255)] + [RegularExpression(AllowedCharactersPatterns.ADDITIONAL_ALLOWED_CHARACTERS_PATTERN)] + [JsonPropertyName("title")] + public string? Title { get; init; } + + /// + /// Gets or sets the expiration date and time of the transaction. If not specified, the transaction remains valid indefinitely. + /// Transactions not completed by this timestamp will be automatically cancelled. + /// + [JsonPropertyName("activeTo")] + public DateTime? ActiveTo { get; init; } + + internal void SetServiceId(string serviceId) + => ServiceId = serviceId; +} diff --git a/src/Axepta.SDK/Entities/Response/ResponseRoot.cs b/src/Axepta.SDK/Entities/Response/AxeptaResponseRoot.cs similarity index 82% rename from src/Axepta.SDK/Entities/Response/ResponseRoot.cs rename to src/Axepta.SDK/Entities/Response/AxeptaResponseRoot.cs index 195202f..05b5637 100644 --- a/src/Axepta.SDK/Entities/Response/ResponseRoot.cs +++ b/src/Axepta.SDK/Entities/Response/AxeptaResponseRoot.cs @@ -1,6 +1,6 @@ namespace Axepta.SDK; -public sealed class ResponseRoot +public sealed class AxeptaResponseRoot { [JsonPropertyName("status")] public required string Status { get; set; } diff --git a/src/Axepta.SDK/Entities/Response/Data.cs b/src/Axepta.SDK/Entities/Response/Data.cs index f901396..dd97ea4 100644 --- a/src/Axepta.SDK/Entities/Response/Data.cs +++ b/src/Axepta.SDK/Entities/Response/Data.cs @@ -8,6 +8,9 @@ public sealed record Data [JsonPropertyName("payment")] public PaymentResponse? Payment { get; init; } + [JsonPropertyName("paymentLink")] + public PaymentLink? PaymentLink { get; init; } + [JsonPropertyName("action")] public Action? Action { get; init; } diff --git a/src/Axepta.SDK/Entities/Response/PaymentLink.cs b/src/Axepta.SDK/Entities/Response/PaymentLink.cs new file mode 100644 index 0000000..15aaea6 --- /dev/null +++ b/src/Axepta.SDK/Entities/Response/PaymentLink.cs @@ -0,0 +1,10 @@ +namespace Axepta.SDK; + +public sealed record PaymentLink +{ + [JsonPropertyName("paymentId")] + public required string PaymentId { get; init; } + + [JsonPropertyName("url")] + public required string Url { get; init; } +} \ No newline at end of file diff --git a/src/Axepta.SDK/Extensions.cs b/src/Axepta.SDK/Extensions.cs index 629bc67..09c2de1 100644 --- a/src/Axepta.SDK/Extensions.cs +++ b/src/Axepta.SDK/Extensions.cs @@ -143,9 +143,9 @@ await httpResContentAsJson().ConfigureAwait(false), { var resBody = JsonSerializer.Deserialize( await httpResContentAsJson().ConfigureAwait(false), - typeof(ResponseRoot), + typeof(AxeptaResponseRoot), JsonSerializerOptions - )! as ResponseRoot; + )! as AxeptaResponseRoot; throw new AxeptaException(resBody?.Data.ValidationErrors); } diff --git a/src/Axepta.SDK/JSON/JsonContext.cs b/src/Axepta.SDK/JSON/JsonContext.cs index 0b68de5..1db6cd8 100644 --- a/src/Axepta.SDK/JSON/JsonContext.cs +++ b/src/Axepta.SDK/JSON/JsonContext.cs @@ -1,6 +1,7 @@ namespace Axepta.SDK.JSON; [JsonSerializable(typeof(Payment))] -[JsonSerializable(typeof(ResponseRoot))] +[JsonSerializable(typeof(GeneratePaymentLink))] +[JsonSerializable(typeof(AxeptaResponseRoot))] [JsonSerializable(typeof(Notification))] internal sealed partial class JsonContext : JsonSerializerContext { } \ 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 2988e58..e9b581b 100644 --- a/src/Axepta.SDK/Services/Abstractions/IAxepta.cs +++ b/src/Axepta.SDK/Services/Abstractions/IAxepta.cs @@ -11,20 +11,25 @@ public interface IAxepta /// /// The payment details required to process the payment. /// An optional cancellation token to cancel the request. - /// A task that represents the asynchronous operation. The task result contains the object with the payment response. - Task CreatePaymentAsync( + /// A task that represents the asynchronous operation. The task result contains the object with the payment response. + Task CreatePaymentAsync( Payment payment, CancellationToken ct = default ); + Task CreatePaymentUrlAsync( + GeneratePaymentLink paymentLink, + CancellationToken ct = default + ); + /// /// Creates a refund asynchronously. /// /// The unique identifier of the payment to be refunded. /// The refund details required to process the refund. /// An optional cancellation token to cancel the request. - /// A task that represents the asynchronous operation. The task result contains the object with the refund response. - Task CreateRefundAsync( + /// A task that represents the asynchronous operation. The task result contains the object with the refund response. + Task CreateRefundAsync( Guid paymentId, Refund refund, CancellationToken ct = default diff --git a/src/Axepta.SDK/Services/Axepta.cs b/src/Axepta.SDK/Services/Axepta.cs index 60c5fe2..64f39a9 100644 --- a/src/Axepta.SDK/Services/Axepta.cs +++ b/src/Axepta.SDK/Services/Axepta.cs @@ -5,27 +5,40 @@ internal sealed class Axepta( IOptions options ) : IAxepta { - public Task CreatePaymentAsync( + public Task CreatePaymentAsync( Payment payment, CancellationToken ct = default ) { payment.SetServiceId(options.Value.Service.Id); - return http.PostAsync( + return http.PostAsync( "transaction", payment, ct ); } - public Task CreateRefundAsync( + public Task CreatePaymentUrlAsync( + GeneratePaymentLink paymentLink, + CancellationToken ct = default + ) + { + paymentLink.SetServiceId(options.Value.Service.Id); + return http.PostAsync( + "payment-link", + paymentLink, + ct + ); + } + + public Task CreateRefundAsync( Guid paymentId, Refund refund, CancellationToken ct = default ) { refund.SetServiceId(options.Value.Service.Id); - return http.PostAsync( + return http.PostAsync( $"payment/{paymentId}/refund", refund, ct @@ -36,7 +49,7 @@ public async Task GetTransactionAsync( Guid transactionId, CancellationToken ct = default ) - => (await http.GetAsync( + => (await http.GetAsync( $"transaction/{transactionId}", ct )).Data.Transaction!; @@ -45,7 +58,7 @@ public async Task GetPaymentAsync( Guid paymentId, CancellationToken ct = default ) - => (await http.GetAsync( + => (await http.GetAsync( $"payment/{paymentId}", ct )).Data.Payment!;