2021-11-14 21:25:59 -08:00
|
|
|
#nullable enable
|
2020-06-28 21:44:35 -05:00
|
|
|
using System;
|
2021-04-13 10:36:49 +02:00
|
|
|
using System.Collections.Generic;
|
2024-03-14 19:07:11 +09:00
|
|
|
using System.Collections.Specialized;
|
|
|
|
using System.Diagnostics.CodeAnalysis;
|
2023-12-21 10:29:28 +09:00
|
|
|
using System.IO.IsolatedStorage;
|
2020-06-24 10:34:09 +09:00
|
|
|
using System.Linq;
|
2023-12-06 09:17:58 +09:00
|
|
|
using System.Text.RegularExpressions;
|
2020-06-24 13:44:26 +09:00
|
|
|
using System.Threading;
|
2020-06-24 10:34:09 +09:00
|
|
|
using System.Threading.Tasks;
|
2020-11-17 13:46:23 +01:00
|
|
|
using BTCPayServer.Abstractions.Constants;
|
2022-02-24 09:00:44 +01:00
|
|
|
using BTCPayServer.Abstractions.Extensions;
|
2020-06-24 10:34:09 +09:00
|
|
|
using BTCPayServer.Client;
|
|
|
|
using BTCPayServer.Client.Models;
|
|
|
|
using BTCPayServer.Data;
|
|
|
|
using BTCPayServer.HostedServices;
|
2024-03-14 19:07:11 +09:00
|
|
|
using BTCPayServer.Logging;
|
2023-12-06 09:17:58 +09:00
|
|
|
using BTCPayServer.NTag424;
|
2020-06-24 10:34:09 +09:00
|
|
|
using BTCPayServer.Payments;
|
2023-01-26 01:46:05 +01:00
|
|
|
using BTCPayServer.Security;
|
2020-06-24 10:34:09 +09:00
|
|
|
using BTCPayServer.Services;
|
|
|
|
using BTCPayServer.Services.Rates;
|
|
|
|
using Microsoft.AspNetCore.Authorization;
|
2020-06-30 08:26:19 +02:00
|
|
|
using Microsoft.AspNetCore.Cors;
|
2020-06-24 10:34:09 +09:00
|
|
|
using Microsoft.AspNetCore.Mvc;
|
|
|
|
using Microsoft.AspNetCore.Routing;
|
|
|
|
using Microsoft.EntityFrameworkCore;
|
2024-03-14 19:07:11 +09:00
|
|
|
using Microsoft.Extensions.Logging;
|
2023-12-06 09:17:58 +09:00
|
|
|
using NBitcoin.DataEncoders;
|
2024-03-14 19:07:11 +09:00
|
|
|
using Newtonsoft.Json;
|
2023-07-24 11:37:18 +02:00
|
|
|
using Newtonsoft.Json.Linq;
|
2024-03-14 19:07:11 +09:00
|
|
|
using Org.BouncyCastle.Bcpg.OpenPgp;
|
2022-11-15 10:40:57 +01:00
|
|
|
using MarkPayoutRequest = BTCPayServer.HostedServices.MarkPayoutRequest;
|
2020-06-24 10:34:09 +09:00
|
|
|
|
2022-01-14 13:05:23 +09:00
|
|
|
namespace BTCPayServer.Controllers.Greenfield
|
2020-06-24 10:34:09 +09:00
|
|
|
{
|
|
|
|
[ApiController]
|
|
|
|
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
2020-06-30 08:26:19 +02:00
|
|
|
[EnableCors(CorsPolicies.All)]
|
2020-06-24 10:34:09 +09:00
|
|
|
public class GreenfieldPullPaymentController : ControllerBase
|
|
|
|
{
|
|
|
|
private readonly PullPaymentHostedService _pullPaymentService;
|
|
|
|
private readonly LinkGenerator _linkGenerator;
|
|
|
|
private readonly ApplicationDbContextFactory _dbContextFactory;
|
|
|
|
private readonly CurrencyNameTable _currencyNameTable;
|
|
|
|
private readonly BTCPayNetworkJsonSerializerSettings _serializerSettings;
|
2021-04-13 10:36:49 +02:00
|
|
|
private readonly IEnumerable<IPayoutHandler> _payoutHandlers;
|
2023-01-25 21:43:07 -08:00
|
|
|
private readonly BTCPayNetworkProvider _networkProvider;
|
2023-01-26 01:46:05 +01:00
|
|
|
private readonly IAuthorizationService _authorizationService;
|
2023-12-06 09:17:58 +09:00
|
|
|
private readonly SettingsRepository _settingsRepository;
|
|
|
|
private readonly BTCPayServerEnvironment _env;
|
2024-03-14 19:07:11 +09:00
|
|
|
private readonly Logs _logs;
|
2020-06-24 10:34:09 +09:00
|
|
|
|
|
|
|
public GreenfieldPullPaymentController(PullPaymentHostedService pullPaymentService,
|
|
|
|
LinkGenerator linkGenerator,
|
|
|
|
ApplicationDbContextFactory dbContextFactory,
|
|
|
|
CurrencyNameTable currencyNameTable,
|
|
|
|
Services.BTCPayNetworkJsonSerializerSettings serializerSettings,
|
2023-01-26 01:46:05 +01:00
|
|
|
IEnumerable<IPayoutHandler> payoutHandlers,
|
2023-01-25 21:43:07 -08:00
|
|
|
BTCPayNetworkProvider btcPayNetworkProvider,
|
2023-12-06 09:17:58 +09:00
|
|
|
IAuthorizationService authorizationService,
|
|
|
|
SettingsRepository settingsRepository,
|
2024-03-14 19:07:11 +09:00
|
|
|
BTCPayServerEnvironment env, Logs logs)
|
2020-06-24 10:34:09 +09:00
|
|
|
{
|
|
|
|
_pullPaymentService = pullPaymentService;
|
|
|
|
_linkGenerator = linkGenerator;
|
|
|
|
_dbContextFactory = dbContextFactory;
|
|
|
|
_currencyNameTable = currencyNameTable;
|
|
|
|
_serializerSettings = serializerSettings;
|
2021-04-13 10:36:49 +02:00
|
|
|
_payoutHandlers = payoutHandlers;
|
2023-01-25 21:43:07 -08:00
|
|
|
_networkProvider = btcPayNetworkProvider;
|
2023-01-26 01:46:05 +01:00
|
|
|
_authorizationService = authorizationService;
|
2023-12-06 09:17:58 +09:00
|
|
|
_settingsRepository = settingsRepository;
|
|
|
|
_env = env;
|
2024-03-14 19:07:11 +09:00
|
|
|
_logs = logs;
|
2020-06-24 10:34:09 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
[HttpGet("~/api/v1/stores/{storeId}/pull-payments")]
|
2023-12-01 09:12:02 +01:00
|
|
|
[Authorize(Policy = Policies.CanViewPullPayments, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
2020-06-24 10:34:09 +09:00
|
|
|
public async Task<IActionResult> GetPullPayments(string storeId, bool includeArchived = false)
|
|
|
|
{
|
|
|
|
using var ctx = _dbContextFactory.CreateContext();
|
|
|
|
var pps = await ctx.PullPayments
|
|
|
|
.Where(p => p.StoreId == storeId && (includeArchived || !p.Archived))
|
|
|
|
.OrderByDescending(p => p.StartDate)
|
|
|
|
.ToListAsync();
|
|
|
|
return Ok(pps.Select(CreatePullPaymentData).ToArray());
|
|
|
|
}
|
|
|
|
|
|
|
|
[HttpPost("~/api/v1/stores/{storeId}/pull-payments")]
|
2023-01-26 01:46:05 +01:00
|
|
|
[Authorize(Policy = Policies.CanCreateNonApprovedPullPayments, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
2020-06-24 10:34:09 +09:00
|
|
|
public async Task<IActionResult> CreatePullPayment(string storeId, CreatePullPaymentRequest request)
|
|
|
|
{
|
|
|
|
if (request is null)
|
|
|
|
{
|
|
|
|
ModelState.AddModelError(string.Empty, "Missing body");
|
|
|
|
return this.CreateValidationError(ModelState);
|
|
|
|
}
|
2023-01-26 01:46:05 +01:00
|
|
|
|
|
|
|
if (request.AutoApproveClaims)
|
|
|
|
{
|
|
|
|
if (!(await _authorizationService.AuthorizeAsync(User, null,
|
|
|
|
new PolicyRequirement(Policies.CanCreatePullPayments))).Succeeded)
|
|
|
|
{
|
|
|
|
return this.CreateAPIPermissionError(Policies.CanCreatePullPayments);
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
2020-06-24 10:34:09 +09:00
|
|
|
if (request.Amount <= 0.0m)
|
|
|
|
{
|
|
|
|
ModelState.AddModelError(nameof(request.Amount), "The amount should more than 0.");
|
|
|
|
}
|
|
|
|
if (request.Name is String name && name.Length > 50)
|
|
|
|
{
|
|
|
|
ModelState.AddModelError(nameof(request.Name), "The name should be maximum 50 characters.");
|
|
|
|
}
|
|
|
|
if (request.Currency is String currency)
|
|
|
|
{
|
2020-06-24 13:44:26 +09:00
|
|
|
request.Currency = currency.ToUpperInvariant().Trim();
|
|
|
|
if (_currencyNameTable.GetCurrencyData(request.Currency, false) is null)
|
2020-06-24 10:34:09 +09:00
|
|
|
{
|
2020-06-24 13:44:26 +09:00
|
|
|
ModelState.AddModelError(nameof(request.Currency), "Invalid currency");
|
2020-06-24 10:34:09 +09:00
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
ModelState.AddModelError(nameof(request.Currency), "This field is required");
|
|
|
|
}
|
|
|
|
if (request.ExpiresAt is DateTimeOffset expires && request.StartsAt is DateTimeOffset start && expires < start)
|
|
|
|
{
|
|
|
|
ModelState.AddModelError(nameof(request.ExpiresAt), $"expiresAt should be higher than startAt");
|
|
|
|
}
|
|
|
|
if (request.Period <= TimeSpan.Zero)
|
|
|
|
{
|
|
|
|
ModelState.AddModelError(nameof(request.Period), $"The period should be positive");
|
|
|
|
}
|
2023-04-06 08:54:19 +02:00
|
|
|
if (request.BOLT11Expiration < TimeSpan.Zero)
|
2022-01-24 20:17:09 +09:00
|
|
|
{
|
|
|
|
ModelState.AddModelError(nameof(request.BOLT11Expiration), $"The BOLT11 expiration should be positive");
|
|
|
|
}
|
2021-11-14 21:25:59 -08:00
|
|
|
PaymentMethodId?[]? paymentMethods = null;
|
2021-10-18 05:37:59 +02:00
|
|
|
if (request.PaymentMethods is { } paymentMethodsStr)
|
2020-06-24 10:34:09 +09:00
|
|
|
{
|
2021-10-18 05:37:59 +02:00
|
|
|
paymentMethods = paymentMethodsStr.Select(s =>
|
2020-06-24 17:51:00 +09:00
|
|
|
{
|
2021-10-18 05:37:59 +02:00
|
|
|
PaymentMethodId.TryParse(s, out var pmi);
|
|
|
|
return pmi;
|
|
|
|
}).ToArray();
|
2021-12-31 16:59:02 +09:00
|
|
|
var supported = (await _payoutHandlers.GetSupportedPaymentMethods(HttpContext.GetStoreData())).ToArray();
|
|
|
|
for (int i = 0; i < paymentMethods.Length; i++)
|
|
|
|
{
|
|
|
|
if (!supported.Contains(paymentMethods[i]))
|
|
|
|
{
|
|
|
|
request.AddModelError(paymentRequest => paymentRequest.PaymentMethods[i], "Invalid or unsupported payment method", this);
|
|
|
|
}
|
|
|
|
}
|
2020-06-24 10:34:09 +09:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
ModelState.AddModelError(nameof(request.PaymentMethods), "This field is required");
|
|
|
|
}
|
|
|
|
if (!ModelState.IsValid)
|
|
|
|
return this.CreateValidationError(ModelState);
|
2023-04-06 08:54:19 +02:00
|
|
|
var ppId = await _pullPaymentService.CreatePullPayment(new CreatePullPayment()
|
2020-06-24 10:34:09 +09:00
|
|
|
{
|
|
|
|
StartsAt = request.StartsAt,
|
|
|
|
ExpiresAt = request.ExpiresAt,
|
|
|
|
Period = request.Period,
|
2022-01-24 20:17:09 +09:00
|
|
|
BOLT11Expiration = request.BOLT11Expiration,
|
2020-06-24 10:34:09 +09:00
|
|
|
Name = request.Name,
|
2022-02-09 21:54:00 -08:00
|
|
|
Description = request.Description,
|
2020-06-24 10:34:09 +09:00
|
|
|
Amount = request.Amount,
|
2020-06-24 13:44:26 +09:00
|
|
|
Currency = request.Currency,
|
2020-06-24 10:34:09 +09:00
|
|
|
StoreId = storeId,
|
2022-04-28 02:51:04 +02:00
|
|
|
PaymentMethodIds = paymentMethods,
|
|
|
|
AutoApproveClaims = request.AutoApproveClaims
|
2020-06-24 10:34:09 +09:00
|
|
|
});
|
2021-06-10 18:54:27 +09:00
|
|
|
var pp = await _pullPaymentService.GetPullPayment(ppId, false);
|
2020-06-24 10:34:09 +09:00
|
|
|
return this.Ok(CreatePullPaymentData(pp));
|
|
|
|
}
|
|
|
|
|
|
|
|
private Client.Models.PullPaymentData CreatePullPaymentData(Data.PullPaymentData pp)
|
|
|
|
{
|
|
|
|
var ppBlob = pp.GetBlob();
|
|
|
|
return new BTCPayServer.Client.Models.PullPaymentData()
|
|
|
|
{
|
|
|
|
Id = pp.Id,
|
|
|
|
StartsAt = pp.StartDate,
|
|
|
|
ExpiresAt = pp.EndDate,
|
|
|
|
Amount = ppBlob.Limit,
|
|
|
|
Name = ppBlob.Name,
|
2022-02-09 21:54:00 -08:00
|
|
|
Description = ppBlob.Description,
|
2020-06-24 10:34:09 +09:00
|
|
|
Currency = ppBlob.Currency,
|
|
|
|
Period = ppBlob.Period,
|
|
|
|
Archived = pp.Archived,
|
2022-04-28 02:51:04 +02:00
|
|
|
AutoApproveClaims = ppBlob.AutoApproveClaims,
|
2022-01-24 20:17:09 +09:00
|
|
|
BOLT11Expiration = ppBlob.BOLT11Expiration,
|
2020-06-24 10:34:09 +09:00
|
|
|
ViewLink = _linkGenerator.GetUriByAction(
|
2022-01-07 12:32:00 +09:00
|
|
|
nameof(UIPullPaymentController.ViewPullPayment),
|
|
|
|
"UIPullPayment",
|
2020-06-24 10:34:09 +09:00
|
|
|
new { pullPaymentId = pp.Id },
|
|
|
|
Request.Scheme,
|
|
|
|
Request.Host,
|
|
|
|
Request.PathBase)
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-12-06 09:17:58 +09:00
|
|
|
[HttpPost]
|
|
|
|
[Route("~/api/v1/pull-payments/{pullPaymentId}/boltcards")]
|
|
|
|
[AllowAnonymous]
|
2024-03-14 19:07:11 +09:00
|
|
|
public async Task<IActionResult> RegisterBoltcard(string pullPaymentId, RegisterBoltcardRequest request, string? onExisting = null)
|
2023-12-06 09:17:58 +09:00
|
|
|
{
|
|
|
|
if (pullPaymentId is null)
|
|
|
|
return PullPaymentNotFound();
|
2024-03-14 19:07:11 +09:00
|
|
|
this._logs.PayServer.LogInformation($"RegisterBoltcard: onExisting queryParam: {onExisting}");
|
|
|
|
this._logs.PayServer.LogInformation($"{JsonConvert.SerializeObject(request)}");
|
2023-12-06 09:17:58 +09:00
|
|
|
var pp = await _pullPaymentService.GetPullPayment(pullPaymentId, false);
|
|
|
|
if (pp is null)
|
|
|
|
return PullPaymentNotFound();
|
2024-03-14 19:07:11 +09:00
|
|
|
var issuerKey = await _settingsRepository.GetIssuerKey(_env);
|
|
|
|
|
|
|
|
// LNURLW is used by deeplinks
|
|
|
|
if (request?.LNURLW is not null)
|
|
|
|
{
|
|
|
|
if (request.UID is not null)
|
|
|
|
{
|
|
|
|
ModelState.AddModelError(nameof(request.LNURLW), "You should pass either LNURLW or UID but not both");
|
|
|
|
return this.CreateValidationError(ModelState);
|
|
|
|
}
|
|
|
|
var p = ExtractP(request.LNURLW);
|
|
|
|
if (p is null)
|
|
|
|
{
|
|
|
|
ModelState.AddModelError(nameof(request.LNURLW), "The LNURLW should contains a 'p=' parameter");
|
|
|
|
return this.CreateValidationError(ModelState);
|
|
|
|
}
|
|
|
|
if (issuerKey.TryDecrypt(p) is not BoltcardPICCData picc)
|
|
|
|
{
|
|
|
|
ModelState.AddModelError(nameof(request.LNURLW), "The LNURLW 'p=' parameter cannot be decrypted");
|
|
|
|
return this.CreateValidationError(ModelState);
|
|
|
|
}
|
|
|
|
request.UID = picc.Uid;
|
|
|
|
}
|
|
|
|
|
2023-12-06 09:17:58 +09:00
|
|
|
if (request?.UID is null || request.UID.Length != 7)
|
|
|
|
{
|
|
|
|
ModelState.AddModelError(nameof(request.UID), "The UID is required and should be 7 bytes");
|
|
|
|
return this.CreateValidationError(ModelState);
|
|
|
|
}
|
|
|
|
if (!_pullPaymentService.SupportsLNURL(pp.GetBlob()))
|
|
|
|
{
|
|
|
|
return this.CreateAPIError(400, "lnurl-not-supported", "This pull payment currency should be BTC or SATS and accept lightning");
|
|
|
|
}
|
|
|
|
|
2024-03-14 19:07:11 +09:00
|
|
|
// Passing onExisting as a query parameter is used by deeplink
|
|
|
|
request.OnExisting = onExisting switch
|
|
|
|
{
|
|
|
|
nameof(OnExistingBehavior.UpdateVersion) => OnExistingBehavior.UpdateVersion,
|
|
|
|
nameof(OnExistingBehavior.KeepVersion) => OnExistingBehavior.KeepVersion,
|
|
|
|
_ => request.OnExisting
|
|
|
|
};
|
|
|
|
|
|
|
|
this._logs.PayServer.LogInformation($"After");
|
|
|
|
this._logs.PayServer.LogInformation($"{JsonConvert.SerializeObject(request)}");
|
|
|
|
|
2023-12-06 09:17:58 +09:00
|
|
|
var version = await _dbContextFactory.LinkBoltcardToPullPayment(pullPaymentId, issuerKey, request.UID, request.OnExisting);
|
2024-03-14 19:07:11 +09:00
|
|
|
this._logs.PayServer.LogInformation($"Version: " + version);
|
|
|
|
this._logs.PayServer.LogInformation($"ID: " + Encoders.Hex.EncodeData(issuerKey.GetId(request.UID)));
|
|
|
|
|
2023-12-21 10:29:28 +09:00
|
|
|
var keys = issuerKey.CreatePullPaymentCardKey(request.UID, version, pullPaymentId).DeriveBoltcardKeys(issuerKey);
|
2023-12-06 09:17:58 +09:00
|
|
|
|
|
|
|
var boltcardUrl = Url.Action(nameof(UIBoltcardController.GetWithdrawRequest), "UIBoltcard");
|
|
|
|
boltcardUrl = Request.GetAbsoluteUri(boltcardUrl);
|
|
|
|
boltcardUrl = Regex.Replace(boltcardUrl, "^https?://", "lnurlw://");
|
|
|
|
|
2024-03-14 19:07:11 +09:00
|
|
|
var resp = new RegisterBoltcardResponse()
|
2023-12-06 09:17:58 +09:00
|
|
|
{
|
|
|
|
LNURLW = boltcardUrl,
|
|
|
|
Version = version,
|
|
|
|
K0 = Encoders.Hex.EncodeData(keys.AppMasterKey.ToBytes()).ToUpperInvariant(),
|
|
|
|
K1 = Encoders.Hex.EncodeData(keys.EncryptionKey.ToBytes()).ToUpperInvariant(),
|
|
|
|
K2 = Encoders.Hex.EncodeData(keys.AuthenticationKey.ToBytes()).ToUpperInvariant(),
|
|
|
|
K3 = Encoders.Hex.EncodeData(keys.K3.ToBytes()).ToUpperInvariant(),
|
|
|
|
K4 = Encoders.Hex.EncodeData(keys.K4.ToBytes()).ToUpperInvariant(),
|
2024-03-14 19:07:11 +09:00
|
|
|
};
|
|
|
|
this._logs.PayServer.LogInformation($"Response");
|
|
|
|
this._logs.PayServer.LogInformation($"{JsonConvert.SerializeObject(resp)}");
|
|
|
|
|
|
|
|
return Ok(resp);
|
|
|
|
}
|
|
|
|
|
|
|
|
private string? ExtractP(string? url)
|
|
|
|
{
|
|
|
|
if (url is null || !Uri.TryCreate(url, UriKind.Absolute, out var uri))
|
|
|
|
return null;
|
|
|
|
int num = uri.AbsoluteUri.IndexOf('?');
|
|
|
|
if (num == -1)
|
|
|
|
return null;
|
|
|
|
string input = uri.AbsoluteUri.Substring(num);
|
|
|
|
Match match = Regex.Match(input, "p=([a-f0-9A-F]{32})");
|
|
|
|
if (!match.Success)
|
|
|
|
return null;
|
|
|
|
return match.Groups[1].Value;
|
2023-12-06 09:17:58 +09:00
|
|
|
}
|
|
|
|
|
2020-06-24 10:34:09 +09:00
|
|
|
[HttpGet("~/api/v1/pull-payments/{pullPaymentId}")]
|
|
|
|
[AllowAnonymous]
|
|
|
|
public async Task<IActionResult> GetPullPayment(string pullPaymentId)
|
|
|
|
{
|
|
|
|
if (pullPaymentId is null)
|
2021-06-10 11:43:45 +02:00
|
|
|
return PullPaymentNotFound();
|
2021-06-10 18:54:27 +09:00
|
|
|
var pp = await _pullPaymentService.GetPullPayment(pullPaymentId, false);
|
2020-06-24 10:34:09 +09:00
|
|
|
if (pp is null)
|
2021-06-10 11:43:45 +02:00
|
|
|
return PullPaymentNotFound();
|
2020-06-24 10:34:09 +09:00
|
|
|
return Ok(CreatePullPaymentData(pp));
|
|
|
|
}
|
|
|
|
|
2022-08-17 09:45:51 +02:00
|
|
|
private PayoutState[]? GetStateFilter(bool includeCancelled) =>
|
|
|
|
includeCancelled
|
|
|
|
? null
|
|
|
|
: new[]
|
|
|
|
{
|
|
|
|
PayoutState.Completed, PayoutState.AwaitingApproval, PayoutState.AwaitingPayment,
|
|
|
|
PayoutState.InProgress
|
|
|
|
};
|
|
|
|
|
2020-06-24 10:34:09 +09:00
|
|
|
[HttpGet("~/api/v1/pull-payments/{pullPaymentId}/payouts")]
|
|
|
|
[AllowAnonymous]
|
|
|
|
public async Task<IActionResult> GetPayouts(string pullPaymentId, bool includeCancelled = false)
|
|
|
|
{
|
|
|
|
if (pullPaymentId is null)
|
2021-06-10 11:43:45 +02:00
|
|
|
return PullPaymentNotFound();
|
2021-06-10 18:54:27 +09:00
|
|
|
var pp = await _pullPaymentService.GetPullPayment(pullPaymentId, true);
|
2020-06-24 10:34:09 +09:00
|
|
|
if (pp is null)
|
2021-06-10 11:43:45 +02:00
|
|
|
return PullPaymentNotFound();
|
2023-01-06 14:18:07 +01:00
|
|
|
|
|
|
|
var payouts = await _pullPaymentService.GetPayouts(new PullPaymentHostedService.PayoutQuery()
|
2022-08-17 09:45:51 +02:00
|
|
|
{
|
2023-01-06 14:18:07 +01:00
|
|
|
PullPayments = new[] { pullPaymentId },
|
2022-08-17 09:45:51 +02:00
|
|
|
States = GetStateFilter(includeCancelled)
|
|
|
|
});
|
2020-06-24 10:34:09 +09:00
|
|
|
return base.Ok(payouts
|
2022-04-24 05:19:34 +02:00
|
|
|
.Select(ToModel).ToList());
|
2020-06-24 10:34:09 +09:00
|
|
|
}
|
|
|
|
|
2021-06-10 11:43:45 +02:00
|
|
|
[HttpGet("~/api/v1/pull-payments/{pullPaymentId}/payouts/{payoutId}")]
|
|
|
|
[AllowAnonymous]
|
|
|
|
public async Task<IActionResult> GetPayout(string pullPaymentId, string payoutId)
|
|
|
|
{
|
|
|
|
if (payoutId is null)
|
|
|
|
return PayoutNotFound();
|
|
|
|
await using var ctx = _dbContextFactory.CreateContext();
|
2022-08-17 09:45:51 +02:00
|
|
|
|
|
|
|
var payout = (await _pullPaymentService.GetPayouts(new PullPaymentHostedService.PayoutQuery()
|
|
|
|
{
|
2023-01-06 14:18:07 +01:00
|
|
|
PullPayments = new[] { pullPaymentId },
|
|
|
|
PayoutIds = new[] { payoutId }
|
2022-08-17 09:45:51 +02:00
|
|
|
})).FirstOrDefault();
|
2023-01-06 14:18:07 +01:00
|
|
|
|
|
|
|
|
2021-12-31 16:59:02 +09:00
|
|
|
if (payout is null)
|
2021-06-10 11:43:45 +02:00
|
|
|
return PayoutNotFound();
|
2022-04-24 05:19:34 +02:00
|
|
|
return base.Ok(ToModel(payout));
|
2021-06-10 11:43:45 +02:00
|
|
|
}
|
|
|
|
|
2023-01-25 21:43:07 -08:00
|
|
|
[HttpGet("~/api/v1/pull-payments/{pullPaymentId}/lnurl")]
|
|
|
|
[AllowAnonymous]
|
|
|
|
public async Task<IActionResult> GetPullPaymentLNURL(string pullPaymentId)
|
|
|
|
{
|
|
|
|
var pp = await _pullPaymentService.GetPullPayment(pullPaymentId, false);
|
|
|
|
if (pp is null)
|
|
|
|
return PullPaymentNotFound();
|
|
|
|
|
|
|
|
var blob = pp.GetBlob();
|
2023-06-16 03:56:17 +02:00
|
|
|
if (_pullPaymentService.SupportsLNURL(blob))
|
2023-01-25 21:43:07 -08:00
|
|
|
{
|
|
|
|
var lnurlEndpoint = new Uri(Url.Action("GetLNURLForPullPayment", "UILNURL", new
|
|
|
|
{
|
|
|
|
cryptoCode = _networkProvider.DefaultNetwork.CryptoCode,
|
2023-06-16 03:56:17 +02:00
|
|
|
pullPaymentId
|
2023-02-08 21:29:20 +09:00
|
|
|
}, Request.Scheme, Request.Host.ToString())!);
|
2023-01-25 21:43:07 -08:00
|
|
|
|
2023-06-16 03:56:17 +02:00
|
|
|
return base.Ok(new PullPaymentLNURL
|
2023-04-10 11:07:03 +09:00
|
|
|
{
|
2023-01-25 21:43:07 -08:00
|
|
|
LNURLBech32 = LNURL.LNURL.EncodeUri(lnurlEndpoint, "withdrawRequest", true).ToString(),
|
|
|
|
LNURLUri = LNURL.LNURL.EncodeUri(lnurlEndpoint, "withdrawRequest", false).ToString()
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.CreateAPIError("lnurl-not-supported", "LNURL not supported for this pull payment");
|
|
|
|
}
|
|
|
|
|
2022-04-24 05:19:34 +02:00
|
|
|
private Client.Models.PayoutData ToModel(Data.PayoutData p)
|
2020-06-24 10:34:09 +09:00
|
|
|
{
|
|
|
|
var blob = p.GetBlob(_serializerSettings);
|
|
|
|
var model = new Client.Models.PayoutData()
|
|
|
|
{
|
|
|
|
Id = p.Id,
|
|
|
|
PullPaymentId = p.PullPaymentDataId,
|
|
|
|
Date = p.Date,
|
|
|
|
Amount = blob.Amount,
|
|
|
|
PaymentMethodAmount = blob.CryptoAmount,
|
2020-06-24 13:44:26 +09:00
|
|
|
Revision = blob.Revision,
|
2023-07-24 11:37:18 +02:00
|
|
|
State = p.State,
|
|
|
|
Metadata = blob.Metadata?? new JObject(),
|
2020-06-24 10:34:09 +09:00
|
|
|
};
|
2021-04-13 10:36:49 +02:00
|
|
|
model.Destination = blob.Destination;
|
2020-06-24 10:34:09 +09:00
|
|
|
model.PaymentMethod = p.PaymentMethodId;
|
2021-11-14 21:25:59 -08:00
|
|
|
model.CryptoCode = p.GetPaymentMethodId().CryptoCode;
|
2022-11-15 10:40:57 +01:00
|
|
|
model.PaymentProof = p.GetProofBlobJson();
|
2020-06-24 10:34:09 +09:00
|
|
|
return model;
|
|
|
|
}
|
|
|
|
|
|
|
|
[HttpPost("~/api/v1/pull-payments/{pullPaymentId}/payouts")]
|
|
|
|
[AllowAnonymous]
|
2022-11-22 20:17:29 +09:00
|
|
|
public async Task<IActionResult> CreatePayout(string pullPaymentId, CreatePayoutRequest request, CancellationToken cancellationToken)
|
2020-06-24 10:34:09 +09:00
|
|
|
{
|
2021-04-13 10:36:49 +02:00
|
|
|
if (!PaymentMethodId.TryParse(request?.PaymentMethod, out var paymentMethodId))
|
|
|
|
{
|
|
|
|
ModelState.AddModelError(nameof(request.PaymentMethod), "Invalid payment method");
|
|
|
|
return this.CreateValidationError(ModelState);
|
|
|
|
}
|
2021-12-31 16:59:02 +09:00
|
|
|
|
2021-10-18 15:00:38 +09:00
|
|
|
var payoutHandler = _payoutHandlers.FindPayoutHandler(paymentMethodId);
|
2021-04-13 10:36:49 +02:00
|
|
|
if (payoutHandler is null)
|
2020-06-24 10:34:09 +09:00
|
|
|
{
|
|
|
|
ModelState.AddModelError(nameof(request.PaymentMethod), "Invalid payment method");
|
|
|
|
return this.CreateValidationError(ModelState);
|
|
|
|
}
|
|
|
|
|
2021-06-10 11:43:45 +02:00
|
|
|
await using var ctx = _dbContextFactory.CreateContext();
|
2020-06-24 10:34:09 +09:00
|
|
|
var pp = await ctx.PullPayments.FindAsync(pullPaymentId);
|
|
|
|
if (pp is null)
|
2021-06-10 11:43:45 +02:00
|
|
|
return PullPaymentNotFound();
|
2020-06-24 10:34:09 +09:00
|
|
|
var ppBlob = pp.GetBlob();
|
2022-11-22 20:17:29 +09:00
|
|
|
var destination = await payoutHandler.ParseAndValidateClaimDestination(paymentMethodId, request!.Destination, ppBlob, cancellationToken);
|
2021-10-18 05:37:59 +02:00
|
|
|
if (destination.destination is null)
|
2020-06-24 10:34:09 +09:00
|
|
|
{
|
2021-12-31 16:59:02 +09:00
|
|
|
ModelState.AddModelError(nameof(request.Destination), destination.error ?? "The destination is invalid for the payment specified");
|
2020-06-24 10:34:09 +09:00
|
|
|
return this.CreateValidationError(ModelState);
|
|
|
|
}
|
2023-07-24 13:40:26 +02:00
|
|
|
|
|
|
|
var amtError = ClaimRequest.IsPayoutAmountOk(destination.destination, request.Amount, paymentMethodId.CryptoCode, ppBlob.Currency);
|
|
|
|
if (amtError.error is not null)
|
2020-06-24 10:34:09 +09:00
|
|
|
{
|
2023-07-24 13:40:26 +02:00
|
|
|
ModelState.AddModelError(nameof(request.Amount), amtError.error );
|
2020-06-24 10:34:09 +09:00
|
|
|
return this.CreateValidationError(ModelState);
|
|
|
|
}
|
2023-07-24 13:40:26 +02:00
|
|
|
request.Amount = amtError.amount;
|
2023-01-06 14:18:07 +01:00
|
|
|
var result = await _pullPaymentService.Claim(new ClaimRequest()
|
2020-06-24 10:34:09 +09:00
|
|
|
{
|
2021-10-18 05:37:59 +02:00
|
|
|
Destination = destination.destination,
|
2020-06-24 10:34:09 +09:00
|
|
|
PullPaymentId = pullPaymentId,
|
|
|
|
Value = request.Amount,
|
2024-02-20 18:42:38 +09:00
|
|
|
PaymentMethodId = paymentMethodId,
|
|
|
|
StoreId = pp.StoreId
|
2020-06-24 10:34:09 +09:00
|
|
|
});
|
2023-01-06 14:18:07 +01:00
|
|
|
|
|
|
|
return HandleClaimResult(result);
|
2022-04-24 05:19:34 +02:00
|
|
|
}
|
2023-01-06 14:18:07 +01:00
|
|
|
|
2022-04-24 05:19:34 +02:00
|
|
|
[HttpPost("~/api/v1/stores/{storeId}/payouts")]
|
2023-01-26 01:46:05 +01:00
|
|
|
[Authorize(Policy = Policies.CanCreateNonApprovedPullPayments, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
2022-04-24 05:19:34 +02:00
|
|
|
public async Task<IActionResult> CreatePayoutThroughStore(string storeId, CreatePayoutThroughStoreRequest request)
|
|
|
|
{
|
2023-05-19 08:41:21 +09:00
|
|
|
if (request?.Approved is true)
|
2023-01-26 01:46:05 +01:00
|
|
|
{
|
|
|
|
if (!(await _authorizationService.AuthorizeAsync(User, null,
|
|
|
|
new PolicyRequirement(Policies.CanCreatePullPayments))).Succeeded)
|
|
|
|
{
|
|
|
|
return this.CreateAPIPermissionError(Policies.CanCreatePullPayments);
|
|
|
|
}
|
|
|
|
}
|
2023-04-10 11:07:03 +09:00
|
|
|
|
2022-04-24 05:19:34 +02:00
|
|
|
if (request is null || !PaymentMethodId.TryParse(request?.PaymentMethod, out var paymentMethodId))
|
|
|
|
{
|
|
|
|
ModelState.AddModelError(nameof(request.PaymentMethod), "Invalid payment method");
|
|
|
|
return this.CreateValidationError(ModelState);
|
|
|
|
}
|
|
|
|
|
|
|
|
var payoutHandler = _payoutHandlers.FindPayoutHandler(paymentMethodId);
|
|
|
|
if (payoutHandler is null)
|
|
|
|
{
|
|
|
|
ModelState.AddModelError(nameof(request.PaymentMethod), "Invalid payment method");
|
|
|
|
return this.CreateValidationError(ModelState);
|
|
|
|
}
|
|
|
|
|
|
|
|
await using var ctx = _dbContextFactory.CreateContext();
|
|
|
|
|
|
|
|
|
|
|
|
PullPaymentBlob? ppBlob = null;
|
|
|
|
if (request?.PullPaymentId is not null)
|
|
|
|
{
|
|
|
|
|
|
|
|
var pp = await ctx.PullPayments.FirstOrDefaultAsync(data =>
|
|
|
|
data.Id == request.PullPaymentId && data.StoreId == storeId);
|
|
|
|
if (pp is null)
|
|
|
|
return PullPaymentNotFound();
|
|
|
|
ppBlob = pp.GetBlob();
|
|
|
|
}
|
2022-11-22 20:17:29 +09:00
|
|
|
var destination = await payoutHandler.ParseAndValidateClaimDestination(paymentMethodId, request!.Destination, ppBlob, default);
|
2022-04-24 05:19:34 +02:00
|
|
|
if (destination.destination is null)
|
|
|
|
{
|
|
|
|
ModelState.AddModelError(nameof(request.Destination), destination.error ?? "The destination is invalid for the payment specified");
|
|
|
|
return this.CreateValidationError(ModelState);
|
|
|
|
}
|
|
|
|
|
2023-07-24 13:40:26 +02:00
|
|
|
var amtError = ClaimRequest.IsPayoutAmountOk(destination.destination, request.Amount);
|
|
|
|
if (amtError.error is not null)
|
2022-04-24 05:19:34 +02:00
|
|
|
{
|
2023-07-24 13:40:26 +02:00
|
|
|
ModelState.AddModelError(nameof(request.Amount), amtError.error );
|
2022-04-24 05:19:34 +02:00
|
|
|
return this.CreateValidationError(ModelState);
|
|
|
|
}
|
2023-07-24 13:40:26 +02:00
|
|
|
request.Amount = amtError.amount;
|
2022-04-24 05:19:34 +02:00
|
|
|
if (request.Amount is { } v && (v < ppBlob?.MinimumClaim || v == 0.0m))
|
|
|
|
{
|
|
|
|
var minimumClaim = ppBlob?.MinimumClaim is decimal val ? val : 0.0m;
|
|
|
|
ModelState.AddModelError(nameof(request.Amount), $"Amount too small (should be at least {minimumClaim})");
|
|
|
|
return this.CreateValidationError(ModelState);
|
|
|
|
}
|
|
|
|
var result = await _pullPaymentService.Claim(new ClaimRequest()
|
|
|
|
{
|
|
|
|
Destination = destination.destination,
|
|
|
|
PullPaymentId = request.PullPaymentId,
|
|
|
|
PreApprove = request.Approved,
|
|
|
|
Value = request.Amount,
|
|
|
|
PaymentMethodId = paymentMethodId,
|
2023-07-24 11:37:18 +02:00
|
|
|
StoreId = storeId,
|
|
|
|
Metadata = request.Metadata
|
2022-04-24 05:19:34 +02:00
|
|
|
});
|
|
|
|
return HandleClaimResult(result);
|
|
|
|
}
|
|
|
|
|
|
|
|
private IActionResult HandleClaimResult(ClaimRequest.ClaimResponse result)
|
|
|
|
{
|
2020-06-24 10:34:09 +09:00
|
|
|
switch (result.Result)
|
|
|
|
{
|
|
|
|
case ClaimRequest.ClaimResult.Ok:
|
|
|
|
break;
|
|
|
|
case ClaimRequest.ClaimResult.Duplicate:
|
|
|
|
return this.CreateAPIError("duplicate-destination", ClaimRequest.GetErrorMessage(result.Result));
|
|
|
|
case ClaimRequest.ClaimResult.Expired:
|
|
|
|
return this.CreateAPIError("expired", ClaimRequest.GetErrorMessage(result.Result));
|
|
|
|
case ClaimRequest.ClaimResult.NotStarted:
|
|
|
|
return this.CreateAPIError("not-started", ClaimRequest.GetErrorMessage(result.Result));
|
|
|
|
case ClaimRequest.ClaimResult.Archived:
|
|
|
|
return this.CreateAPIError("archived", ClaimRequest.GetErrorMessage(result.Result));
|
|
|
|
case ClaimRequest.ClaimResult.Overdraft:
|
|
|
|
return this.CreateAPIError("overdraft", ClaimRequest.GetErrorMessage(result.Result));
|
|
|
|
case ClaimRequest.ClaimResult.AmountTooLow:
|
|
|
|
return this.CreateAPIError("amount-too-low", ClaimRequest.GetErrorMessage(result.Result));
|
|
|
|
case ClaimRequest.ClaimResult.PaymentMethodNotSupported:
|
|
|
|
return this.CreateAPIError("payment-method-not-supported", ClaimRequest.GetErrorMessage(result.Result));
|
|
|
|
default:
|
|
|
|
throw new NotSupportedException("Unsupported ClaimResult");
|
|
|
|
}
|
2022-04-24 05:19:34 +02:00
|
|
|
|
|
|
|
return Ok(ToModel(result.PayoutData));
|
2020-06-24 10:34:09 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
[HttpDelete("~/api/v1/stores/{storeId}/pull-payments/{pullPaymentId}")]
|
2023-09-22 10:24:53 +02:00
|
|
|
[Authorize(Policy = Policies.CanArchivePullPayments, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
2020-06-24 10:34:09 +09:00
|
|
|
public async Task<IActionResult> ArchivePullPayment(string storeId, string pullPaymentId)
|
|
|
|
{
|
|
|
|
using var ctx = _dbContextFactory.CreateContext();
|
|
|
|
var pp = await ctx.PullPayments.FindAsync(pullPaymentId);
|
|
|
|
if (pp is null || pp.StoreId != storeId)
|
2021-06-10 11:43:45 +02:00
|
|
|
return PullPaymentNotFound();
|
2020-06-24 10:34:09 +09:00
|
|
|
await _pullPaymentService.Cancel(new PullPaymentHostedService.CancelRequest(pullPaymentId));
|
|
|
|
return Ok();
|
|
|
|
}
|
|
|
|
|
2022-04-24 05:19:34 +02:00
|
|
|
|
|
|
|
|
|
|
|
[HttpGet("~/api/v1/stores/{storeId}/payouts")]
|
2023-12-01 09:12:02 +01:00
|
|
|
[Authorize(Policy = Policies.CanViewPayouts, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
2022-04-24 05:19:34 +02:00
|
|
|
public async Task<IActionResult> GetStorePayouts(string storeId, bool includeCancelled = false)
|
|
|
|
{
|
2022-08-17 09:45:51 +02:00
|
|
|
var payouts = await _pullPaymentService.GetPayouts(new PullPaymentHostedService.PayoutQuery()
|
|
|
|
{
|
2023-01-06 14:18:07 +01:00
|
|
|
Stores = new[] { storeId },
|
2022-08-17 09:45:51 +02:00
|
|
|
States = GetStateFilter(includeCancelled)
|
|
|
|
});
|
2023-01-06 14:18:07 +01:00
|
|
|
|
|
|
|
|
2022-04-24 05:19:34 +02:00
|
|
|
return base.Ok(payouts
|
2022-11-15 10:40:57 +01:00
|
|
|
.Select(ToModel).ToArray());
|
2022-04-24 05:19:34 +02:00
|
|
|
}
|
|
|
|
|
2020-06-24 10:34:09 +09:00
|
|
|
[HttpDelete("~/api/v1/stores/{storeId}/payouts/{payoutId}")]
|
2023-12-01 09:12:02 +01:00
|
|
|
[Authorize(Policy = Policies.CanManagePayouts, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
2020-06-24 10:34:09 +09:00
|
|
|
public async Task<IActionResult> CancelPayout(string storeId, string payoutId)
|
|
|
|
{
|
2023-01-06 14:18:07 +01:00
|
|
|
var res = await _pullPaymentService.Cancel(new PullPaymentHostedService.CancelRequest(new[] { payoutId }, new[] { storeId }));
|
2022-11-15 10:40:57 +01:00
|
|
|
return MapResult(res.First().Value);
|
2020-06-24 10:34:09 +09:00
|
|
|
}
|
2020-06-24 13:44:26 +09:00
|
|
|
|
|
|
|
[HttpPost("~/api/v1/stores/{storeId}/payouts/{payoutId}")]
|
2023-12-01 09:12:02 +01:00
|
|
|
[Authorize(Policy = Policies.CanManagePayouts, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
2020-06-24 13:44:26 +09:00
|
|
|
public async Task<IActionResult> ApprovePayout(string storeId, string payoutId, ApprovePayoutRequest approvePayoutRequest, CancellationToken cancellationToken = default)
|
|
|
|
{
|
|
|
|
using var ctx = _dbContextFactory.CreateContext();
|
|
|
|
ctx.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
|
|
|
var revision = approvePayoutRequest?.Revision;
|
|
|
|
if (revision is null)
|
|
|
|
{
|
|
|
|
ModelState.AddModelError(nameof(approvePayoutRequest.Revision), "The `revision` property is required");
|
|
|
|
}
|
|
|
|
if (!ModelState.IsValid)
|
|
|
|
return this.CreateValidationError(ModelState);
|
|
|
|
var payout = await ctx.Payouts.GetPayout(payoutId, storeId, true, true);
|
|
|
|
if (payout is null)
|
2021-06-10 11:43:45 +02:00
|
|
|
return PayoutNotFound();
|
2021-11-14 21:25:59 -08:00
|
|
|
RateResult? rateResult = null;
|
2020-06-24 13:44:26 +09:00
|
|
|
try
|
|
|
|
{
|
|
|
|
rateResult = await _pullPaymentService.GetRate(payout, approvePayoutRequest?.RateRule, cancellationToken);
|
|
|
|
if (rateResult.BidAsk == null)
|
|
|
|
{
|
|
|
|
return this.CreateAPIError("rate-unavailable", $"Rate unavailable: {rateResult.EvaluatedRule}");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
catch (FormatException)
|
|
|
|
{
|
|
|
|
ModelState.AddModelError(nameof(approvePayoutRequest.RateRule), "Invalid RateRule");
|
|
|
|
return this.CreateValidationError(ModelState);
|
|
|
|
}
|
2022-12-04 13:23:59 +01:00
|
|
|
var result = (await _pullPaymentService.Approve(new PullPaymentHostedService.PayoutApproval()
|
2020-06-24 13:44:26 +09:00
|
|
|
{
|
|
|
|
PayoutId = payoutId,
|
2021-11-14 21:25:59 -08:00
|
|
|
Revision = revision!.Value,
|
2020-06-24 13:44:26 +09:00
|
|
|
Rate = rateResult.BidAsk.Ask
|
2022-12-04 13:23:59 +01:00
|
|
|
})).Result;
|
2020-06-24 13:44:26 +09:00
|
|
|
var errorMessage = PullPaymentHostedService.PayoutApproval.GetErrorMessage(result);
|
|
|
|
switch (result)
|
|
|
|
{
|
|
|
|
case PullPaymentHostedService.PayoutApproval.Result.Ok:
|
2022-04-24 05:19:34 +02:00
|
|
|
return Ok(ToModel(await ctx.Payouts.GetPayout(payoutId, storeId, true)));
|
2020-06-24 13:44:26 +09:00
|
|
|
case PullPaymentHostedService.PayoutApproval.Result.InvalidState:
|
|
|
|
return this.CreateAPIError("invalid-state", errorMessage);
|
|
|
|
case PullPaymentHostedService.PayoutApproval.Result.TooLowAmount:
|
|
|
|
return this.CreateAPIError("amount-too-low", errorMessage);
|
|
|
|
case PullPaymentHostedService.PayoutApproval.Result.OldRevision:
|
|
|
|
return this.CreateAPIError("old-revision", errorMessage);
|
|
|
|
case PullPaymentHostedService.PayoutApproval.Result.NotFound:
|
2021-06-10 11:43:45 +02:00
|
|
|
return PayoutNotFound();
|
2020-06-24 13:44:26 +09:00
|
|
|
default:
|
|
|
|
throw new NotSupportedException();
|
|
|
|
}
|
|
|
|
}
|
2021-06-10 11:43:45 +02:00
|
|
|
|
|
|
|
[HttpPost("~/api/v1/stores/{storeId}/payouts/{payoutId}/mark-paid")]
|
2023-12-01 09:12:02 +01:00
|
|
|
[Authorize(Policy = Policies.CanManagePayouts, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
2021-06-10 11:43:45 +02:00
|
|
|
public async Task<IActionResult> MarkPayoutPaid(string storeId, string payoutId, CancellationToken cancellationToken = default)
|
|
|
|
{
|
2022-11-15 10:40:57 +01:00
|
|
|
return await MarkPayout(storeId, payoutId, new Client.Models.MarkPayoutRequest()
|
|
|
|
{
|
|
|
|
State = PayoutState.Completed,
|
|
|
|
PaymentProof = null
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
[HttpPost("~/api/v1/stores/{storeId}/payouts/{payoutId}/mark")]
|
2023-12-01 09:12:02 +01:00
|
|
|
[Authorize(Policy = Policies.CanManagePayouts, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
2022-11-15 10:40:57 +01:00
|
|
|
public async Task<IActionResult> MarkPayout(string storeId, string payoutId, Client.Models.MarkPayoutRequest request)
|
|
|
|
{
|
|
|
|
request ??= new();
|
|
|
|
|
|
|
|
if (request.State == PayoutState.Cancelled)
|
|
|
|
{
|
|
|
|
return await CancelPayout(storeId, payoutId);
|
|
|
|
}
|
|
|
|
if (request.PaymentProof is not null &&
|
|
|
|
!BitcoinLikePayoutHandler.TryParseProofType(request.PaymentProof, out string _))
|
|
|
|
{
|
|
|
|
ModelState.AddModelError(nameof(request.PaymentProof), "Payment proof must have a 'proofType' property");
|
|
|
|
}
|
2021-06-10 11:43:45 +02:00
|
|
|
if (!ModelState.IsValid)
|
|
|
|
return this.CreateValidationError(ModelState);
|
|
|
|
|
2022-11-15 10:40:57 +01:00
|
|
|
var result = await _pullPaymentService.MarkPaid(new MarkPayoutRequest()
|
2021-06-10 11:43:45 +02:00
|
|
|
{
|
2022-11-15 10:40:57 +01:00
|
|
|
Proof = request.PaymentProof,
|
|
|
|
PayoutId = payoutId,
|
|
|
|
State = request.State
|
2021-06-10 11:43:45 +02:00
|
|
|
});
|
2022-11-15 10:40:57 +01:00
|
|
|
return MapResult(result);
|
2021-06-10 11:43:45 +02:00
|
|
|
}
|
2021-12-31 16:59:02 +09:00
|
|
|
|
2022-11-15 10:40:57 +01:00
|
|
|
[HttpGet("~/api/v1/stores/{storeId}/payouts/{payoutId}")]
|
2023-12-01 09:12:02 +01:00
|
|
|
[Authorize(Policy = Policies.CanViewPayouts, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
2022-11-15 10:40:57 +01:00
|
|
|
public async Task<IActionResult> GetStorePayout(string storeId, string payoutId)
|
|
|
|
{
|
|
|
|
await using var ctx = _dbContextFactory.CreateContext();
|
|
|
|
|
|
|
|
var payout = (await _pullPaymentService.GetPayouts(new PullPaymentHostedService.PayoutQuery()
|
|
|
|
{
|
2023-01-06 14:18:07 +01:00
|
|
|
Stores = new[] { storeId },
|
|
|
|
PayoutIds = new[] { payoutId }
|
2022-11-15 10:40:57 +01:00
|
|
|
})).FirstOrDefault();
|
|
|
|
|
|
|
|
if (payout is null)
|
|
|
|
return PayoutNotFound();
|
|
|
|
return base.Ok(ToModel(payout));
|
|
|
|
}
|
2023-01-06 14:18:07 +01:00
|
|
|
|
2022-11-15 10:40:57 +01:00
|
|
|
private IActionResult MapResult(MarkPayoutRequest.PayoutPaidResult result)
|
|
|
|
{
|
|
|
|
var errorMessage = MarkPayoutRequest.GetErrorMessage(result);
|
|
|
|
return result switch
|
|
|
|
{
|
|
|
|
MarkPayoutRequest.PayoutPaidResult.Ok => Ok(),
|
|
|
|
MarkPayoutRequest.PayoutPaidResult.InvalidState => this.CreateAPIError("invalid-state", errorMessage),
|
|
|
|
MarkPayoutRequest.PayoutPaidResult.NotFound => PayoutNotFound(),
|
|
|
|
_ => throw new NotSupportedException()
|
|
|
|
};
|
|
|
|
}
|
2021-06-10 11:43:45 +02:00
|
|
|
private IActionResult PayoutNotFound()
|
|
|
|
{
|
|
|
|
return this.CreateAPIError(404, "payout-not-found", "The payout was not found");
|
|
|
|
}
|
|
|
|
private IActionResult PullPaymentNotFound()
|
|
|
|
{
|
|
|
|
return this.CreateAPIError(404, "pullpayment-not-found", "The pull payment was not found");
|
|
|
|
}
|
2020-06-24 10:34:09 +09:00
|
|
|
}
|
|
|
|
}
|