make individual action items

This commit is contained in:
Kukks 2020-07-27 10:43:35 +02:00 committed by nicolas.dorier
parent 5f6f54db36
commit 8dea7df82a
No known key found for this signature in database
GPG Key ID: 6618763EF09186FE
9 changed files with 296 additions and 87 deletions

View File

@ -47,14 +47,35 @@ namespace BTCPayServer.Client
return await HandleResponse<InvoiceData>(response);
}
public virtual async Task<InvoiceData> UpdateInvoice(string storeId, string invoiceId,
UpdateInvoiceRequest request, CancellationToken token = default)
public virtual async Task<InvoiceData> AddCustomerEmailToInvoice(string storeId, string invoiceId,
AddCustomerEmailRequest 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);
CreateHttpRequest($"api/v1/stores/{storeId}/invoices/{invoiceId}/email", bodyPayload: request,
method: HttpMethod.Post), token);
return await HandleResponse<InvoiceData>(response);
}
public virtual async Task<InvoiceData> MarkInvoiceStatus(string storeId, string invoiceId,
MarkInvoiceStatusRequest request, CancellationToken token = default)
{
if (request == null)
throw new ArgumentNullException(nameof(request));
if (request.Status!= InvoiceStatus.Complete && request.Status!= InvoiceStatus.Invalid)
throw new ArgumentOutOfRangeException(nameof(request.Status), "Status can only be Invalid or Complete");
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/invoices/{invoiceId}/status", bodyPayload: request,
method: HttpMethod.Post), token);
return await HandleResponse<InvoiceData>(response);
}
public virtual async Task<InvoiceData> UnarchiveInvoice(string storeId, string invoiceId, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/invoices/{invoiceId}/unarchive",
method: HttpMethod.Post), token);
return await HandleResponse<InvoiceData>(response);
}
}

View File

@ -0,0 +1,7 @@
namespace BTCPayServer.Client.Models
{
public class AddCustomerEmailRequest
{
public string Email { get; set; }
}
}

View File

@ -12,7 +12,7 @@ namespace BTCPayServer.Client.Models
[JsonConverter(typeof(StringEnumConverter))]
public InvoiceStatus Status { get; set; }
[JsonConverter(typeof(StringEnumConverter))]
public InvoiceExceptionStatus ExceptionStatus { get; set; }
public InvoiceExceptionStatus AdditionalStatus { get; set; }
public Dictionary<string, PaymentMethodDataModel> PaymentMethodData { get; set; }
public class PaymentMethodDataModel

View File

@ -0,0 +1,11 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace BTCPayServer.Client.Models
{
public class MarkInvoiceStatusRequest
{
[JsonConverter(typeof(StringEnumConverter))]
public InvoiceStatus Status { get; set; }
}
}

View File

@ -1,9 +0,0 @@
namespace BTCPayServer.Client.Models
{
public class UpdateInvoiceRequest
{
public bool? Archived { get; set; }
public InvoiceStatus? Status { get; set; }
public string Email { get; set; }
}
}

View File

