diff --git a/BTCPayServer.Client/BTCPayServerClient.Invoices.cs b/BTCPayServer.Client/BTCPayServerClient.Invoices.cs index 7710ea118..8a34fffec 100644 --- a/BTCPayServer.Client/BTCPayServerClient.Invoices.cs +++ b/BTCPayServer.Client/BTCPayServerClient.Invoices.cs @@ -26,6 +26,13 @@ namespace BTCPayServer.Client CreateHttpRequest($"api/v1/stores/{storeId}/invoices/{invoiceId}"), token); return await HandleResponse(response); } + public virtual async Task GetInvoicePaymentMethods(string storeId, string invoiceId, + CancellationToken token = default) + { + var response = await _httpClient.SendAsync( + CreateHttpRequest($"api/v1/stores/{storeId}/invoices/{invoiceId}/payment-methods"), token); + return await HandleResponse(response); + } public virtual async Task ArchiveInvoice(string storeId, string invoiceId, CancellationToken token = default) diff --git a/BTCPayServer.Client/Models/InvoiceData.cs b/BTCPayServer.Client/Models/InvoiceData.cs index f31644f94..63f391182 100644 --- a/BTCPayServer.Client/Models/InvoiceData.cs +++ b/BTCPayServer.Client/Models/InvoiceData.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using BTCPayServer.JsonConverters; using Newtonsoft.Json; using Newtonsoft.Json.Converters; diff --git a/BTCPayServer.Client/Models/InvoicePaymentMethodDataModel.cs b/BTCPayServer.Client/Models/InvoicePaymentMethodDataModel.cs new file mode 100644 index 000000000..afd59a652 --- /dev/null +++ b/BTCPayServer.Client/Models/InvoicePaymentMethodDataModel.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using BTCPayServer.JsonConverters; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace BTCPayServer.Client.Models +{ + public class InvoicePaymentMethodDataModel + { + public string Destination { get; set; } + public string PaymentLink { get; set; } + + [JsonConverter(typeof(NumericStringJsonConverter))] + public decimal Rate { get; set; } + + [JsonConverter(typeof(NumericStringJsonConverter))] + public decimal PaymentMethodPaid { get; set; } + + [JsonConverter(typeof(NumericStringJsonConverter))] + public decimal TotalPaid { get; set; } + + [JsonConverter(typeof(NumericStringJsonConverter))] + public decimal Due { get; set; } + + [JsonConverter(typeof(NumericStringJsonConverter))] + public decimal Amount { get; set; } + + [JsonConverter(typeof(NumericStringJsonConverter))] + public decimal NetworkFee { get; set; } + + public List Payments { get; set; } + public string PaymentMethod { get; set; } + + public class Payment + { + public string Id { get; set; } + + [JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))] + public DateTime ReceivedDate { get; set; } + + [JsonConverter(typeof(NumericStringJsonConverter))] + public decimal Value { get; set; } + + [JsonConverter(typeof(NumericStringJsonConverter))] + public decimal Fee { get; set; } + + [JsonConverter(typeof(StringEnumConverter))] + public PaymentStatus Status { get; set; } + + public string Destination { get; set; } + + public enum PaymentStatus + { + Invalid, + AwaitingCompletion, + Complete + } + } + } +} diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index 696e16ccc..af437e032 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -945,9 +945,15 @@ namespace BTCPayServer.Tests Assert.Single(invoices); Assert.Equal(newInvoice.Id, invoices.First().Id); - //get payment request + //get var invoice = await viewOnly.GetInvoice(user.StoreId, newInvoice.Id); Assert.Equal(newInvoice.Metadata, invoice.Metadata); + var paymentMethods = await viewOnly.GetInvoicePaymentMethods(user.StoreId, newInvoice.Id); + Assert.Equal(1, paymentMethods.Length); + var paymentMethod = paymentMethods.First(); + Assert.Equal("BTC", paymentMethod.PaymentMethod); + Assert.Equal(0, paymentMethod.Payments.Count); + //update invoice = await viewOnly.GetInvoice(user.StoreId, newInvoice.Id); diff --git a/BTCPayServer/Controllers/GreenField/InvoiceController.cs b/BTCPayServer/Controllers/GreenField/InvoiceController.cs index d05e2075d..52f5bdded 100644 --- a/BTCPayServer/Controllers/GreenField/InvoiceController.cs +++ b/BTCPayServer/Controllers/GreenField/InvoiceController.cs @@ -213,8 +213,71 @@ namespace BTCPayServer.Controllers.GreenField await _invoiceRepository.ToggleInvoiceArchival(invoiceId, false, storeId); return await GetInvoice(storeId, invoiceId); } + + [Authorize(Policy = Policies.CanViewInvoices, + AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [HttpGet("~/api/v1/stores/{storeId}/invoices/{invoiceId}/payment-methods")] + public async Task GetInvoicePaymentMethods(string storeId, string invoiceId) + { + var store = HttpContext.GetStoreData(); + if (store == null) + { + return NotFound(); + } + var invoice = await _invoiceRepository.GetInvoice(invoiceId, true); + if (invoice.StoreId != store.Id) + { + return NotFound(); + } + return Ok(ToPaymentMethodModels(invoice)); + } + + private InvoicePaymentMethodDataModel[] ToPaymentMethodModels(InvoiceEntity entity) + { + return entity.GetPaymentMethods().Select( + method => + { + var accounting = method.Calculate(); + var details = method.GetPaymentMethodDetails(); + var payments = method.ParentEntity.GetPayments().Where(paymentEntity => + paymentEntity.GetPaymentMethodId() == method.GetId()); + + return new InvoicePaymentMethodDataModel() + { + PaymentMethod = method.GetId().ToStringNormalized(), + Destination = details.GetPaymentDestination(), + Rate = method.Rate, + Due = accounting.Due.ToDecimal(MoneyUnit.BTC), + TotalPaid = accounting.Paid.ToDecimal(MoneyUnit.BTC), + PaymentMethodPaid = accounting.CryptoPaid.ToDecimal(MoneyUnit.BTC), + Amount = accounting.Due.ToDecimal(MoneyUnit.BTC), + NetworkFee = accounting.NetworkFee.ToDecimal(MoneyUnit.BTC), + PaymentLink = + method.GetId().PaymentType.GetPaymentLink(method.Network, details, accounting.Due, + Request.GetAbsoluteRoot()), + Payments = payments.Select(paymentEntity => + { + var data = paymentEntity.GetCryptoPaymentData(); + return new InvoicePaymentMethodDataModel.Payment() + { + Destination = data.GetDestination(), + Id = data.GetPaymentId(), + Status = !paymentEntity.Accounted + ? InvoicePaymentMethodDataModel.Payment.PaymentStatus.Invalid + : data.PaymentConfirmed(paymentEntity, entity.SpeedPolicy) || + data.PaymentCompleted(paymentEntity) + ? InvoicePaymentMethodDataModel.Payment.PaymentStatus.Complete + : InvoicePaymentMethodDataModel.Payment.PaymentStatus.AwaitingCompletion, + Fee = paymentEntity.NetworkFee, + Value = data.GetValue(), + ReceivedDate = paymentEntity.ReceivedTime.DateTime + }; + }).ToList() + }; + }).ToArray(); + } private InvoiceData ToModel(InvoiceEntity entity) { return new InvoiceData() diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json index aee6d8cf9..5da8a0230 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json @@ -216,6 +216,123 @@ ] } }, + "/api/v1/stores/{storeId}/invoices/{invoiceId}/payment-methods": { + "get": { + "tags": [ + "Invoices" + ], + "summary": "Get invoice payment methods", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The store to fetch", + "schema": { + "type": "string" + } + }, + { + "name": "invoiceId", + "in": "path", + "required": true, + "description": "The invoice to fetch", + "schema": { + "type": "string" + } + } + ], + "description": "View information about the specified invoice's payment methods", + "operationId": "Invoices_GetInvoicePaymentMethods", + "responses": { + "200": { + "description": "specified invoice payment methods data", + "content": { + "application/json": { + "schema": { + "type": "array", + "nullable": false, + "items": { + "$ref": "#/components/schemas/InvoicePaymentMethodDataModel" + } + } + } + } + }, + "403": { + "description": "If you are authenticated but forbidden to view the specified invoie" + }, + "404": { + "description": "The key is not found for this invoice" + } + }, + "security": [ + { + "API Key": [ + "btcpay.store.canviewinvoices" + ], + "Basic": [] + } + ] + }, + "delete": { + "tags": [ + "Invoices" + ], + "summary": "Archive invoice", + "description": "Archives the specified invoice.", + "operationId": "Invoices_ArchiveInvoice", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The store the invoice belongs to", + "schema": { + "type": "string" + } + }, + { + "name": "invoiceId", + "in": "path", + "required": true, + "description": "The invoice to remove", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The invoice has been archived" + }, + "400": { + "description": "A list of errors that occurred when archiving the invoice", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidationProblemDetails" + } + } + } + }, + "403": { + "description": "If you are authenticated but forbidden to archive the specified invoice" + }, + "404": { + "description": "The key is not found for this invoice" + } + }, + "security": [ + { + "API Key": [ + "btcpay.store.canmodifystoresettings" + ], + "Basic": [] + } + ] + } + }, "/api/v1/stores/{storeId}/invoices/{invoiceId}/status": { "post": { "tags": [ @@ -565,6 +682,110 @@ "LowSpeed", "LowMediumSpeed" ] + }, + "InvoicePaymentMethodDataModel": { + "type": "object", + "additionalProperties": false, + "properties": { + "paymentMethod": { + "type": "string", + "description": "The payment method" + }, + "destination": { + "type": "string", + "description": "The destination the payment must be made to" + }, + "paymentLink": { + "type": "string", + "nullable": true, + "description": "A payment link that helps pay to the payment destination" + }, + "rate": { + "type": "string", + "format": "decimal", + "description": "The rate between this payment method's currency and the invoice currency" + }, + "paymentMethodPaid": { + "type": "string", + "format": "decimal", + "description": "The amount paid by this payment method" + }, + "totalPaid": { + "type": "string", + "format": "decimal", + "description": "The total amount paid by all payment methods to the invoice, converted to this payment method's currency" + }, + "due": { + "type": "string", + "format": "decimal", + "description": "The total amount left to be paid, converted to this payment method's currency" + }, + "amount": { + "type": "string", + "format": "decimal", + "description": "The invoice amount, converted to this payment method's currency" + }, + "networkFee": { + "type": "string", + "format": "decimal", + "description": "The added merchant fee to pay for network costs of this payment method." + }, + "payments": { + "type": "array", + "nullable": false, + "items": { + "$ref": "#/components/schemas/Payment" + }, + "description": "Payments made with this payment method." + } + } + }, + "Payment": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "description": "A unique identifier for this payment" + }, + "receivedDate": { + "type": "string", + "format": "int64", + "description": "The date the payment was recorded" + }, + "value": { + "type": "string", + "format": "decimal", + "description": "The value of the payment" + }, + "fee": { + "type": "string", + "format": "decimal", + "description": "The fee paid for the payment" + }, + "status": { + "$ref": "#/components/schemas/PaymentStatus", + "description": "The status of the payment" + }, + "destination": { + "type": "string", + "description": "The destination the payment was made to" + } + } + }, + "PaymentStatus": { + "type": "string", + "description": "", + "x-enumNames": [ + "Invalid", + "AwaitingCompletion", + "Complete" + ], + "enum": [ + "Invalid", + "AwaitingCompletion", + "Complete" + ] } } }, diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.payment-requests.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.payment-requests.json index 9bbde2c4b..90dc62bee 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.payment-requests.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.payment-requests.json @@ -299,7 +299,7 @@ "type": "string", "description": "The creation date of the payment request", "nullable": false, - "format": "date-time" + "format": "int64" } } }, @@ -342,7 +342,7 @@ "type": "string", "description": "The expiry date of the payment request", "nullable": true, - "format": "date-time" + "format": "int64" }, "embeddedCSS": { "type": "string",