Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Potential API #445

Open
simenandre opened this issue Jul 1, 2022 · 3 comments
Open

Potential API #445

simenandre opened this issue Jul 1, 2022 · 3 comments

Comments

@simenandre
Copy link
Contributor

During in the meeting with me and @Xillians, we talked about how the API could potentially look like.

import { SwedbankPay } from 'swedbank-pay';

const swedbank = new Swedbank({ ... });

const payment = await swedbank.operation({ ... });
const prices = await payment.prices();

const [prices, subscriptions] = Promise.all([ payment.prices(), payment.subscriptions() ];

await payment.operation.redirectAuthorization();
@simenandre simenandre self-assigned this Jul 1, 2022
@simenandre
Copy link
Contributor Author

@asbjornu you had some thoughts on this?

@simenandre simenandre removed their assignment Oct 18, 2022
@asbjornu
Copy link
Contributor

I'm quite happy by how we modelled this in the .NET SDK . We start out with the ISwedbankPayClient interface, which looks like this:

using SwedbankPay.Sdk.Consumers;
using SwedbankPay.Sdk.PaymentInstruments;
using SwedbankPay.Sdk.PaymentOrders;

namespace SwedbankPay.Sdk
{
    /// <summary>
    /// The entrypoint of this SDK!
    /// Used to access the different APIs'.
    /// </summary>
    public interface ISwedbankPayClient
    {
        /// <summary>
        /// Resource to create and get payment orders.
        /// </summary>
        IPaymentOrdersResource PaymentOrders { get; }

        /// <summary>
        /// Resource to access consumer information.
        /// </summary>
        IConsumersResource Consumers { get; }

        /// <summary>
        /// Resource to create and get payments on several payment instruments.
        /// </summary>
        IPaymentInstrumentsResource Payments { get; }
    }
}

Then, if we take a look at the IPaymentOrderResource.cs interface, it contains the "globally available" methods Create and Get:

using System;
using System.Threading.Tasks;

namespace SwedbankPay.Sdk.PaymentOrders
{
    /// <summary>
    /// Entrypoint to our PaymentOrders API resource.
    /// </summary>
    public interface IPaymentOrdersResource
    {
        /// <summary>
        ///     Creates a payment order
        /// </summary>
        /// <param name="paymentOrderRequest"></param>
        /// <param name="paymentOrderExpand"></param>
        /// <exception cref="ArgumentNullException"></exception>
        /// <exception cref="InvalidOperationException"></exception>
        /// <exception cref="System.Net.Http.HttpRequestException"></exception>
        /// <returns></returns>
        Task<IPaymentOrderResponse> Create(PaymentOrderRequest paymentOrderRequest, PaymentOrderExpand paymentOrderExpand = PaymentOrderExpand.None);


        /// <summary>
        ///     Get payment order for the given id
        /// </summary>
        /// <param name="id"></param>
        /// <param name="paymentOrderExpand"></param>
        /// <exception cref="ArgumentNullException"></exception>
        /// <exception cref="InvalidOperationException"></exception>
        /// <exception cref="System.Net.Http.HttpRequestException"></exception>
        /// <returns></returns>
        Task<IPaymentOrderResponse> Get(Uri id, PaymentOrderExpand paymentOrderExpand = PaymentOrderExpand.None);
    }
}

Now, when an instance of IPaymentOrderResponse is received, it will have the following shape:

namespace SwedbankPay.Sdk.PaymentOrders
{
    /// <summary>
    /// API response giving access to the current payment
    /// order and available operations.
    /// </summary>
    public interface IPaymentOrderResponse
    {
        /// <summary>
        /// Currently available operations of this payment order.
        /// </summary>
        IPaymentOrderOperations Operations { get; }

        /// <summary>
        /// The current payment order.
        /// </summary>
        IPaymentOrder PaymentOrder { get; }
    }
}

And the IPaymentOrderOperations will make available only the operations that are valid for the request by setting the unavailable ones to null, forcing the consumer to check whether i.e. Operations.Abort is null or not before invoking it.

using SwedbankPay.Sdk.PaymentInstruments;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace SwedbankPay.Sdk.PaymentOrders
{
    /// <summary>
    /// Mapps the currently known operations in payment order to easily
    /// accessable methods.
    /// </summary>
    public interface IPaymentOrderOperations : IDictionary<LinkRelation, HttpOperation>
    {
        /// <summary>
        /// Aborts the payment order, if available.
        /// </summary>
        Func<PaymentOrderAbortRequest, Task<IPaymentOrderResponse>> Abort { get; }

        /// <summary>
        /// Cancels the payment order, if available.
        /// </summary>
        Func<PaymentOrderCancelRequest, Task<CancellationResponse>> Cancel { get; }

        /// <summary>
        /// Captures the authorized <seealso cref="Amount"/> on the payment order, if available.
        /// </summary>
        Func<PaymentOrderCaptureRequest, Task<ICaptureResponse>> Capture { get; }

        /// <summary>
        /// Reverses previously captured <seealso cref="Amount"/> on the payment order, if available.
        /// </summary>
        Func<PaymentOrderReversalRequest, Task<IReversalResponse>> Reverse { get; }

        /// <summary>
        /// Updates the contents of the payment order, if available.
        /// </summary>
        Func<PaymentOrderUpdateRequest, Task<IPaymentOrderResponse>> Update { get; }

        /// <summary>
        /// Returns details needed to created a hosted view for the payer to see the payment order, if available.
        /// </summary>
        HttpOperation View { get; }
    }
}

For properties that require expansion we have an incomplete design that I'd love to elaborate, but an example can be seen in OrderItemListResponse:

using System;
using System.Collections.Generic;

namespace SwedbankPay.Sdk.PaymentOrders
{
    /// <summary>
    /// API resource to access order items on a payment order.
    /// </summary>
    public class OrderItemListResponse : Identifiable
    {
        /// <summary>
        /// Instantiates a <see cref="OrderItemListResponse"/> with the provided parameters.
        /// </summary>
        /// <param name="id">Unique ID for this resource.</param>
        public OrderItemListResponse(Uri id): base(id)
        {
        }

        /// <summary>
        ///     The orderItems property of the paymentOrder is an array containing the items being purchased with the order. Used
        ///     to print on invoices if
        ///     the payer chooses to pay with invoice, among other things. Order items can be specified on both payment order
        ///     creation as well as on Capture.
        /// </summary>
        public IEnumerable<IOrderItem> OrderItemList { get; set; }
    }
}

It would be neat if we could build knowledge into this class about whether it is already filled with data (through expansion) or whether a request is required to GET the data. That could be solved by exposing the data properties, but throwing an exception if they are accessed without first doing expansion or a separate Get() request on the class. Something like that.

Is this design possible to achieve in the TypeScript SDK? As the current interfaces are mostly copies from the .NET SDK, I would think it shouldn't be too much work to mimic this behaviour?

@simenandre
Copy link
Contributor Author

I think this looks good!

We can probably use some type guarding/assertion in combination here to get it really nice. I can try and draw something out!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants