-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[PM-12420] Stripe events recovery (#4793)
* Billing: Add event recovery endpoints * Core: Add InternalBilling to BaseServiceUriSettings * Admin: Scaffold billing section * Admin: Scaffold ProcessStripeEvents section * Admin: Implement event processing * Run dotnet format
- Loading branch information
1 parent
150c780
commit cd43535
Showing
21 changed files
with
379 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
71 changes: 71 additions & 0 deletions
71
src/Admin/Billing/Controllers/ProcessStripeEventsController.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
using System.Text.Json; | ||
using Bit.Admin.Billing.Models.ProcessStripeEvents; | ||
using Bit.Core.Settings; | ||
using Bit.Core.Utilities; | ||
using Microsoft.AspNetCore.Authorization; | ||
using Microsoft.AspNetCore.Mvc; | ||
|
||
namespace Bit.Admin.Billing.Controllers; | ||
|
||
[Authorize] | ||
[Route("process-stripe-events")] | ||
[SelfHosted(NotSelfHostedOnly = true)] | ||
public class ProcessStripeEventsController( | ||
IHttpClientFactory httpClientFactory, | ||
IGlobalSettings globalSettings) : Controller | ||
{ | ||
[HttpGet] | ||
public ActionResult Index() | ||
{ | ||
return View(new EventsFormModel()); | ||
} | ||
|
||
[HttpPost] | ||
[ValidateAntiForgeryToken] | ||
public async Task<IActionResult> ProcessAsync([FromForm] EventsFormModel model) | ||
{ | ||
var eventIds = model.GetEventIds(); | ||
|
||
const string baseEndpoint = "stripe/recovery/events"; | ||
|
||
var endpoint = model.Inspect ? $"{baseEndpoint}/inspect" : $"{baseEndpoint}/process"; | ||
|
||
var (response, failedResponseMessage) = await PostAsync(endpoint, new EventsRequestBody | ||
{ | ||
EventIds = eventIds | ||
}); | ||
|
||
if (response == null) | ||
{ | ||
return StatusCode((int)failedResponseMessage.StatusCode, "An error occurred during your request."); | ||
} | ||
|
||
response.ActionType = model.Inspect ? EventActionType.Inspect : EventActionType.Process; | ||
|
||
return View("Results", response); | ||
} | ||
|
||
private async Task<(EventsResponseBody, HttpResponseMessage)> PostAsync( | ||
string endpoint, | ||
EventsRequestBody requestModel) | ||
{ | ||
var client = httpClientFactory.CreateClient("InternalBilling"); | ||
client.BaseAddress = new Uri(globalSettings.BaseServiceUri.InternalBilling); | ||
|
||
var json = JsonSerializer.Serialize(requestModel); | ||
var requestBody = new StringContent(json, System.Text.Encoding.UTF8, "application/json"); | ||
|
||
var responseMessage = await client.PostAsync(endpoint, requestBody); | ||
|
||
if (!responseMessage.IsSuccessStatusCode) | ||
{ | ||
return (null, responseMessage); | ||
} | ||
|
||
var responseContent = await responseMessage.Content.ReadAsStringAsync(); | ||
|
||
var response = JsonSerializer.Deserialize<EventsResponseBody>(responseContent); | ||
|
||
return (response, null); | ||
} | ||
} |
29 changes: 29 additions & 0 deletions
29
src/Admin/Billing/Models/ProcessStripeEvents/EventsFormModel.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
using System.ComponentModel; | ||
using System.ComponentModel.DataAnnotations; | ||
|
||
namespace Bit.Admin.Billing.Models.ProcessStripeEvents; | ||
|
||
public class EventsFormModel : IValidatableObject | ||
{ | ||
[Required] | ||
public string EventIds { get; set; } | ||
|
||
[Required] | ||
[DisplayName("Inspect Only")] | ||
public bool Inspect { get; set; } | ||
|
||
public List<string> GetEventIds() => | ||
EventIds?.Split([Environment.NewLine], StringSplitOptions.RemoveEmptyEntries) | ||
.Select(eventId => eventId.Trim()) | ||
.ToList() ?? []; | ||
|
||
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) | ||
{ | ||
var eventIds = GetEventIds(); | ||
|
||
if (eventIds.Any(eventId => !eventId.StartsWith("evt_"))) | ||
{ | ||
yield return new ValidationResult("Event Ids must start with 'evt_'."); | ||
} | ||
} | ||
} |
9 changes: 9 additions & 0 deletions
9
src/Admin/Billing/Models/ProcessStripeEvents/EventsRequestBody.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
using System.Text.Json.Serialization; | ||
|
||
namespace Bit.Admin.Billing.Models.ProcessStripeEvents; | ||
|
||
public class EventsRequestBody | ||
{ | ||
[JsonPropertyName("eventIds")] | ||
public List<string> EventIds { get; set; } | ||
} |
39 changes: 39 additions & 0 deletions
39
src/Admin/Billing/Models/ProcessStripeEvents/EventsResponseBody.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
using System.Text.Json.Serialization; | ||
|
||
namespace Bit.Admin.Billing.Models.ProcessStripeEvents; | ||
|
||
public class EventsResponseBody | ||
{ | ||
[JsonPropertyName("events")] | ||
public List<EventResponseBody> Events { get; set; } | ||
|
||
[JsonIgnore] | ||
public EventActionType ActionType { get; set; } | ||
} | ||
|
||
public class EventResponseBody | ||
{ | ||
[JsonPropertyName("id")] | ||
public string Id { get; set; } | ||
|
||
[JsonPropertyName("url")] | ||
public string URL { get; set; } | ||
|
||
[JsonPropertyName("apiVersion")] | ||
public string APIVersion { get; set; } | ||
|
||
[JsonPropertyName("type")] | ||
public string Type { get; set; } | ||
|
||
[JsonPropertyName("createdUTC")] | ||
public DateTime CreatedUTC { get; set; } | ||
|
||
[JsonPropertyName("processingError")] | ||
public string ProcessingError { get; set; } | ||
} | ||
|
||
public enum EventActionType | ||
{ | ||
Inspect, | ||
Process | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
@model Bit.Admin.Billing.Models.ProcessStripeEvents.EventsFormModel | ||
|
||
@{ | ||
ViewData["Title"] = "Process Stripe Events"; | ||
} | ||
|
||
<h1>Process Stripe Events</h1> | ||
<form method="post" asp-controller="ProcessStripeEvents" asp-action="Process"> | ||
<div class="row"> | ||
<div class="col-1"> | ||
<div class="form-group"> | ||
<input type="submit" value="Process" class="btn btn-primary mb-2"/> | ||
</div> | ||
</div> | ||
<div class="col-2"> | ||
<div class="form-group form-check"> | ||
<input type="checkbox" class="form-check-input" asp-for="Inspect"> | ||
<label class="form-check-label" asp-for="Inspect"></label> | ||
</div> | ||
</div> | ||
</div> | ||
<div class="form-group"> | ||
<textarea id="event-ids" type="text" class="form-control" rows="100" asp-for="EventIds"></textarea> | ||
</div> | ||
</form> |
49 changes: 49 additions & 0 deletions
49
src/Admin/Billing/Views/ProcessStripeEvents/Results.cshtml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
@using Bit.Admin.Billing.Models.ProcessStripeEvents | ||
@model Bit.Admin.Billing.Models.ProcessStripeEvents.EventsResponseBody | ||
|
||
@{ | ||
var title = Model.ActionType == EventActionType.Inspect ? "Inspect Stripe Events" : "Process Stripe Events"; | ||
ViewData["Title"] = title; | ||
} | ||
|
||
<h1>@title</h1> | ||
<h2>Results</h2> | ||
|
||
<div class="table-responsive"> | ||
@if (!Model.Events.Any()) | ||
{ | ||
<p>No data found.</p> | ||
} | ||
else | ||
{ | ||
<table class="table table-striped table-hover"> | ||
<thead> | ||
<tr> | ||
<th>ID</th> | ||
<th>Type</th> | ||
<th>API Version</th> | ||
<th>Created</th> | ||
@if (Model.ActionType == EventActionType.Process) | ||
{ | ||
<th>Processing Error</th> | ||
} | ||
</tr> | ||
</thead> | ||
<tbody> | ||
@foreach (var eventResponseBody in Model.Events) | ||
{ | ||
<tr> | ||
<td><a href="@eventResponseBody.URL">@eventResponseBody.Id</a></td> | ||
<td>@eventResponseBody.Type</td> | ||
<td>@eventResponseBody.APIVersion</td> | ||
<td>@eventResponseBody.CreatedUTC</td> | ||
@if (Model.ActionType == EventActionType.Process) | ||
{ | ||
<td>@eventResponseBody.ProcessingError</td> | ||
} | ||
</tr> | ||
} | ||
</tbody> | ||
</table> | ||
} | ||
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
@using Microsoft.AspNetCore.Identity | ||
@using Bit.Admin.AdminConsole | ||
@using Bit.Admin.AdminConsole.Models | ||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers | ||
@addTagHelper "*, Admin" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
@{ | ||
Layout = "_Layout"; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
using Bit.Billing.Models.Recovery; | ||
using Bit.Billing.Services; | ||
using Bit.Core.Utilities; | ||
using Microsoft.AspNetCore.Http.HttpResults; | ||
using Microsoft.AspNetCore.Mvc; | ||
using Stripe; | ||
|
||
namespace Bit.Billing.Controllers; | ||
|
||
[Route("stripe/recovery")] | ||
[SelfHosted(NotSelfHostedOnly = true)] | ||
public class RecoveryController( | ||
IStripeEventProcessor stripeEventProcessor, | ||
IStripeFacade stripeFacade, | ||
IWebHostEnvironment webHostEnvironment) : Controller | ||
{ | ||
private readonly string _stripeURL = webHostEnvironment.IsDevelopment() || webHostEnvironment.IsEnvironment("QA") | ||
? "https://dashboard.stripe.com/test" | ||
: "https://dashboard.stripe.com"; | ||
|
||
// ReSharper disable once RouteTemplates.ActionRoutePrefixCanBeExtractedToControllerRoute | ||
[HttpPost("events/inspect")] | ||
public async Task<Ok<EventsResponseBody>> InspectEventsAsync([FromBody] EventsRequestBody requestBody) | ||
{ | ||
var inspected = await Task.WhenAll(requestBody.EventIds.Select(async eventId => | ||
{ | ||
var @event = await stripeFacade.GetEvent(eventId); | ||
return Map(@event); | ||
})); | ||
|
||
var response = new EventsResponseBody { Events = inspected.ToList() }; | ||
|
||
return TypedResults.Ok(response); | ||
} | ||
|
||
// ReSharper disable once RouteTemplates.ActionRoutePrefixCanBeExtractedToControllerRoute | ||
[HttpPost("events/process")] | ||
public async Task<Ok<EventsResponseBody>> ProcessEventsAsync([FromBody] EventsRequestBody requestBody) | ||
{ | ||
var processed = await Task.WhenAll(requestBody.EventIds.Select(async eventId => | ||
{ | ||
var @event = await stripeFacade.GetEvent(eventId); | ||
try | ||
{ | ||
await stripeEventProcessor.ProcessEventAsync(@event); | ||
return Map(@event); | ||
} | ||
catch (Exception exception) | ||
{ | ||
return Map(@event, exception.Message); | ||
} | ||
})); | ||
|
||
var response = new EventsResponseBody { Events = processed.ToList() }; | ||
|
||
return TypedResults.Ok(response); | ||
} | ||
|
||
private EventResponseBody Map(Event @event, string processingError = null) => new() | ||
{ | ||
Id = @event.Id, | ||
URL = $"{_stripeURL}/workbench/events/{@event.Id}", | ||
APIVersion = @event.ApiVersion, | ||
Type = @event.Type, | ||
CreatedUTC = @event.Created, | ||
ProcessingError = processingError | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
using System.Text.Json.Serialization; | ||
|
||
namespace Bit.Billing.Models.Recovery; | ||
|
||
public class EventsRequestBody | ||
{ | ||
[JsonPropertyName("eventIds")] | ||
public List<string> EventIds { get; set; } | ||
} |
Oops, something went wrong.