using System;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using IOL.VippsEcommerce.Models;
using IOL.VippsEcommerce.Models.Api;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace IOL.VippsEcommerce
{
///
/// The main class for interacting with the vipps api.
///
public class VippsEcommerceService : IVippsEcommerceService
{
private readonly HttpClient _client;
private readonly ILogger _logger;
private readonly string _vippsClientId;
private readonly string _vippsClientSecret;
private readonly string _vippsMsn;
private readonly string _cacheEncryptionKey;
private readonly string _cacheDirectoryPath;
private readonly JsonSerializerOptions _requestJsonSerializerOptions = new() {
IgnoreNullValues = true
};
private const string VIPPS_CACHE_FILE_NAME = "vipps_ecommerce_credentials.json";
private string CacheFilePath => Path.Combine(_cacheDirectoryPath, VIPPS_CACHE_FILE_NAME);
public VippsConfiguration Configuration { get; }
public VippsEcommerceService(
HttpClient client,
ILogger logger,
IOptions options
) {
Configuration = options.Value;
Configuration.Verify();
var vippsApiUrl = Configuration.GetValue(VippsConfigurationKeyNames.VIPPS_API_URL);
client.BaseAddress = new Uri(vippsApiUrl);
_client = client;
_logger = logger;
_vippsClientId = Configuration.GetValue(VippsConfigurationKeyNames.VIPPS_CLIENT_ID);
_vippsClientSecret = Configuration.GetValue(VippsConfigurationKeyNames.VIPPS_CLIENT_SECRET);
client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key",
Configuration.GetValue(VippsConfigurationKeyNames
.VIPPS_SUBSCRIPTION_KEY_PRIMARY)
?? Configuration.GetValue(VippsConfigurationKeyNames
.VIPPS_SUBSCRIPTION_KEY_SECONDARY));
var msn = Configuration.GetValue(VippsConfigurationKeyNames.VIPPS_MSN);
if (msn.IsPresent()) {
client.DefaultRequestHeaders.Add("Merchant-Serial-Number", msn);
_vippsMsn = msn;
}
var systemName = Configuration.GetValue(VippsConfigurationKeyNames.VIPPS_SYSTEM_NAME);
if (systemName.IsPresent()) {
client.DefaultRequestHeaders.Add("Vipps-System-Name", systemName);
}
var systemVersion = Configuration.GetValue(VippsConfigurationKeyNames.VIPPS_SYSTEM_VERSION);
if (systemVersion.IsPresent()) {
client.DefaultRequestHeaders.Add("Vipps-System-Version", systemVersion);
}
var systemPluginName = Configuration.GetValue(VippsConfigurationKeyNames.VIPPS_SYSTEM_PLUGIN_NAME);
if (systemPluginName.IsPresent()) {
client.DefaultRequestHeaders.Add("Vipps-System-Plugin-Name", systemPluginName);
}
var systemPluginVersion = Configuration.GetValue(VippsConfigurationKeyNames.VIPPS_SYSTEM_PLUGIN_VERSION);
if (systemPluginVersion.IsPresent()) {
client.DefaultRequestHeaders.Add("Vipps-System-Plugin-Version", systemPluginVersion);
}
_cacheEncryptionKey = Configuration.GetValue(VippsConfigurationKeyNames.VIPPS_CACHE_KEY);
_cacheDirectoryPath = Configuration.GetValue(VippsConfigurationKeyNames.VIPPS_CACHE_PATH);
if (_cacheDirectoryPath.IsPresent()) {
if (!_cacheDirectoryPath.IsDirectoryWritable()) {
_logger.LogError("Could not write to cache file directory ("
+ _cacheDirectoryPath
+ "). Disabling caching.");
_cacheDirectoryPath = default;
_cacheEncryptionKey = default;
}
}
_logger.LogInformation("VippsEcommerceService was successfully initialised with api url: " + vippsApiUrl);
}
///
/// The access token endpoint is used to get the JWT (JSON Web Token) that must be passed in every API request in the Authorization header.
/// The access token is a base64-encoded string value that must be aquired first before making any Vipps api calls.
/// The access token is valid for 1 hour in the test environment and 24 hours in the production environment.
///
///
/// Throws if the api returns unsuccessfully
private async Task GetAuthorizationTokenAsync(
bool forceRefresh = false,
CancellationToken ct = default
) {
if (!forceRefresh) {
if (_cacheDirectoryPath.IsPresent() && File.Exists(CacheFilePath)) {
var fileContents = await File.ReadAllTextAsync(CacheFilePath, ct);
if (fileContents.IsPresent()) {
VippsAuthorizationTokenResponse credentials = default;
try {
credentials = JsonSerializer.Deserialize(fileContents);
} catch (Exception e) {
if (e is JsonException && _cacheEncryptionKey.IsPresent()) {
// most likely encrypted, try to decrypt
var decryptedContents = fileContents.DecryptWithAes(_cacheEncryptionKey);
credentials =
JsonSerializer.Deserialize(decryptedContents);
}
}
if (credentials != default) {
var currentEpoch = new DateTimeOffset(DateTime.UtcNow).ToUnixTimeSeconds();
if (long.TryParse(credentials.ExpiresOn, out var expires)
&& credentials.AccessToken.IsPresent()) {
if (expires - 600 > currentEpoch) {
_logger.LogDebug("VippsEcommerceService: Got tokens from cache");
return credentials;
}
}
}
}
}
}
var requestMessage = new HttpRequestMessage {
Headers = {
{
"client_id", _vippsClientId
}, {
"client_secret", _vippsClientSecret
},
},
RequestUri = new Uri(_client.BaseAddress + "accesstoken/get"),
Method = HttpMethod.Post
};
var response = await _client.SendAsync(requestMessage, ct);
try {
response.EnsureSuccessStatusCode();
var credentials = await response.Content.ReadAsStringAsync(ct);
if (_cacheDirectoryPath.IsPresent()) {
await File.WriteAllTextAsync(CacheFilePath,
_cacheEncryptionKey.IsPresent()
? credentials.EncryptWithAes(_cacheEncryptionKey)
: credentials,
ct);
}
_logger.LogDebug("VippsEcommerceService: Got tokens from " + requestMessage.RequestUri);
return JsonSerializer.Deserialize(credentials);
} catch (Exception e) {
var exception =
new VippsRequestException("Vipps get token request returned unsuccessfully.", e);
if (e is HttpRequestException) {
try {
exception.ErrorResponse =
await response.Content.ReadFromJsonAsync(cancellationToken: ct);
_logger.LogError("ErrorResponse: " + JsonSerializer.Serialize(response.Content));
} catch (Exception e1) {
_logger.LogError("Unknown ErrorResponse: " + JsonSerializer.Serialize(response.Content));
Console.WriteLine(e1);
}
}
Console.WriteLine(e);
throw exception;
}
}
///
/// This API call allows the merchants to initiate payments.
/// The merchantSerialNumber (MSN) specifies which sales unit the payments is for.
/// Payments are uniquely identified with the merchantSerialNumber and orderId together.
/// The merchant-provided orderId must be unique per sales channel.
/// Once the transaction is successfully initiated in Vipps, you will receive a response with a fallBack URL which will direct the customer to the Vipps landing page.
/// The landing page detects if the request comes from a mobile or laptop/desktop device, and if on a mobile device automatically switches to the Vipps app if it is intalled.
/// The merchant may also pass the 'isApp: true' parameter that will make Vipps respond with a app-switch deeplink that will take the customer directly to the Vipps app.
/// URLs passed to Vipps must validate with the Apache Commons UrlValidator.
///
///
/// Throws if the api returns unsuccessfully
public async Task InitiatePaymentAsync(
VippsInitiatePaymentRequest payload,
CancellationToken ct = default
) {
if (_client.DefaultRequestHeaders.Authorization?.Parameter.IsNullOrWhiteSpace() ?? true) {
var credentials = await GetAuthorizationTokenAsync(false, ct);
_client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", credentials.AccessToken);
}
var response = await _client.PostAsJsonAsync("ecomm/v2/payments",
payload,
_requestJsonSerializerOptions,
ct);
try {
response.EnsureSuccessStatusCode();
_logger.LogDebug("VippsEcommerceService: Sent InitiatePaymentRequest");
return await response.Content
.ReadFromJsonAsync(cancellationToken: ct);
} catch (Exception e) {
var exception =
new VippsRequestException("Vipps initiate payment request returned unsuccessfully.", e);
if (e is HttpRequestException) {
try {
exception.ErrorResponse =
await response.Content.ReadFromJsonAsync(cancellationToken: ct);
_logger.LogError("ErrorResponse: " + JsonSerializer.Serialize(response.Content));
} catch (Exception e1) {
_logger.LogError("Unknown ErrorResponse: " + JsonSerializer.Serialize(response.Content));
Console.WriteLine(e1);
}
}
Console.WriteLine(e);
throw exception;
}
}
///
/// This API call allows merchant to capture the reserved amount.
/// Amount to capture cannot be higher than reserved.
/// The API also allows capturing partial amount of the reserved amount.
/// Partial capture can be called as many times as required so long there is reserved amount to capture.
/// Transaction text is not optional and is used as a proof of delivery (tracking code, consignment number etc.).
/// In a case of direct capture, both fund reservation and capture are executed in a single operation.
/// It is important to check the response, and the capture is only successful when the response is HTTP 200 OK.
///
///
/// Throws if the api returns unsuccessfully
public async Task CapturePaymentAsync(
string orderId,
VippsPaymentActionRequest payload,
CancellationToken ct = default
) {
if (_client.DefaultRequestHeaders.Authorization?.Parameter.IsNullOrWhiteSpace() ?? true) {
var credentials = await GetAuthorizationTokenAsync(false, ct);
_client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", credentials.AccessToken);
}
if (payload.MerchantInfo?.MerchantSerialNumber.IsNullOrWhiteSpace() ?? false) {
payload.MerchantInfo = new TMerchantInfoPayment {
MerchantSerialNumber = _vippsMsn
};
}
var response = await _client.PostAsJsonAsync("ecomm/v2/payments/" + orderId + "/capture",
payload,
_requestJsonSerializerOptions,
ct);
try {
response.EnsureSuccessStatusCode();
_logger.LogDebug("VippsEcommerceService: Sent CapturePaymentRequest");
return await response.Content.ReadFromJsonAsync(cancellationToken: ct);
} catch (Exception e) {
var exception =
new VippsRequestException("Vipps capture payment request returned unsuccessfully.", e);
if (e is HttpRequestException) {
try {
exception.ErrorResponse =
await response.Content.ReadFromJsonAsync(cancellationToken: ct);
_logger.LogError("ErrorResponse: " + JsonSerializer.Serialize(response.Content));
} catch (Exception e1) {
_logger.LogError("Unknown ErrorResponse: " + JsonSerializer.Serialize(response.Content));
Console.WriteLine(e1);
}
}
Console.WriteLine(e);
throw exception;
}
}
///
/// The API call allows merchant to cancel the reserved or initiated transaction.
/// The API will not allow partial cancellation which has the consequence that partially captured transactions cannot be cancelled.
/// Please note that in a case of communication errors during initiate payment service call between Vipps and PSP/Acquirer/Issuer; even in a case that customer has confirmed a payment, the payment will be cancelled by Vipps.
/// Note this means you can not cancel a captured payment.
///
///
/// Throws if the api returns unsuccessfully
public async Task CancelPaymentAsync(
string orderId,
VippsPaymentActionRequest payload,
CancellationToken ct = default
) {
if (_client.DefaultRequestHeaders.Authorization?.Parameter.IsNullOrWhiteSpace() ?? true) {
var credentials = await GetAuthorizationTokenAsync(false, ct);
_client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", credentials.AccessToken);
}
if (payload.MerchantInfo?.MerchantSerialNumber.IsNullOrWhiteSpace() ?? false) {
payload.MerchantInfo = new TMerchantInfoPayment {
MerchantSerialNumber = _vippsMsn
};
}
var response = await _client.PutAsJsonAsync("ecomm/v2/payments/" + orderId + "/cancel",
payload,
_requestJsonSerializerOptions,
ct);
try {
response.EnsureSuccessStatusCode();
_logger.LogDebug("VippsEcommerceService: Sent CancelPaymentRequest");
return await response.Content.ReadFromJsonAsync(cancellationToken: ct);
} catch (Exception e) {
var exception =
new VippsRequestException("Vipps cancel payment request returned unsuccessfully.", e);
if (e is HttpRequestException) {
try {
exception.ErrorResponse =
await response.Content.ReadFromJsonAsync(cancellationToken: ct);
_logger.LogError("ErrorResponse: " + JsonSerializer.Serialize(response.Content));
} catch (Exception e1) {
_logger.LogError("Unknown ErrorResponse: " + JsonSerializer.Serialize(response.Content));
Console.WriteLine(e1);
}
}
Console.WriteLine(e);
throw exception;
}
}
///
/// The API call allows merchant to refresh the authorizations of the payment.
/// A reservation's lifetime is defined by the scheme. Typically 7 days for Visa, and 30 days for Mastercard.
/// This is currently not live in production and will be added shortly.
///
///
/// Throws if the api returns unsuccessfully
public async Task AuthorizePaymentAsync(
string orderId,
VippsPaymentActionRequest payload,
CancellationToken ct = default
) {
if (_client.DefaultRequestHeaders.Authorization?.Parameter.IsNullOrWhiteSpace() ?? true) {
var credentials = await GetAuthorizationTokenAsync(false, ct);
_client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", credentials.AccessToken);
}
if (payload.MerchantInfo?.MerchantSerialNumber.IsNullOrWhiteSpace() ?? false) {
payload.MerchantInfo = new TMerchantInfoPayment {
MerchantSerialNumber = _vippsMsn
};
}
var response = await _client.PutAsJsonAsync("ecomm/v2/payments/" + orderId + "/authorize",
payload,
_requestJsonSerializerOptions,
ct);
try {
response.EnsureSuccessStatusCode();
_logger.LogDebug("VippsEcommerceService: Sent AuthorizePaymentRequest");
return await response.Content.ReadFromJsonAsync(cancellationToken: ct);
} catch (Exception e) {
var exception =
new VippsRequestException("Vipps authorize payment request returned unsuccessfully.", e);
if (e is HttpRequestException) {
try {
exception.ErrorResponse =
await response.Content.ReadFromJsonAsync(cancellationToken: ct);
_logger.LogError("ErrorResponse: " + JsonSerializer.Serialize(response.Content));
} catch (Exception e1) {
_logger.LogError("Unknown ErrorResponse: " + JsonSerializer.Serialize(response.Content));
Console.WriteLine(e1);
}
}
Console.WriteLine(e);
throw exception;
}
}
///
/// The API allows a merchant to do a refund of already captured transaction.
/// There is an option to do a partial refund of the captured amount.
/// Refunded amount cannot be larger than captured.
/// Timeframe for issuing a refund for a payment is 365 days from the date payment has been captured.
/// If the refund payment service call is called after the refund timeframe, service call will respond with an error.
/// Refunded funds will be transferred from the merchant account to the customer credit card that was used in payment flow.
/// Pay attention that in order to perform refund, there must be enough funds at merchant settlements account.
///
///
/// Throws if the api returns unsuccessfully
public async Task RefundPaymentAsync(
string orderId,
VippsPaymentActionRequest payload,
CancellationToken ct = default
) {
if (_client.DefaultRequestHeaders.Authorization?.Parameter.IsNullOrWhiteSpace() ?? true) {
var credentials = await GetAuthorizationTokenAsync(false, ct);
_client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", credentials.AccessToken);
}
if (payload.MerchantInfo?.MerchantSerialNumber.IsNullOrWhiteSpace() ?? false) {
payload.MerchantInfo = new TMerchantInfoPayment {
MerchantSerialNumber = _vippsMsn
};
}
var response = await _client.PostAsJsonAsync("ecomm/v2/payments/" + orderId + "/refund",
payload,
_requestJsonSerializerOptions,
ct);
try {
response.EnsureSuccessStatusCode();
_logger.LogDebug("VippsEcommerceService: Sent RefundPaymentRequest");
return await response.Content.ReadFromJsonAsync(cancellationToken: ct);
} catch (Exception e) {
var exception =
new VippsRequestException("Vipps refund payment request returned unsuccessfully.", e);
if (e is HttpRequestException) {
try {
exception.ErrorResponse =
await response.Content.ReadFromJsonAsync(cancellationToken: ct);
_logger.LogError("ErrorResponse: " + JsonSerializer.Serialize(response.Content));
} catch (Exception e1) {
_logger.LogError("Unknown ErrorResponse: " + JsonSerializer.Serialize(response.Content));
Console.WriteLine(e1);
}
}
Console.WriteLine(e);
throw exception;
}
}
///
/// This endpoint allows developers to approve a payment through the Vipps eCom API without the use of the Vipps app.
/// This is useful for automated testing.
/// Express checkout is not supported for this endpoint.
/// The endpoint is only available in our Test environment.
/// Attempted use of the endpoint in production is not allowed, and will fail.
///
///
/// Throws if the api returns unsuccessfully
public async Task ForceApprovePaymentAsync(
string orderId,
VippsForceApproveRequest payload,
CancellationToken ct = default
) {
if (_client.DefaultRequestHeaders.Authorization?.Parameter.IsNullOrWhiteSpace() ?? true) {
var credentials = await GetAuthorizationTokenAsync(false, ct);
_client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", credentials.AccessToken);
}
var response =
await _client.PostAsJsonAsync("ecomm/v2/integration-test/payments/" + orderId + "/approve",
payload,
_requestJsonSerializerOptions,
ct);
try {
response.EnsureSuccessStatusCode();
_logger.LogDebug("VippsEcommerceService: Sent ForceApprovePaymentRequest");
return true;
} catch (Exception e) {
var exception =
new VippsRequestException("Vipps force approve payment request returned unsuccessfully.", e);
if (e is HttpRequestException) {
try {
exception.ErrorResponse =
await response.Content.ReadFromJsonAsync(cancellationToken: ct);
_logger.LogError("ErrorResponse: " + JsonSerializer.Serialize(response.Content));
} catch (Exception e1) {
_logger.LogError("Unknown ErrorResponse: " + JsonSerializer.Serialize(response.Content));
Console.WriteLine(e1);
}
}
Console.WriteLine(e);
throw exception;
}
}
///
/// This API call allows merchant to get the details of a payment transaction.
/// Service call returns detailed transaction history of given payment where events are sorted from newest to oldest for when the transaction occurred.
///
///
/// Throws if the api returns unsuccessfully
public async Task GetPaymentDetailsAsync(
string orderId,
CancellationToken ct = default
) {
if (_client.DefaultRequestHeaders.Authorization?.Parameter.IsNullOrWhiteSpace() ?? true) {
var credentials = await GetAuthorizationTokenAsync(false, ct);
_client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", credentials.AccessToken);
}
var response = await _client.GetAsync("ecomm/v2/payments/" + orderId + "/details", ct);
try {
response.EnsureSuccessStatusCode();
_logger.LogDebug("VippsEcommerceService: Sent GetPaymentDetailsRequest");
return await
response.Content.ReadFromJsonAsync(cancellationToken: ct);
} catch (Exception e) {
var exception =
new VippsRequestException("Vipps get payment detailsG request returned unsuccessfully.", e);
if (e is HttpRequestException) {
try {
exception.ErrorResponse =
await response.Content.ReadFromJsonAsync(cancellationToken: ct);
_logger.LogError("ErrorResponse: " + JsonSerializer.Serialize(response.Content));
} catch (Exception e1) {
_logger.LogError("Unknown ErrorResponse: " + JsonSerializer.Serialize(response.Content));
Console.WriteLine(e1);
}
}
Console.WriteLine(e);
throw exception;
}
}
}
}