Packages

phoenix_kit

1.7.21
1.7.209 1.7.208 1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

Files

Jump to
phoenix_kit lib modules billing schemas billing_profile.ex
Raw

lib/modules/billing/schemas/billing_profile.ex

defmodule PhoenixKit.Modules.Billing.BillingProfile do
@moduledoc """
Billing profile schema for PhoenixKit Billing system.
Stores user billing information for individuals and companies (EU Standard).
Used for generating invoices and order billing snapshots.
## Schema Fields
### Profile Identity
- `user_id`: Foreign key to the user
- `type`: Profile type - "individual" or "company"
- `is_default`: Whether this is the user's default billing profile
- `name`: Display name for the profile
### Individual Fields
- `first_name`, `last_name`, `middle_name`: Person's name
- `phone`: Contact phone number
- `email`: Billing email (can differ from user email)
### Company Fields (EU Standard)
- `company_name`: Legal company name
- `company_vat_number`: EU VAT Number (e.g., "EE123456789")
- `company_registration_number`: Company registration number
- `company_legal_address`: Registered legal address
### Billing Address
- `address_line1`, `address_line2`: Street address
- `city`, `state`, `postal_code`, `country`: Location
## Usage Examples
# Create individual billing profile
{:ok, profile} = Billing.create_billing_profile(user, %{
type: "individual",
first_name: "John",
last_name: "Doe",
email: "john@example.com",
address_line1: "123 Main St",
city: "Tallinn",
country: "EE",
is_default: true
})
# Create company billing profile
{:ok, profile} = Billing.create_billing_profile(user, %{
type: "company",
company_name: "Acme Corp OÜ",
company_vat_number: "EE123456789",
company_registration_number: "12345678",
address_line1: "Business Park 1",
city: "Tallinn",
country: "EE"
})
"""
use Ecto.Schema
import Ecto.Changeset
import Ecto.Query, warn: false
alias PhoenixKit.Modules.Billing.CountryData
alias PhoenixKit.Users.Auth.User
@primary_key {:id, :id, autogenerate: true}
@valid_types ~w(individual company)
schema "phoenix_kit_billing_profiles" do
field :uuid, Ecto.UUID
field :type, :string, default: "individual"
field :is_default, :boolean, default: false
field :name, :string
# Individual fields
field :first_name, :string
field :last_name, :string
field :middle_name, :string
field :phone, :string
field :email, :string
# Company fields (EU Standard)
field :company_name, :string
field :company_vat_number, :string
field :company_registration_number, :string
field :company_legal_address, :string
# Billing address
field :address_line1, :string
field :address_line2, :string
field :city, :string
field :state, :string
field :postal_code, :string
field :country, :string, default: "EE"
field :metadata, :map, default: %{}
belongs_to :user, User
timestamps(type: :utc_datetime_usec)
end
@doc """
Creates a changeset for billing profile creation and updates.
"""
def changeset(profile, attrs) do
profile
|> cast(attrs, [
:user_id,
:type,
:is_default,
:name,
:first_name,
:last_name,
:middle_name,
:phone,
:email,
:company_name,
:company_vat_number,
:company_registration_number,
:company_legal_address,
:address_line1,
:address_line2,
:city,
:state,
:postal_code,
:country,
:metadata
])
|> validate_required([:user_id, :type])
|> validate_inclusion(:type, @valid_types)
|> validate_length(:country, is: 2)
|> validate_format(:email, ~r/^[^\s]+@[^\s]+$/, message: "must be a valid email address")
|> validate_type_specific_fields()
|> validate_vat_number()
|> maybe_set_display_name()
|> foreign_key_constraint(:user_id)
|> maybe_generate_uuid()
end
defp maybe_generate_uuid(changeset) do
case get_field(changeset, :uuid) do
nil -> put_change(changeset, :uuid, UUIDv7.generate())
_ -> changeset
end
end
defp validate_type_specific_fields(changeset) do
type = get_field(changeset, :type)
case type do
"individual" ->
changeset
|> validate_required([:first_name, :last_name], message: "is required for individuals")
"company" ->
changeset
|> validate_required([:company_name], message: "is required for companies")
_ ->
changeset
end
end
defp validate_vat_number(changeset) do
vat = get_field(changeset, :company_vat_number)
country = get_field(changeset, :country)
cond do
is_nil(vat) or vat == "" ->
changeset
CountryData.eu_member?(country) ->
# Basic EU VAT format validation
if Regex.match?(~r/^[A-Z]{2}[0-9A-Z]{2,12}$/, String.upcase(vat)) do
put_change(changeset, :company_vat_number, String.upcase(vat))
else
add_error(
changeset,
:company_vat_number,
"must be a valid EU VAT number (e.g., #{country}123456789)"
)
end
true ->
changeset
end
end
defp maybe_set_display_name(changeset) do
if get_field(changeset, :name) do
changeset
else
type = get_field(changeset, :type)
name =
case type do
"individual" ->
first = get_field(changeset, :first_name) || ""
last = get_field(changeset, :last_name) || ""
String.trim("#{first} #{last}")
"company" ->
get_field(changeset, :company_name) || ""
_ ->
""
end
if name != "" do
put_change(changeset, :name, name)
else
changeset
end
end
end
@doc """
Returns a snapshot of billing profile for order/invoice storage.
This creates an immutable copy of billing details at a point in time.
"""
def to_snapshot(%__MODULE__{} = profile) do
%{
profile_id: profile.id,
type: profile.type,
name: profile.name,
# Individual
first_name: profile.first_name,
last_name: profile.last_name,
middle_name: profile.middle_name,
phone: profile.phone,
email: profile.email,
# Company
company_name: profile.company_name,
company_vat_number: profile.company_vat_number,
company_registration_number: profile.company_registration_number,
company_legal_address: profile.company_legal_address,
# Address
address_line1: profile.address_line1,
address_line2: profile.address_line2,
city: profile.city,
state: profile.state,
postal_code: profile.postal_code,
country: profile.country,
# Timestamp
snapshot_at: DateTime.utc_now()
}
|> Enum.reject(fn {_k, v} -> is_nil(v) end)
|> Map.new()
end
@doc """
Returns formatted address as a multi-line string.
"""
def formatted_address(%__MODULE__{} = profile) do
[
profile.address_line1,
profile.address_line2,
[profile.postal_code, profile.city] |> Enum.reject(&is_nil/1) |> Enum.join(" "),
profile.state,
profile.country
]
|> Enum.reject(&(is_nil(&1) or &1 == ""))
|> Enum.join("\n")
end
@doc """
Returns the display name for the billing profile.
"""
def display_name(%__MODULE__{name: name}) when is_binary(name) and name != "", do: name
def display_name(%__MODULE__{type: "individual", first_name: first, last_name: last}) do
"#{first} #{last}" |> String.trim()
end
def display_name(%__MODULE__{type: "company", company_name: name}), do: name || ""
def display_name(_), do: ""
end