first
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using Csob.Client;
|
||||
|
||||
namespace Csob.Services;
|
||||
|
||||
/// <summary>AISP – account information (accounts, balance, transactions, standing orders, direct debits).</summary>
|
||||
public sealed class AccountsService
|
||||
{
|
||||
private readonly CsobApiAccessor _accessor;
|
||||
|
||||
public AccountsService(CsobApiAccessor accessor) => _accessor = accessor;
|
||||
|
||||
public Task<JsonNode?> ListAsync(int? page, int? size, string? sort, string? order, CancellationToken ct)
|
||||
=> _accessor.Client.GetAsync(CsobApiPaths.Accounts, Paging(page, size, sort, order), ct);
|
||||
|
||||
public Task<JsonNode?> BalanceAsync(string accountId, string? currency, CancellationToken ct)
|
||||
=> _accessor.Client.GetAsync(CsobApiPaths.Balance(accountId),
|
||||
new Dictionary<string, string?> { ["currency"] = currency }, ct);
|
||||
|
||||
public Task<JsonNode?> TransactionsAsync(string accountId, TransactionQuery query, CancellationToken ct)
|
||||
=> _accessor.Client.GetAsync(CsobApiPaths.Transactions(accountId), query.ToDictionary(), ct);
|
||||
|
||||
public Task<JsonNode?> AwaitingTransactionsAsync(string accountId, int? page, int? size, CancellationToken ct)
|
||||
=> _accessor.Client.GetAsync(CsobApiPaths.AwaitingTransactions(accountId), Paging(page, size, null, null), ct);
|
||||
|
||||
public Task<JsonNode?> StandingOrdersAsync(string accountId, int? page, int? size, CancellationToken ct)
|
||||
=> _accessor.Client.GetAsync(CsobApiPaths.AccountStandingOrders(accountId), Paging(page, size, null, null), ct);
|
||||
|
||||
public Task<JsonNode?> StandingOrderDetailAsync(string accountId, string standingOrderId, CancellationToken ct)
|
||||
=> _accessor.Client.GetAsync(CsobApiPaths.AccountStandingOrder(accountId, standingOrderId), null, ct);
|
||||
|
||||
public Task<JsonNode?> DirectDebitsAsync(string accountId, int? page, int? size, CancellationToken ct)
|
||||
=> _accessor.Client.GetAsync(CsobApiPaths.AccountDirectDebits(accountId), Paging(page, size, null, null), ct);
|
||||
|
||||
private static Dictionary<string, string?> Paging(int? page, int? size, string? sort, string? order) => new()
|
||||
{
|
||||
["page"] = page?.ToString(),
|
||||
["size"] = size?.ToString(),
|
||||
["sort"] = sort,
|
||||
["order"] = order,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Optional filters for the transactions listing (forwarded as query parameters).</summary>
|
||||
public sealed class TransactionQuery
|
||||
{
|
||||
public int? Page { get; set; }
|
||||
public int? Size { get; set; }
|
||||
public string? Sort { get; set; }
|
||||
public string? Order { get; set; }
|
||||
/// <summary>ISO date (YYYY-MM-DD) lower bound.</summary>
|
||||
public string? DateFrom { get; set; }
|
||||
/// <summary>ISO date (YYYY-MM-DD) upper bound.</summary>
|
||||
public string? DateTo { get; set; }
|
||||
|
||||
public Dictionary<string, string?> ToDictionary() => new()
|
||||
{
|
||||
["page"] = Page?.ToString(),
|
||||
["size"] = Size?.ToString(),
|
||||
["sort"] = Sort,
|
||||
["order"] = Order,
|
||||
["dateFrom"] = DateFrom,
|
||||
["dateTo"] = DateTo,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using Csob.Client;
|
||||
|
||||
namespace Csob.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Common – PSU consent lifecycle (create / detail / revoke). The consent request body varies
|
||||
/// across COBS profiles, so it is accepted and forwarded as raw JSON.
|
||||
/// </summary>
|
||||
public sealed class ConsentsService
|
||||
{
|
||||
private readonly CsobApiAccessor _accessor;
|
||||
|
||||
public ConsentsService(CsobApiAccessor accessor) => _accessor = accessor;
|
||||
|
||||
public Task<JsonNode?> CreateAsync(JsonNode request, CancellationToken ct)
|
||||
=> _accessor.Client.PostAsync(CsobApiPaths.Consents, request, ct);
|
||||
|
||||
public Task<JsonNode?> DetailAsync(string id, CancellationToken ct)
|
||||
=> _accessor.Client.GetAsync(CsobApiPaths.Consent(id), null, ct);
|
||||
|
||||
public Task<JsonNode?> DeleteAsync(string id, CancellationToken ct)
|
||||
=> _accessor.Client.DeleteAsync(CsobApiPaths.Consent(id), ct);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using Csob.Client;
|
||||
|
||||
namespace Csob.Services;
|
||||
|
||||
/// <summary>
|
||||
/// PISP – direct-debit mandate initiation, detail/status, revocation and the sign (SCA) flow.
|
||||
/// The mandate creation body varies across COBS profiles, so it is accepted and forwarded as raw JSON.
|
||||
/// </summary>
|
||||
public sealed class DirectDebitsService
|
||||
{
|
||||
private readonly CsobApiAccessor _accessor;
|
||||
|
||||
public DirectDebitsService(CsobApiAccessor accessor) => _accessor = accessor;
|
||||
|
||||
public Task<JsonNode?> InitiateAsync(JsonNode request, CancellationToken ct)
|
||||
=> _accessor.Client.PostAsync(CsobApiPaths.DirectDebits, request, ct);
|
||||
|
||||
public Task<JsonNode?> DetailAsync(string id, CancellationToken ct)
|
||||
=> _accessor.Client.GetAsync(CsobApiPaths.DirectDebit(id), null, ct);
|
||||
|
||||
public Task<JsonNode?> StatusAsync(string id, CancellationToken ct)
|
||||
=> _accessor.Client.GetAsync(CsobApiPaths.DirectDebitStatus(id), null, ct);
|
||||
|
||||
public Task<JsonNode?> CancelAsync(string id, CancellationToken ct)
|
||||
=> _accessor.Client.DeleteAsync(CsobApiPaths.DirectDebit(id), ct);
|
||||
|
||||
public Task<JsonNode?> StartSignAsync(string id, string signId, JsonNode? body, CancellationToken ct)
|
||||
=> _accessor.Client.PostAsync(CsobApiPaths.DirectDebitSign(id, signId), body, ct);
|
||||
|
||||
public Task<JsonNode?> SignStatusAsync(string id, string signId, CancellationToken ct)
|
||||
=> _accessor.Client.GetAsync(CsobApiPaths.DirectDebitSign(id, signId), null, ct);
|
||||
|
||||
public Task<JsonNode?> FinalizeSignAsync(string id, string signId, JsonNode? body, CancellationToken ct)
|
||||
=> _accessor.Client.PutAsync(CsobApiPaths.DirectDebitSign(id, signId), body, ct);
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
using System.Net;
|
||||
using System.Text.Json.Nodes;
|
||||
using Csob.Client;
|
||||
using Csob.Configuration;
|
||||
using Csob.Credentials;
|
||||
using Csob.Models;
|
||||
|
||||
namespace Csob.Services;
|
||||
|
||||
/// <summary>
|
||||
/// OAuth2 Authorization Code helper for the ČSOB PSD2 flow. Builds the PSU authorization URL and
|
||||
/// exchanges/refreshes tokens against the ČSOB token endpoint. The token endpoint is behind mutual
|
||||
/// TLS, so the client certificate header is required for the token/refresh calls. Client id/secret
|
||||
/// are taken from per-request headers (this service is multi-tenant and stores no app credentials).
|
||||
/// </summary>
|
||||
public sealed class OAuthService
|
||||
{
|
||||
private readonly RequestCredentialsProvider _credentials;
|
||||
private readonly CsobHttpClientProvider _httpClientProvider;
|
||||
private readonly CsobSettings _settings;
|
||||
|
||||
public OAuthService(
|
||||
RequestCredentialsProvider credentials,
|
||||
CsobHttpClientProvider httpClientProvider,
|
||||
CsobSettings settings)
|
||||
{
|
||||
_credentials = credentials;
|
||||
_httpClientProvider = httpClientProvider;
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
/// <summary>Builds the authorization URL to which the PSU must be redirected.</summary>
|
||||
public AuthorizationUrlResponse BuildAuthorizationUrl(string redirectUri, string? scope, string? state)
|
||||
{
|
||||
var clientId = RequireHeader(CredentialConstants.ClientIdHeader);
|
||||
|
||||
var query = new Dictionary<string, string?>
|
||||
{
|
||||
["response_type"] = "code",
|
||||
["client_id"] = clientId,
|
||||
["redirect_uri"] = redirectUri,
|
||||
["scope"] = scope,
|
||||
["state"] = state,
|
||||
};
|
||||
|
||||
var url = _settings.OAuthAuthorizeUrl + "?" + string.Join("&", query
|
||||
.Where(kvp => !string.IsNullOrWhiteSpace(kvp.Value))
|
||||
.Select(kvp => $"{Uri.EscapeDataString(kvp.Key)}={Uri.EscapeDataString(kvp.Value!)}"));
|
||||
|
||||
return new AuthorizationUrlResponse { AuthorizationUrl = url, State = state };
|
||||
}
|
||||
|
||||
public Task<JsonNode?> ExchangeCodeAsync(TokenExchangeRequest request, CancellationToken ct)
|
||||
=> PostTokenAsync(new Dictionary<string, string>
|
||||
{
|
||||
["grant_type"] = "authorization_code",
|
||||
["code"] = request.Code,
|
||||
["redirect_uri"] = request.RedirectUri,
|
||||
}, ct);
|
||||
|
||||
public Task<JsonNode?> RefreshAsync(TokenRefreshRequest request, CancellationToken ct)
|
||||
=> PostTokenAsync(new Dictionary<string, string>
|
||||
{
|
||||
["grant_type"] = "refresh_token",
|
||||
["refresh_token"] = request.RefreshToken,
|
||||
}, ct);
|
||||
|
||||
private async Task<JsonNode?> PostTokenAsync(Dictionary<string, string> form, CancellationToken ct)
|
||||
{
|
||||
var clientId = RequireHeader(CredentialConstants.ClientIdHeader);
|
||||
var clientSecret = RequireHeader(CredentialConstants.ClientSecretHeader);
|
||||
|
||||
var certificate = _credentials.TryBuildCertificate()
|
||||
?? throw new MissingCredentialsException(new[] { CredentialConstants.CertificateHeader });
|
||||
|
||||
form["client_id"] = clientId;
|
||||
form["client_secret"] = clientSecret;
|
||||
|
||||
var http = _httpClientProvider.GetClient(certificate);
|
||||
|
||||
using var content = new FormUrlEncodedContent(form);
|
||||
using var response = await http.PostAsync(_settings.OAuthTokenUrl, content, ct);
|
||||
var payload = await response.Content.ReadAsStringAsync(ct);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
throw new CsobApiException(response.StatusCode, ParseOAuthError(payload), payload);
|
||||
}
|
||||
|
||||
return string.IsNullOrWhiteSpace(payload) ? null : JsonNode.Parse(payload);
|
||||
}
|
||||
|
||||
private string RequireHeader(string name)
|
||||
=> _credentials.Header(name) ?? throw new MissingCredentialsException(new[] { name });
|
||||
|
||||
private static IReadOnlyList<string> ParseOAuthError(string payload)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(payload))
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// OAuth2 errors use { "error": "...", "error_description": "..." }.
|
||||
var error = JsonNode.Parse(payload)?["error"]?.GetValue<string>();
|
||||
return string.IsNullOrWhiteSpace(error) ? Array.Empty<string>() : new[] { error! };
|
||||
}
|
||||
catch (System.Text.Json.JsonException)
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using Csob.Client;
|
||||
using Csob.Models;
|
||||
|
||||
namespace Csob.Services;
|
||||
|
||||
/// <summary>PISP – single payment initiation, status/detail, cancellation and the sign (SCA) flow.</summary>
|
||||
public sealed class PaymentsService
|
||||
{
|
||||
private readonly CsobApiAccessor _accessor;
|
||||
|
||||
public PaymentsService(CsobApiAccessor accessor) => _accessor = accessor;
|
||||
|
||||
public Task<JsonNode?> InitiateAsync(PaymentInitiationRequest request, CancellationToken ct)
|
||||
=> _accessor.Client.PostAsync(CsobApiPaths.Payments, request, ct);
|
||||
|
||||
public Task<JsonNode?> DetailAsync(string id, CancellationToken ct)
|
||||
=> _accessor.Client.GetAsync(CsobApiPaths.Payment(id), null, ct);
|
||||
|
||||
public Task<JsonNode?> StatusAsync(string id, CancellationToken ct)
|
||||
=> _accessor.Client.GetAsync(CsobApiPaths.PaymentStatus(id), null, ct);
|
||||
|
||||
public Task<JsonNode?> CancelAsync(string id, CancellationToken ct)
|
||||
=> _accessor.Client.DeleteAsync(CsobApiPaths.Payment(id), ct);
|
||||
|
||||
/// <summary>Starts transaction authorization (SCA). Returns the redirect details for the PSU.</summary>
|
||||
public Task<JsonNode?> StartSignAsync(string id, string signId, JsonNode? body, CancellationToken ct)
|
||||
=> _accessor.Client.PostAsync(CsobApiPaths.PaymentSign(id, signId), body, ct);
|
||||
|
||||
/// <summary>Gets the current state of an authorization (sign) transaction.</summary>
|
||||
public Task<JsonNode?> SignStatusAsync(string id, string signId, CancellationToken ct)
|
||||
=> _accessor.Client.GetAsync(CsobApiPaths.PaymentSign(id, signId), null, ct);
|
||||
|
||||
/// <summary>Finalizes an authorization (sign) transaction.</summary>
|
||||
public Task<JsonNode?> FinalizeSignAsync(string id, string signId, JsonNode? body, CancellationToken ct)
|
||||
=> _accessor.Client.PutAsync(CsobApiPaths.PaymentSign(id, signId), body, ct);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using Csob.Client;
|
||||
using Csob.Models;
|
||||
|
||||
namespace Csob.Services;
|
||||
|
||||
/// <summary>PISP – standing-order initiation, detail/status, cancellation and the sign (SCA) flow.</summary>
|
||||
public sealed class StandingOrdersService
|
||||
{
|
||||
private readonly CsobApiAccessor _accessor;
|
||||
|
||||
public StandingOrdersService(CsobApiAccessor accessor) => _accessor = accessor;
|
||||
|
||||
public Task<JsonNode?> InitiateAsync(StandingOrderInitiationRequest request, CancellationToken ct)
|
||||
=> _accessor.Client.PostAsync(CsobApiPaths.StandingOrders, request, ct);
|
||||
|
||||
public Task<JsonNode?> DetailAsync(string id, CancellationToken ct)
|
||||
=> _accessor.Client.GetAsync(CsobApiPaths.StandingOrder(id), null, ct);
|
||||
|
||||
public Task<JsonNode?> StatusAsync(string id, CancellationToken ct)
|
||||
=> _accessor.Client.GetAsync(CsobApiPaths.StandingOrderStatus(id), null, ct);
|
||||
|
||||
public Task<JsonNode?> CancelAsync(string id, CancellationToken ct)
|
||||
=> _accessor.Client.DeleteAsync(CsobApiPaths.StandingOrder(id), ct);
|
||||
|
||||
public Task<JsonNode?> StartSignAsync(string id, string signId, JsonNode? body, CancellationToken ct)
|
||||
=> _accessor.Client.PostAsync(CsobApiPaths.StandingOrderSign(id, signId), body, ct);
|
||||
|
||||
public Task<JsonNode?> SignStatusAsync(string id, string signId, CancellationToken ct)
|
||||
=> _accessor.Client.GetAsync(CsobApiPaths.StandingOrderSign(id, signId), null, ct);
|
||||
|
||||
public Task<JsonNode?> FinalizeSignAsync(string id, string signId, JsonNode? body, CancellationToken ct)
|
||||
=> _accessor.Client.PutAsync(CsobApiPaths.StandingOrderSign(id, signId), body, ct);
|
||||
}
|
||||
Reference in New Issue
Block a user