diff --git a/src/Altinn.App.Core/Features/Payment/Models/Address.cs b/src/Altinn.App.Core/Features/Payment/Models/Address.cs
index efc672a2c..de44c113d 100644
--- a/src/Altinn.App.Core/Features/Payment/Models/Address.cs
+++ b/src/Altinn.App.Core/Features/Payment/Models/Address.cs
@@ -31,7 +31,7 @@ public class Address
public string? City { get; set; }
///
- /// 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.
///
public string? Country { get; set; }
}
diff --git a/src/Altinn.App.Core/Features/Payment/Models/OrderDetails.cs b/src/Altinn.App.Core/Features/Payment/Models/OrderDetails.cs
index 147b6fae7..724d2ca72 100644
--- a/src/Altinn.App.Core/Features/Payment/Models/OrderDetails.cs
+++ b/src/Altinn.App.Core/Features/Payment/Models/OrderDetails.cs
@@ -15,6 +15,11 @@ public class OrderDetails
///
public required PaymentReceiver Receiver { get; set; }
+ ///
+ /// 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.
+ ///
+ public Payer? Payer { get; set; }
+
///
/// Monetary unit of the prices in the order.
///
diff --git a/src/Altinn.App.Core/Features/Payment/Models/Payer.cs b/src/Altinn.App.Core/Features/Payment/Models/Payer.cs
index 299ca5084..7f6813361 100644
--- a/src/Altinn.App.Core/Features/Payment/Models/Payer.cs
+++ b/src/Altinn.App.Core/Features/Payment/Models/Payer.cs
@@ -6,12 +6,12 @@ namespace Altinn.App.Core.Features.Payment.Models;
public class Payer
{
///
- /// 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 .
///
public PayerPrivatePerson? PrivatePerson { get; set; }
///
- /// 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 .
///
public PayerCompany? Company { get; set; }
diff --git a/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsCheckout.cs b/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsCheckout.cs
index 35c3b2a87..660b5038a 100644
--- a/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsCheckout.cs
+++ b/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsCheckout.cs
@@ -39,6 +39,12 @@ internal class NetsCheckout
///
public string? CancelUrl { get; set; }
+ ///
+ /// 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.
+ ///
+ public NetsCheckoutConsumerDetails? Consumer { get; set; }
+
///
/// The URL to the terms and conditions of your webshop.
/// Whitelist: “[&]” => “”
diff --git a/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsCheckoutConsumerDetails.cs b/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsCheckoutConsumerDetails.cs
new file mode 100644
index 000000000..70ea4dea2
--- /dev/null
+++ b/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsCheckoutConsumerDetails.cs
@@ -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; }
+}
+
+///
+/// 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.
+///
+internal class NetsCheckoutPrivatePerson
+{
+ public string? FirstName { get; set; }
+ public string? LastName { get; set; }
+}
+
+///
+/// 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.
+///
+internal class NetsCheckoutCompany
+{
+ public string? Name { get; set; }
+ public NetsCheckoutPrivatePerson? Contact { get; set; }
+}
diff --git a/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/README.md b/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/README.md
new file mode 100644
index 000000000..aaffee75d
--- /dev/null
+++ b/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/README.md
@@ -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.
+
diff --git a/src/Altinn.App.Core/Features/Payment/Processors/Nets/NetsMapper.cs b/src/Altinn.App.Core/Features/Payment/Processors/Nets/NetsMapper.cs
index 310b8b37f..fe5c145ed 100644
--- a/src/Altinn.App.Core/Features/Payment/Processors/Nets/NetsMapper.cs
+++ b/src/Altinn.App.Core/Features/Payment/Processors/Nets/NetsMapper.cs
@@ -62,6 +62,54 @@ internal static class NetsMapper
};
}
+ ///
+ /// Map from out Payer type to NetsCheckoutConsumerDetails.
+ ///
+ 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),
+ };
+ }
+
///
/// Map from our PayerType enum to Nets consumer types.
///
@@ -125,4 +173,23 @@ public static List MapConsumerTypes(PayerType[]? payerTypes)
Country = address.Country,
};
}
+
+ ///
+ /// Map from NetsAddress to our Address type.
+ ///
+ 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,
+ };
+ }
}
diff --git a/src/Altinn.App.Core/Features/Payment/Processors/Nets/NetsPaymentProcessor.cs b/src/Altinn.App.Core/Features/Payment/Processors/Nets/NetsPaymentProcessor.cs
index 15cd36915..1dd5a89f4 100644
--- a/src/Altinn.App.Core/Features/Payment/Processors/Nets/NetsPaymentProcessor.cs
+++ b/src/Altinn.App.Core/Features/Payment/Processors/Nets/NetsPaymentProcessor.cs
@@ -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;
@@ -49,6 +48,13 @@ public async Task 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
@@ -82,6 +88,8 @@ public async Task 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),
@@ -153,9 +161,9 @@ public async Task 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 =
diff --git a/src/Altinn.App.Core/Features/Payment/Processors/Nets/NetsPaymentSettings.cs b/src/Altinn.App.Core/Features/Payment/Processors/Nets/NetsPaymentSettings.cs
index 6b1d768e7..4f0b3551a 100644
--- a/src/Altinn.App.Core/Features/Payment/Processors/Nets/NetsPaymentSettings.cs
+++ b/src/Altinn.App.Core/Features/Payment/Processors/Nets/NetsPaymentSettings.cs
@@ -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.
///
public bool ShowMerchantName { get; set; } = true;
+
+ ///
+ /// 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.
+ ///
+ public bool? MerchantHandlesConsumerData { get; set; }
}
diff --git a/test/Altinn.App.Api.Tests/OpenApi/swagger.json b/test/Altinn.App.Api.Tests/OpenApi/swagger.json
index 263152a67..411d5ce30 100644
--- a/test/Altinn.App.Api.Tests/OpenApi/swagger.json
+++ b/test/Altinn.App.Api.Tests/OpenApi/swagger.json
@@ -5974,6 +5974,9 @@
"receiver": {
"$ref": "#/components/schemas/PaymentReceiver"
},
+ "payer": {
+ "$ref": "#/components/schemas/Payer"
+ },
"currency": {
"type": "string",
"nullable": true
diff --git a/test/Altinn.App.Api.Tests/OpenApi/swagger.yaml b/test/Altinn.App.Api.Tests/OpenApi/swagger.yaml
index cc5934e7f..15dd94350 100644
--- a/test/Altinn.App.Api.Tests/OpenApi/swagger.yaml
+++ b/test/Altinn.App.Api.Tests/OpenApi/swagger.yaml
@@ -3791,6 +3791,8 @@ components:
nullable: true
receiver:
$ref: '#/components/schemas/PaymentReceiver'
+ payer:
+ $ref: '#/components/schemas/Payer'
currency:
type: string
nullable: true
diff --git a/test/Altinn.App.Core.Tests/Features/Payment/Providers/Nets/NetsMapperTests.cs b/test/Altinn.App.Core.Tests/Features/Payment/Providers/Nets/NetsMapperTests.cs
index 01b96af36..2a63629c8 100644
--- a/test/Altinn.App.Core.Tests/Features/Payment/Providers/Nets/NetsMapperTests.cs
+++ b/test/Altinn.App.Core.Tests/Features/Payment/Providers/Nets/NetsMapperTests.cs
@@ -93,6 +93,154 @@ public void MapPayerDetails_ValidConsumer_ReturnsPayer()
result.BillingAddress.Country.Should().Be("Land");
}
+ [Fact]
+ public void MapPayerDetails_ValidPayerCompany_ReturnsNetsConsumer()
+ {
+ // Arrange
+ var payer = new Payer
+ {
+ Company = new PayerCompany
+ {
+ Name = "Firmanavn",
+ ContactPerson = new PayerPrivatePerson
+ {
+ FirstName = "Ola",
+ LastName = "Normann",
+ Email = "ola.normann@example.com",
+ PhoneNumber = new PhoneNumber { Prefix = "+47", Number = "12345678" }
+ }
+ },
+ ShippingAddress = new Address
+ {
+ Name = "Ola Normann",
+ AddressLine1 = "Gate 1",
+ AddressLine2 = "Adresselinje 1",
+ PostalCode = "1234",
+ City = "By",
+ Country = "Land"
+ },
+ BillingAddress = new Address
+ {
+ Name = "Kari Normann",
+ AddressLine1 = "Gate 2",
+ AddressLine2 = "Adresselinje 2",
+ PostalCode = "5678",
+ City = "By",
+ Country = "Land"
+ }
+ };
+
+ // Act
+ NetsCheckoutConsumerDetails? result = NetsMapper.MapConsumerDetails(payer);
+
+ // Assert
+ result.Should().NotBeNull();
+
+ result!.Company.Should().NotBeNull();
+ result.Company!.Name.Should().Be("Firmanavn");
+ result.Company.Contact.Should().NotBeNull();
+ result.Company.Contact!.FirstName.Should().Be("Ola");
+ result.Company.Contact.LastName.Should().Be("Normann");
+ result.Email.Should().Be("ola.normann@example.com");
+ result.PhoneNumber.Should().NotBeNull();
+ result.PhoneNumber!.Prefix.Should().Be("+47");
+ result.PhoneNumber.Number.Should().Be("12345678");
+
+ result.PrivatePerson.Should().BeNull();
+
+ result.ShippingAddress.Should().NotBeNull();
+ result.ShippingAddress!.ReceiverLine.Should().Be("Ola Normann");
+ result.ShippingAddress.AddressLine1.Should().Be("Gate 1");
+ result.ShippingAddress.AddressLine2.Should().Be("Adresselinje 1");
+ result.ShippingAddress.PostalCode.Should().Be("1234");
+ result.ShippingAddress.City.Should().Be("By");
+ result.ShippingAddress.Country.Should().Be("Land");
+
+ result.BillingAddress.Should().NotBeNull();
+ result.BillingAddress!.ReceiverLine.Should().Be("Kari Normann");
+ result.BillingAddress.AddressLine1.Should().Be("Gate 2");
+ result.BillingAddress.AddressLine2.Should().Be("Adresselinje 2");
+ result.BillingAddress.PostalCode.Should().Be("5678");
+ result.BillingAddress.City.Should().Be("By");
+ result.BillingAddress.Country.Should().Be("Land");
+ }
+
+ [Fact]
+ public void MapPayerDetails_ValidPayerPrivatePerson_ReturnsNetsConsumer()
+ {
+ // Arrange
+ var payer = new Payer
+ {
+ PrivatePerson = new PayerPrivatePerson
+ {
+ FirstName = "Kari",
+ LastName = "Normann",
+ Email = "ola.normann@example.com",
+ PhoneNumber = new PhoneNumber { Prefix = "+47", Number = "87654321" }
+ },
+ ShippingAddress = new Address
+ {
+ Name = "Ola Normann",
+ AddressLine1 = "Gate 1",
+ AddressLine2 = "Adresselinje 1",
+ PostalCode = "1234",
+ City = "By",
+ Country = "Land"
+ },
+ BillingAddress = new Address
+ {
+ Name = "Kari Normann",
+ AddressLine1 = "Gate 2",
+ AddressLine2 = "Adresselinje 2",
+ PostalCode = "5678",
+ City = "By",
+ Country = "Land"
+ }
+ };
+
+ // Act
+ NetsCheckoutConsumerDetails? result = NetsMapper.MapConsumerDetails(payer);
+
+ // Assert
+ result.Should().NotBeNull();
+
+ result!.Company.Should().BeNull();
+
+ result.PrivatePerson.Should().NotBeNull();
+ result.PrivatePerson!.FirstName.Should().Be("Kari");
+ result.PrivatePerson.LastName.Should().Be("Normann");
+ result.Email.Should().Be("ola.normann@example.com");
+ result.PhoneNumber.Should().NotBeNull();
+ result.PhoneNumber!.Prefix.Should().Be("+47");
+ result.PhoneNumber.Number.Should().Be("87654321");
+
+ result.ShippingAddress.Should().NotBeNull();
+ result.ShippingAddress!.ReceiverLine.Should().Be("Ola Normann");
+ result.ShippingAddress.AddressLine1.Should().Be("Gate 1");
+ result.ShippingAddress.AddressLine2.Should().Be("Adresselinje 1");
+ result.ShippingAddress.PostalCode.Should().Be("1234");
+ result.ShippingAddress.City.Should().Be("By");
+ result.ShippingAddress.Country.Should().Be("Land");
+
+ result.BillingAddress.Should().NotBeNull();
+ result.BillingAddress!.ReceiverLine.Should().Be("Kari Normann");
+ result.BillingAddress.AddressLine1.Should().Be("Gate 2");
+ result.BillingAddress.AddressLine2.Should().Be("Adresselinje 2");
+ result.BillingAddress.PostalCode.Should().Be("5678");
+ result.BillingAddress.City.Should().Be("By");
+ result.BillingAddress.Country.Should().Be("Land");
+ }
+
+ [Fact]
+ public void MapPayerDetails_BothCompanyAndPrivatePersonIsSet_ThrowsArgumentException()
+ {
+ // Arrange
+ var payer = new Payer { Company = new PayerCompany(), PrivatePerson = new PayerPrivatePerson(), };
+
+ // Act & assert
+ Assert.Throws(() => NetsMapper.MapConsumerDetails(payer));
+ }
+
[Fact]
public void MapConsumerTypes_ValidPayerTypes_ReturnsCorrectConsumerTypes()
{