@ -762,7 +762,7 @@ namespace BTCPayServer.Tests
});
await user.RegisterDerivationSchemeAsync("BTC");
var newInvoice = await client.CreateInvoice(user.StoreId,
new CreateInvoiceRequest() { Currency = "USD", Amount = 1, Metadata = new CreateInvoiceRequest.ProductInformation(){ ItemCode = "testitem"}});
new CreateInvoiceRequest() { Currency = "USD", Amount = 1, Metadata = "{\"itemCode\": \"testitem\"}"});
//list
var invoices = await viewOnly.GetInvoices(user.StoreId);
@ -773,29 +773,34 @@ namespace BTCPayServer.Tests
//get payment request
var invoice = await viewOnly.GetInvoice(user.StoreId, newInvoice.Id);
Assert.Equal(newInvoice.Metadata.ItemCode, invoice.Metadata.ItemCode);
Assert.Equal(newInvoice.Metadata, invoice.Metadata);
//update
await AssertHttpError(403, async () =>
{
await viewOnly.UpdateInvoice(user.StoreId, invoice.Id, new UpdateInvoiceRequest()
await viewOnly.AddCustomerEmailToInvoice(user.StoreId, invoice.Id, new AddCustomerEmailRequest()
{
Email = "j@g.com"
});
});
await client.UpdateInvoice(user.StoreId, invoice.Id, new UpdateInvoiceRequest()
await client.AddCustomerEmailToInvoice(user.StoreId, invoice.Id, new AddCustomerEmailRequest()
{
Email = "j@g.com"
});
invoice = await viewOnly.GetInvoice(user.StoreId, newInvoice.Id);
Assert.Equal(invoice.Customer.BuyerEmail, "j@g.com");
Assert.Equal("j@g.com", invoice.CustomerEmail);
await AssertValidationError(new[] { nameof(UpdateInvoiceRequest.Email), nameof(UpdateInvoiceRequest.Archived),nameof(UpdateInvoiceRequest.Status) }, async () =>
await AssertValidationError(new[] { nameof(AddCustomerEmailRequest.Email) }, async () =>
{
await client.UpdateInvoice(user.StoreId, invoice.Id, new UpdateInvoiceRequest()
await client.AddCustomerEmailToInvoice(user.StoreId, invoice.Id, new AddCustomerEmailRequest()
{
Email = "j@g2.com",
Archived = true,
});
});
await AssertValidationError(new[] { nameof(MarkInvoiceStatusRequest.Status) }, async () =>
{
await client.MarkInvoiceStatus(user.StoreId, invoice.Id, new MarkInvoiceStatusRequest()
{
Status = InvoiceStatus.Complete
});
});
@ -812,10 +817,7 @@ namespace BTCPayServer.Tests
(await client.GetInvoices(user.StoreId)).Select(data => data.Id));
//unarchive
await client.UpdateInvoice(user.StoreId, invoice.Id, new UpdateInvoiceRequest()
{
Archived = false,
});
await client.UnarchiveInvoice(user.StoreId, invoice.Id);
Assert.NotNull(await client.GetInvoice(user.StoreId,invoice.Id));
}

View File

@ -160,8 +160,9 @@ namespace BTCPayServer.Controllers.GreenField
[Authorize(Policy = Policies.CanModifyStoreSettings,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPut("~/api/v1/stores/{storeId}/invoices/{invoiceId}")]
public async Task<IActionResult> UpdateInvoice(string storeId, string invoiceId, UpdateInvoiceRequest request)
[HttpPost("~/api/v1/stores/{storeId}/invoices/{invoiceId}/status")]
public async Task<IActionResult> MarkInvoiceStatus(string storeId, string invoiceId,
MarkInvoiceStatusRequest request)
{
var store = HttpContext.GetStoreData();
if (store == null)
@ -175,30 +176,36 @@ namespace BTCPayServer.Controllers.GreenField
return NotFound();
}
if (request.Archived.HasValue)
{
if (request.Archived.Value && !invoice.Archived)
{
ModelState.AddModelError(nameof(request.Archived),
"You can only archive an invoice via HTTP DELETE.");
}
else if (!request.Archived.Value && invoice.Archived)
{
await _invoiceRepository.ToggleInvoiceArchival(invoiceId, false, storeId);
}
}
if (request.Status != null)
{
if (!await _invoiceRepository.MarkInvoiceStatus(invoice.Id, request.Status.Value))
if (!await _invoiceRepository.MarkInvoiceStatus(invoice.Id, request.Status))
{
ModelState.AddModelError(nameof(request.Status),
"Status can only be marked to invalid or complete within certain conditions.");
}
if (!ModelState.IsValid)
return this.CreateValidationError(ModelState);
return await GetInvoice(storeId, invoiceId);
}
if (request.Email != null)
[Authorize(Policy = Policies.CanCreateInvoice,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/stores/{storeId}/invoices/{invoiceId}/email")]
public async Task<IActionResult> AddCustomerEmail(string storeId, string invoiceId,
AddCustomerEmailRequest request)
{
var store = HttpContext.GetStoreData();
if (store == null)
{
return NotFound();
}
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
if (invoice.StoreId != store.Id)
{
return NotFound();
}
if (!EmailValidator.IsEmail(request.Email))
{
request.AddModelError(invoiceRequest => invoiceRequest.Email, "Invalid email address",
@ -210,15 +217,45 @@ namespace BTCPayServer.Controllers.GreenField
this);
}
if (!ModelState.IsValid)
return this.CreateValidationError(ModelState);
await _invoiceRepository.UpdateInvoice(invoice.Id, new UpdateCustomerModel() {Email = request.Email});
return await GetInvoice(storeId, invoiceId);
}
[Authorize(Policy = Policies.CanModifyStoreSettings,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/stores/{storeId}/invoices/{invoiceId}/unarchive")]
public async Task<IActionResult> UnarchiveInvoice(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();
}
if (!invoice.Archived)
{
return this.CreateAPIError("already-unarchived", "Invoice is already unarchived");
}
if (!ModelState.IsValid)
return this.CreateValidationError(ModelState);
await _invoiceRepository.ToggleInvoiceArchival(invoiceId, false, storeId);
return await GetInvoice(storeId, invoiceId);
}
private InvoiceData ToModel(InvoiceEntity entity)
{
return new InvoiceData()
@ -226,7 +263,7 @@ namespace BTCPayServer.Controllers.GreenField
Amount = entity.ProductInformation.Price,
Id = entity.Id,
Status = entity.Status,
ExceptionStatus = entity.ExceptionStatus,
AdditionalStatus = entity.ExceptionStatus,
Currency = entity.ProductInformation.Currency,
Metadata = entity.PosData,
CustomerEmail = entity.RefundMail ?? entity.BuyerInformation.BuyerEmail,
@ -307,6 +344,7 @@ namespace BTCPayServer.Controllers.GreenField
// ignored
}
}
return new Models.CreateInvoiceRequest()
{
Buyer = buyer,
@ -323,7 +361,7 @@ namespace BTCPayServer.Controllers.GreenField
PaymentCurrencies = entity.Checkout.PaymentMethods,
NotificationURL = entity.Checkout.RedirectUri,
PosData = entity.Metadata,
Physical = pi?.Physical??false,
Physical = pi?.Physical ?? false,
ItemCode = pi?.ItemCode,
ItemDesc = pi?.ItemDesc,
TaxIncluded = pi?.TaxIncluded,

View File

@ -32,10 +32,10 @@ namespace BTCPayServer.Controllers.GreenField
[Authorize(Policy = Policies.CanViewPaymentRequests, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/payment-requests")]
public async Task<ActionResult<IEnumerable<PaymentRequestData>>> GetPaymentRequests(string storeId)
public async Task<ActionResult<IEnumerable<PaymentRequestData>>> GetPaymentRequests(string storeId, bool includeArchived = false)
{
var prs = await _paymentRequestRepository.FindPaymentRequests(
new PaymentRequestQuery() { StoreId = storeId, IncludeArchived = false });
new PaymentRequestQuery() { StoreId = storeId, IncludeArchived = includeArchived });
return Ok(prs.Items.Select(FromModel));
}

View File

@ -214,12 +214,14 @@
"Basic": []
}
]
}
},
"put": {
"/api/v1/stores/{storeId}/invoices/{invoiceId}/email": {
"post": {
"tags": [
"Invoices"
],
"summary": "Update invoice",
"summary": "Add customer email to invoice",
"parameters": [
{
"name": "storeId",
@ -240,8 +242,8 @@
}
}
],
"description": "Update an invoice",
"operationId": "Invoices_UpdateInvoice",
"description": "Adds the customer's email to the invoice if it has not been set already.",
"operationId": "Invoices_AddCustomerEmail",
"responses": {
"200": {
"description": "The updated invoice",
@ -272,11 +274,147 @@
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdateInvoiceRequest"
"$ref": "#/components/schemas/AddCustomerEmailRequest"
}
}
}
},
"security": [
{
"API Key": [
"btcpay.store.cancreateinvoice"
],
"Basic": []
}
]
}
},
"/api/v1/stores/{storeId}/invoices/{invoiceId}/status": {
"post": {
"tags": [
"Invoices"
],
"summary": "Mark invoice status",
"parameters": [
{
"name": "storeId",
"in": "path",
"required": true,
"description": "The store to query",
"schema": {
"type": "string"
}
},
{
"name": "invoiceId",
"in": "path",
"required": true,
"description": "The invoice to update",
"schema": {
"type": "string"
}
}
],
"description": "Mark an invoice as invalid or completed.",
"operationId": "Invoices_MarkInvoiceStatus",
"responses": {
"200": {
"description": "The updated invoice",
"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 invoice"
}
},
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MarkInvoiceStatusRequest"
}
}
}
},
"security": [
{
"API Key": [
"btcpay.store.canmodifystoresettings"
],
"Basic": []
}
]
}
},
"/api/v1/stores/{storeId}/invoices/{invoiceId}/unarchive": {
"post": {
"tags": [
"Invoices"
],
"summary": "Unarchive invoice",
"parameters": [
{
"name": "storeId",
"in": "path",
"required": true,
"description": "The store to query",
"schema": {
"type": "string"
}
},
{
"name": "invoiceId",
"in": "path",
"required": true,
"description": "The invoice to update",
"schema": {
"type": "string"
}
}
],
"description": "Unarchive an invoice",
"operationId": "Invoices_UnarchiveInvoice",
"responses": {
"200": {
"description": "The unarchived invoice",
"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 invoice"
}
},
"security": [
{
"API Key": [
@ -296,27 +434,28 @@
"$ref": "#/components/schemas/InvoiceData"
}
},
"UpdateInvoiceRequest": {
"MarkInvoiceStatusRequest": {
"type": "object",
"additionalProperties": false,
"properties": {
"archived": {
"type": "boolean",
"nullable": true,
"description": "Unarchive an invoice. You can only archive from the HTTP DELETE endpoint."
},
"status": {
"nullable": true,
"nullable": false,
"description": "Mark an invoice as completed or invalid.",
"oneOf": [
{
"$ref": "#/components/schemas/InvoiceStatusMark"
}
]
}
}
},
"AddCustomerEmailRequest": {
"type": "object",
"additionalProperties": false,
"properties": {
"email": {
"type": "string",
"nullable": true,
"nullable": false,
"description": "Sets the customer email, if it was not set before."
}
}
@ -353,9 +492,9 @@
"Confirmed"
]
},
"InvoiceExceptionStatus": {
"InvoiceAdditionalStatus": {
"type": "string",
"description": "",
"description": "An additional status that describes why an invoice is in its current status.",
"x-enumNames": [
"None",
"PaidLate",
@ -391,8 +530,8 @@
"$ref": "#/components/schemas/InvoiceStatus",
"description": "The status of the invoice"
},
"exceptionStatus": {
"$ref": "#/components/schemas/InvoiceExceptionStatus",
"additionalStatus": {
"$ref": "#/components/schemas/InvoiceAdditionalStatus",
"description": "a secondary status of the invoice"
},
"paymentMethodData": {