defmodule Glific.Partners.Billing do
@moduledoc """
We will use this as the main context interface for all billing subscriptions and the stripe
interface.
"""
use Ecto.Schema
use Publicist
import Ecto.Changeset
import Ecto.Query, warn: false
import GlificWeb.Gettext
alias __MODULE__
require Logger
alias Glific.{
Partners,
Partners.Organization,
Partners.Saas,
Repo,
Saas.ConsultingHour,
Stats
}
alias Stripe.{
BillingPortal,
Request,
Subscription,
SubscriptionItem,
SubscriptionItem.Usage
}
# define all the required fields for
@required_fields [
:name,
:email,
:organization_id
]
# define all the optional fields for organization
@optional_fields [
:stripe_customer_id,
:stripe_payment_method_id,
:stripe_subscription_id,
:stripe_subscription_items,
:stripe_current_period_start,
:stripe_subscription_status,
:stripe_current_period_end,
:stripe_last_usage_recorded,
:currency,
:is_delinquent,
:is_active,
:deduct_tds,
:tds_amount,
:billing_period
]
@type t() :: %__MODULE__{
__meta__: Ecto.Schema.Metadata.t(),
id: non_neg_integer | nil,
stripe_customer_id: String.t() | nil,
stripe_payment_method_id: String.t() | nil,
stripe_subscription_id: String.t() | nil,
stripe_subscription_status: String.t() | nil,
stripe_subscription_items: map(),
stripe_current_period_start: DateTime.t() | nil,
stripe_current_period_end: DateTime.t() | nil,
stripe_last_usage_recorded: DateTime.t() | nil,
name: String.t() | nil,
email: String.t() | nil,
currency: String.t() | nil,
is_delinquent: boolean,
is_active: boolean() | true,
deduct_tds: boolean() | false,
tds_amount: float() | nil,
billing_period: String.t() | nil,
inserted_at: :utc_datetime | nil,
updated_at: :utc_datetime | nil
}
schema "billings" do
field :stripe_customer_id, :string
field :stripe_payment_method_id, :string
field :stripe_subscription_id, :string
field :stripe_subscription_status, :string
field :stripe_subscription_items, :map, default: %{}
field :stripe_current_period_start, :utc_datetime
field :stripe_current_period_end, :utc_datetime
field :stripe_last_usage_recorded, :utc_datetime
field :name, :string
field :email, :string
field :currency, :string
field :is_delinquent, :boolean, default: false
field :is_active, :boolean, default: true
field :deduct_tds, :boolean, default: false
field :tds_amount, :float
field :billing_period, :string
belongs_to :organization, Organization
timestamps(type: :utc_datetime)
end
@doc """
Standard changeset pattern we use for all data types
"""
@spec changeset(Billing.t(), map()) :: Ecto.Changeset.t()
def changeset(billing, attrs) do
billing
|> cast(attrs, @required_fields ++ @optional_fields)
|> validate_required(@required_fields)
|> unique_constraint(:stripe_customer_id)
end
@doc """
Create a billing record
"""
@spec create_billing(map()) :: {:ok, Billing.t()} | {:error, Ecto.Changeset.t()}
def create_billing(attrs \\ %{}) do
organization_id = Repo.get_organization_id()
# update is_active = false for all the previous billing
# records for this organizations
Billing
|> where([b], b.organization_id == ^organization_id)
|> where([b], b.is_active == true)
|> Repo.update_all(set: [is_active: false])
%Billing{}
|> Billing.changeset(Map.put(attrs, :organization_id, organization_id))
|> Repo.insert()
end
@doc """
Retrieve a billing record by clauses
"""
@spec get_billing(map()) :: Billing.t() | nil
def get_billing(clauses), do: Repo.get_by(Billing, clauses, skip_organization_id: true)
@doc """
Update the billing record
"""
@spec update_billing(Billing.t(), map()) ::
{:ok, Billing.t()} | {:error, Ecto.Changeset.t()}
def update_billing(%Billing{} = billing, attrs) do
billing
|> Billing.changeset(attrs)
|> Repo.update(skip_organization_id: true)
end
@doc """
Update the stripe customer details record
"""
@spec update_stripe_customer(Billing.t(), map()) ::
{:ok, Billing.t()} | {:error, Stripe.Error.t()}
def update_stripe_customer(%Billing{} = billing, attrs) do
with {:ok, _customer} <-
Stripe.Customer.update(
billing.stripe_customer_id,
Map.take(attrs, [:email, :name])
) do
{:ok, billing}
end
end
@doc """
Delete the billing record
"""
@spec delete_billing(Billing.t()) ::
{:ok, Billing.t()} | {:error, Ecto.Changeset.t()}
def delete_billing(%Billing{} = billing) do
Repo.delete(billing)
end
@doc """
Create a billing record in glific, a billing customer in Stripe, given an organization
"""
@spec create(Organization.t(), map()) ::
{:ok, Billing.t()} | {:error, Ecto.Changeset.t() | String.t()}
def create(organization, attrs) do
case check_required(attrs) do
{:error, error} -> {:error, error}
_ -> do_create(organization, attrs)
end
end
@spec do_create(Organization.t(), map()) :: {:ok, Billing.t()} | {:error, Ecto.Changeset.t()}
defp do_create(organization, attrs) do
{:ok, stripe_customer} =
%{
name: attrs.name,
email: attrs.email,
metadata: %{
"id" => Integer.to_string(organization.id),
"name" => organization.name
}
}
|> Stripe.Customer.create()
create_billing(
attrs
|> Map.put(:organization_id, organization.id)
|> Map.put(:stripe_customer_id, stripe_customer.id)
)
end
@spec check_stripe_key(list()) :: list()
defp check_stripe_key(errors) do
case Application.fetch_env(:stripity_stripe, :api_key) do
{:ok, _} -> errors
_ -> ["Stripe API Key not present" | errors]
end
end
@spec format_errors(list()) :: :ok | {:error, String.t()}
defp format_errors([]), do: :ok
defp format_errors(list), do: {:error, Enum.join(list, ", ")}
# We don't know what to do with billing currency as yet, but we'll figure it out soon
# In Stripe, one contact can only have one currency
@spec check_required(map()) :: :ok | {:error, String.t()}
defp check_required(attrs) do
[:name, :email, :currency]
|> Enum.reduce(
[],
fn field, acc ->
value = Map.get(attrs, field)
if is_nil(value) || value == "",
do: ["#{field} is not set" | acc],
else: acc
end
)
|> check_stripe_key()
|> format_errors()
end
@doc """
Fetch the stripe id's
"""
@spec monthly_stripe_ids :: map()
def monthly_stripe_ids,
do: Saas.stripe_ids()["monthly"]
@doc """
Fetch the stripe id's
"""
@spec quarterly_stripe_ids :: map()
def quarterly_stripe_ids,
do: Saas.stripe_ids()["quarterly"]
@doc """
Fetch the stripe tax rates
"""
@spec tax_rates :: list()
def tax_rates,
do: Saas.tax_rates()
@spec quarterly_subscription_params(Billing.t(), Organization.t()) :: map()
defp quarterly_subscription_params(billing, organization) do
# start date should be start of next quarter
anchor_timestamp =
DateTime.utc_now()
|> Timex.end_of_quarter()
|> Timex.shift(days: 1)
|> Timex.beginning_of_day()
|> DateTime.to_unix()
prices = quarterly_stripe_ids()
%{
customer: billing.stripe_customer_id,
billing_cycle_anchor: anchor_timestamp,
collection_method: "send_invoice",
proration_behavior: "none",
days_until_due: 10,
items: [
%{
price: prices["quarterly"]
}
],
metadata: %{
"id" => Integer.to_string(billing.organization_id),
"name" => organization.name
},
default_tax_rates: tax_rates()
}
end
@spec monthly_subscription_params(Billing.t(), Organization.t()) :: map()
defp monthly_subscription_params(billing, organization) do
# Temporary to make sure that the subscription starts from the beginning of next month
anchor_timestamp =
DateTime.utc_now()
|> Timex.end_of_month()
|> Timex.shift(days: 1)
|> Timex.beginning_of_day()
|> DateTime.to_unix()
prices = monthly_stripe_ids()
%{
customer: billing.stripe_customer_id,
# Temporary for existing customers.
billing_cycle_anchor: anchor_timestamp,
proration_behavior: "create_prorations",
items: [
%{
price: prices["users"]
},
%{
price: prices["messages"]
},
%{
price: prices["consulting_hours"]
}
],
metadata: %{
"id" => Integer.to_string(billing.organization_id),
"name" => organization.name
},
default_tax_rates: tax_rates()
}
end
@doc """
Update organization and stripe customer with the current payment method as returned
by stripe
"""
@spec update_payment_method(Organization.t(), String.t()) ::
{:ok, Billing.t()} | {:error, map()}
def update_payment_method(organization, stripe_payment_method_id) do
# get the billing record
billing = Repo.get_by!(Billing, %{organization_id: organization.id, is_active: true})
# first update the contact with default payment id
with {:ok, _res} <-
Stripe.PaymentMethod.attach(%{
customer: billing.stripe_customer_id,
payment_method: stripe_payment_method_id
}),
{:ok, _customer} <-
Stripe.Customer.update(billing.stripe_customer_id, %{
invoice_settings: %{default_payment_method: stripe_payment_method_id}
}) do
update_billing(billing, %{stripe_payment_method_id: stripe_payment_method_id})
|> send_update_response()
end
end
@spec send_update_response(tuple()) :: {:ok, Billing.t()} | {:error, map()}
defp send_update_response({:ok, billing}), do: {:ok, billing}
defp send_update_response({:error, _}), do: {:error, %{message: "Error while saving details"}}
@doc """
Validate entered coupon code and return with coupon details
"""
@spec get_promo_codes(any()) :: any()
def get_promo_codes(code) do
with {:ok, response} <- make_promocode_request(code) do
make_results(response.data)
end
end
defp make_results([]), do: {:error, "Invalid coupon code"}
defp make_results(response) do
result = List.first(response)
coupon =
%{code: result.code}
|> Map.put(:metadata, result.coupon.metadata)
|> Map.put(:id, result.coupon.id)
{:ok, coupon}
end
defp make_promocode_request(code) do
make_stripe_request("promotion_codes", :get, %{code: code})
end
@doc """
Create a monthly subscription.
Once the organization has entered a new payment card we create a subscription for it.
We'll do updating the card in a separate function
"""
@spec create_monthly_subscription(Organization.t(), map(), map()) ::
{:ok, Stripe.Subscription.t()} | {:pending, map()} | {:error, String.t()}
def create_monthly_subscription(organization, billing, params) do
stripe_payment_method_id = params.stripe_payment_method_id
update_payment_method(organization, stripe_payment_method_id)
|> case do
{:ok, _} ->
billing
|> setup(organization, params)
|> subscription(organization)
{:error, error} ->
Logger.info("Error while updating the card. #{inspect(error)}")
{:error, error.message}
end
end
@doc """
Create a quarterly subscription.
"""
@spec create_quarterly_subscription(Organization.t(), map()) ::
{:ok, Stripe.Subscription.t()} | {:pending, map()} | {:error, String.t()}
def create_quarterly_subscription(organization, billing) do
opts = [expand: ["latest_invoice.payment_intent", "pending_setup_intent"]]
billing
|> quarterly_subscription_params(organization)
|> Subscription.create(opts)
|> case do
{:ok, subscription} ->
update_subscription_details(subscription, organization.id, billing)
{:error, stripe_error} ->
{:error, inspect(stripe_error)}
end
end
@doc """
create subscription on the basis of billing period
"""
@spec create_period_based_subscription(Organization.t(), map(), map()) ::
{:ok, Stripe.Subscription.t()} | {:pending, map()} | {:error, String.t()}
def create_period_based_subscription(organization, billing, params) do
if params |> Map.has_key?(:billing_period) do
params.billing_period
|> case do
"MONTHLY" ->
create_monthly_subscription(organization, billing, params)
"QUARTERLY" ->
create_quarterly_subscription(organization, billing)
_ ->
{:ok, billing}
end
else
# Setting the default as monthly for now so it doesnt need any change for frontend.
# Will remove this once frontend changes are implemented
create_monthly_subscription(organization, billing, params)
end
end
@spec add_billing_period(map(), map()) :: {:error, Ecto.Changeset.t()} | {:ok, any()}
defp add_billing_period(billing, %{billing_period: billing_period}) do
update_billing(billing, %{billing_period: billing_period})
end
defp add_billing_period(billing, _) do
# setting default as monthly for now.
update_billing(billing, %{billing_period: "MONTHLY"})
end
@doc """
create subscription on the basis of billing period
"""
@spec create_subscription(Organization.t(), map()) ::
{:ok, Stripe.Subscription.t()} | {:pending, map()} | {:error, String.t()}
def create_subscription(organization, params) do
billing = Repo.get_by!(Billing, %{organization_id: organization.id, is_active: true})
subscription = create_period_based_subscription(organization, billing, params)
case subscription do
{:ok, _} -> add_billing_period(billing, params)
_ -> subscription
end
end
@spec setup(Billing.t(), Organization.t(), map()) :: Billing.t()
defp setup(billing, organization, params) do
## let's create an invoice items. We are not attaching this to the invoice
## so it will be attached automatically to the next invoice create.
{:ok, invoice_item} =
Stripe.Invoiceitem.create(%{
customer: billing.stripe_customer_id,
currency: billing.currency,
price: Saas.stripe_ids()["setup"],
tax_rates: tax_rates(),
metadata: %{
"id" => Integer.to_string(organization.id),
"name" => organization.name
}
})
apply_coupon(invoice_item.id, params)
{:ok, _invoice} =
Stripe.Invoice.create(%{
customer: billing.stripe_customer_id,
auto_advance: true,
collection_method: "charge_automatically",
metadata: %{
"id" => Integer.to_string(organization.id),
"name" => organization.name
}
})
billing
end
@spec apply_coupon(String.t(), map()) :: nil | {:error, Stripe.Error.t()} | {:ok, any()}
defp apply_coupon(invoice_item_id, %{coupon_code: coupon_code}) do
make_stripe_request("invoiceitems/#{invoice_item_id}", :post, %{
discounts: [%{coupon: coupon_code}]
})
end
defp apply_coupon(_, _), do: nil
@doc """
Adding credit to customer in Stripe
"""
@spec credit_customer(map()) :: any | non_neg_integer()
def credit_customer(transaction) do
with billing <-
get_billing(%{organization_id: transaction.organization_id}),
"draft" <- transaction.status,
true <- billing.deduct_tds do
credit = calculate_credit(billing, transaction)
# Add credit to customer
Stripe.CustomerBalanceTransaction.create(billing.stripe_customer_id, %{
amount: -credit,
currency: billing.currency
})
# Update invoice footer with message
Stripe.Invoice.update(transaction.invoice_id, %{
footer:
"TDS INR #{(credit / 100) |> trunc()} for Month of #{DateTime.utc_now().month |> Timex.month_name()} deducted above under Applied Balance section"
})
credit
end
end
# Calculate the amount to be credited to customer account
@spec calculate_credit(Billing.t(), map()) :: non_neg_integer()
defp calculate_credit(billing, transaction) do
(billing.tds_amount / 100 * transaction.amount_due) |> trunc()
end
@doc """
A common function for making Stripe API calls with params that are not supported withing Stripity Stripe
"""
@spec make_stripe_request(String.t(), atom(), map(), list()) :: any()
def make_stripe_request(endpoint, method, params, opts \\ []) do
Request.new_request(opts)
|> Request.put_endpoint(endpoint)
|> Request.put_method(method)
|> Request.put_params(params)
|> Request.make_request()
end
@spec subscription(Billing.t(), Organization.t()) ::
{:ok, Stripe.Subscription.t()} | {:pending, map()} | {:error, String.t()}
defp subscription(billing, organization) do
opts = [expand: ["latest_invoice.payment_intent", "pending_setup_intent"]]
billing
|> monthly_subscription_params(organization)
|> Subscription.create(opts)
|> case do
# subscription is active, we need to update the same information via the
# webhook call 'invoice.paid' also, so might need to refactor this at
# a later date
{:ok, subscription} ->
update_subscription_details(subscription, organization.id, billing)
# if subscription requires client intervention (most likely for India, we need this)
# we need to send back info to the frontend
cond do
subscription_requires_auth?(subscription) ->
{:pending,
%{
status: :pending,
organization: organization,
client_secret: subscription.pending_setup_intent.client_secret
}}
subscription.status == "active" ->
## we can add more field as per our need
{:ok, %{status: :active}}
true ->
{:error,
dgettext("errors", "Not handling %{return} value", return: inspect(subscription))}
end
{:error, stripe_error} ->
{:error, inspect(stripe_error)}
end
end
@doc """
Update organization subscription plan
"""
@spec update_subscription(Billing.t(), Organization.t()) :: Organization.t()
def update_subscription(billing, %{status: :suspended} = organization) do
billing.stripe_subscription_items
|> Map.values()
|> Enum.each(fn subscription_item ->
SubscriptionItem.delete(subscription_item, %{clear_usage: false}, [])
end)
params = %{
proration_behavior: "create_prorations",
items: [
%{
price: monthly_stripe_ids()["inactive"],
quantity: 1,
tax_rates: tax_rates()
}
],
metadata: %{
"id" => Integer.to_string(billing.organization_id),
"name" => organization.name
}
}
SubscriptionItem.delete(
billing.stripe_subscription_items[monthly_stripe_ids()["monthly"]],
%{},
[]
)
Stripe.Subscription.update(billing.stripe_subscription_id, params, [])
organization
end
def update_subscription(billing, %{status: status} = organization)
when status in [:inactive, :ready_to_delete] do
## let's delete the subscription by end of that month and deactivate the
## billing when we change the status to inactive and ready to delete.
## delete subscription is no longer available.
## Stripe.Subscription.delete(billing.stripe_customer_id, %{at_period_end: true})
update_billing(billing, %{is_active: false})
organization
end
def update_subscription(_billing, organization), do: organization
# return a map which maps glific product ids to subscription item ids
@spec subscription_details(Stripe.Subscription.t()) :: map()
defp subscription_details(subscription),
do: %{
stripe_subscription_id: subscription.id,
is_delinquent: false
}
@spec subscription_dates(Stripe.Subscription.t()) :: map()
defp subscription_dates(subscription) do
period_start = DateTime.from_unix!(subscription.current_period_start)
period_end = DateTime.from_unix!(subscription.current_period_end)
%{
stripe_current_period_start: period_start,
stripe_current_period_end: period_end
}
end
# return a map which maps glific product ids to subscription item ids
@spec subscription_items(Stripe.Subscription.t()) :: map()
defp subscription_items(%{items: items} = _subscription) do
v =
items.data
|> Enum.reduce(
%{},
fn item, acc ->
Map.put(acc, item.price.id, item.id)
end
)
%{stripe_subscription_items: v}
end
# return a map which maps glific product ids to subscription item ids
@spec subscription_status(Stripe.Subscription.t()) :: map()
defp subscription_status(subscription) do
cond do
subscription_requires_auth?(subscription) ->
%{stripe_subscription_status: "pending"}
subscription.status == "active" ->
%{stripe_subscription_status: "active"}
true ->
%{stripe_subscription_status: "pending"}
end
end
# function to check if the subscription requires another authentication i.e 3D
@spec subscription_requires_auth?(Stripe.Subscription.t()) :: boolean()
defp subscription_requires_auth?(%{pending_setup_intent: pending_setup_intent})
when is_map(pending_setup_intent),
do: Map.get(pending_setup_intent, :status, "") == "requires_action"
defp subscription_requires_auth?(_subscription), do: false
@doc """
Update subscription details. We will also use this method while updating the details form webhook.
"""
@spec update_subscription_details(Stripe.Subscription.t(), non_neg_integer(), Billing.t() | nil) ::
{:ok, Stripe.Subscription.t()} | {:error, String.t()}
def update_subscription_details(subscription, organization_id, nil) do
Repo.fetch_by(Billing, %{
stripe_subscription_id: subscription.id,
organization_id: organization_id
})
|> case do
{:ok, billing} ->
update_subscription_details(subscription, organization_id, billing)
_ ->
Logger.info(
"Error while updating the subscription details for subscription #{subscription.id} and organization_id: #{organization_id}"
)
message = """
Did not find Billing object for Subscription: #{subscription.id}, org: #{organization_id}
"""
{:error, message}
end
end
def update_subscription_details(subscription, _organization_id, billing) do
params =
%{}
|> Map.merge(subscription |> subscription_details())
|> Map.merge(subscription |> subscription_dates())
|> Map.merge(subscription |> subscription_items())
|> Map.merge(subscription |> subscription_status())
update_billing(billing, params)
{:ok, subscription}
end
@doc """
Stripe subscription created callback via webhooks.
We are using this to update the prorate data with monthly billing.
"""
@spec subscription_created_callback(Stripe.Subscription.t(), non_neg_integer()) ::
:ok | {:error, Stripe.Error.t()}
def subscription_created_callback(subscription, org_id) do
## we can not add prorate for 3d secure cards. That's why we are using the
## subscription created callback to add the monthly subscription with prorate
## data.
with billing <- get_billing(%{organization_id: org_id}),
"MONTHLY" <- billing.billing_period,
false <-
billing.stripe_subscription_items |> Map.has_key?(monthly_stripe_ids()["monthly"]) do
proration_date = DateTime.utc_now() |> DateTime.to_unix()
make_stripe_request("subscription_items", :post, %{
subscription: subscription.id,
proration_behavior: "create_prorations",
proration_date: proration_date,
price: monthly_stripe_ids()["monthly"],
quantity: 1
})
else
_ -> {:ok, subscription}
end
end
# get dates and times in the right format for other functions
@spec format_dates(DateTime.t(), DateTime.t()) :: map()
defp format_dates(start_date, end_date) do
end_date = end_date |> Timex.end_of_day()
%{
start_usage_date: start_date |> DateTime.to_date(),
end_usage_date: end_date |> DateTime.to_date(),
end_usage_datetime: end_date,
time: end_date |> DateTime.to_unix()
}
end
@doc """
Update the usage record for all active subscriptions on a daily and weekly basis
"""
@spec update_usage(non_neg_integer, map()) :: :ok
def update_usage(_organization_id, %{time: time}) do
record_date = time |> Timex.end_of_day()
# if record date is sunday, we need to record previous weeks usage
# or if it is the end of month then record usage for the remaining days of week
if Date.day_of_week(record_date) == 7 ||
Timex.days_in_month(record_date) - record_date.day == 0,
do: period_usage(record_date)
:ok
end
@doc """
This is called on a regular schedule to update usage.
"""
@spec period_usage(DateTime.t()) :: :ok
def period_usage(record_date) do
Billing
|> where([b], b.is_active == true)
|> Repo.all(skip_organization_id: true)
|> Enum.each(&update_period_usage(&1, record_date))
end
@spec update_period_usage(Billing.t(), DateTime.t()) :: :ok
defp update_period_usage(billing, end_date) do
start_date =
if is_nil(billing.stripe_last_usage_recorded),
# if we don't have last_usage, set it to start of the week as we update it on weekly basis
do: Timex.beginning_of_week(end_date),
# We know the last time recorded usage, we bump the date
# to the next day for this period
else: billing.stripe_last_usage_recorded
record_usage(billing.organization_id, start_date, end_date)
end
@doc """
Record the usage for a specific organization from start_date to end_date
both dates inclusive
"""
@spec record_usage(non_neg_integer(), DateTime.t(), DateTime.t()) :: :ok
def record_usage(organization_id, start_date, end_date) do
# putting organization id in process for fetching stat data
Repo.put_process_state(organization_id)
billing = Repo.get_by!(Billing, %{organization_id: organization_id, is_active: true})
subscription_items = billing.stripe_subscription_items
# formatting dates
dates = format_dates(start_date, end_date)
organization_id
|> update_message_usage(dates, subscription_items)
|> update_consulting_hour(start_date, end_date, dates, subscription_items)
if Timex.days_in_month(end_date) - end_date.day == 0,
do: add_metered_users(organization_id, end_date, subscription_items)
{:ok, _} = update_billing(billing, %{stripe_last_usage_recorded: dates.end_usage_datetime})
:ok
end
@spec update_message_usage(non_neg_integer(), map(), map()) :: non_neg_integer
defp update_message_usage(organization_id, dates, subscription_items) do
case Stats.usage(organization_id, dates.start_usage_date, dates.end_usage_date) do
nil ->
organization_id
usage ->
record_subscription_item(
subscription_items[monthly_stripe_ids()["messages"]],
# dividing the messages as every 10 message is 1 unit in stripe messages subscription item
div(usage.messages, 10),
dates.time,
"messages: #{organization_id}, #{Date.to_string(dates.start_usage_date)}"
)
end
organization_id
end
@spec update_consulting_hour(non_neg_integer(), DateTime.t(), DateTime.t(), map(), map()) ::
true | {:error, Stripe.Error.t()} | {:ok, Stripe.SubscriptionItem.Usage.t()}
defp update_consulting_hour(organization_id, start_date, end_date, dates, subscription_items) do
with consulting_hours <-
calculate_consulting_hours(organization_id, start_date, end_date).duration,
false <- is_nil(consulting_hours) do
record_subscription_item(
subscription_items[monthly_stripe_ids()["consulting_hours"]],
# dividing the consulting hours as every 15 min is 1 unit in stripe consulting hour subscription item
div(consulting_hours, 15),
dates.time,
"consulting: #{organization_id}, #{Date.to_string(dates.start_usage_date)}"
)
end
end
@spec add_metered_users(non_neg_integer(), DateTime.t(), map()) :: :ok
defp add_metered_users(organization_id, end_date, subscription_items) do
start_date = end_date |> Timex.beginning_of_month() |> Timex.beginning_of_day()
dates = format_dates(start_date, end_date)
Logger.info(
"Updating metered user in billing for org_id: #{organization_id} between #{dates.start_usage_date} and #{dates.end_usage_date}"
)
case Stats.usage(organization_id, dates.start_usage_date, dates.end_usage_date) do
%{messages: _messages, users: users} ->
record_subscription_item(
subscription_items[monthly_stripe_ids()["users"]],
users,
dates.time,
"users: #{organization_id}, #{Date.to_string(dates.start_usage_date)}"
)
nil ->
:ok
end
end
# record the usage against a subscription item in stripe
@spec record_subscription_item(String.t(), pos_integer, pos_integer, String.t()) ::
{:ok, Usage.t()} | {:error, Stripe.Error.t()}
defp record_subscription_item(subscription_item_id, quantity, time, idempotency) do
Usage.create(
subscription_item_id,
%{
quantity: quantity,
timestamp: time
},
idempotency_key: idempotency
)
end
@spec calculate_consulting_hours(non_neg_integer(), DateTime.t(), DateTime.t()) :: map()
defp calculate_consulting_hours(organization_id, start_date, end_date) do
ConsultingHour
|> where([ch], ch.organization_id == ^organization_id)
|> where([ch], ch.is_billable == true)
|> where([ch], ch.inserted_at >= ^start_date)
|> where([ch], ch.inserted_at <= ^end_date)
|> select([f], %{duration: sum(f.duration)})
|> Repo.one(skip_organization_id: true)
end
@doc """
fetches customer portal url of organization with billing status as active
"""
@spec customer_portal_link(Billing.t()) :: {:ok, any()} | {:error, String.t()}
def customer_portal_link(billing) do
organization = Partners.organization(billing.organization_id)
params = %{
customer: billing.stripe_customer_id,
return_url: "https://#{organization.shortcode}.tides.coloredcow.com/settings/billing"
}
BillingPortal.Session.create(params)
|> case do
{:ok, response} ->
{:ok, %{url: response.url, return_url: response.return_url}}
_ ->
{:error, "Invalid Stripe Key"}
end
end
@doc """
List of available billing period
"""
@spec list_billing_period() :: [String.t()]
def list_billing_period do
[
"MONTHLY",
"QUARTERLY",
"MANUAL"
]
end
# events that we need to handle, delete comment once handled :)
# invoice.upcoming
# invoice.created - send final usage record here, also send on a weekly basis, to avoid error
# invoice.paid
# invoice.payment_failed
# invoice.payment_action_required
end