Current section
Files
Jump to
Current section
Files
lib/zepto/mock_server/state.ex
defmodule Zepto.MockServer.State do
@moduledoc """
Business logic layer for the mock server.
Handles contact creation with proper structure and delegates to Storage.
"""
alias Zepto.MockServer.Storage
alias Zepto.MockServer.Schema.Contact
alias Zepto.MockServer.Schema.Payment
# Contact operations
@doc """
Lists contacts with optional filters and pagination.
"""
def list_contacts(params \\ %{}) do
{contacts, pagination} = Storage.list_contacts(params)
api_contacts = Enum.map(contacts, &Contact.to_api_response/1)
{api_contacts, pagination}
end
@doc """
Gets a contact by ID.
"""
def get_contact(id) do
case Storage.get_contact(id) do
{:ok, contact} -> {:ok, Contact.to_api_response(contact)}
{:error, :not_found} = error -> error
end
end
@doc """
Creates an "anyone" contact (for payments only).
"""
def create_anyone_contact(params) do
id = generate_uuid()
bank_account_id = generate_uuid()
bank_account = %{
"id" => bank_account_id,
"account_number" => params["account_number"],
"branch_code" => params["branch_code"],
"bank_name" => resolve_bank_name(params["branch_code"]),
"state" => "active",
"iav_provider" => nil,
"iav_status" => nil,
"blocks" => %{"debits_blocked" => false, "credits_blocked" => false}
}
links = %{
"add_bank_connection" => build_bank_connection_url(id)
}
attrs = %{
id: id,
name: params["name"],
email: params["email"],
type: "anyone",
metadata: params["metadata"] || %{},
bank_account: bank_account,
links: links
}
case Storage.insert_contact(attrs) do
{:ok, contact} -> {:ok, Contact.to_api_response(contact)}
{:error, changeset} -> {:error, format_changeset_errors(changeset)}
end
end
@doc """
Creates a receivable contact (can receive funds via PayID or account number).
"""
def create_receivable_contact(params) do
id = generate_uuid()
bank_account_id = generate_uuid()
anyone_account_id = generate_uuid()
# Receivable contacts get Zepto Float Account bank details
bank_account = %{
"id" => bank_account_id,
"account_number" => generate_account_number(),
"branch_code" => "802919",
"bank_name" => "Zepto Float Account",
"state" => "active",
"iav_provider" => nil,
"iav_status" => nil,
"blocks" => %{"debits_blocked" => false, "credits_blocked" => false}
}
anyone_account = %{"id" => anyone_account_id}
payid_details = build_payid_details(params)
attrs = %{
id: id,
name: params["name"],
email: params["email"],
type: "anyone",
metadata: params["metadata"] || %{},
bank_account: bank_account,
anyone_account: anyone_account,
payid_details: payid_details
}
case Storage.insert_contact(attrs) do
{:ok, contact} -> {:ok, Contact.to_api_response(contact)}
{:error, changeset} -> {:error, format_changeset_errors(changeset)}
end
end
@doc """
Updates a contact by ID.
"""
def update_contact(id, params) do
updates =
params
|> maybe_update_bank_account(id)
|> Map.take(["name", "email", "metadata"])
|> atomize_keys()
case Storage.update_contact(id, updates) do
{:ok, contact} -> {:ok, Contact.to_api_response(contact)}
{:error, :not_found} = error -> error
{:error, changeset} -> {:error, format_changeset_errors(changeset)}
end
end
defp maybe_update_bank_account(params, id) do
if params["branch_code"] || params["account_number"] do
case Storage.get_contact(id) do
{:ok, contact} ->
bank_account =
contact.bank_account
|> maybe_put("branch_code", params["branch_code"])
|> maybe_put("account_number", params["account_number"])
|> Map.put("bank_name", resolve_bank_name(params["branch_code"] || contact.bank_account["branch_code"]))
Map.put(params, "bank_account", bank_account)
_ ->
params
end
else
params
end
end
defp maybe_put(map, _key, nil), do: map
defp maybe_put(map, key, value), do: Map.put(map, key, value)
@doc """
Deletes a contact by ID.
"""
def delete_contact(id) do
Storage.delete_contact(id)
end
@doc """
Updates a receivable contact's PayID name.
"""
def update_receivable(contact_id, params) do
case Storage.get_contact(contact_id) do
{:ok, %{payid_details: nil}} ->
{:error, :not_receivable}
{:ok, contact} ->
updated_payid = Map.put(contact.payid_details, "alias_name", params["payid_name"])
Storage.update_contact(contact_id, %{payid_details: updated_payid})
{:error, :not_found} = error ->
error
end
end
@doc """
Disables a receivable contact (rejects all payments).
"""
def disable_receivable(contact_id) do
update_receivable_state(contact_id, "disabled")
end
@doc """
Activates a receivable contact.
"""
def activate_receivable(contact_id) do
update_receivable_state(contact_id, "active")
end
defp update_receivable_state(contact_id, state) do
case Storage.get_contact(contact_id) do
{:ok, %{payid_details: nil}} ->
{:error, :not_receivable}
{:ok, contact} ->
updated_payid = Map.put(contact.payid_details, "state", state)
Storage.update_contact(contact_id, %{payid_details: updated_payid})
:ok
{:error, :not_found} = error ->
error
end
end
# Payment operations
@doc """
Lists payments with optional filters and pagination.
"""
def list_payments(params \\ %{}) do
{payments, pagination} = Storage.list_payments(params)
api_payments = Enum.map(payments, &Payment.to_api_response/1)
{api_payments, pagination}
end
@doc """
Gets a payment by reference (e.g., "PB.1").
"""
def get_payment(ref) do
case Storage.get_payment(ref) do
{:ok, payment} -> {:ok, Payment.to_api_response(payment)}
{:error, :not_found} = error -> error
end
end
@doc """
Simulates an incoming PayID payment.
Creates a payment record for a receivable contact matched by PayID email.
"""
def simulate_payid_payment(params) do
ref = Storage.next_payment_ref()
attrs = %{
ref: ref,
amount: params["amount"],
description: params["description"] || "PayID payment from #{params["payer_name"]}",
status: "approved",
channel: "new_payments_platform",
to_id: params["payid_email"],
payer_name: params["payer_name"],
metadata: params["metadata"] || %{}
}
case Storage.insert_payment(attrs) do
{:ok, payment} -> {:ok, Payment.to_api_response(payment)}
{:error, changeset} -> {:error, format_changeset_errors(changeset)}
end
end
@doc """
Simulates an incoming NPP payment by BSB and account number.
Creates a payment record targeting a specific bank account.
"""
def simulate_npp_payment(params) do
ref = Storage.next_payment_ref()
attrs = %{
ref: ref,
amount: params["amount"],
description: params["description"] || "NPP payment from #{params["payer_name"]}",
status: "approved",
channel: "new_payments_platform",
to_id: "#{params["to_bsb"]}-#{params["to_account_number"]}",
payer_name: params["payer_name"],
metadata: params["metadata"] || %{}
}
case Storage.insert_payment(attrs) do
{:ok, payment} -> {:ok, Payment.to_api_response(payment)}
{:error, changeset} -> {:error, format_changeset_errors(changeset)}
end
end
@doc """
Simulates an incoming Direct Entry payment.
Creates a payment record using the DE channel.
"""
def simulate_de_payment(params) do
ref = Storage.next_payment_ref()
attrs = %{
ref: ref,
amount: params["amount"],
description: params["description"] || "DE payment from #{params["payer_name"]}",
status: "approved",
channel: "direct_entry",
to_id: "#{params["to_bsb"]}-#{params["to_account_number"]}",
payer_name: params["payer_name"],
metadata: params["metadata"] || %{}
}
case Storage.insert_payment(attrs) do
{:ok, payment} -> {:ok, Payment.to_api_response(payment)}
{:error, changeset} -> {:error, format_changeset_errors(changeset)}
end
end
# Token operations
@doc """
Generates an authorization code for the OAuth flow.
"""
def generate_authorization_code do
Ecto.UUID.generate()
end
@doc """
Creates a new OAuth token pair.
"""
def create_token(_grant_type, _params) do
Storage.create_token()
end
@doc """
Validates an access token.
"""
def validate_token(access_token) do
Storage.validate_token(access_token)
end
@doc """
Refreshes tokens using a refresh token.
"""
def refresh_token(refresh_token) do
Storage.refresh_token(refresh_token)
end
# Utility operations
@doc """
Resets all mock data.
"""
def reset do
Storage.clear_all()
end
@doc """
Returns current state for debugging.
"""
def get_state do
Storage.get_state()
end
# Private helpers
defp generate_uuid do
Ecto.UUID.generate()
end
defp generate_account_number do
:rand.uniform(9_999_999) |> to_string() |> String.pad_leading(7, "0")
end
defp resolve_bank_name(branch_code) when is_binary(branch_code) do
# Mock bank name resolution based on BSB prefix
case String.slice(branch_code, 0, 2) do
"01" -> "Australia and New Zealand Banking Group"
"03" -> "Westpac Banking Corporation"
"06" -> "Commonwealth Bank of Australia"
"08" -> "National Australia Bank"
"12" -> "Bank of Queensland"
"30" -> "Bankwest"
"48" -> "Macquarie Bank"
"80" -> "Zepto Float Account"
_ -> "Zepto SANDBOX Bank"
end
end
defp resolve_bank_name(_), do: "Zepto SANDBOX Bank"
defp build_payid_details(params) do
alias_value = params["payid_email"] || params["payid_email_domain"]
# Simulate failure for emails containing "+failure"
state =
if alias_value && String.contains?(alias_value, "+failure") do
"failed"
else
"pending"
end
%{
"alias_value" => alias_value,
"alias_type" => "email",
"alias_name" => params["payid_name"] || "Zepto Merchant",
"state" => state
}
end
defp build_bank_connection_url(contact_id) do
mock_config = Application.get_env(:zepto, :mock_server, [])
base_url = mock_config[:base_url] || "http://localhost:#{mock_config[:port] || 4100}"
"#{base_url}/invite_contact/#{contact_id}"
end
defp format_changeset_errors(%Ecto.Changeset{} = changeset) do
Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} ->
Regex.replace(~r"%{(\w+)}", msg, fn _, key ->
opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
end)
end)
end
defp atomize_keys(map) when is_map(map) do
Map.new(map, fn
{k, v} when is_binary(k) -> {String.to_atom(k), v}
{k, v} -> {k, v}
end)
end
end