359 lines
15 KiB
Plaintext
359 lines
15 KiB
Plaintext
<% @page_title = "Project Billing"
|
|
|
|
if @project.billing_info
|
|
@invoices = Serializers::Invoice.serialize(@project.invoices.prepend(@project.current_invoice))
|
|
end
|
|
|
|
@usage_alerts = Serializers::UsageAlert.serialize(@project.usage_alerts_dataset.eager(:user)) %>
|
|
|
|
<% if billing_info = @project.billing_info %>
|
|
<%== part(
|
|
"components/page_header",
|
|
breadcrumbs: [%w[Projects /project], [@project_data[:name], @project_data[:path]], %w[Billing #]]
|
|
) %>
|
|
|
|
<div class="grid gap-6">
|
|
<!-- Summary -->
|
|
<div>
|
|
<dl class="grid grid-cols-2 gap-5 <%= (@project_data[:discount] > 0) ? "sm:grid-cols-4" : "sm:grid-cols-3" %>">
|
|
<div class="overflow-hidden rounded-lg bg-white px-4 py-5 shadow sm:p-6">
|
|
<dt class="truncate text-sm font-medium text-gray-500">Current Usage</dt>
|
|
<dd class="mt-1 text-3xl font-semibold tracking-tight text-gray-900"><%= @invoices.first[:subtotal] %></dd>
|
|
</div>
|
|
<div class="overflow-hidden rounded-lg bg-white px-4 py-5 shadow sm:p-6">
|
|
<dt class="truncate text-sm font-medium text-gray-500">Last Month</dt>
|
|
<dd class="mt-1 text-3xl font-semibold tracking-tight text-gray-900"><%= (@invoices.count > 1) ? @invoices[1][:subtotal] : "-" %></dd>
|
|
</div>
|
|
<div class="overflow-hidden rounded-lg bg-white px-4 py-5 shadow sm:p-6">
|
|
<dt class="truncate text-sm font-medium text-gray-500">Remaining Credit</dt>
|
|
<dd class="mt-1 text-3xl font-semibold tracking-tight text-gray-900"><%= "$%0.02f" % @project_data[:credit] %></dd>
|
|
</div>
|
|
<% if @project_data[:discount] > 0 %>
|
|
<div class="overflow-hidden rounded-lg bg-white px-4 py-5 shadow sm:p-6">
|
|
<dt class="truncate text-sm font-medium text-gray-500">Discount</dt>
|
|
<dd class="mt-1 text-3xl font-semibold tracking-tight text-gray-900"><%= @project_data[:discount] %>%</dd>
|
|
</div>
|
|
<% end %>
|
|
</dl>
|
|
</div>
|
|
<!-- Billing Info Update Card -->
|
|
<% stripe_data = billing_info.stripe_data %>
|
|
<div class="md:flex md:items-center md:justify-between pb-1 lg:pb-2">
|
|
<div class="min-w-0 flex-1">
|
|
<h3 class="text-2xl font-bold leading-7 text-gray-900 sm:truncate sm:text-2xl sm:tracking-tight">
|
|
Billing Details
|
|
</h3>
|
|
</div>
|
|
</div>
|
|
<div class="overflow-hidden rounded-lg shadow ring-1 ring-black ring-opacity-5 bg-white divide-y divide-gray-200">
|
|
<form action="<%= "#{@project_data[:path]}/billing" %>" method="POST">
|
|
<%== csrf_tag("#{@project_data[:path]}/billing") %>
|
|
<div class="px-4 py-5 sm:p-6">
|
|
<div class="space-y-12">
|
|
<div>
|
|
<div class="mt-6 grid grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-8">
|
|
<div class="sm:col-span-4">
|
|
<%== part(
|
|
"components/form/text",
|
|
name: "name",
|
|
label: "Billing Name",
|
|
value: stripe_data["name"],
|
|
attributes: {
|
|
required: true
|
|
}
|
|
) %>
|
|
</div>
|
|
<div class="sm:col-span-4">
|
|
<%== part(
|
|
"components/form/text",
|
|
name: "email",
|
|
label: "Billing Email",
|
|
value: stripe_data["email"],
|
|
attributes: {
|
|
required: true
|
|
}
|
|
) %>
|
|
</div>
|
|
<div class="sm:col-span-4 md:col-span-2">
|
|
<%== part(
|
|
"components/form/select",
|
|
name: "country",
|
|
label: "Country",
|
|
placeholder: "Select a Country",
|
|
selected: stripe_data["country"],
|
|
options:
|
|
ISO3166::Country
|
|
.all
|
|
.reject { Config.sanctioned_countries.include?(it.alpha2) }
|
|
.sort_by(&:common_name)
|
|
.map { |c| [c.alpha2, c.common_name] }
|
|
.to_h
|
|
.to_a,
|
|
attributes: {
|
|
required: true
|
|
}
|
|
) %>
|
|
</div>
|
|
<div class="sm:col-span-4 md:col-span-2">
|
|
<%== part("components/form/text", name: "state", label: "State", value: stripe_data["state"]) %>
|
|
</div>
|
|
<div class="sm:col-span-4 md:col-span-2">
|
|
<%== part("components/form/text", name: "city", label: "City", value: stripe_data["city"]) %>
|
|
</div>
|
|
<div class="sm:col-span-4 md:col-span-2">
|
|
<%== part("components/form/text", name: "postal_code", label: "Postal Code", value: stripe_data["postal_code"]) %>
|
|
</div>
|
|
<div class="col-span-full">
|
|
<%== part(
|
|
"components/form/textarea",
|
|
name: "address",
|
|
label: "Address",
|
|
value: stripe_data["address"],
|
|
attributes: {
|
|
required: true
|
|
}
|
|
) %>
|
|
</div>
|
|
<div class="sm:col-span-4">
|
|
<%== part(
|
|
"components/form/text",
|
|
name: "tax_id",
|
|
label: billing_info.country&.in_eu_vat? ? "VAT ID" : "Tax ID",
|
|
value: stripe_data["tax_id"]
|
|
) %>
|
|
<% if billing_info.country&.in_eu_vat? %>
|
|
<div class="mt-2 text-sm text-gray-500">
|
|
<% if billing_info.country.alpha2 == "NL" %>
|
|
21% VAT is collected from customers located in the Netherlands.
|
|
<% elsif stripe_data["tax_id"].to_s.strip.empty? %>
|
|
EU registered business can enter their VAT ID to remove VAT from future invoices.
|
|
<% else %>
|
|
VAT subject to reverse charge.
|
|
<% end %>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
<div class="sm:col-span-4">
|
|
<%== part("components/form/text", name: "company_name", label: "Company Name", value: stripe_data["company_name"]) %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="px-4 py-5 sm:p-6">
|
|
<div class="flex items-center justify-end gap-x-6">
|
|
<%== part("components/form/submit_button", text: "Update") %>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<!-- Payment Methods Card -->
|
|
<%== part(
|
|
"components/table_card",
|
|
title: "Payment Methods",
|
|
right_items: [
|
|
part("components/button", text: "Add Payment Method", link: "#{@project_data[:path]}/billing/payment-method/create")
|
|
],
|
|
rows:
|
|
billing_info.payment_methods.map do |pm|
|
|
[
|
|
[
|
|
"#{pm.stripe_data["brand"].capitalize} ending in #{pm.stripe_data["last4"]}",
|
|
"Expires #{pm.stripe_data["exp_month"]}/#{pm.stripe_data["exp_year"]}",
|
|
"Added on #{pm.created_at.strftime("%B %d, %Y")}",
|
|
[
|
|
"delete_button",
|
|
{
|
|
component: {
|
|
url: "#{@project_data[:path]}/billing/payment-method/#{pm.ubid}?project_id=#{@project.ubid}",
|
|
csrf_url: "#{@project_data[:path]}/billing/payment-method/#{pm.ubid}",
|
|
confirmation: pm.stripe_data["last4"]
|
|
},
|
|
extra_class: "flex justify-end"
|
|
}
|
|
]
|
|
],
|
|
{ id: "payment-method-#{pm.ubid}" }
|
|
]
|
|
end,
|
|
empty_state: "No payment methods. Add new payment method to able create resources in project."
|
|
) %>
|
|
<%== part("components/discount_code") %>
|
|
<!-- Invoices -->
|
|
<div>
|
|
<div class="md:flex md:items-center md:justify-between pb-1 lg:pb-2">
|
|
<div class="min-w-0 flex-1">
|
|
<h3 class="text-2xl font-bold leading-7 text-gray-900 sm:truncate sm:text-2xl sm:tracking-tight">
|
|
Invoices
|
|
</h3>
|
|
</div>
|
|
</div>
|
|
<div class="overflow-hidden rounded-lg shadow ring-1 ring-black ring-opacity-5 bg-white divide-y divide-gray-200">
|
|
<table class="min-w-full divide-y divide-gray-300">
|
|
<thead class="bg-gray-50">
|
|
<tr>
|
|
<th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6">Invoice</th>
|
|
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Amount</th>
|
|
<th scope="col" class="py-3.5 pl-3 pr-4 sm:pr-6 text-left text-sm font-semibold text-gray-900">Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-gray-200 bg-white">
|
|
<% @invoices.each do |inv| %>
|
|
<tr id="invoice-<%= inv[:ubid]%>">
|
|
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6" scope="row">
|
|
<a
|
|
href="<%= @project_data[:path] + "/billing" + inv[:path] %>"
|
|
target="_blank"
|
|
class="text-orange-600 hover:text-orange-700"
|
|
>
|
|
<%= inv[:name] %>
|
|
</a>
|
|
<span class="text-xs text-gray-400 italic">
|
|
<% if inv[:invoice_number] %>
|
|
#<%= inv[:invoice_number] %>
|
|
<% else %>
|
|
(not finalized)
|
|
<% end %>
|
|
</span>
|
|
</td>
|
|
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
|
<%= inv[:total] %>
|
|
<% if inv[:total] != inv[:subtotal] %>
|
|
<span class="text-xs italic">(<%= inv[:subtotal] %>)</span>
|
|
<% end %>
|
|
</td>
|
|
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
|
<%= inv[:status] %>
|
|
</td>
|
|
</tr>
|
|
<% end %>
|
|
<% if @invoices.count == 1 %>
|
|
<tr>
|
|
<td colspan="3">
|
|
<div class="text-center text-lg p-4">No invoices finalized yet. Invoice for the current month will be created on the first day of next
|
|
month.</div>
|
|
</td>
|
|
</tr>
|
|
<% end %>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<% else %>
|
|
<form action="<%= "#{@project_data[:path]}/billing" %>" method="POST">
|
|
<%== csrf_tag("#{@project_data[:path]}/billing") %>
|
|
|
|
<%== part(
|
|
"components/empty_state",
|
|
icon: "hero-banknotes",
|
|
title: "No billing information",
|
|
description: "Get started by adding new billing information.",
|
|
button_title: "Add new billing information"
|
|
) %>
|
|
</form>
|
|
<br/>
|
|
<%== part("components/discount_code") %>
|
|
<% end %>
|
|
<br/>
|
|
<!-- Usage alerts -->
|
|
<div>
|
|
<div class="md:flex md:items-center md:justify-between pb-1 lg:pb-2">
|
|
<div class="min-w-0 flex-1">
|
|
<h3 class="text-2xl font-bold leading-7 text-gray-900 sm:truncate sm:text-2xl sm:tracking-tight">
|
|
Usage Alerts
|
|
</h3>
|
|
</div>
|
|
</div>
|
|
<div class="grid gap-6">
|
|
<div class="overflow-hidden rounded-lg shadow ring-1 ring-black ring-opacity-5 bg-white divide-y divide-gray-200">
|
|
<!-- Add usage alert -->
|
|
<div class="px-4 py-5 sm:p-6">
|
|
<form action="<%= "#{@project_data[:path]}/usage-alert" %>" role="form" method="POST">
|
|
<%== csrf_tag("#{@project_data[:path]}/usage-alert") %>
|
|
<div class="space-y-4">
|
|
<div>
|
|
<h2 class="text-lg font-medium leading-6 text-gray-900">Add new usage alert</h2>
|
|
<p class="mt-1 text-sm text-gray-500">
|
|
Usage alerts will be sent to your email address. If you want to send alerts to specific email
|
|
addresses, you need to log in with that email address.
|
|
<br/>
|
|
<br/>
|
|
Please note that alerts are only for informational purposes and no action is taken automatically.
|
|
</p>
|
|
</div>
|
|
<div class="grid grid-cols-12 gap-6">
|
|
<div class="col-span-5 sm:col-span-3">
|
|
<%== part(
|
|
"components/form/text",
|
|
name: "alert_name",
|
|
label: "Alert Name",
|
|
attributes: {
|
|
required: true,
|
|
placeholder: "Alert Name"
|
|
},
|
|
extra_class: "pr-3"
|
|
) %>
|
|
</div>
|
|
<div class="col-span-5 sm:col-span-3">
|
|
<%== part(
|
|
"components/form/text",
|
|
name: "limit",
|
|
label: "Limit",
|
|
type: "number",
|
|
attributes: {
|
|
required: true,
|
|
placeholder: "100"
|
|
},
|
|
extra_class: "pr-3"
|
|
) %>
|
|
</div>
|
|
<div class="col-span-2 sm:col-span-3 flex justify-begin items-end">
|
|
<%== part("components/form/submit_button", text: "Add") %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
|
|
</div>
|
|
<!-- List alerts -->
|
|
<% if @usage_alerts.count > 0 %>
|
|
<table class="min-w-full divide-y divide-gray-300">
|
|
<thead class="bg-gray-50">
|
|
<tr>
|
|
<th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-4">Name</th>
|
|
<th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-4">Limit</th>
|
|
<th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-4">E-mail</th>
|
|
<th scope="col" class="py-3.5 pl-3 pr-4 sm:pr-4">
|
|
<span class="sr-only">Remove</span>
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-gray-200 bg-white">
|
|
<% @usage_alerts.each do |alert| %>
|
|
<tr id="alert-<%= alert[:ubid]%>" class="whitespace-nowrap text-sm font-medium">
|
|
<td class="py-4 pl-4 pr-3 text-gray-900 sm:pl-6" scope="row"><%= alert[:name] %></td>
|
|
<td class="py-4 pl-4 pr-3 text-gray-900 sm:pl-6" scope="row"><%= alert[:limit] %></td>
|
|
<td class="py-4 pl-4 pr-3 text-gray-900 sm:pl-6" scope="row"><%= alert[:email] %></td>
|
|
<td class="py-4 pl-3 pr-4 text-right sm:pr-6">
|
|
<%== part(
|
|
"components/delete_button",
|
|
text: "Remove",
|
|
url: "#{@project_data[:path]}/usage-alert/#{alert[:ubid]}",
|
|
confirmation: alert[:name]
|
|
) %>
|
|
</td>
|
|
</tr>
|
|
<% end %>
|
|
</tbody>
|
|
</table>
|
|
<% else %>
|
|
<div class="text-center p-4">
|
|
No usage alerts...
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
</div>
|