From e7b565d0076ac844747344f6803a701981a27a9f Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 25 Oct 2017 00:45:11 -0400 Subject: [PATCH] invoice pdf generation api --- src/Api/Api.csproj | 3 + .../Controllers/OrganizationsController.cs | 39 +++++ src/Api/Models/InvoiceModel.cs | 67 ++++++++ src/Api/Startup.cs | 11 ++ src/Api/Views/Organizations/Invoice.cshtml | 145 ++++++++++++++++++ 5 files changed, 265 insertions(+) create mode 100644 src/Api/Models/InvoiceModel.cs create mode 100644 src/Api/Views/Organizations/Invoice.cshtml diff --git a/src/Api/Api.csproj b/src/Api/Api.csproj index 990df65c94..df51b64f10 100644 --- a/src/Api/Api.csproj +++ b/src/Api/Api.csproj @@ -13,6 +13,9 @@ + + + diff --git a/src/Api/Controllers/OrganizationsController.cs b/src/Api/Controllers/OrganizationsController.cs index c39ed514b1..d780af84cc 100644 --- a/src/Api/Controllers/OrganizationsController.cs +++ b/src/Api/Controllers/OrganizationsController.cs @@ -12,6 +12,10 @@ using Microsoft.AspNetCore.Identity; using Bit.Core.Models.Table; using Bit.Api.Utilities; using Bit.Core.Models.Business; +using jsreport.AspNetCore; +using jsreport.Types; +using Bit.Api.Models; +using Stripe; namespace Bit.Api.Controllers { @@ -94,6 +98,41 @@ namespace Bit.Api.Controllers } } + [HttpGet("{id}/billing-invoice/{invoiceId}")] + [SelfHosted(NotSelfHostedOnly = true)] + [MiddlewareFilter(typeof(JsReportPipeline))] + public async Task GetBillingInvoice(string id, string invoiceId) + { + var orgIdGuid = new Guid(id); + if(!_currentContext.OrganizationOwner(orgIdGuid)) + { + throw new NotFoundException(); + } + + var organization = await _organizationRepository.GetByIdAsync(orgIdGuid); + if(organization == null) + { + throw new NotFoundException(); + } + + try + { + var invoice = await new StripeInvoiceService().GetAsync(invoiceId); + if(invoice == null || invoice.CustomerId != organization.GatewayCustomerId) + { + throw new NotFoundException(); + } + + var model = new InvoiceModel(organization, invoice); + HttpContext.JsReportFeature().Recipe(Recipe.PhantomPdf); + return View("Invoice", model); + } + catch(StripeException) + { + throw new NotFoundException(); + } + } + [HttpGet("{id}/license")] [SelfHosted(NotSelfHostedOnly = true)] public async Task GetLicense(string id, [FromQuery]Guid installationId) diff --git a/src/Api/Models/InvoiceModel.cs b/src/Api/Models/InvoiceModel.cs new file mode 100644 index 0000000000..b40ba2cb79 --- /dev/null +++ b/src/Api/Models/InvoiceModel.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using System.Linq; +using Bit.Core.Models.Table; +using Stripe; + +namespace Bit.Api.Models +{ + public class InvoiceModel + { + public InvoiceModel(Organization organization, StripeInvoice invoice) + { + // TODO: address + OurAddress1 = "567 Green St"; + OurAddress2 = "Jacksonville, FL 32256"; + OurAddress3 = "United States"; + + CustomerName = organization.BusinessName ?? "--"; + // TODO: address and vat + CustomerAddress1 = "123 Any St"; + CustomerAddress2 = "New York, NY 10001"; + CustomerAddress3 = "United States"; + CustomerVatNumber = "PT 123456789"; + + InvoiceDate = invoice.Date?.ToLongDateString(); + InvoiceDueDate = invoice.DueDate?.ToLongDateString(); + InvoiceNumber = invoice.Id; + Items = invoice.StripeInvoiceLineItems.Select(i => new Item(i)); + + SubtotalAmount = (invoice.Total / 100).ToString("C"); + VatTotalAmount = 0.ToString("C"); + TotalAmount = SubtotalAmount; + Paid = invoice.Paid; + } + + public string OurAddress1 { get; set; } + public string OurAddress2 { get; set; } + public string OurAddress3 { get; set; } + public string InvoiceDate { get; set; } + public string InvoiceDueDate { get; set; } + public string InvoiceNumber { get; set; } + public string CustomerName { get; set; } + public string CustomerVatNumber { get; set; } + public string CustomerAddress1 { get; set; } + public string CustomerAddress2 { get; set; } + public string CustomerAddress3 { get; set; } + public IEnumerable Items { get; set; } + public string SubtotalAmount { get; set; } + public string VatTotalAmount { get; set; } + public string TotalAmount { get; set; } + public bool Paid { get; set; } + public bool UsesVat => !string.IsNullOrWhiteSpace(CustomerVatNumber); + + public class Item + { + public Item(StripeInvoiceLineItem item) + { + Quantity = item.Quantity?.ToString() ?? "-"; + Amount = (item.Amount / 100).ToString("F"); + Description = item.Description ?? "--"; + } + + public string Description { get; set; } + public string Quantity { get; set; } + public string Amount { get; set; } + } + } +} diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index 06c6b6c9a3..35e7adc825 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -20,6 +20,7 @@ using Stripe; using Bit.Core.Utilities; using IdentityModel; using IdentityServer4.AccessTokenValidation; +using jsreport.AspNetCore; namespace Bit.Api { @@ -140,6 +141,16 @@ namespace Bit.Api jsonFormatter.SupportedMediaTypes.Add(textPlainMediaType); } }).AddJsonOptions(options => options.SerializerSettings.ContractResolver = new DefaultContractResolver()); + + // PDF generation + if(!globalSettings.SelfHosted) + { + services + .AddJsReport(new jsreport.Local.LocalReporting() + .UseBinary(jsreport.Binary.JsReportBinary.GetBinary()) + .AsUtility() + .Create()); + } } public void Configure( diff --git a/src/Api/Views/Organizations/Invoice.cshtml b/src/Api/Views/Organizations/Invoice.cshtml new file mode 100644 index 0000000000..000546ce17 --- /dev/null +++ b/src/Api/Views/Organizations/Invoice.cshtml @@ -0,0 +1,145 @@ +@model Bit.Api.Models.InvoiceModel + + + + + +
@Model.InvoiceNumber
+

INVOICE

+

+ @if(!string.IsNullOrWhiteSpace(Model.InvoiceDate)) + { + Date: @Model.InvoiceDate
+ } + @if(!string.IsNullOrWhiteSpace(Model.InvoiceDueDate)) + { + Due Date: @Model.InvoiceDueDate + } +

+ + + + + + + +
+

From

+

+ 8bit Solutions LLC
+ @Model.OurAddress1
+ @Model.OurAddress2
+ @Model.OurAddress3 +

+
+

To

+

+ @Model.CustomerName
+ @if(Model.UsesVat) + { + @:VAT @Model.CustomerVatNumber
+ } + @if(!string.IsNullOrWhiteSpace(Model.CustomerAddress1)) + { + @Model.CustomerAddress1
+ } + @if(!string.IsNullOrWhiteSpace(Model.CustomerAddress2)) + { + @Model.CustomerAddress2
+ } + @if(!string.IsNullOrWhiteSpace(Model.CustomerAddress3)) + { + @Model.CustomerAddress3 + } +

+
+

Items

+ + + + + + @if(Model.UsesVat) + { + + + } + + + + + @foreach(var item in Model.Items) + { + + + + @if(Model.UsesVat) + { + + + } + + + } + +
DescriptionQtyVAT %VATTotal
@item.Description@item.Quantity00.00@item.Amount
+

Totals

+ Subtotal: @Model.SubtotalAmount
+ @if(Model.UsesVat) + { + Total VAT: @Model.VatTotalAmount
+ } +
+ Total: USD @Model.TotalAmount + @if(Model.Paid) + { + + } + +