Skip to content

Commit

Permalink
Merge pull request #21 from starkbank/feature/approval
Browse files Browse the repository at this point in the history
Feature/approval
  • Loading branch information
cdottori-stark authored Oct 20, 2020
2 parents 61b3de7 + eb1146e commit 3c316c1
Show file tree
Hide file tree
Showing 11 changed files with 359 additions and 13 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Given a version number MAJOR.MINOR.PATCH, increment:
### Added
- ids parameter to Transaction.query
- ids parameter to Transfer.query
- PaymentRequest resource to pass payments through manual approval flow

## [0.6.0] - 2020-08-20
### Added
Expand Down
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,53 @@ transaction = StarkBank.Transaction.get!("6677396233125888")
|> IO.inspect
```

### Create payment requests to be approved by authorized people in a cost center

You can also request payments that must pass through a specific cost center approval flow to be executed.
In certain structures, this allows double checks for cash-outs and also gives time to load your account
with the required amount before the payments take place.
The approvals can be granted at our website and must be performed according to the rules
specified in the cost center.

**Note**: The value of the center\_id parameter can be consulted by logging into our website and going
to the desired cost center page.

```elixir
requests = StarkBank.PaymentRequest.create!(
[
%StarkBank.PaymentRequest{
center_id: "5967314465849344",
due: Date.utc_today |> Date.add(30),
payment: %StarkBank.Transfer{
amount: 100,
bank_code: "01",
branch_code: "0001",
account_number: "10000-0",
tax_id: "012.345.678-90",
name: "Tony Stark",
},
tags: ["iron", "suit"],
}
]
) |> IO.inspect
```

**Note**: Instead of using PaymentRequest structs, you can also pass each payment request element in map format


### Query payment requests

To search for payment requests, run:

```elixir
requests = StarkBank.PaymentRequest.query!(
center_id: "5967314465849344",
after: Date.utc_today |> Date.add(-2),
before: Date.utc_today |> Date.add(-1),
limit: 10
) |> Enum.take(10) |> IO.inspect
```

### Create webhook subscription

To create a webhook subscription and be notified whenever an event occurs, run:
Expand Down
212 changes: 212 additions & 0 deletions lib/payment_request/payment_request.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
defmodule StarkBank.PaymentRequest do
alias __MODULE__, as: PaymentRequest
alias StarkBank.Utils.Rest
alias StarkBank.Utils.Check
alias StarkBank.Utils.API
alias StarkBank.User.Project
alias StarkBank.Error
alias StarkBank.Boleto, as: Boleto
alias StarkBank.Transfer, as: Transfer
alias StarkBank.Transaction, as: Transaction
alias StarkBank.BoletoPayment, as: BoletoPayment
alias StarkBank.UtilityPayment, as: UtilityPayment

@moduledoc """
Groups PaymentRequest related functions
"""

@doc """
A PaymentRequest is an indirect request to access a specific cash-out service
(such as Transfer, BoletoPayments, etc.) which goes through the cost center
approval flow on our web banking. To emit a PaymentRequest, you must direct it to
a specific cost center by its ID, which can be retrieved on our web banking at the
cost center page.
## Parameters (required):
- `:center_id` [string]: target cost center ID. ex: "5656565656565656"
- `:payment` [Transfer, BoletoPayment, UtilityPayment, Transaction or map]: payment entity that should be approved and executed.
## Parameters (conditionally required):
- `:type` [string]: payment type, inferred from the payment parameter if it is not a map. ex: "transfer", "boleto-payment"
## Parameters (optional):
- `:due` [Date, DateTime, Time or string]: Payment target date in ISO format. ex: 2020-12-31
- `:tags` [list of strings]: list of strings for tagging
## Attributes (return-only):
- `:id` [string, default nil]: unique id returned when PaymentRequest is created. ex: "5656565656565656"
- `:amount` [integer, default nil]: PaymentRequest amount. ex: 100000 = R$1.000,00
- `:status` [string, default nil]: current PaymentRequest status.ex: "pending" or "approved"
- `:actions` [list of maps, default nil]: list of actions that are affecting this PaymentRequest. ex: [%{"type": "member", "id": "56565656565656, "action": "requested"}]
- `:updated` [DateTime, default nil]: latest update datetime for the PaymentRequest. ex: 2020-12-31
- `:created` [DateTime, default nil]: creation datetime for the PaymentRequest. ex: 2020-12-31
"""

@enforce_keys [:center_id, :payment]
defstruct [
:id,
:payment,
:center_id,
:due,
:tags,
:amount,
:status,
:actions,
:updated,
:created,
:type
]

@type t() :: %__MODULE__{}

@doc """
Sends a list of PaymentRequests structs for creating in the Stark Bank API
## Paramenters (required):
- `payment_requests` [list of PaymentRequest structs]: list of PaymentRequest objects to be created in the API
## Options:
- `:user` [Project]: Project struct returned from StarkBank.project(). Only necessary if default project has not been set in configs.
## Return:
- list of PaymentRequest structs with updated attributes
"""
@spec create([PaymentRequest.t() | map()], user: Project.t() | nil) ::
{:ok, [PaymentRequest.t()]} | {:error, [Error.t()]}
def create(payment_requests, options \\ []) do
case Rest.post(
resource(),
Enum.map(payment_requests, fn request -> %PaymentRequest{request | type: get_type(request.payment)} end),
options
) do
{:ok, requests} -> {:ok, requests |> Enum.map(&parse_request!/1)}
response -> response
end
end

@doc """
Same as create(), but it will unwrap the error tuple and raise in case of errors.
"""
@spec create!([PaymentRequest.t() | map()], user: Project.t() | nil) :: any
def create!(payment_requests, options \\ []) do
Rest.post!(
resource(),
Enum.map(payment_requests, fn request -> %PaymentRequest{request | type: get_type(request.payment)} end),
options
) |> Enum.map(&parse_request!/1)
end

@doc """
Receive a stream of PaymentRequest structs previously created by this user in the Stark Bank API
## Options:
- `:limit` [integer, default nil]: maximum number of structs to be retrieved. Unlimited if nil. ex: 35
- `:after` [Date, DateTime or string, default nil]: date filter for structs created only after specified date. ex: ~D[2020-03-25]
- `:before` [Date, DateTime or string, default nil]: date filter for structs created only before specified date. ex: ~D[2020-03-25]
- `:sort` [string, default "-created"]: sort order considered in response. Valid options are "-created" or "-due".
- `:status` [string, default nil]: filter for status of retrieved structs. ex: "paid" or "registered"
- `:type` [string, default nil]: payment type, inferred from the payment parameter if it is not a dictionary. ex: "transfer", "boleto-payment"
- `:tags` [list of strings, default nil]: tags to filter retrieved structs. ex: ["tony", "stark"]
- `:ids` [list of strings, default nil]: list of ids to filter retrieved structs. ex: ["5656565656565656", "4545454545454545"]
- `:user` [Project]: Project struct returned from StarkBank.project(). Only necessary if default project has not been set in configs.
## Return:
- stream of PaymentRequest structs with updated attributes
"""
@spec query(
limit: integer,
after: Date.t() | DateTime.t() | binary,
before: Date.t() | DateTime.t() | binary,
sort: binary,
status: binary,
type: binary,
tags: [binary],
ids: [binary],
user: Project.t()
) ::
({:cont, {:ok, [PaymentRequest.t()]}}
| {:error, [Error.t()]}
| {:halt, any}
| {:suspend, any},
any ->
any)
def query(options \\ []) do
Rest.get_list(resource(), options) |> Enum.map(&parse_request/1)
end

@doc """
Same as query(), but it will unwrap the error tuple and raise in case of errors.
"""
@spec query!(
limit: integer,
after: Date.t() | DateTime.t() | binary,
before: Date.t() | DateTime.t() | binary,
sort: binary,
status: binary,
type: binary,
tags: [binary],
ids: [binary],
user: Project.t()
) ::
({:cont, [PaymentRequest.t()]} | {:halt, any} | {:suspend, any}, any -> any)
def query!(options \\ []) do
Rest.get_list!(resource(), options) |> Enum.map(&parse_request!/1)
end

defp get_type(resource) do
case resource do
%Transfer{} -> "transfer"
%Transaction{} -> "transaction"
%BoletoPayment{} -> "boleto-payment"
%UtilityPayment{} -> "utility-payment"
end
end

defp parse_request(request_tuple) do
case request_tuple do
{:ok, request} -> {:ok, parse_request!(request)}
_ -> request_tuple
end
end

defp parse_request!(request) do
%PaymentRequest{request | payment: request.payment |> API.from_api_json(resource_maker_by_type(request.type))}
rescue
CaseClauseError -> request
end

defp resource_maker_by_type(subscription) do
case subscription do
"transfer" -> &Transfer.resource_maker/1
"transaction" -> &Transaction.resource_maker/1
"boleto" -> &Boleto.resource_maker/1
"boleto-payment" -> &BoletoPayment.resource_maker/1
"utility-payment" -> &UtilityPayment.resource_maker/1
end
end

@doc false
def resource() do
{
"PaymentRequest",
&resource_maker/1
}
end

@doc false
def resource_maker(json) do
%PaymentRequest{
id: json[:id],
payment: json[:payment],
center_id: json[:center_id],
type: json[:type],
tags: json[:tags],
amount: json[:amount],
status: json[:status],
actions: json[:actions],
updated: json[:updated] |> Check.datetime(),
created: json[:created] |> Check.datetime(),
due: json[:due] |> Check.datetime()
}
end
end
4 changes: 4 additions & 0 deletions lib/utils/api.ex
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ defmodule StarkBank.Utils.API do
"#{datetime.year}-#{datetime.month}-#{datetime.day}"
end

defp coerce_types(%{__struct__: _} = struct) do
api_json(struct)
end

defp coerce_types(value) do
value
end
Expand Down
9 changes: 7 additions & 2 deletions test/boleto_payment_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,17 @@ defmodule StarkBankTest.BoletoPayment do
assert !is_nil(deleted_payment)
end

defp example_payment() do
def example_payment(push_schedule \\ false)

def example_payment(push_schedule) when push_schedule do
%{example_payment(false) | scheduled: Date.utc_today() |> Date.add(1)}
end

def example_payment(_push_schedule) do
boleto = StarkBank.Boleto.create!([StarkBankTest.Boleto.example_boleto()]) |> hd

%StarkBank.BoletoPayment{
line: boleto.line,
scheduled: Date.utc_today() |> Date.add(1),
description: "loading a random account",
tax_id: boleto.tax_id
}
Expand Down
2 changes: 1 addition & 1 deletion test/boleto_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ defmodule StarkBankTest.Boleto do
def example_boleto() do
%StarkBank.Boleto{
amount: 200,
due: Date.utc_today() |> Date.add(5),
due: Date.utc_today() |> Date.add(8),
name: "Random Company",
street_line_1: "Rua ABC",
street_line_2: "Ap 123",
Expand Down
71 changes: 71 additions & 0 deletions test/payment_request_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
defmodule StarkBankTest.PaymentRequest do
use ExUnit.Case

@tag :payment_request
test "create! PaymentRequest" do
requests = for i <- 1..10, i > 0 do
request_example()
end
received = StarkBank.PaymentRequest.create!(requests)
for item <- received do
assert !is_nil(item.id)
end
end

@tag :payment_request
test "create PaymentRequest" do
requests = for i <- 1..10, i > 0 do
request_example()
end
{:ok, received} = StarkBank.PaymentRequest.create(requests)
for item <- received do
assert !is_nil(item.id)
end
end

@tag :payment_request
test "query payment request" do
StarkBank.PaymentRequest.query(center_id: System.get_env("SANDBOX_CENTER_ID"), limit: 101, before: DateTime.utc_now())
|> Enum.take(200)
|> (fn list -> assert length(list) <= 101 end).()
end

@tag :payment_request
test "query! payment request" do
StarkBank.PaymentRequest.query!(center_id: System.get_env("SANDBOX_CENTER_ID"), limit: 101, before: DateTime.utc_now())
|> Enum.take(200)
|> (fn list -> assert length(list) <= 101 end).()
end

def request_example() do
payment = create_payment()
%StarkBank.PaymentRequest{
center_id: System.get_env("SANDBOX_CENTER_ID"),
payment: payment,
due: get_due_date(payment)
}
end

defp get_days() do
days = Enum.random(1..7)
Date.utc_today() |> Date.add(days)
end

defp get_due_date(payment) do
case payment do
%StarkBank.Transfer{} -> get_days()
%StarkBank.BoletoPayment{} -> get_days()
%StarkBank.UtilityPayment{} -> get_days()
%StarkBank.Transaction{} -> nil
end
end

defp create_payment() do
case Enum.random(0..3) do
0 -> StarkBankTest.Transfer.example_transfer(false)
1 -> StarkBankTest.Transaction.example_transaction()
2 -> StarkBankTest.BoletoPayment.example_payment(false)
3 -> StarkBankTest.UtilityPayment.example_payment(false)
end
end
end
1 change: 1 addition & 0 deletions test/test_helper.exs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ ExUnit.start(
# :transfer,
# :utility_payment_log,
# :utility_payment,
# :payment_request,
# :webhook,
# :event
]
Expand Down
Loading

0 comments on commit 3c316c1

Please sign in to comment.