diff --git a/BTCPayServer.Client/BTCPayServerClient.Invoices.cs b/BTCPayServer.Client/BTCPayServerClient.Invoices.cs index a7518aafb..87f3215db 100644 --- a/BTCPayServer.Client/BTCPayServerClient.Invoices.cs +++ b/BTCPayServer.Client/BTCPayServerClient.Invoices.cs @@ -54,6 +54,17 @@ namespace BTCPayServer.Client return await HandleResponse(response); } + public virtual async Task UpdateInvoice(string storeId, string invoiceId, + UpdateInvoiceRequest request, CancellationToken token = default) + { + if (request == null) + throw new ArgumentNullException(nameof(request)); + var response = await _httpClient.SendAsync( + CreateHttpRequest($"api/v1/stores/{storeId}/invoices/{invoiceId}", bodyPayload: request, + method: HttpMethod.Put), token); + return await HandleResponse(response); + } + public virtual async Task MarkInvoiceStatus(string storeId, string invoiceId, MarkInvoiceStatusRequest request, CancellationToken token = default) { diff --git a/BTCPayServer.Client/Models/UpdateInvoiceRequest.cs b/BTCPayServer.Client/Models/UpdateInvoiceRequest.cs new file mode 100644 index 000000000..af4586863 --- /dev/null +++ b/BTCPayServer.Client/Models/UpdateInvoiceRequest.cs @@ -0,0 +1,9 @@ +using Newtonsoft.Json.Linq; + +namespace BTCPayServer.Client.Models +{ + public class UpdateInvoiceRequest + { + public JObject Metadata { get; set; } + } +} diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index ae2c15ceb..7b030056d 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -978,7 +978,22 @@ namespace BTCPayServer.Tests }); }); + await AssertHttpError(403, async () => + { + await viewOnly.UpdateInvoice(user.StoreId, newInvoice.Id, + new UpdateInvoiceRequest() + { + Metadata = JObject.Parse("{\"itemCode\": \"updated\", newstuff: [1,2,3,4,5]}") + }); + }); + invoice = await client.UpdateInvoice(user.StoreId, newInvoice.Id, + new UpdateInvoiceRequest() + { + Metadata = JObject.Parse("{\"itemCode\": \"updated\", newstuff: [1,2,3,4,5]}") + }); + Assert.Equal("updated",invoice.Metadata["itemCode"].Value()); + Assert.Equal(15,((JArray) invoice.Metadata["newstuff"]).Values().Sum()); //archive await AssertHttpError(403, async () => { diff --git a/BTCPayServer/Controllers/GreenField/InvoiceController.cs b/BTCPayServer/Controllers/GreenField/InvoiceController.cs index bcd303718..0b3235f17 100644 --- a/BTCPayServer/Controllers/GreenField/InvoiceController.cs +++ b/BTCPayServer/Controllers/GreenField/InvoiceController.cs @@ -4,21 +4,16 @@ using System.Threading.Tasks; using BTCPayServer.Abstractions.Constants; using BTCPayServer.Client; using BTCPayServer.Client.Models; -using BTCPayServer.Models.InvoicingModels; using BTCPayServer.Payments; using BTCPayServer.Security; using BTCPayServer.Services; using BTCPayServer.Services.Invoices; -using BTCPayServer.Validation; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using Microsoft.EntityFrameworkCore.Metadata.Conventions; using NBitcoin; -using NBitpayClient; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using CreateInvoiceRequest = BTCPayServer.Client.Models.CreateInvoiceRequest; using InvoiceData = BTCPayServer.Client.Models.InvoiceData; @@ -100,6 +95,26 @@ namespace BTCPayServer.Controllers.GreenField return Ok(); } + [Authorize(Policy = Policies.CanModifyStoreSettings, + AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [HttpPut("~/api/v1/stores/{storeId}/invoices/{invoiceId}")] + public async Task UpdateInvoice(string storeId, string invoiceId, UpdateInvoiceRequest request) + { + var store = HttpContext.GetStoreData(); + if (store == null) + { + return NotFound(); + } + + var result = await _invoiceRepository.UpdateInvoiceMetadata(invoiceId, storeId, request.Metadata); + if (result != null) + { + return Ok(ToModel(result)); + } + + return NotFound(); + } + [Authorize(Policy = Policies.CanCreateInvoice, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] [HttpPost("~/api/v1/stores/{storeId}/invoices")] diff --git a/BTCPayServer/Services/Invoices/InvoiceRepository.cs b/BTCPayServer/Services/Invoices/InvoiceRepository.cs index 4b6d83255..ca19f2fcf 100644 --- a/BTCPayServer/Services/Invoices/InvoiceRepository.cs +++ b/BTCPayServer/Services/Invoices/InvoiceRepository.cs @@ -441,6 +441,22 @@ retry: await context.SaveChangesAsync().ConfigureAwait(false); } } + public async Task UpdateInvoiceMetadata(string invoiceId, string storeId, JObject metadata) + { + using (var context = _ContextFactory.CreateContext()) + { + var invoiceData = await GetInvoiceRaw(invoiceId); + if (invoiceData == null || (storeId != null && + !invoiceData.StoreDataId.Equals(storeId, + StringComparison.InvariantCultureIgnoreCase))) + return null; + var blob = invoiceData.GetBlob(_Networks); + blob.Metadata = InvoiceMetadata.FromJObject(metadata); + invoiceData.Blob = ToBytes(blob); + await context.SaveChangesAsync().ConfigureAwait(false); + return ToEntity(invoiceData); + } + } public async Task MarkInvoiceStatus(string invoiceId, InvoiceStatus status) { using (var context = _ContextFactory.CreateContext()) diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json index ccc133cf8..ade84ea05 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json @@ -214,6 +214,80 @@ "Basic": [] } ] + }, + "put": { + "tags": [ + "Invoices" + ], + "summary": "Update invoice", + "description": "Updates the specified invoice.", + "operationId": "Invoices_UpdateInvoice", + "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 update", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The invoice that has been updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvoiceData" + } + } + } + }, + "400": { + "description": "A list of errors that occurred when updating the invoice", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidationProblemDetails" + } + } + } + }, + "403": { + "description": "If you are authenticated but forbidden to update the specified invoice" + }, + "404": { + "description": "The key is not found for this invoice" + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateInvoiceRequest" + } + } + } + }, + "security": [ + { + "API Key": [ + "btcpay.store.canmodifystoresettings" + ], + "Basic": [] + } + ] } }, "/api/v1/stores/{storeId}/invoices/{invoiceId}/payment-methods": { @@ -628,6 +702,17 @@ } } }, + "UpdateInvoiceRequest": { + "type": "object", + "additionalProperties": false, + "properties": { + "metadata": { + "type": "object", + "nullable": true, + "description": "Additional information around the invoice that can be supplied." + } + } + }, "CheckoutOptions": { "type": "object", "additionalProperties": false,