Skip to content

Commit

Permalink
Payment: Add support for specifying payer details in OrderDetails (#726)
Browse files Browse the repository at this point in the history
* Payment: Add support for specifying payer details in OrderDetails, allowing payment processors like Nets Easy to utalize the information when available.

* Add comment about country code.

* Add setting for MerchandHandlesConsumerData.
  • Loading branch information
bjorntore authored Aug 19, 2024
1 parent fb48258 commit 1248b95
Show file tree
Hide file tree
Showing 12 changed files with 303 additions and 6 deletions.
2 changes: 1 addition & 1 deletion src/Altinn.App.Core/Features/Payment/Models/Address.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public class Address
public string? City { get; set; }

/// <summary>
/// The country of the address.
/// The country of the address. What format this is expected in might differ between payment processors. For instance, Nets Easy requires 3-letter ISO 3166 country codes.
/// </summary>
public string? Country { get; set; }
}
5 changes: 5 additions & 0 deletions src/Altinn.App.Core/Features/Payment/Models/OrderDetails.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ public class OrderDetails
/// </summary>
public required PaymentReceiver Receiver { get; set; }

/// <summary>
/// The party that will make the payment. How this is used/respected can vary between payment processors. Some payment processors might require this to be set.
/// </summary>
public Payer? Payer { get; set; }

