defmodule ExCheckout.Server do
use GenServer
alias ExCheckout.Customer
alias ExCheckout.Address
alias ExCheckout.Adjustment
alias ExCheckout.Invoice
alias ExCheckout.Package
alias ExCheckout.Shipment
alias ExCheckout.Receipt
alias ExCheckout.Transaction
defstruct sub_total: 0,
total: 0,
items: [],
adjustments: [],
products: [],
customer: %Customer{},
addresses: [],
transaction: %Transaction{},
transaction_module: %Transaction{},
invoice: %Invoice{},
receipt: %Receipt{}
def child_spec(init) do
%{
id: __MODULE__,
start: {__MODULE__, :start_link, [init]},
restart: :transient,
type: :worker
}
end
def start_link() do
GenServer.start_link(__MODULE__, nil)
end
def start_link([]) do
GenServer.start_link(__MODULE__, nil)
end
def start_link([cart]) do
GenServer.start_link(__MODULE__, cart)
end
@impl true
def init(nil) do
{:ok, %__MODULE__{}}
end
@impl true
def init(cart) do
adjustments =
Enum.filter(cart.adjustments, fn x ->
adjustment_valid(x)
end)
|> Enum.map(fn x ->
%Adjustment{
id: x.id,
name: x.name,
description: x.description,
type: x.type,
function: x.function
}
end)
initial_state = %__MODULE__{
items: cart.items,
products: [],
adjustments: adjustments
}
{:ok, initial_state}
end
@impl true
def handle_call({:items, data}, _, state) do
state = %{state | items: data}
{:reply, state.items, state}
end
@impl true
def handle_call({:customer, data}, _, state) do
customer = struct(Customer, data)
state = %{state | customer: customer}
{:reply, customer, state}
end
@impl true
def handle_call({:invoice}, _, state) do
{:reply, state.invoice, state}
end
@impl true
def handle_call({:payment_transaction, data}, _, state) do
receipt = %Receipt{
data: %{
subtotal: state.sub_total,
total: state.total,
adjustments: state.adjustments,
items: state.products,
transaction: state.transaction
}
}
state = %{state | transaction: data}
state = %{state | receipt: receipt}
{:reply, data, state}
end
@impl true
def handle_call({:receipt}, _, state) do
{:reply, state.receipt, state}
end
@impl true
def handle_call({:scan_items}, _, state) do
products = ExCheckout.Products.fetch(state)
state = %{state | products: products}
{:reply, products, state}
end
@impl true
def handle_call({:apply_adjustments}, _, state) do
adjustments =
Enum.reduce(state.adjustments, 0, fn x, acc ->
Adjustment.value(state, x) + acc
end)
{:reply, adjustments, state}
end
@impl true
# we prolly can add config here
def handle_call({:shipping_quote, {carriers, package}}, _, state) do
[shipping_module] = Application.get_env(:ex_checkout, :shipping_module, :not_found)
[origin_address] = Enum.filter(state.address, fn x -> x.type == :origin end)
[destination_address] = Enum.filter(state.address, fn x -> x.type == :destination end)
rates =
case shipping_module do
:not_found ->
:not_found
_ ->
origin = Address.new(origin_address)
destination = Address.new(destination_address)
package = Package.new(package)
{:ok, origin} = origin
{:ok, destination} = destination
shipment = Shipment.new(origin, destination, package)
{:ok, shipment} = shipment
shipping_module.fetch_rates(shipment, carriers)
end
{:reply, rates, state}
end
@impl true
def handle_call({:shipping_transaction, {shipment, service}}, _, state) do
[shipping_module] = Application.get_env(:ex_checkout, :shipping_module, :not_found)
transaction =
case shipping_module do
:not_found -> :not_found
_ -> shipping_module.create_transaction(shipment, service)
end
{:reply, transaction, state}
end
@impl true
def handle_call({:subtotal}, _, state) do
sub_total = Enum.map(state.products, fn x -> x.price end) |> Enum.sum()
updated = %{state | sub_total: sub_total}
{:reply, sub_total, updated}
end
@impl true
def handle_call({:total}, _, state) do
subtotal = state.sub_total
adjustments =
Enum.reduce(state.adjustments, 0, fn x, acc ->
Adjustment.value(state, x) + acc
end)
total = subtotal + adjustments
invoice = %Invoice{
data: %{subtotal: subtotal, total: total, adjustments: adjustments, items: state.products}
}
state = %{state | total: total}
state = %{state | invoice: invoice}
{:reply, state.total, state}
end
@impl true
def handle_call({:adjustments, data}, _, state) do
adjustments =
Enum.filter(data, fn x ->
adjustment_valid(%{name: x.name, type: x.type})
end)
|> Enum.map(fn x ->
%Adjustment{
id: x.id,
name: x.name,
description: x.description,
type: x.type,
function: x.function
}
end)
state = %{state | adjustments: adjustments}
{:reply, state, state}
end
@impl true
def handle_call({:addresses, data}, _, state) do
addresses =
Enum.map(data, fn x ->
%Address{
address: x.address,
type: x.type
}
end)
state = %{state | addresses: addresses}
{:reply, state, state}
end
@impl true
def handle_call({:transaction_module, transaction_module}, _, state) do
state =
case ExCheckout.Transaction.module_exists(transaction_module) do
true -> %{state | transaction_module: transaction_module}
false -> state
end
{:reply, state, state}
end
@impl true
def handle_call({:incoming_message, incoming_message}, _, state) do
handler = state.transaction_module
data = handler.process(incoming_message)
payment_transaction(self(), data)
{:reply, incoming_message, state}
end
@impl true
def handle_call({:state}, _, state) do
{:reply, state, state}
end
@impl true
def handle_call({:cart, products, adjustments}, _, state) do
state = %{state | products: products}
state = %{state | adjustments: adjustments}
{:reply, state, state}
end
defp adjustment_valid(adjustment, dataset \\ nil) do
dataset =
case(is_nil(dataset)) do
true -> Application.get_env(:ex_checkout, :adjustments, [])
false -> dataset
end
Enum.member?(dataset, adjustment)
end
def cart(pid, data) do
products = ExCheckout.Products.fetch(data)
adjustments =
Enum.filter(data.adjustments, fn x ->
adjustment_valid(%{name: x.name, type: x.type})
end)
GenServer.call(pid, {:cart, products, adjustments})
end
def state(pid) do
GenServer.call(pid, {:state})
end
def subtotal(pid) do
GenServer.call(pid, {:subtotal})
end
def total(pid) do
GenServer.call(pid, {:total})
end
def customer(pid, data) do
GenServer.call(pid, {:customer, data})
end
def address(pid, data) do
GenServer.call(pid, {:address, data})
end
def items(pid, data) do
GenServer.call(pid, {:items, data})
end
def adjustments(pid, data) do
GenServer.call(pid, {:adjustments, data})
end
def invoice(pid) do
GenServer.call(pid, {:invoice})
end
def shipping_transaction(pid, data) do
GenServer.call(pid, {:shipping_transaction, data})
end
def payment_transaction(pid, data) do
GenServer.call(pid, {:payment_transaction, data})
end
def receipt(pid) do
GenServer.call(pid, {:receipt})
end
def scan_items(pid) do
GenServer.call(pid, {:scan_items})
end
def shipping_quote(pid, data) do
GenServer.call(pid, {:shipping_quote, data})
end
def apply_adjustments(pid) do
GenServer.call(pid, {:apply_adjustments})
end
def isn(pid, type) do
GenServer.call(pid, {:isn_module, type})
end
def ipn(pid, type) do
GenServer.call(pid, {:transaction_module, type})
end
def ipn_message(pid, message) do
GenServer.call(pid, {:incoming_message, message})
end
def isn_message(pid, message) do
GenServer.call(pid, {:incoming_message, message})
end
end