/// <summary>
/// Monetary unit of the prices in the order.
/// </summary>
Expand Down
4 changes: 2 additions & 2 deletions src/Altinn.App.Core/Features/Payment/Models/Payer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ namespace Altinn.App.Core.Features.Payment.Models;
public class Payer
{
/// <summary>
/// If the payer is a private person, this property should be set.
/// If the payer is a private person, this property should be set. Do not set both this and <see cref="Company"/>.
/// </summary>
public PayerPrivatePerson? PrivatePerson { get; set; }

/// <summary>
/// If the payer is a company, this property should be set.
/// If the payer is a company, this property should be set. Do not set both this and <see cref="PrivatePerson"/>.
/// </summary>
public PayerCompany? Company { get; set; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ internal class NetsCheckout
/// </summary>
public string? CancelUrl { get; set; }

/// <summary>
/// Contains information about the customer. If provided, this information will be used for initating the consumer data of the payment object.
/// See also the property merchantHandlesConsumerData which controls what fields to show on the checkout page.
/// </summary>
public NetsCheckoutConsumerDetails? Consumer { get; set; }

/// <summary>
/// The URL to the terms and conditions of your webshop.
/// Whitelist: “[&amp;]” =&gt; “”
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
namespace Altinn.App.Core.Features.Payment.Processors.Nets.Models;

internal class NetsCheckoutConsumerDetails
{
public string? Reference { get; set; }
public string? Email { get; set; }
public NetsAddress? ShippingAddress { get; set; }
public NetsAddress? BillingAddress { get; set; }
public NetsPhoneNumber? PhoneNumber { get; set; }
public NetsCheckoutPrivatePerson? PrivatePerson { get; set; }
public NetsCheckoutCompany? Company { get; set; }
}

/// <remarks>
/// Warning: Nets Easy API reference has multiple variants of private person objects.
/// This is used for create payment, while NetsPaymentFull uses a different object to represent a private person.
/// </remarks>
internal class NetsCheckoutPrivatePerson
{
public string? FirstName { get; set; }
public string? LastName { get; set; }
}

/// <remarks>
/// Warning: Nets Easy API reference has multiple variants of company objects.
/// This is used for create payment, while NetsPaymentFull uses a different object to represent a private person.
/// </remarks>
internal class NetsCheckoutCompany
{
public string? Name { get; set; }
public NetsCheckoutPrivatePerson? Contact { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# API Models Information

This folder has the C# models used for the Nets Easy API.

## Overview

These models were manually created based on the JSON examples in the Nets API documentation. Since they were written by
hand, there might be some unintentional differences from the actual API objects.

## API Reference

The models are based on the following API documentation:

- [Nets Easy Payment API](https://developer.nexigroup.com/nexi-checkout/en-EU/api/payment-v1/)

## Notes

- **Manual Creation**: Since these were manually created, there could be some mismatches between the models and the
actual API.
- **Check for Updates**: Refer back to the official API docs for the most up-to-date information.

67 changes: 67 additions & 0 deletions src/Altinn.App.Core/Features/Payment/Processors/Nets/NetsMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,54 @@ internal static class NetsMapper
};
}

/// <summary>
/// Map from out Payer type to NetsCheckoutConsumerDetails.
/// </summary>
public static NetsCheckoutConsumerDetails? MapConsumerDetails(Payer? payer)
{
if (payer == null)
{
return null;
}

if (payer.PrivatePerson != null && payer.Company != null)
{
throw new ArgumentException("Use either PrivatePerson or Company fields, not both.");
}

string? email = payer.PrivatePerson != null ? payer.PrivatePerson.Email : payer.Company?.ContactPerson?.Email;
PhoneNumber? phoneNumber =
payer.PrivatePerson != null ? payer.PrivatePerson.PhoneNumber : payer.Company?.ContactPerson?.PhoneNumber;

return new NetsCheckoutConsumerDetails
{
Email = email,
PhoneNumber = new NetsPhoneNumber { Prefix = phoneNumber?.Prefix, Number = phoneNumber?.Number, },
Company =
payer.Company != null
? new NetsCheckoutCompany
{
Name = payer.Company.Name,
Contact = new NetsCheckoutPrivatePerson
{
FirstName = payer.Company.ContactPerson?.FirstName,
LastName = payer.Company.ContactPerson?.LastName,
}
}
: null,
PrivatePerson =
payer.PrivatePerson != null
? new NetsCheckoutPrivatePerson()
{
FirstName = payer.PrivatePerson.FirstName,
LastName = payer.PrivatePerson.LastName,
}
: null,
ShippingAddress = MapNetsAddress(payer.ShippingAddress),
BillingAddress = MapNetsAddress(payer.BillingAddress),
};
}

/// <summary>
/// Map from our PayerType enum to Nets consumer types.
/// </summary>
Expand Down Expand Up @@ -125,4 +173,23 @@ public static List<string> MapConsumerTypes(PayerType[]? payerTypes)
Country = address.Country,
};
}

/// <summary>
/// Map from NetsAddress to our Address type.
/// </summary>
public static NetsAddress? MapNetsAddress(Address? address)
{
if (address == null)
return null;

return new NetsAddress
{
ReceiverLine = address.Name,
AddressLine1 = address.AddressLine1,
AddressLine2 = address.AddressLine2,
PostalCode = address.PostalCode,
City = address.City,
Country = address.Country,
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
using Altinn.Platform.Storage.Interface.Models;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Options;
using OrderDetails = Altinn.App.Core.Features.Payment.Models.OrderDetails;

namespace Altinn.App.Core.Features.Payment.Processors.Nets;

Expand Down Expand Up @@ -49,6 +48,13 @@ public async Task<PaymentDetails> StartPayment(Instance instance, OrderDetails o
string baseUrl = _generalSettings.FormattedExternalAppBaseUrl(new AppIdentifier(instance));
var altinnAppUrl = $"{baseUrl}#/instance/{instanceIdentifier}";

if (_settings.MerchantHandlesConsumerData == true && orderDetails.Payer is null)
{
throw new PaymentException(
"Payer is missing in orderDetails. MerchantHandlesConsumerData is set to true. Payer must be provided."
);
}

var payment = new NetsCreatePayment()
{
Order = new NetsOrder
Expand Down Expand Up @@ -82,6 +88,8 @@ public async Task<PaymentDetails> StartPayment(Instance instance, OrderDetails o
TermsUrl = _settings.TermsUrl,
ReturnUrl = altinnAppUrl,
CancelUrl = altinnAppUrl,
Consumer = NetsMapper.MapConsumerDetails(orderDetails.Payer),
MerchantHandlesConsumerData = _settings.MerchantHandlesConsumerData ?? orderDetails.Payer is not null,
ConsumerType = new NetsConsumerType
{
SupportedTypes = NetsMapper.MapConsumerTypes(orderDetails.AllowedPayerTypes),
Expand Down Expand Up @@ -153,9 +161,9 @@ public async Task<bool> TerminatePayment(Instance instance, PaymentInformation p
PaymentStatus status = chargedAmount > 0 ? PaymentStatus.Paid : PaymentStatus.Created;
NetsPaymentDetails? paymentPaymentDetails = payment.PaymentDetails;

var checkout =
NetsCheckoutUrls checkout =
payment.Checkout ?? throw new PaymentException("Checkout information is missing in the response from Nets");
var checkoutUrl =
string checkoutUrl =
checkout.Url ?? throw new PaymentException("Checkout URL is missing in the response from Nets");

PaymentDetails paymentDetails =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,9 @@ public class NetsPaymentSettings
/// If true, the name of the merchant will be shown to the user before they confirm the payment.
/// </summary>
public bool ShowMerchantName { get; set; } = true;

/// <summary>
/// Allows you to initiate the checkout with customer data so that your customer only need to provide payment details. If set to true, information about the paying party must be supplied.
/// </summary>
public bool? MerchantHandlesConsumerData { get; set; }
}
3 changes: 3 additions & 0 deletions test/Altinn.App.Api.Tests/OpenApi/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -5974,6 +5974,9 @@
"receiver": {
"$ref": "#/components/schemas/PaymentReceiver"
},
"payer": {
"$ref": "#/components/schemas/Payer"
},
"currency": {
"type": "string",
"nullable": true
Expand Down
2 changes: 2 additions & 0 deletions test/Altinn.App.Api.Tests/OpenApi/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3791,6 +3791,8 @@ components:
nullable: true
receiver:
$ref: '#/components/schemas/PaymentReceiver'
payer:
$ref: '#/components/schemas/Payer'
currency:
type: string
nullable: true
Expand Down
Loading

0 comments on commit 1248b95

Please sign in to comment.