diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/.gitignore b/.gitignore index f639afd..fb10ccc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,31 @@ -_build -deps -.idea -doc - -*.dump -*.lock -*.pdf \ No newline at end of file +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where third-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# Ignore +/.elixir_ls +/keys +mix.lock + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Ignore package tarball (built via "mix hex.build"). +starkbank-*.tar + +# Debug configuration directory. +/.vscode/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..19d17cd --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,15 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) +and this project adheres to the following versioning pattern: + +Given a version number MAJOR.MINOR.PATCH, increment: + +- MAJOR version when the **API** version is incremented. This may include backwards incompatible changes; +- MINOR version when **breaking changes** are introduced OR **new functionalities** are added in a backwards compatible manner; +- PATCH version when backwards compatible bug **fixes** are implemented. + +## [Unreleased] +### Added +- Full Stark Bank API v2 compatibility diff --git a/README.md b/README.md index fdee1ac..c273b3f 100644 --- a/README.md +++ b/README.md @@ -1,195 +1,693 @@ -# StarkBank +# Stark Bank Elixir SDK Beta -## Overview +Welcome to the Stark Bank Elixir SDK! This tool is made for Elixir +developers who want to easily integrate with our API. +This SDK version is compatible with the Stark Bank API v2. -This is a simplified pure Elixir SDK to ease integrations with the Auth and Charge services of the [Stark Bank](https://starkbank.com) [API](https://docs.api.starkbank.com/?version=latest) v1. +If you have no idea what Stark Bank is, check out our [website](https://www.StarkBank.com/) +and discover a world where receiving or making payments +is as easy as sending a text message to your client! -## Installation +## Supported Elixir Versions -The package can be installed by adding `stark_bank` to your list of dependencies in `mix.exs`: +This library supports Elixir versions 1.9+. + +## Stark Bank API documentation + +Feel free to take a look at our [API docs](https://www.starkbank.com/docs/api). + +## Versioning + +This project adheres to the following versioning pattern: + +Given a version number MAJOR.MINOR.PATCH, increment: + +- MAJOR version when the **API** version is incremented. This may include backwards incompatible changes; +- MINOR version when **breaking changes** are introduced OR **new functionalities** are added in a backwards compatible manner; +- PATCH version when backwards compatible bug **fixes** are implemented. + +## Setup + +### 1. Install our SDK + +To install the package with mix, add this to your deps and run `mix deps.get`: ```elixir def deps do [ - {:stark_bank, "~> 1.1.2"} + {:starkbank, "~> 0.1.0"} ] end ``` -## Usage +### 2. Create your Private and Public Keys + +We use ECDSA. That means you need to generate a secp256k1 private +key to sign your requests to our API, and register your public key +with us so we can validate those requests. + +You can use one of following methods: + +2.1. Check out the options in our [tutorial](https://starkbank.com/faq/how-to-create-ecdsa-keys). + +2.2. Use our SDK: + +```elixir +{private_key, public_key} = StarkBank.Key.create() + +# or, to also save .pem files in a specific path +{private_key, public_key} = StarkBank.Key.create("file/keys/") +``` + +**Note**: When you are creating a new Project, it is recommended that you create the +keys inside the infrastructure that will use it, in order to avoid risky internet +transmissions of your **private-key**. Then you can export the **public-key** alone to the +computer where it will be used in the new Project creation. + +### 3. Create a Project + +You need a project for direct API integrations. To create one in Sandbox: + +3.1. Log into [Starkbank Sandbox](https://sandbox.web.starkbank.com) + +3.2. Go to Menu > Usuários (Users) > Projetos (Projects) + +3.3. Create a Project: Give it a name and upload the public key you created in section 2. + +3.4. After creating the Project, get its Project ID + +3.5. Use the Project ID and private key to create the object below: + +```elixir +# Get your private key from an environment variable or an encrypted database. +# This is only an example of a private key content. You should use your own key. +private_key_content = " +-----BEGIN EC PARAMETERS----- +BgUrgQQACg== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MHQCAQEEIMCwW74H6egQkTiz87WDvLNm7fK/cA+ctA2vg/bbHx3woAcGBSuBBAAK +oUQDQgAE0iaeEHEgr3oTbCfh8U2L+r7zoaeOX964xaAnND5jATGpD/tHec6Oe9U1 +IF16ZoTVt1FzZ8WkYQ3XomRD4HS13A== +-----END EC PRIVATE KEY----- +" + +user = StarkBank.project( + :sandbox, + "5671398416568321", + private_key_content +) +``` + +NOTE 1: Never hard-code your private key. Get it from an environment variable or an encrypted database. -### Login +NOTE 2: We support `'sandbox'` and `'production'` as environments. + +NOTE 3: The project you created in `sandbox` does not exist in `production` and vice versa. + + +### 4. Setting up the user + +There are two kinds of users that can access our API: **Project** and **Member**. + +- `Member` is the one you use when you log into our webpage with your e-mail. +- `Project` is designed for integrations and is the one meant for our SDK. + +To inform the user to the SDK, pass it as first argument in all functions that interact with the API: ```elixir -{:ok, credentials} = StarkBank.Auth.login(:sandbox, "username", "email@email.com", "password") +balance = StarkBank.Balance.get!(user) ``` -### Logout +## Testing in Sandbox + +Your initial balance is zero. For many operations in Stark Bank, you'll need funds +in your account, which can be added to your balance by creating a Boleto. + +In the Sandbox environment, 90% of the created Boletos will be automatically paid, +so there's nothing else you need to do to add funds to your account. Just create +a few and wait around a bit. + +In Production, you (or one of your clients) will need to actually pay this Boleto +for the value to be credited to your account. + + +## Usage + +Here are a few examples on how to use the SDK. If you have any doubts, use the built-in +`h()` function to get more info on the desired functionality +(for example: `StarkBank.Boleto |> h`) + +**Note**: Almost all SDK functions also provide a bang (!) version. To simplify the examples, they will be used the most throughout this README. + +### Get balance + +To know how much money you have in your workspace, run: ```elixir -{:ok, response} = StarkBank.Auth.logout(credentials) +balance = StarkBank.Balance.get!(user) +IO.puts(balance.amount / 100) ``` -### Create charge customers +### Create boletos + +You can create boletos to charge customers or to receive money from accounts +you have in other banks. ```elixir -customers = [ - %StarkBank.Charge.Structs.CustomerData{ - name: "Arya Stark", - email: "arya.stark@westeros.com", - tax_id: "416.631.524-20", - phone: "(11) 98300-0000", - tags: ["little girl", "no one", "valar morghulis", "Stark"], - address: %StarkBank.Charge.Structs.AddressData{ - street_line_1: "Av. Faria Lima, 1844", - street_line_2: "CJ 13", - district: "Itaim Bibi", - city: "São Paulo", - state_code: "SP", - zip_code: "01500-000" +boletos = StarkBank.Boleto.create!( + user, + [ + %StarkBank.Boleto{ + amount: 23571, # R$ 235,71 + name: "Buzz Aldrin", + tax_id: "012.345.678-90", + street_line_1: "Av. Paulista, 200", + street_line_2: "10 andar", + district: "Bela Vista", + city: "São Paulo", + state_code: "SP", + zip_code: "01310-000", + due: Date.utc_today |> Date.add(30), + fine: 5, # 5% + interest: 2.5, # 2.5% per month } - }, - %StarkBank.Charge.Structs.CustomerData{ - name: "Jon Snow", - email: "jon.snow@westeros.com", - tax_id: "012.345.678-90", - phone: "(11) 98300-0001", - tags: ["night`s watch", "lord commander", "knows nothing", "Stark"], - address: %StarkBank.Charge.Structs.AddressData{ - street_line_1: "Av. Faria Lima, 1844", - street_line_2: "CJ 13", - district: "Itaim Bibi", - city: "São Paulo", - state_code: "SP", - zip_code: "01500-000" + ] +) |> IO.inspect +``` + +### Get boleto + +After its creation, information on a boleto may be retrieved by passing its id. +Its status indicates whether it's been paid. + +```elixir +boleto = StarkBank.Boleto.get!(user, "6750458353811456") + |> IO.inspect +``` + +### Get boleto PDF + +After its creation, a boleto PDF may be retrieved by passing its id. + +```elixir +pdf = StarkBank.Boleto.pdf!(user, "6750458353811456") + +file = File.open!("boleto.pdf", [:write]) +IO.binwrite(file, pdf) +File.close(file) +``` + +Be careful not to accidentally enforce any encoding on the raw pdf content, +as it may yield abnormal results in the final file, such as missing images +and strange characters. + +### Delete boleto + +You can also cancel a boleto by its id. +Note that this is not possible if it has been processed already. + +```elixir +boleto = StarkBank.Boleto.delete!(user, "5202697619767296") + |> IO.inspect +``` + +### Query boletos + +You can get a stream of created boletos given some filters. + +```elixir +boletos = StarkBank.Boleto.query!( + user, + after: Date.utc_today |> Date.add(-2), + before: Date.utc_today |> Date.add(-1), + limit: 10 +) |> Enum.take(10) |> IO.inspect +``` + +### Query boleto logs + +Logs are pretty important to understand the life cycle of a boleto. + +```elixir +for log <- StarkBank.Boleto.Log.query!(user, boleto_ids: ["6750458353811456"]) do + log |> IO.inspect +end +``` + +### Get a boleto log + +You can get a single log by its id. + +```elixir +log = StarkBank.Boleto.Log.get!(user, "6288576484474880") + |> IO.inspect +``` + +### Create transfers + +You can also create transfers in the SDK (TED/DOC). + +```elixir +transfers = StarkBank.Transfer.create!( + user, + [ + %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"] + }, + %StarkBank.Transfer{ + amount: 200, + bank_code: "341", + branch_code: "1234", + account_number: "123456-7", + tax_id: "012.345.678-90", + name: "Jon Snow", } - } -] +]) |> IO.inspect +``` + +### Query transfers + +You can query multiple transfers according to filters. -{:ok, customers} = StarkBank.Charge.Customer.post(credentials, customers) +```elixir +for transfer <- StarkBank.Transfer.query!( + user, + after: Date.utc_today |> Date.add(-2), + before: Date.utc_today |> Date.add(-1), + limit: 10 +) do + transfer |> IO.inspect +end ``` -### Get charge customers +### Get transfer + +To get a single transfer by its id, run: ```elixir -{:ok, all_customers} = StarkBank.Charge.Customer.get(credentials) -# or -{:ok, customer} = StarkBank.Charge.Customer.get_by_id(credentials, hd(customers).id) +transfer = StarkBank.Transfer.get!(user, "4882890932355072") + |> IO.inspect ``` -### Get charge customers +### Get transfer PDF + +A transfer PDF may also be retrieved by passing its id. +This operation is only valid for transfers with "processing" or "success" status. ```elixir -{:ok, all_customers} = StarkBank.Charge.Customer.get(credentials) -# or -{:ok, customer} = StarkBank.Charge.Customer.get_by_id(credentials, hd(customers).id) +pdf = StarkBank.Transfer.pdf!(user, "4882890932355072") + +file = File.open!("transfer.pdf", [:write]) +IO.binwrite(file, pdf) +File.close(file) ``` -### Delete charge customers +Be careful not to accidentally enforce any encoding on the raw pdf content, +as it may yield abnormal results in the final file, such as missing images +and strange characters. + +### Query transfer logs + +You can query transfer logs to better understand transfer life cycles. ```elixir -{:ok, response} = StarkBank.Charge.Customer.delete(credentials, customers) +logs = StarkBank.Transfer.Log.query!(user, limit: 50) + |> Enum.take(50) + |> IO.inspect ``` -### Overwrite charge customers information +### Get a transfer log + +You can also get a specific log by its id. ```elixir -{:ok, altered_customer} = StarkBank.Charge.Customer.put(credentials, altered_customer) +log = StarkBank.Transfer.Log.get!(user, "6610264099127296") + |> IO.inspect ``` -### Create charges +### Pay a boleto + +Paying a boleto is also simple. ```elixir -charges = [ - %StarkBank.Charge.Structs.ChargeData{ - amount: 100_00, - customer: altered_customer.id - }, - %StarkBank.Charge.Structs.ChargeData{ - amount: 1_000_00, - customer: "self", - due_date: Date.utc_today(), - fine: 10, - interest: 15, - overdue_limit: 3, - tags: ["cash-in"], - descriptions: [ - %StarkBank.Charge.Structs.ChargeDescriptionData{ - text: "part-1", - amount: 30_000 - }, - %StarkBank.Charge.Structs.ChargeDescriptionData{ - text: "part-2", - amount: 70_000 - } - ] - }, - %StarkBank.Charge.Structs.ChargeData{ - amount: 32_171_32, - customer: %StarkBank.Charge.Structs.CustomerData{ - name: "Brandon Stark", - email: "bran.builder@westeros.com", - tax_id: "123.456.789-09", - phone: "(11) 98300-0000", - tags: ["builder", "raven", "Stark", "test"], - address: %StarkBank.Charge.Structs.AddressData{ - street_line_1: "Av. Faria Lima, 1844", - street_line_2: "CJ 13", - district: "Itaim Bibi", - city: "São Paulo", - state_code: "SP", - zip_code: "01500-000" - } +payments = StarkBank.BoletoPayment.create!( + user, + [ + %StarkBank.BoletoPayment{ + line: "34191.09008 64694.017308 71444.640008 1 96610000014500", + tax_id: "012.345.678-90", + scheduled: Date.utc_today |> Date.add(10), + description: "take my money", + tags: ["take", "my", "money"], + }, + %StarkBank.BoletoPayment{ + bar_code: "34191972300000289001090064694197307144464000", + tax_id: "012.345.678-90", + scheduled: Date.utc_today |> Date.add(40), + description: "take my money one more time", + tags: ["again"], }, - tags: ["test"] - } -] + ] +) |> IO.inspect +``` + +### Get boleto payment + +To get a single boleto payment by its id, run: -{:ok, charges} = StarkBank.Charge.post(credentials, charges) +```elixir +payment = StarkBank.BoletoPayment.get!(user, "5629412477239296") + |> IO.inspect ``` -### Get created charges +### Get boleto payment PDF + +After its creation, a boleto payment PDF may be retrieved by passing its id. ```elixir -{:ok, all_charges} = StarkBank.Charge.get(credentials) +pdf = StarkBank.BoletoPayment.pdf!(user, "5629412477239296") + +file = File.open!("boleto-payment.pdf", [:write]) +IO.binwrite(file, pdf) +File.close(file) ``` -### Get charge PDF +Be careful not to accidentally enforce any encoding on the raw pdf content, +as it may yield abnormal results in the final file, such as missing images +and strange characters. + +### Delete boleto payment + +You can also cancel a boleto payment by its id. +Note that this is not possible if it has been processed already. + +```elixir +payment = StarkBank.BoletoPayment.delete!(user, "5629412477239296") + |> IO.inspect +``` + +### Query boleto payments + +You can search for boleto payments using filters. + +```elixir +payments = StarkBank.BoletoPayment.query!( + user, + tags: ["company_1", "company_2"] +) |> Enum.take(10) |> IO.inspect +``` + +### Query boleto payment logs + +Searches are also possible with boleto payment logs: + +```elixir +for log <- StarkBank.BoletoPayment.Log.query!( + user, + payment_ids: ["5629412477239296", "5199478290120704"], +) do + log |> IO.inspect +end +``` + +### Get boleto payment log + +You can also get a boleto payment log by specifying its id. ```elixir -{:ok, pdf} = - StarkBank.Charge.get_pdf( - credentials, - hd(all_charges).id - ) -{:ok, file} = File.open("charge.pdf", [:write]) +log = StarkBank.BoletoPayment.Log.get!(user, "5391671273455616") + |> IO.inspect +``` + +### Create utility payment + +It's also simple to pay utility bills (such as electricity and water bills) in the SDK. + +```elixir +payments = StarkBank.UtilityPayment.create!( + user, + [ + %StarkBank.UtilityPayment{ + bar_code: "83600000001522801380037107172881100021296561", + scheduled: Date.utc_today |> Date.add(2), + description: "paying some bills", + tags: ["take", "my", "money"], + }, + %StarkBank.UtilityPayment{ + line: "83680000001 7 08430138003 0 71070987611 8 00041351685 7", + scheduled: Date.utc_today |> Date.add(3), + description: "never ending bills", + tags: ["again"], + }, + ] +) |> IO.inspect +``` + +### Query utility payments + +To search for utility payments using filters, run: + +```elixir +payments = StarkBank.UtilityPayment.query!( + user, + tags: ["electricity", "gas"] +) |> Enum.take(10) |> IO.inspect +``` + +### Get utility payment + +You can get a specific bill by its id: + +```elixir +payment = StarkBank.UtilityPayment.get!(user, "6619425641857024") + |> IO.inspect +``` + +### Get utility payment PDF + +After its creation, a utility payment PDF may also be retrieved by passing its id. + +```elixir +pdf = StarkBank.UtilityPayment.pdf!(user, "6619425641857024") + +file = File.open!("utility-payment.pdf", [:write]) IO.binwrite(file, pdf) File.close(file) ``` -### Delete created charge +Be careful not to accidentally enforce any encoding on the raw pdf content, +as it may yield abnormal results in the final file, such as missing images +and strange characters. + +### Delete utility payment + +You can also cancel a utility payment by its id. +Note that this is not possible if it has been processed already. ```elixir -StarkBank.Charge.delete( - credentials, - [hd(all_charges).id] -) +payment = StarkBank.UtilityPayment.delete!(user, "6619425641857024") + |> IO.inspect +``` + +### Query utility bill payment logs + +You can search for payments by specifying filters. Use this to understand the +bills life cycles. + +```elixir +for log <- StarkBank.UtilityPayment.Log.query!( + user, + payment_ids: ["6619425641857024", "5738969660653568"] +) do + log |> IO.inspect +end +``` + +### Get utility bill payment log + +If you want to get a specific payment log by its id, just run: + +```elixir +log = StarkBank.UtilityPayment.Log.get!(user, "6197807794880512") + |> IO.inspect +``` + +### Create transactions + +To send money between Stark Bank accounts, you can create transactions: + +```elixir +transactions = StarkBank.Transaction.create!( + user, + [ + %StarkBank.Transaction{ + amount: 100, # (R$ 1.00) + receiver_id: "5768064935133184", + description: "Transaction to dear provider", + external_id: "12345", # so we can block anything you send twice by mistake + tags: ["provider"] + }, + %StarkBank.Transaction{ + amount: 234, # (R$ 2.34) + receiver_id: "5768064935133184", + description: "Transaction to the other provider", + external_id: "12346", # so we can block anything you send twice by mistake + tags: ["provider"] + } + ] +) |> IO.inspect +``` + +### Query transactions + +To understand your balance changes (bank statement), you can query +transactions. Note that our system creates transactions for you when +you receive boleto payments, pay a bill or make transfers, for example. + +```elixir +transactions = StarkBank.Transaction.query!( + user, + after: "2020-03-20", + before: "2020-03-30" +) |> Enum.take(10) |> IO.inspect ``` -### Get charge logs +### Get transaction + +You can get a specific transaction by its id: + +```elixir +transaction = StarkBank.Transaction.get!(user, "6677396233125888") + |> IO.inspect +``` + +### Create webhook subscription + +To create a webhook subscription and be notified whenever an event occurs, run: ```elixir -{:ok, response} = StarkBank.Charge.Log.get(credentials, [hd(all_charges).id]) -# or -{:ok, response} = StarkBank.Charge.Log.get_by_id(credentials, hd(charge_logs).id) +webhook = StarkBank.Webhook.create!( + user, + "https://webhook.site/dd784f26-1d6a-4ca6-81cb-fda0267761ec", + ["transfer", "boleto", "boleto-payment", "utility-payment"] +) |> IO.inspect ``` -## Test +### Query webhooks -Alter @env, @username, @email, @password to your values on test/stark_bank_test.exs. IMPORTANT: Avoid using your production credentials to run this test script, as it will request creations and deletions of charges and related entities. +To search for registered webhooks, run: -Afterwards, run: -```sh -mix test --trace +```elixir +for webhook <- StarkBank.Webhook.query!(user) do + webhook |> IO.inspect +end ``` + +### Get webhook + +You can get a specific webhook by its id. + +```elixir +webhook = StarkBank.Webhook.get!(user, "6178044066660352") + |> IO.inspect +``` + +### Delete webhook + +You can also delete a specific webhook by its id. + +```elixir +webhook = StarkBank.Webhook.delete!(user, "6178044066660352") + |> IO.inspect +``` + +### Process webhook events + +It's easy to process events that have arrived in your webhook. Remember to pass the +signature header so the SDK can make sure it's really StarkBank that has sent you +the event. + +```elixir +response = listen() # this is the function you made to get the events posted to your webhook + +{event, cache_pid} = StarkBank.Event.parse!( + user, + response.content, + response.headers["Digital-Signature"] +) |> IO.inspect +``` + +To avoid making unnecessary requests to the API (/GET public-key), you can pass the `cache_pid` (returned on all requests) +on your next parse. The process referred by the PID `cache_pid` will store the latest Stark Bank public key +and automatically refresh it if an inconsistency is found between the content, signature and current public key. + +**Note**: If you don't send the cache_pid to the parser, a new cache process will be generated. + +```elixir +{event, _cache_pid} = StarkBank.Event.parse!( + user, + response.content, + response.headers["Digital-Signature"], + cache_pid +) |> IO.inspect +``` + +If the data does not check out with the Stark Bank public-key, the function will automatically request the +key from the API and try to validate the signature once more. If it still does not check out, it will raise an error. + +### Query webhook events + +To search for webhooks events, run: + +```elixir +events = StarkBank.Event.query!( + user, + after: "2020-03-20", + is_delivered: false, + limit: 10 +) |> Enum.take(10) |> IO.inspect +``` + +### Get webhook event + +You can get a specific webhook event by its id. + +```elixir +event = StarkBank.Event.get!(user, "4568139664719872") + |> IO.inspect +``` + +### Delete webhook event + +You can also delete a specific webhook event by its id. + +```elixir +event = StarkBank.Event.delete!(user, "4568139664719872") + |> IO.inspect +``` + +### Set webhook events as delivered + +This can be used in case you've lost events. +With this function, you can manually set events retrieved from the API as +"delivered" to help future event queries with `is_delivered: false`. + +```elixir +event = StarkBank.Event.update!(user, "5764442407043072", is_delivered: true) + |> IO.inspect +``` + +## Handling errors + +The SDK may raise or return errors as the StarkBank.Error struct, which contains the "code" and "message" attributes. + +If you use bang functions, the list of errors will be converted into a string and raised. +If you use normal functions, the list of error structs will be returned so you can better analyse them. diff --git a/lib/auth/auth.ex b/lib/auth/auth.ex deleted file mode 100644 index 7f8e43e..0000000 --- a/lib/auth/auth.ex +++ /dev/null @@ -1,306 +0,0 @@ -defmodule StarkBank.Auth do - @moduledoc """ - Used to manage credentials and to create a new session (login) with the StarkBank API - - Functions: - - login - - update_access_token - - insert_external_access_token - - logout - - get_env - - get_workspace - - get_email - - get_access_token - - get_member_id - - get_workspace_id - - get_name - - get_permissions - - get_auto_refresh - """ - - alias StarkBank.Utils.Requests, as: Requests - - @doc """ - Creates a new access-token and invalidates all others - - Parameters: - - env [atom]: :sandbox, :production - - workspace [string]: workspace name - - email [string]: email - - password [string]: password - - options [keyword list]: refines request - - access_token [string, default nil]: if nil, a new access_token will be requested from the API, else, it will be saved for further use without making any requests - - auto_refresh [bool, default true]: if true, any credentials errors returned by the API will be responded with one attempt to retrieve a new access_token and remake the request; - - Returns {:ok, credentials}: - - credentials: PID of agent that holds the credentials information, including the access-token. This PID must be passed as parameter to all SDK calls - - ## Examples - - iex> StarkBank.Auth.login(:sandbox, "workspace", "user@email.com", "password") - {:ok, #PID<0.178.0>} - """ - def login(env, workspace, email, password, options \\ []) do - %{auto_refresh: auto_refresh, access_token: access_token} = - Enum.into(options, %{auto_refresh: true, access_token: nil}) - - {:ok, credentials} = Agent.start_link(fn -> %{} end) - - Agent.update(credentials, fn map -> Map.put(map, :env, env) end) - Agent.update(credentials, fn map -> Map.put(map, :workspace, workspace) end) - Agent.update(credentials, fn map -> Map.put(map, :email, email) end) - Agent.update(credentials, fn map -> Map.put(map, :password, password) end) - Agent.update(credentials, fn map -> Map.put(map, :auto_refresh, auto_refresh) end) - - if is_nil(access_token) do - update_access_token(credentials) - else - Agent.update(credentials, fn map -> Map.put(map, :access_token, access_token) end) - end - - {:ok, credentials} - end - - @doc """ - Recicles the access-token present in the credentials agent - - Parameters: - - credentials [PID]: credentials returned by Auth.login - - Returns {:ok, credentials}: - - credentials: PID of agent that holds the credentials information, including the access-token. This PID must be passed as parameter to all SDK calls - - ## Examples - - iex> StarkBank.Auth.update_access_token(credentials) - {:ok, #PID<0.190.0>} - """ - def update_access_token(credentials) do - {:ok, body} = - Requests.post(credentials, 'auth/access-token', %{ - workspace: get_workspace(credentials), - email: get_email(credentials), - password: get_password(credentials), - platform: "api" - }) - - access_token = body["accessToken"] - member_info = body["member"] - - member_id = member_info["id"] - workspace_id = member_info["workspaceId"] - name = member_info["name"] - permissions = member_info["permissions"] - - Agent.update(credentials, fn map -> Map.put(map, :access_token, access_token) end) - Agent.update(credentials, fn map -> Map.put(map, :member_id, member_id) end) - Agent.update(credentials, fn map -> Map.put(map, :workspace_id, workspace_id) end) - Agent.update(credentials, fn map -> Map.put(map, :name, name) end) - Agent.update(credentials, fn map -> Map.put(map, :permissions, permissions) end) - - {:ok, credentials} - end - - @doc """ - Inserts an external access-token into the current credentials agent. This is used mostly along with {auto_refresh: false} when managing multiple workers that are using the same session. - - Parameters: - - credentials [PID]: credentials returned by Auth.login - - access_token [string]: access_token that should be used by this worker - - ## Examples - - iex> StarkBank.Auth.update_access_token(credentials, "50783765030502405711452971204608ac4d4a86e5934c858afb3e493bd3424457566319ae5244629854d36e76649f13") - """ - def insert_external_access_token(credentials, access_token) do - Agent.update(credentials, fn map -> Map.put(map, :access_token, access_token) end) - end - - @doc """ - Deletes current session and invalidates current access-token - - Parameters: - - credentials [PID]: agent PID returned by StarkBank.Auth.login; - - Returns: - - parsed API response json - - ## Examples - - iex> StarkBank.Auth.logout(credentials) - {:ok, %{"message" => "Your session has been successfully closed"}} - """ - def logout(credentials) do - Requests.delete( - credentials, - 'auth/access-token/' ++ to_charlist(get_access_token(credentials)) - ) - end - - defp get_password(credentials) do - Agent.get(credentials, fn map -> Map.get(map, :password) end) - end - - @doc """ - Gets the environment saved in the credentials agent - - Parameters: - - credentials [PID]: credentials returned by Auth.login - - Returns: - - environment [:sandbox or :production] - - ## Examples - - iex> StarkBank.Auth.get_env(credentials) - :sandbox - """ - def get_env(credentials) do - Agent.get(credentials, fn map -> Map.get(map, :env) end) - end - - @doc """ - Gets the workspace saved in the credentials agent - - Parameters: - - credentials [PID]: credentials returned by Auth.login - - Returns: - - workspace [string] - - ## Examples - - iex> StarkBank.Auth.get_workspace(credentials) - "workspace" - """ - def get_workspace(credentials) do - Agent.get(credentials, fn map -> Map.get(map, :workspace) end) - end - - @doc """ - Gets the email saved in the credentials agent - - Parameters: - - credentials [PID]: credentials returned by Auth.login - - Returns: - - email [string] - - ## Examples - - iex> StarkBank.Auth.get_email(credentials) - "user@email.com" - """ - def get_email(credentials) do - Agent.get(credentials, fn map -> Map.get(map, :email) end) - end - - @doc """ - Gets the access_token saved in the credentials agent after login - - Parameters: - - credentials [PID]: credentials returned by Auth.login - - Returns: - - access_token [string] - - ## Examples - - iex> StarkBank.Auth.get_access_token(credentials) - "507837650305024057114529712046081608a18e96724397ad149ab182785568cddee9381a714acc903d9e0a5d17ef71" - """ - def get_access_token(credentials) do - Agent.get(credentials, fn map -> Map.get(map, :access_token) end) - end - - @doc """ - Gets the member_id saved in the credentials agent after login - - Parameters: - - credentials [PID]: credentials returned by Auth.login - - Returns: - - password [string] - - ## Examples - - iex> StarkBank.Auth.get_member_id(credentials) - "5711452971204608" - """ - def get_member_id(credentials) do - Agent.get(credentials, fn map -> Map.get(map, :member_id) end) - end - - @doc """ - Gets the workspace_id saved in the credentials agent after login - - Parameters: - - credentials [PID]: credentials returned by Auth.login - - Returns: - - workspace_id [string] - - ## Examples - - iex> StarkBank.Auth.get_workspace_id(credentials) - "5078376503050240" - "" - """ - def get_workspace_id(credentials) do - Agent.get(credentials, fn map -> Map.get(map, :workspace_id) end) - end - - @doc """ - Gets the member name saved in the credentials agent after login - - Parameters: - - credentials [PID]: credentials returned by Auth.login - - Returns: - - name [string] - - ## Examples - - iex> StarkBank.Auth.get_name(credentials) - "Arya Stark" - """ - def get_name(credentials) do - Agent.get(credentials, fn map -> Map.get(map, :name) end) - end - - @doc """ - Gets the user permissions saved in the credentials agent after login - - Parameters: - - credentials [PID]: credentials returned by Auth.login - - Returns: - - permissions [list of string] - - ## Examples - - iex> StarkBank.Auth.get_permissions(credentials) - ["admin"] - """ - def get_permissions(credentials) do - Agent.get(credentials, fn map -> Map.get(map, :permissions) end) - end - - @doc """ - Gets the auto_refresh setting saved in the credentials agent after login - - Parameters: - - credentials: credentials returned by Auth.login [PID] - - Returns: - - auto_refresh [boolean] - - ## Examples - - iex> StarkBank.Auth.get_permissions(credentials) - ["admin"] - """ - def get_auto_refresh(credentials) do - Agent.get(credentials, fn map -> Map.get(map, :auto_refresh) end) - end -end diff --git a/lib/boleto/boleto.ex b/lib/boleto/boleto.ex new file mode 100644 index 0000000..ba27895 --- /dev/null +++ b/lib/boleto/boleto.ex @@ -0,0 +1,217 @@ +defmodule StarkBank.Boleto do + + alias __MODULE__, as: Boleto + alias StarkBank.Utils.Rest, as: Rest + alias StarkBank.Utils.Checks, as: Checks + alias StarkBank.User.Project, as: Project + alias StarkBank.Error, as: Error + + @moduledoc """ + Groups Boleto related functions + """ + + @doc """ + When you initialize a Boleto struct, the entity will not be automatically + sent to the Stark Bank API. The 'create' function sends the structs + to the Stark Bank API and returns the list of created structs. + + ## Parameters (required): + - amount [integer]: Boleto value in cents. Minimum amount = 200 (R$2,00). ex: 1234 (= R$ 12.34) + - name [string]: payer full name. ex: "Anthony Edward Stark" + - tax_id [string]: payer tax ID (CPF or CNPJ) with or without formatting. ex: "01234567890" or "20.018.183/0001-80" + - street_line_1 [string]: payer main address. ex: Av. Paulista, 200 + - street_line_2 [string]: payer address complement. ex: Apto. 123 + - district [string]: payer address district / neighbourhood. ex: Bela Vista + - city [string]: payer address city. ex: Rio de Janeiro + - state_code [string]: payer address state. ex: GO + - zip_code [string]: payer address zip code. ex: 01311-200 + - due [Date, default today + 2 days]: Boleto due date in ISO format. ex: 2020-04-30 + + ## Parameters (optional): + - fine [float, default 0.0]: Boleto fine for overdue payment in %. ex: 2.5 + - interest [float, default 0.0]: Boleto monthly interest for overdue payment in %. ex: 5.2 + - overdue_limit [integer, default 59]: limit in days for automatic Boleto cancellation after due date. ex: 7 (max: 59) + - descriptions [list of maps, default nil]: list of maps with :text (string) and :amount (int, optional) pairs + - tags [list of strings]: list of strings for tagging + + ## Attributes (return-only): + - id [string, default nil]: unique id returned when Boleto is created. ex: "5656565656565656" + - fee [integer, default nil]: fee charged when Boleto is paid. ex: 200 (= R$ 2.00) + - line [string, default nil]: generated Boleto line for payment. ex: "34191.09008 63571.277308 71444.640008 5 81960000000062" + - bar_code [string, default nil]: generated Boleto bar-code for payment. ex: "34195819600000000621090063571277307144464000" + - status [string, default nil]: current Boleto status. ex: "registered" or "paid" + - created [DateTime, default nil]: creation datetime for the Boleto. ex: ~U[2020-03-26 19:32:35.418698Z] + """ + @enforce_keys [:amount, :name, :tax_id, :street_line_1, :street_line_2, :district, :city, :state_code, :zip_code] + defstruct [:amount, :name, :tax_id, :street_line_1, :street_line_2, :district, :city, :state_code, :zip_code, :due, :fine, :interest, :overdue_limit, :tags, :descriptions, :id, :fee, :line, :bar_code, :status, :created] + + @type t() :: %__MODULE__{} + + @doc """ + Send a list of Boleto structs for creation in the Stark Bank API + + ## Parameters (required): + - user [Project]: Project struct returned from StarkBank.project(). + - boletos [list of Boleto structs]: list of Boleto structs to be created in the API + + ## Return: + - list of Boleto structs with updated attributes + """ + @spec create(Project.t(), [Boleto.t()]) :: + {:ok, [Boleto.t()]} | {:error, [Error.t()]} + def create(%Project{} = user, boletos) do + Rest.post( + user, + resource(), + boletos + ) + end + + @doc """ + Same as create(), but it will unwrap the error tuple and raise in case of errors. + """ + @spec create!(Project.t(), [Boleto.t()]) :: any + def create!(%Project{} = user, boletos) do + Rest.post!( + user, + resource(), + boletos + ) + end + + @doc """ + Receive a single Boleto struct previously created in the Stark Bank API by passing its id + + ## Parameters (required): + - user [Project]: Project struct returned from StarkBank.project(). + - id [string]: struct unique id. ex: "5656565656565656" + + ## Return: + - Boleto struct with updated attributes + """ + @spec get(Project.t(), binary) :: {:ok, Boleto.t()} | {:error, [%Error{}]} + def get(%Project{} = user, id) do + Rest.get_id(user, resource(), id) + end + + @doc """ + Same as get(), but it will unwrap the error tuple and raise in case of errors. + """ + @spec get!(Project.t(), binary) :: Boleto.t() + def get!(%Project{} = user, id) do + Rest.get_id!(user, resource(), id) + end + + @doc """ + Receive a single Boleto pdf file generated in the Stark Bank API by passing its id. + + ## Parameters (required): + - user [Project]: Project struct returned from StarkBank.project(). + - id [string]: struct unique id. ex: "5656565656565656" + + ## Return: + - Boleto pdf file content + """ + @spec pdf(Project.t(), binary) :: {:ok, binary} | {:error, [%Error{}]} + def pdf(%Project{} = user, id) do + Rest.get_pdf(user, resource(), id) + end + + @doc """ + Same as pdf(), but it will unwrap the error tuple and raise in case of errors. + """ + @spec pdf!(Project.t(), binary) :: binary + def pdf!(%Project{} = user, id) do + Rest.get_pdf!(user, resource(), id) + end + + @doc """ + Receive a stream of Boleto structs previously created in the Stark Bank API + + ## Parameters (required): + - user [Project]: Project struct returned from StarkBank.project(). + + ## Parameters (optional): + - limit [integer, default nil]: maximum number of structs to be retrieved. Unlimited if nil. ex: 35 + - after [Date, default nil] date filter for structs created only after specified date. ex: Date(2020, 3, 10) + - before [Date, default nil] date filter for structs only before specified date. ex: Date(2020, 3, 10) + - status [string, default nil]: filter for status of retrieved structs. ex: "paid" or "registered" + - 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"] + + ## Return: + - stream of Boleto structs with updated attributes + """ + @spec query(Project.t(), any) :: + ({:cont, {:ok, [Boleto.t()]}} | {:error, [Error.t()]} | {:halt, any} | {:suspend, any}, any -> any) + def query(%Project{} = user, options \\ []) do + Rest.get_list(user, resource(), options |> Checks.check_options(true)) + end + + @doc """ + Same as query(), but it will unwrap the error tuple and raise in case of errors. + """ + @spec query!(Project.t(), any) :: + ({:cont, [Boleto.t()]} | {:halt, any} | {:suspend, any}, any -> any) + def query!(%Project{} = user, options \\ []) do + Rest.get_list!(user, resource(), options |> Checks.check_options(true)) + end + + @doc """ + Delete a list of Boleto entities previously created in the Stark Bank API + + ## Parameters (required): + - user [Project]: Project struct returned from StarkBank.project(). + - id [string]: Boleto unique id. ex: "5656565656565656" + + ## Return: + - deleted Boleto struct with updated attributes + """ + @spec delete(Project.t(), binary) :: {:ok, Boleto.t()} | {:error, [%Error{}]} + def delete(%Project{} = user, id) do + Rest.delete_id(user, resource(), id) + end + + @doc """ + Same as delete(), but it will unwrap the error tuple and raise in case of errors. + """ + @spec delete!(Project.t(), binary) :: Boleto.t() + def delete!(%Project{} = user, id) do + Rest.delete_id!(user, resource(), id) + end + + @doc false + def resource() do + { + "Boleto", + &resource_maker/1 + } + end + + @doc false + def resource_maker(json) do + %Boleto{ + amount: json[:amount], + name: json[:name], + tax_id: json[:tax_id], + street_line_1: json[:street_line_1], + street_line_2: json[:street_line_2], + district: json[:district], + city: json[:city], + state_code: json[:state_code], + zip_code: json[:zip_code], + due: json[:due] |> Checks.check_datetime, + fine: json[:fine], + interest: json[:interest], + overdue_limit: json[:overdue_limit], + tags: json[:tags], + descriptions: json[:descriptions], + id: json[:id], + fee: json[:fee], + line: json[:line], + bar_code: json[:bar_code], + status: json[:status], + created: json[:created] |> Checks.check_datetime + } + end +end diff --git a/lib/boleto/boleto_log.ex b/lib/boleto/boleto_log.ex new file mode 100644 index 0000000..b60f53a --- /dev/null +++ b/lib/boleto/boleto_log.ex @@ -0,0 +1,105 @@ +defmodule StarkBank.Boleto.Log do + + alias __MODULE__, as: Log + alias StarkBank.Utils.Rest, as: Rest + alias StarkBank.Utils.Checks, as: Checks + alias StarkBank.Utils.API, as: API + alias StarkBank.Boleto, as: Boleto + alias StarkBank.User.Project, as: Project + alias StarkBank.Error, as: Error + + @moduledoc """ + Groups Boleto.Log related functions + """ + + @doc """ + Every time a Boleto entity is updated, a corresponding Boleto.Log + is generated for the entity. This log is never generated by the + user, but it can be retrieved to check additional information + on the Boleto. + + ## Attributes: + - id [string]: unique id returned when the log is created. ex: "5656565656565656" + - boleto [Boleto]: Boleto entity to which the log refers to. + - errors [list of strings]: list of errors linked to this Boleto event + - type [string]: type of the Boleto event which triggered the log creation. ex: "registered" or "paid" + - created [DateTime]: creation datetime for the boleto. ex: ~U[2020-03-26 19:32:35.418698Z] + """ + @enforce_keys [:id, :boleto, :errors, :type, :created] + defstruct [:id, :boleto, :errors, :type, :created] + + @type t() :: %__MODULE__{} + + @doc """ + Receive a single Log struct previously created by the Stark Bank API by passing its id + + ## Parameters (required): + - user [Project]: Project struct returned from StarkBank.project(). + - id [string]: struct unique id. ex: "5656565656565656" + + ## Return: + - Log struct with updated attributes + """ + @spec get(Project.t(), binary) :: {:ok, Log.t()} | {:error, [%Error{}]} + def get(%Project{} = user, id) do + Rest.get_id(user, resource(), id) + end + + @doc """ + Same as get(), but it will unwrap the error tuple and raise in case of errors. + """ + @spec get!(Project.t(), binary) :: Log.t() + def get!(%Project{} = user, id) do + Rest.get_id!(user, resource(), id) + end + + @doc """ + Receive a stream of Log structs previously created in the Stark Bank API + + ## Parameters (required): + - user [Project]: Project struct returned from StarkBank.project(). + + ## Parameters (optional): + - limit [integer, default nil]: maximum number of structs to be retrieved. Unlimited if nil. ex: 35 + - after [Date, default nil] date filter for structs created only after specified date. ex: Date(2020, 3, 10) + - before [Date, default nil] date filter for structs only before specified date. ex: Date(2020, 3, 10) + - types [list of strings, default nil]: filter for log event types. ex: "paid" or "registered" + - boleto_ids [list of strings, default nil]: list of Boleto ids to filter logs. ex: ["5656565656565656", "4545454545454545"] + + ## Return: + - stream of Log structs with updated attributes + """ + @spec query(Project.t(), any) :: + ({:cont, {:ok, [Log.t()]}} | {:error, [Error.t()]} | {:halt, any} | {:suspend, any}, any -> any) + def query(%Project{} = user, options \\ []) do + Rest.get_list(user, resource(), options |> Checks.check_options(true)) + end + + @doc """ + Same as query(), but it will unwrap the error tuple and raise in case of errors. + """ + @spec query!(Project.t(), any) :: + ({:cont, [Log.t()]} | {:halt, any} | {:suspend, any}, any -> any) + def query!(%Project{} = user, options \\ []) do + Rest.get_list!(user, resource(), options |> Checks.check_options(true)) + end + + @doc false + def resource() do + { + "BoletoLog", + &resource_maker/1 + } + end + + @doc false + def resource_maker(json) do + %Log{ + id: json[:id], + boleto: json[:boleto] |> API.from_api_json(&Boleto.resource_maker/1), + created: json[:created] |> Checks.check_datetime, + type: json[:type], + errors: json[:errors] + } + end +end diff --git a/lib/charge/charge.ex b/lib/charge/charge.ex deleted file mode 100644 index 1beaa45..0000000 --- a/lib/charge/charge.ex +++ /dev/null @@ -1,760 +0,0 @@ -defmodule StarkBank.Charge do - @moduledoc """ - Used to create and consult charges - - Submodules: - - StarkBank.Charge.Customer: Used to create and consult charge customers; - - StarkBank.Charge.Log: Used to consult charge logs; - - Functions: - - post - - get - - delete - - get_pdf - """ - - alias StarkBank.Utils.Helpers, as: Helpers - alias StarkBank.Utils.Requests, as: Requests - alias StarkBank.Charge.Helpers, as: ChargeHelpers - - defmodule Customer do - @moduledoc """ - Used to create, update and delete charge customers - - Functions: - - post - - get - - get_by_id - - delete - - put - """ - - @doc """ - Registers new customers that can be linked with charge emissions - - Parameters: - - credentials [PID]: agent PID returned by StarkBank.Auth.login; - - customers: list of StarkBank.Charge.Structs.CustomerData; - - Returns {:ok, posted_customers}: - - posted_customers [list of StarkBank.Charge.Structs.CustomerData]: lists all posted customers; - - ## Example: - - iex> StarkBank.Charge.Customer.post(credentials, [customer_1, customer_2]) - {:ok, [customer_1, customer_2]} - """ - def post(credentials, customers) do - registrations = - for partial_customers <- Helpers.chunk_list_by_max_limit(customers), - do: partial_post(credentials, partial_customers) - - Helpers.flatten_responses(registrations) - end - - defp partial_post(credentials, customers) do - encoded_customers = for customer <- customers, do: ChargeHelpers.Customer.encode(customer) - body = %{customers: encoded_customers} - - {response_status, response} = Requests.post(credentials, 'charge/customer', body) - - if response_status != :ok do - {response_status, response} - else - {response_status, - for(customer <- response["customers"], do: ChargeHelpers.Customer.decode(customer))} - end - end - - @doc """ - Gets charge customers data according to informed parameters - - Parameters: - - credentials [PID]: agent PID returned by StarkBank.Auth.login; - - options [keyword list]: refines request - - fields [list of strings]: list of customer fields that should be retrieved from the API; - - tags [list of strings]: filters customers by the provided tags; - - tax_id [string]: filters customers by tax ID; - - limit [int]: maximum results retrieved; - - Returns {:ok, retrieved_customers}: - - retrieved_customers [list of StarkBank.Charge.Structs.CustomerData]: lists all retrieved customers; - - ## Example: - - iex> StarkBank.Charge.Customer.get(credentials, fields: ["tax_id", "name"], limit: 30) - {:ok, [customer_1, customer_2, ... customer_30]} - """ - def get(credentials, options \\ []) do - %{fields: fields, tags: tags, tax_id: tax_id, limit: limit} = - Enum.into(options, %{fields: nil, tags: nil, tax_id: nil, limit: nil}) - - recursive_get( - credentials, - Helpers.snake_to_camel_list_of_strings(fields), - Helpers.lowercase_list_of_strings(tags), - tax_id, - limit, - nil - ) - end - - defp recursive_get(credentials, fields, tags, tax_id, limit, cursor) do - {response_status, response} = partial_get(credentials, fields, tags, tax_id, limit, cursor) - - if response_status != :ok do - {response_status, response} - else - %{cursor: new_cursor, customers: customers} = response - - if is_nil(new_cursor) or Helpers.limit_below_maximum?(limit) do - {response_status, response[:customers]} - else - {new_response_status, new_response} = - recursive_get( - credentials, - fields, - tags, - tax_id, - Helpers.get_recursive_limit(limit), - new_cursor - ) - - if new_response_status != :ok do - {new_response_status, new_response} - else - {new_response_status, customers ++ new_response} - end - end - end - end - - defp partial_get( - credentials, - fields, - tags, - tax_id, - limit, - cursor - ) do - parameters = [ - fields: Helpers.list_to_url_arg(fields), - tags: Helpers.list_to_url_arg(tags), - taxId: tax_id, - limit: Helpers.truncate_limit(limit), - cursor: cursor - ] - - {response_status, response} = Requests.get(credentials, 'charge/customer', parameters) - - if response_status != :ok do - {response_status, response} - else - { - response_status, - %{ - cursor: response["cursor"], - customers: - for(customer <- response["customers"], do: ChargeHelpers.Customer.decode(customer)) - } - } - end - end - - @doc """ - Gets the charge customer with the specified ID - - Parameters: - - credentials [PID]: agent PID returned by StarkBank.Auth.login; - - customer [string or StarkBank.Charge.Structs.CustomerData (with valid ID)]: charge customer ID, e.g.: "6307371336859648"; - - Returns {:ok, retrieved_customer}: - - retrieved_customer [StarkBank.Charge.Structs.CustomerData]: retrieved customer; - - ## Example: - - iex> StarkBank.Charge.Customer.get_by_id(credentials, "6307371336859648") - {:ok, customer_1} - iex> StarkBank.Charge.Customer.get_by_id(credentials, customer_1) - {:ok, customer_1} - """ - def get_by_id(credentials, customer) do - id = Helpers.extract_id(customer) - - {response_status, response} = - Requests.get(credentials, 'charge/customer/' ++ to_charlist(id)) - - if response_status != :ok do - {response_status, response} - else - {response_status, ChargeHelpers.Customer.decode(response["customer"])} - end - end - - @doc """ - Deletes the specified charge customers - - Parameters: - - credentials [PID]: agent PID returned by StarkBank.Auth.login; - - customers [list of strings or list of StarkBank.Charge.Structs.CustomerData (with valid IDs)]: charge customer data or IDs, e.g.: ["6307371336859648"]; - - Returns {:ok, deleted_customers}: - - deleted_customers [list of StarkBank.Charge.Structs.CustomerData]: deleted customers; - - ## Example: - - iex> StarkBank.Charge.Customer.delete(credentials, ["6307371336859648", "5087311326867881"]) - {:ok, [deleted_customer_1, deleted_customer_2]} - """ - def delete(credentials, customers) do - deletions = - for partial_customers <- Helpers.chunk_list_by_max_limit(customers), - do: partial_delete(credentials, partial_customers) - - Helpers.flatten_responses(deletions) - end - - defp partial_delete(credentials, customers) do - parameters = [ - ids: Helpers.treat_nullable_id_or_struct_list(customers) - ] - - {response_status, response} = Requests.delete(credentials, 'charge/customer', parameters) - - if response_status != :ok do - {response_status, response} - else - {response_status, - for(customer <- response["customers"], do: ChargeHelpers.Customer.decode(customer))} - end - end - - @doc """ - Overwrites the charge customer with the specified ID - - Parameters: - - credentials [PID]: agent PID returned by StarkBank.Auth.login; - - customer [StarkBank.Charge.Structs.CustomerData]: charge customer data; - - Returns {:ok, overwritten_customer}: - - overwritten_customer [StarkBank.Charge.Structs.CustomerData]: overwritten customer; - - ## Example: - - iex> StarkBank.Charge.Customer.put(credentials, customer_1) - {:ok, customer_1} - """ - def put(credentials, customer) do - encoded_customers = ChargeHelpers.Customer.encode(customer) - body = %{customer: encoded_customers} - - {response_status, response} = - Requests.put(credentials, 'charge/customer/' ++ to_charlist(customer.id), body) - - if response_status != :ok do - {response_status, response} - else - {response_status, ChargeHelpers.Customer.decode(response["customer"])} - end - end - end - - defmodule Log do - @moduledoc """ - Used to consult charge events; - - Functions: - - get - - get_by_id - """ - - @doc """ - Gets the charge logs according to the provided parameters - - Parameters: - - credentials [PID]: agent PID returned by StarkBank.Auth.login; - - charge_ids [list of strings or list of StarkBank.Charge.Structs.ChargeData]: charge IDs or charge structs, e.g.: ["5618308887871488"]; - - options [keyword list]: refines request - - events [list of string]: filter by log events, namely: "register", "registered", "overdue", "updated", "canceled", "failed", "paid" or "bank"; - - limit [int]: maximum results retrieved; - - Returns {:ok, charge_logs}: - - charge_logs [list of StarkBank.Charge.Structs.ChargeLogData]: retrieved charge logs; - - ## Example: - - iex> StarkBank.Charge.Log.get(credentials, ["6307371336859648", charge]) - {:ok, [charge_log_1, charge_log_2, ..., charge_log_n]} - """ - def get(credentials, charge_ids, options \\ []) do - %{events: events, limit: limit} = Enum.into(options, %{events: nil, limit: nil}) - - recursive_get( - credentials, - Helpers.treat_nullable_id_or_struct_list(charge_ids), - Helpers.list_to_url_arg(events), - limit, - nil - ) - end - - defp recursive_get(credentials, charge_ids, events, limit, cursor) do - {response_status, response} = partial_get(credentials, charge_ids, events, limit, cursor) - - if response_status != :ok do - {response_status, response} - else - %{cursor: new_cursor, logs: logs} = response - - if is_nil(new_cursor) or Helpers.limit_below_maximum?(limit) do - {response_status, response[:logs]} - else - {new_response_status, new_response} = - recursive_get( - credentials, - charge_ids, - events, - Helpers.get_recursive_limit(limit), - new_cursor - ) - - if new_response_status != :ok do - {new_response_status, new_response} - else - {new_response_status, logs ++ new_response} - end - end - end - end - - defp partial_get(credentials, charge_ids, events, limit, cursor) do - parameters = [ - chargeIds: charge_ids, - events: events, - limit: limit, - cursor: cursor - ] - - {response_status, response} = Requests.get(credentials, 'charge/log', parameters) - - if response_status != :ok do - {response_status, response} - else - { - response_status, - %{ - cursor: response["cursor"], - logs: for(log <- response["logs"], do: ChargeHelpers.ChargeLog.decode(log)) - } - } - end - end - - @doc """ - Gets the charge log specified by the provided ID; - - Parameters: - - credentials [PID]: agent PID returned by StarkBank.Auth.login; - - charge_log_id [string or StarkBank.Charge.Structs.ChargeLogData]: charge log ID or struct, e.g.: "6743665380687872"; - - Returns {:ok, charge_log}: - - charge_log [StarkBank.Charge.Structs.ChargeLogData]: retrieved charge log; - - ## Example: - - iex> StarkBank.Charge.Log.get_by_id(credentials, "6307371336859648") - {:ok, charge_log} - """ - def get_by_id(credentials, charge_log_id) do - id = Helpers.extract_id(charge_log_id) - - {response_status, response} = Requests.get(credentials, 'charge/log/' ++ to_charlist(id)) - - if response_status != :ok do - {response_status, response} - else - {response_status, ChargeHelpers.ChargeLog.decode(response["log"])} - end - end - end - - @doc """ - Creates new charges. - For each charge customer that is specified without an ID, - the SDK will first get customers by its tax_id and, - if no customers are a full match or if the 'overwrite_customer_on_mismatch' is false (default), - a new customer will be created and associated with the charge. - If 'overwrite_customer_on_mismatch' is true, the first retrieved customer will be overwritten with the provided customer data. - Therefore, providing ID-less customers may slow the function down significantly, due to the possibly great number of subcalls to the API. - - Parameters: - - credentials [PID]: agent PID returned by StarkBank.Auth.login; - - charges [list of StarkBank.Charge.Structs.ChargeData]: charge structs; - - options [keyword list]: refines request - - overwrite_customer_on_mismatch [bool, default false]: if true, first mismatching customer will be overwritten, if any; if false, new customer will be created (only active if no matching customers are located) - - discount [int]: defines discount in cents if charge is paid before discountDate (if discount is defined, discountDate must also be defined) - - discount_date [date or string ("%Y-%m-%d")]: defines up to when the defined discount will be valid (if discount is defined, discountDate must also be defined) - - Returns {:ok, posted_charges}: - - posted_charges [list of StarkBank.Charge.Structs.ChargeData]: posted charges; - - ## Example: - - iex> StarkBank.Charge.post(credentials, [charge_1, charge_2]) - {:ok, [charge_1, charge_2]} - """ - def post(credentials, charges, options \\ []) do - %{overwrite_customer_on_mismatch: overwrite_customer_on_mismatch} = - Enum.into(options, %{overwrite_customer_on_mismatch: false}) - - charges = - for partial_charges <- Helpers.chunk_list_by_max_limit(charges), - do: partial_post(credentials, partial_charges, overwrite_customer_on_mismatch) - - Helpers.flatten_responses(charges) - end - - defp partial_post(credentials, charges, overwrite_customer_on_mismatch) do - filled_charges = - fill_charges_with_customer_ids(credentials, charges, overwrite_customer_on_mismatch) - - encoded_charges = for charge <- filled_charges, do: ChargeHelpers.Charge.encode(charge) - body = %{charges: encoded_charges} - - {response_status, response} = Requests.post(credentials, 'charge', body) - - if response_status != :ok do - {response_status, response} - else - {response_status, - for(charge <- response["charges"], do: ChargeHelpers.Charge.decode(charge))} - end - rescue - e in MatchError -> {:error, e} - end - - defp fill_charges_with_customer_ids(credentials, charges, overwrite_customer_on_mismatch) do - charges_with_customer_ids = - for charge <- charges, !is_nil(Helpers.extract_id(charge.customer)), do: charge - - charges_without_customer_ids = - for charge <- charges, is_nil(Helpers.extract_id(charge.customer)), do: charge - - located_customers = - for charge <- charges_without_customer_ids, - do: locate_or_make_customer(credentials, charge, overwrite_customer_on_mismatch) - - charges_with_located_customers = - for charge <- charges_without_customer_ids, - do: fill_charge_customer_id(charge, located_customers) - - charges_with_customer_ids ++ charges_with_located_customers - end - - defp locate_or_make_customer(credentials, charge, overwrite_customer_on_mismatch) do - customer = charge.customer - - {:ok, customer_candidates} = - Customer.get( - credentials, - tags: customer.tags, - tax_id: customer.tax_id - ) - - matching_customer = find_matching_customer(customer, customer_candidates) - - if !is_nil(matching_customer) do - matching_customer - else - post_or_put_customer( - credentials, - customer, - customer_candidates, - overwrite_customer_on_mismatch - ) - end - end - - defp post_or_put_customer( - credentials, - customer, - customer_candidates, - overwrite_customer_on_mismatch - ) - when overwrite_customer_on_mismatch and length(customer_candidates) > 0 do - {:ok, put_customer} = - Customer.put( - credentials, - %StarkBank.Charge.Structs.CustomerData{ - customer - | id: hd(customer_candidates).id - } - ) - - put_customer - end - - defp post_or_put_customer( - credentials, - customer, - _customer_candidates, - _overwrite_customer_on_mismatch - ) do - {:ok, post_customers} = Customer.post(credentials, [customer]) - hd(post_customers) - end - - defp fill_charge_customer_id(charge, temp_customers) do - %StarkBank.Charge.Structs.ChargeData{ - charge - | customer: find_matching_customer(charge.customer, temp_customers) - } - end - - defp find_matching_customer(base_customer, [comp_customer | other_comp_customers]) do - if customers_match?(base_customer, comp_customer) do - comp_customer - else - find_matching_customer(base_customer, other_comp_customers) - end - end - - defp find_matching_customer(_base_customer, []) do - nil - end - - defp customers_match?(base_customer, comp_customer) do - base_address = base_customer.address - comp_address = comp_customer.address - - customer_tax_id = base_customer.tax_id |> ChargeHelpers.Customer.normalize_tax_id() - comp_customer_tax_id = comp_customer.tax_id |> ChargeHelpers.Customer.normalize_tax_id() - - Helpers.nullable_fields_match?(base_customer.name, comp_customer.name) and - Helpers.nullable_fields_match?(base_customer.email, comp_customer.email) and - Helpers.nullable_fields_match?(customer_tax_id, comp_customer_tax_id) and - Helpers.nullable_fields_match?( - Helpers.lowercase_list_of_strings(base_customer.tags), - comp_customer.tags - ) and - Helpers.nullable_fields_match?(base_address.street_line_1, comp_address.street_line_1) and - Helpers.nullable_fields_match?(base_address.street_line_2, comp_address.street_line_2) and - Helpers.nullable_fields_match?(base_address.district, comp_address.district) and - Helpers.nullable_fields_match?(base_address.city, comp_address.city) and - Helpers.nullable_fields_match?(base_address.state_code, comp_address.state_code) and - Helpers.nullable_fields_match?(base_address.zip_code, comp_address.zip_code) - end - - @doc """ - Gets charges according to the provided parameters - - Parameters: - - credentials [PID]: agent PID returned by StarkBank.Auth.login; - - options [keyword list]: refines request - - status [string]: filters specified charge status, namely: "created", "registered", "paid", "overdue", "canceled" or "failed"; - - tags [list of strings]: filters charges by tags, e.g.: ["client1", "cash-in"]; - - ids [list of strings or StarkBank.Charge.Structs.ChargeData]: charge IDs or data structs to be retrieved, e.g.: ["5718322100305920", "5705293853884416"]; - - fields [list of strings]: selects charge data fields on API response, e.g.: ["id", "amount", "status"]; - - filter_after [date or string ("%Y-%m-%d")]: only gets charges created after this date, e.g.: "2019-04-01"; - - filter_before [date or string ("%Y-%m-%d")]: only gets charges created before this date, e.g.: "2019-05-01"; - - limit [int]: maximum results retrieved; - - Returns {:ok, retrieved_charges}: - - retrieved_charges [list of StarkBank.Charge.Structs.ChargeData]: retrieved charges; - - ## Example: - - iex> StarkBank.Charge.get(credentials, tags: ["test", "stark"], filter_after: Date.add(Date.utc_today(), -7)) - {:ok, [charge_1, charge_2, ..., charge_n]} - """ - def get( - credentials, - options \\ [] - ) do - %{ - status: status, - tags: tags, - ids: ids, - fields: fields, - filter_after: filter_after, - filter_before: filter_before, - limit: limit - } = - Enum.into(options, %{ - status: nil, - tags: nil, - ids: nil, - fields: nil, - filter_after: nil, - filter_before: nil, - limit: nil - }) - - recursive_get( - credentials, - status, - Helpers.lowercase_list_of_strings(tags), - ids, - Helpers.snake_to_camel_list_of_strings(fields), - filter_after, - filter_before, - limit, - nil - ) - end - - defp recursive_get( - credentials, - status, - tags, - ids, - fields, - filter_after, - filter_before, - limit, - cursor - ) do - {response_status, response} = - partial_get( - credentials, - status, - tags, - ids, - fields, - filter_after, - filter_before, - limit, - cursor - ) - - if response_status != :ok do - {response_status, response} - else - %{cursor: new_cursor, charges: charges} = response - - if is_nil(new_cursor) or Helpers.limit_below_maximum?(limit) do - {response_status, response[:charges]} - else - {new_response_status, new_response} = - recursive_get( - credentials, - status, - tags, - ids, - fields, - filter_after, - filter_before, - Helpers.get_recursive_limit(limit), - new_cursor - ) - - if new_response_status != :ok do - {new_response_status, new_response} - else - {new_response_status, charges ++ new_response} - end - end - end - end - - defp partial_get( - credentials, - status, - tags, - ids, - fields, - filter_after, - filter_before, - limit, - cursor - ) do - parameters = [ - status: status, - tags: Helpers.list_to_url_arg(tags), - ids: Helpers.treat_nullable_id_or_struct_list(ids), - fields: Helpers.list_to_url_arg(fields), - after: Helpers.date_to_string(filter_after), - before: Helpers.date_to_string(filter_before), - limit: limit, - cursor: cursor - ] - - {response_status, response} = Requests.get(credentials, 'charge', parameters) - - if response_status != :ok do - {response_status, response} - else - { - response_status, - %{ - cursor: response["cursor"], - charges: for(charge <- response["charges"], do: ChargeHelpers.Charge.decode(charge)) - } - } - end - end - - @doc """ - Deletes the specified charges - - Parameters: - - credentials [PID]: agent PID returned by StarkBank.Auth.login; - - charges [list of strings or StarkBank.Charge.Structs.ChargeData]: charge IDs or data structs to be deleted, e.g.: ["5718322100305920", "5705293853884416"]; - - Returns {:ok, deleted_charges}: - - deleted_charges [list of StarkBank.Charge.Structs.ChargeData]: deleted charges; - - ## Example: - - iex> StarkBank.Charge.delete(credentials, ["1872563178531872", charge_2, "1092381029381092", charge_4]) - {:ok, [charge_1, charge_2, charge_3, charge_4]} - """ - def delete(credentials, charges) do - deletions = - for partial_charges <- Helpers.chunk_list_by_max_limit(charges), - do: partial_delete(credentials, partial_charges) - - Helpers.flatten_responses(deletions) - end - - defp partial_delete(credentials, charges) do - parameters = [ - ids: Helpers.treat_nullable_id_or_struct_list(charges) - ] - - {response_status, response} = Requests.delete(credentials, 'charge', parameters) - - if response_status != :ok do - {response_status, response} - else - {response_status, - for(charge <- response["charges"], do: ChargeHelpers.Charge.decode(charge))} - end - end - - @doc """ - Gets the specified charge PDF file content - - Parameters: - - credentials [PID]: agent PID returned by StarkBank.Auth.login; - - charge [string or StarkBank.Charge.Structs.ChargeData]: charge ID or data struct, e.g.: "5718322100305920"; - - Returns {:ok, pdf_content}: - - pdf_content [string]: pdf file content; - - ## Example: - - iex> StarkBank.Charge.get_pdf(credentials, "1872563178531872") - {:ok, pdf_content} - iex> StarkBank.Charge.get_pdf(credentials, charge) - {:ok, pdf_content} - """ - def get_pdf(credentials, charge) do - Requests.get( - credentials, - 'charge/' ++ to_charlist(Helpers.extract_id(charge)) ++ '/pdf', - nil, - false - ) - end -end diff --git a/lib/charge/helpers.ex b/lib/charge/helpers.ex deleted file mode 100644 index d79da46..0000000 --- a/lib/charge/helpers.ex +++ /dev/null @@ -1,149 +0,0 @@ -defmodule StarkBank.Charge.Helpers do - @moduledoc false - - alias StarkBank.Utils.Helpers, as: MainHelpers - - defmodule Customer do - @moduledoc false - def encode(customer) do - address = customer.address - - %{ - name: customer.name, - email: customer.email, - taxId: customer.tax_id, - phone: customer.phone, - tags: customer.tags, - address: %{ - streetLine1: address.street_line_1, - streetLine2: address.street_line_2, - district: address.district, - city: address.city, - stateCode: address.state_code, - zipCode: address.zip_code - } - } - end - - def decode(customer_map) do - charge_count = customer_map["chargeCount"] - address = customer_map["address"] - - %StarkBank.Charge.Structs.CustomerData{ - name: customer_map["name"], - email: customer_map["email"], - tax_id: customer_map["taxId"], - phone: customer_map["phone"], - id: customer_map["id"], - charge_count: %StarkBank.Charge.Structs.ChargeCountData{ - overdue: charge_count["overdue"], - pending: charge_count["pending"] - }, - address: %StarkBank.Charge.Structs.AddressData{ - street_line_1: address["streetLine1"], - street_line_2: address["streetLine2"], - district: address["district"], - city: address["city"], - state_code: address["stateCode"], - zip_code: address["zipCode"] - }, - tags: customer_map["tags"] - } - end - - def normalize_tax_id(tax_id) when is_binary(tax_id) do - tax_id - |> String.replace(".", "") - |> String.replace("-", "") - end - - def normalize_tax_id(tax_id) do - tax_id - |> to_string() - |> normalize_tax_id() - end - end - - defmodule Charge do - @moduledoc false - def encode(charge) do - %{ - amount: charge.amount, - customerId: MainHelpers.extract_id(charge.customer), - dueDate: MainHelpers.date_to_string(charge.due_date), - fine: charge.fine, - interest: charge.interest, - overdueLimit: charge.overdue_limit, - discount: charge.discount, - discountDate: MainHelpers.date_to_string(charge.discount_date), - tags: charge.tags, - descriptions: for(description <- charge.descriptions, do: encode_description(description)) - } - end - - def decode(charge_map) do - %StarkBank.Charge.Structs.ChargeData{ - amount: charge_map["amount"], - id: charge_map["id"], - status: charge_map["status"], - issue_date: MainHelpers.string_to_datetime(charge_map["issueDate"]), - workspace_id: charge_map["workspaceId"], - bar_code: charge_map["barCode"], - line: charge_map["line"], - due_date: MainHelpers.string_to_datetime(charge_map["dueDate"]), - fine: charge_map["fine"], - interest: charge_map["interest"], - overdue_limit: charge_map["overdueLimit"], - discount: charge_map["discount"], - discount_date: MainHelpers.string_to_datetime(charge_map["discountDate"]), - tags: charge_map["tags"], - descriptions: decode_descriptions(charge_map["descriptions"]), - customer: %StarkBank.Charge.Structs.CustomerData{ - name: charge_map["name"], - tax_id: charge_map["taxId"], - id: charge_map["customerId"], - address: %StarkBank.Charge.Structs.AddressData{ - street_line_1: charge_map["streetLine1"], - street_line_2: charge_map["streetLine2"], - district: charge_map["district"], - city: charge_map["city"], - state_code: charge_map["stateCode"], - zip_code: charge_map["zipCode"] - } - } - } - end - - defp encode_description(description) do - %{text: description.text, amount: description.amount} - end - - defp decode_descriptions(descriptions) when is_nil(descriptions) do - [] - end - - defp decode_descriptions(descriptions) do - for description_map <- descriptions, do: decode_description(description_map) - end - - defp decode_description(description) do - %StarkBank.Charge.Structs.ChargeDescriptionData{ - text: description["text"], - amount: description["amount"] - } - end - end - - defmodule ChargeLog do - @moduledoc false - def decode(charge_log_map) do - %StarkBank.Charge.Structs.ChargeLogData{ - id: charge_log_map["id"], - event: charge_log_map["event"], - created: charge_log_map["created"], - errors: charge_log_map["errors"], - charge: Charge.decode(charge_log_map["charge"]) - } - end - end -end diff --git a/lib/charge/structs/address.ex b/lib/charge/structs/address.ex deleted file mode 100644 index 720c18f..0000000 --- a/lib/charge/structs/address.ex +++ /dev/null @@ -1,20 +0,0 @@ -defmodule StarkBank.Charge.Structs.AddressData do - @doc """ - Holds charge customer address data - Is usually nested in StarkBank.Charge.Structs.Customer - - Parameters: - - street_line_1 [string]: e.g.: "Av. Faria Lima, 1844"; - - street_line_2 [string]: e.g.: "CJ 13"; - - district [string]: e.g.: "Itaim Bibi"; - - city [string]: e.g.: "Sao Paulo"; - - state_code [string]: e.g.: "SP"; - - zip_code [string]: e.g.: "01500-000"; - """ - defstruct street_line_1: "", - street_line_2: "", - district: "", - city: "", - state_code: "", - zip_code: "" -end diff --git a/lib/charge/structs/charge.ex b/lib/charge/structs/charge.ex deleted file mode 100644 index 1690014..0000000 --- a/lib/charge/structs/charge.ex +++ /dev/null @@ -1,41 +0,0 @@ -defmodule StarkBank.Charge.Structs.ChargeData do - @doc """ - Holds data from a single charge - - Parameters: - - customer [StarkBank.Charge.Structs.CustomerData]: charge customer data; - - amount [int]: total charged amount in cents, e.g.: 150000 (= R$1.500,00); - - id [string]: charge unique ID, e.g.: "5730684534521856"; - - bar_code [string]: charge bar code, e.g.: "34198777500000500001090000788367307144464000"; - - line [string]: charge line number, e.g.: "34191.09008 00788.367308 71444.640008 8 77750000050000"; - - due_date [timestamp as string]: charge due date, e.g.: "2019-01-21T01:59:59.999999+00:00"; - - issue_date [timestamp as string]: charge issue date, e.g.: "2018-12-29T20:05:33.812908+00:00"; - - overdue_limit [int]: number of days after due date when the charge will expire, 0<= n <= 59, e.g.: 5; - - fine [float]: percentage of the charge amount to be charged if paid after due date, e.g.: 2.00 (= 2%); - - interest [float]: monthly interest, in percentage, charged if paid after due date, e.g.: 1.50 (= 1.5%); - - discount [float]: defines the discount percentage applicable if charge is paid before discount_date (if discount is defined, discountDate must also be defined) - - discount_date [date or string ("%Y-%m-%d")]: defines limit date until when the defined discount will be valid (if discount is defined, discountDate must also be defined) - - status [string]: charge status, e.g.: created, registered, paid, overdue, canceled, failed; - - tags [list of strings]: custom tags used when searching charges, e.g.: ["client1", "cash-in"]; - - workspace_id [strings]: workspace_id that created the charge, e.g.: "5078376503050240"; - - descriptions [list of StarkBank.Charge.Structs.ChargeDescriptionData]: list of charge descriptions; - """ - defstruct [ - :customer, - :amount, - id: nil, - bar_code: nil, - line: nil, - due_date: nil, - issue_date: nil, - overdue_limit: nil, - fine: nil, - interest: nil, - discount: nil, - discount_date: nil, - status: nil, - workspace_id: nil, - tags: [], - descriptions: [] - ] -end diff --git a/lib/charge/structs/charge_count.ex b/lib/charge/structs/charge_count.ex deleted file mode 100644 index 60dba02..0000000 --- a/lib/charge/structs/charge_count.ex +++ /dev/null @@ -1,12 +0,0 @@ -defmodule StarkBank.Charge.Structs.ChargeCountData do - @doc """ - Holds customer charge counters - Is usually nested in StarkBank.Charge.Structs.Charge - - Parameters: - - overdue [int]: counts the customer overdue charges, e.g.: 3; - - pending [int]: counts the customer pending charges, e.g.: 2; - """ - defstruct overdue: nil, - pending: nil -end diff --git a/lib/charge/structs/charge_description.ex b/lib/charge/structs/charge_description.ex deleted file mode 100644 index bee1fea..0000000 --- a/lib/charge/structs/charge_description.ex +++ /dev/null @@ -1,11 +0,0 @@ -defmodule StarkBank.Charge.Structs.ChargeDescriptionData do - @doc """ - Holds data from a single charge description - Is usually nested in StarkBank.Charge.Structs.Charge - - Parameters: - - text [string]: text describing the apointed amount, e.g.: "- Taxes"; - - amount [int]: part of the charge total amount (in cents) that is being described, e.g.: 579; - """ - defstruct [:text, :amount] -end diff --git a/lib/charge/structs/charge_log.ex b/lib/charge/structs/charge_log.ex deleted file mode 100644 index 41dd90a..0000000 --- a/lib/charge/structs/charge_log.ex +++ /dev/null @@ -1,14 +0,0 @@ -defmodule StarkBank.Charge.Structs.ChargeLogData do - @doc """ - Holds data from a single charge log - Is usually nested in StarkBank.Charge.Structs.Charge - - Parameters: - - id [string]: charge log id, e.g.: 312387192837; - - event [string]: log event, namely: "register", "registered", "overdue", "updated", "canceled", "failed", "paid" or "bank"; - - created [timestamp as string]: log creation timestamp, e.g.: "2019-05-21T23:15:50.567533+00:00"; - - errors [list of strings]: list of errors logs; - - charge [StarkBank.Charge.Structs.Charge]: charge data; - """ - defstruct [:id, :event, :created, :errors, :charge] -end diff --git a/lib/charge/structs/customer.ex b/lib/charge/structs/customer.ex deleted file mode 100644 index 2104ba7..0000000 --- a/lib/charge/structs/customer.ex +++ /dev/null @@ -1,23 +0,0 @@ -defmodule StarkBank.Charge.Structs.CustomerData do - @doc """ - Holds data from a single customer - Can be nested in StarkBank.Charge.Structs.Charge - - Parameters: - - name [string]: customer name, e.g.: "Arya Stark"; - - email [string]: customer email, e.g.: "arya.stark@westeros.com"; - - tax_id [string]: customer tax ID (CPF, CNPJ), e.g.: "012.345.678-90"; - - phone [string] customer phone number, e.g.: "(11) 98300-0000"; - - tags [list of strings]: customer custom tags, e.g.: ["little girl", "no one", "valar morghulis", "Stark"]; - - address [StarkBank.Charge.Structs.AddressData]: customer adress data; - """ - @enforce_keys [:name] - defstruct name: "", - email: "", - tax_id: "", - phone: "", - id: nil, - charge_count: %StarkBank.Charge.Structs.ChargeCountData{}, - address: %StarkBank.Charge.Structs.AddressData{}, - tags: [] -end diff --git a/lib/error.ex b/lib/error.ex new file mode 100644 index 0000000..1abff12 --- /dev/null +++ b/lib/error.ex @@ -0,0 +1,15 @@ +defmodule StarkBank.Error do + @doc """ + Error generated on interactions with the API + + If the error code is: + - "internalServerError": the API has run into an internal error. If you ever stumble upon this one, rest assured that the development team is already rushing in to fix the mistake and get you back up to speed. + - "unknownException": a request encounters an error that has not been sent by the API, such as connectivity problems. + - any other binary: the API has detected a mistake in your request + + ## Attributes: + - code [string]: defines de error code. ex: "invalidCredentials" + - message [string]: explains the detected error. ex: "Provided digital signature in the header Access-Signature does not check out. See https://docs.api.starkbank.com/#auth for details." + """ + defstruct [:code, :message] +end diff --git a/lib/key.ex b/lib/key.ex new file mode 100644 index 0000000..dacd738 --- /dev/null +++ b/lib/key.ex @@ -0,0 +1,39 @@ +defmodule StarkBank.Key do + + @moduledoc """ + Used to generate API-compatible key pairs + """ + + alias EllipticCurve.PrivateKey, as: PrivateKey + alias EllipticCurve.PublicKey, as: PublicKey + + @doc """ + Generates a secp256k1 ECDSA private/public key pair to be used in the API authentications + + ## Parameters (optional): + - path [string, default nil]: path to save the keys .pem files. No files will be saved if this parameter isn't provided. + """ + @spec create(any) :: {binary, binary} + def create(path \\ nil) do + private = PrivateKey.generate() + public = PrivateKey.getPublicKey(private) + + private_pem = private |> PrivateKey.toPem() + public_pem = public |> PublicKey.toPem() + + save_file(private_pem, path, "privateKey.pem") + save_file(public_pem, path, "publicKey.pem") + + {private_pem, public_pem} + end + + defp save_file(_pem, path, _suffix) when is_nil(path) do + end + + defp save_file(pem, path, suffix) do + File.mkdir_p!(path) + file = File.open!(Path.join(path, suffix), [:write]) + IO.binwrite(file, pem) + File.close(file) + end +end diff --git a/lib/ledger/balance.ex b/lib/ledger/balance.ex new file mode 100644 index 0000000..3edb7f2 --- /dev/null +++ b/lib/ledger/balance.ex @@ -0,0 +1,72 @@ +defmodule StarkBank.Balance do + + alias __MODULE__, as: Balance + alias StarkBank.Utils.Rest, as: Rest + alias StarkBank.Utils.Checks, as: Checks + alias StarkBank.User.Project, as: Project + alias StarkBank.Error, as: Error + + @moduledoc """ + Groups Balance related functions + """ + + @doc """ + The Balance struct displays the current balance of the workspace, + which is the result of the sum of all transactions within this + workspace. The balance is never generated by the user, but it + can be retrieved to see the information available. + + ## Attributes (return-only): + - id [string, default nil]: unique id returned when Boleto is created. ex: "5656565656565656" + - amount [integer, default nil]: current balance amount of the workspace in cents. ex: 200 (= R$ 2.00) + - currency [string, default nil]: currency of the current workspace. Expect others to be added eventually. ex: "BRL" + - updated [DateTime, default nil]: update datetime for the balance. ex: ~U[2020-03-26 19:32:35.418698Z] + """ + defstruct [:id, :amount, :currency, :updated] + + @type t() :: %__MODULE__{} + + @doc """ + Receive the Balance entity linked to your workspace in the Stark Bank API + + ## Parameters (required): + - user [Project]: Project struct returned from StarkBank.project(). + + ## Return: + - Balance struct with updated attributes + """ + @spec get(Project.t()) :: {:ok, Balance.t()} | {:error, [Error]} + def get(%Project{} = user) do + case Rest.get_list(user, resource()) |> Enum.take(1) do + [{:ok, balance}] -> {:ok, balance} + [{:error, error}] -> {:error, error} + end + end + + @doc """ + Same as get(), but it will unwrap the error tuple and raise in case of errors. + """ + @spec get!(Project.t()) :: Balance.t() + def get!(%Project{} = user) do + {:ok, balance} = get(user) + balance + end + + @doc false + def resource() do + { + "Balance", + &resource_maker/1 + } + end + + @doc false + def resource_maker(json) do + %Balance{ + id: json[:id], + amount: json[:amount], + currency: json[:currency], + updated: json[:updated] |> Checks.check_datetime + } + end +end diff --git a/lib/ledger/transaction.ex b/lib/ledger/transaction.ex new file mode 100644 index 0000000..4f08b0b --- /dev/null +++ b/lib/ledger/transaction.ex @@ -0,0 +1,151 @@ +defmodule StarkBank.Transaction do + + alias __MODULE__, as: Transaction + alias StarkBank.Utils.Rest, as: Rest + alias StarkBank.Utils.Checks, as: Checks + alias StarkBank.User.Project, as: Project + alias StarkBank.Error, as: Error + + @moduledoc """ + Groups Transaction related functions + """ + + @doc """ + A Transaction is a transfer of funds between workspaces inside Stark Bank. + Transactions created by the user are only for internal transactions. + Other operations (such as transfer or charge-payment) will automatically + create a transaction for the user which can be retrieved for the statement. + When you initialize a Transaction, the entity will not be automatically + created in the Stark Bank API. The 'create' function sends the structs + to the Stark Bank API and returns the list of created structs. + + ## Parameters (required): + - amount [integer]: amount in cents to be transferred. ex: 1234 (= R$ 12.34) + - description [string]: text to be displayed in the receiver and the sender statements (Min. 10 characters). ex: "funds redistribution" + - external_id [string]: unique id, generated by user, to avoid duplicated transactions. ex: "transaction ABC 2020-03-30" + - received_id [string]: unique id of the receiving workspace. ex: "5656565656565656" + + ## Parameters (optional): + - tags [list of strings]: list of strings for reference when searching transactions (may be empty). ex: ["abc", "test"] + + ## Attributes (return-only): + - id [string, default nil]: unique id returned when Transaction is created. ex: "7656565656565656" + - sender_id [string]: unique id of the sending workspace. ex: "5656565656565656" + - fee [integer, default nil]: fee charged when transfer is created. ex: 200 (= R$ 2.00) + - source [string, default nil]: locator of the entity that generated the transaction. ex: "charge/18276318736" or "transfer/19381639871263/chargeback" + - created [DateTime, default nil]: creation datetime for the boleto. ex: ~U[2020-03-26 19:32:35.418698Z] + """ + @enforce_keys [:amount, :description, :external_id, :receiver_id] + defstruct [:amount, :description, :external_id, :receiver_id, :sender_id, :tags, :id, :fee, :created, :source] + + @type t() :: %__MODULE__{} + + @doc """ + Send a list of Transaction entities for creation in the Stark Bank API + + ## Parameters (required): + - user [Project]: Project struct returned from StarkBank.project(). + - transactions [list of Transaction entities]: list of Transaction entities to be created in the API + + ## Return: + - list of Transaction structs with updated attributes + """ + @spec create(Project.t(), [Transaction.t()]) :: + {:ok, [Transaction.t()]} | {:error, [Error.t()]} + def create(%Project{} = user, transactions) do + Rest.post( + user, + resource(), + transactions + ) + end + + @doc """ + Same as create(), but it will unwrap the error tuple and raise in case of errors. + """ + @spec create!(Project.t(), [Transaction.t()]) :: any + def create!(%Project{} = user, transactions) do + Rest.post!( + user, + resource(), + transactions + ) + end + + @doc """ + Receive a single Transaction entity previously created in the Stark Bank API by passing its id + + ## Parameters (required): + - user [Project]: Project struct returned from StarkBank.project(). + - id [string]: entity unique id. ex: "5656565656565656" + + ## Return: + - Transaction struct with updated attributes + """ + @spec get(Project.t(), binary) :: {:ok, Transaction.t()} | {:error, [%Error{}]} + def get(%Project{} = user, id) do + Rest.get_id(user, resource(), id) + end + + @doc """ + Same as get(), but it will unwrap the error tuple and raise in case of errors. + """ + @spec get!(Project.t(), binary) :: Transaction.t() + def get!(%Project{} = user, id) do + Rest.get_id!(user, resource(), id) + end + + @doc """ + Receive a stream of Transaction entities previously created in the Stark Bank API + + ## Parameters (required): + - user [Project]: Project struct returned from StarkBank.project(). + + ## Parameters (optional): + - limit [integer, default nil]: maximum number of entities to be retrieved. Unlimited if nil. ex: 35 + - after [Date, default nil] date filter for entities created only after specified date. ex: Date(2020, 3, 10) + - before [Date, default nil] date filter for entities created only before specified date. ex: Date(2020, 3, 10) + - external_ids [list of strings, default nil]: list of external ids to filter retrieved entities. ex: ["5656565656565656", "4545454545454545"] + + ## Return: + - stream of Transaction structs with updated attributes + """ + @spec query(Project.t(), any) :: + ({:cont, {:ok, [Transaction.t()]}} | {:error, [Error.t()]} | {:halt, any} | {:suspend, any}, any -> any) + def query(%Project{} = user, options \\ []) do + Rest.get_list(user, resource(), options |> Checks.check_options(true)) + end + + @doc """ + Same as query(), but it will unwrap the error tuple and raise in case of errors. + """ + @spec query!(Project.t(), any) :: + ({:cont, [Transaction.t()]} | {:halt, any} | {:suspend, any}, any -> any) + def query!(%Project{} = user, options \\ []) do + Rest.get_list!(user, resource(), options |> Checks.check_options(true)) + end + + @doc false + def resource() do + { + "Transaction", + &resource_maker/1 + } + end + + @doc false + def resource_maker(json) do + %Transaction{ + amount: json[:amount], + description: json[:description], + external_id: json[:external_id], + receiver_id: json[:receiver_id], + sender_id: json[:sender_id], + tags: json[:tags], + id: json[:id], + fee: json[:fee], + created: json[:created], + source: json[:source] + } + end +end diff --git a/lib/payment/boleto/boleto_payment.ex b/lib/payment/boleto/boleto_payment.ex new file mode 100644 index 0000000..91a9f29 --- /dev/null +++ b/lib/payment/boleto/boleto_payment.ex @@ -0,0 +1,200 @@ +defmodule StarkBank.BoletoPayment do + + alias StarkBank.Utils.Rest, as: Rest + alias StarkBank.Utils.Checks, as: Checks + alias StarkBank.BoletoPayment, as: BoletoPayment + alias StarkBank.User.Project, as: Project + alias StarkBank.Error, as: Error + + @moduledoc """ + Groups BoletoPayment related functions + """ + + @doc """ + When you initialize a BoletoPayment, the entity will not be automatically + created in the Stark Bank API. The 'create' function sends the structs + to the Stark Bank API and returns the list of created structs. + + ## Parameters (conditionally required): + - line [string, default nil]: Number sequence that describes the payment. Either 'line' or 'bar_code' parameters are required. If both are sent, they must match. ex: "34191.09008 63571.277308 71444.640008 5 81960000000062" + - bar_code [string, default nil]: Bar code number that describes the payment. Either 'line' or 'barCode' parameters are required. If both are sent, they must match. ex: "34195819600000000621090063571277307144464000" + + ## Parameters (required): + - tax_id [string]: receiver tax ID (CPF or CNPJ) with or without formatting. ex: "01234567890" or "20.018.183/0001-80" + - description [string]: Text to be displayed in your statement (min. 10 characters). ex: "payment ABC" + + ## Parameters (optional): + - scheduled [Date, default today]: payment scheduled date. ex: ~D[2020-03-25] + - tags [list of strings]: list of strings for tagging + + ## Attributes (return-only): + - id [string, default nil]: unique id returned when payment is created. ex: "5656565656565656" + - status [string, default nil]: current payment status. ex: "registered" or "paid" + - amount [int, default nil]: amount automatically calculated from line or bar_code. ex: 23456 (= R$ 234.56) + - fee [integer, default nil]: fee charged when a boleto payment is created. ex: 200 (= R$ 2.00) + - created [DateTime, default nil]: creation datetime for the payment. ex: ~U[2020-03-26 19:32:35.418698Z] + """ + @enforce_keys [:tax_id, :description] + defstruct [:line, :bar_code, :tax_id, :description, :scheduled, :tags, :id, :status, :amount, :fee, :created] + + @type t() :: %__MODULE__{} + + @doc """ + Send a list of BoletoPayment structs for creation in the Stark Bank API + + ## Parameters (required): + - user [Project]: Project struct returned from StarkBank.project(). + - payments [list of BoletoPayment structs]: list of BoletoPayment structs to be created in the API + + ## Return: + - list of BoletoPayment structs with updated attributes + """ + @spec create(Project.t(), [BoletoPayment.t()]) :: + {:ok, [BoletoPayment.t()]} | {:error, [Error.t()]} + def create(%Project{} = user, payments) do + Rest.post( + user, + resource(), + payments + ) + end + + @doc """ + Same as create(), but it will unwrap the error tuple and raise in case of errors. + """ + @spec create!(Project.t(), [BoletoPayment.t()]) :: any + def create!(%Project{} = user, payments) do + Rest.post!( + user, + resource(), + payments + ) + end + + @doc """ + Receive a single BoletoPayment struct previously created by the Stark Bank API by passing its id + + ## Parameters (required): + - user [Project]: Project struct returned from StarkBank.project(). + - id [string]: struct unique id. ex: "5656565656565656" + + ## Return: + - BoletoPayment struct with updated attributes + """ + @spec get(Project.t(), binary) :: {:ok, BoletoPayment.t()} | {:error, [%Error{}]} + def get(%Project{} = user, id) do + Rest.get_id(user, resource(), id) + end + + @doc """ + Same as get(), but it will unwrap the error tuple and raise in case of errors. + """ + @spec get!(Project.t(), binary) :: BoletoPayment.t() + def get!(%Project{} = user, id) do + Rest.get_id!(user, resource(), id) + end + + @doc """ + Receive a single BoletoPayment pdf file generated in the Stark Bank API by passing its id. + Only valid for boleto payments with "success" status. + + ## Parameters (required): + - user [Project]: Project struct returned from StarkBank.project(). + - id [string]: struct unique id. ex: "5656565656565656" + + ## Return: + - BoletoPayment pdf file content + """ + @spec pdf(Project.t(), binary) :: {:ok, binary} | {:error, [%Error{}]} + def pdf(%Project{} = user, id) do + Rest.get_pdf(user, resource(), id) + end + + @doc """ + Same as pdf(), but it will unwrap the error tuple and raise in case of errors. + """ + @spec pdf!(Project.t(), binary) :: binary + def pdf!(%Project{} = user, id) do + Rest.get_pdf!(user, resource(), id) + end + + @doc """ + Receive a stream of BoletoPayment structs previously created in the Stark Bank API + + ## Parameters (required): + - user [Project]: Project struct returned from StarkBank.project(). + + ## Parameters (optional): + - limit [integer, default nil]: maximum number of structs to be retrieved. Unlimited if nil. ex: 35 + - after [Date, default nil] date filter for structs created only after specified date. ex: Date(2020, 3, 10) + - before [Date, default nil] date filter for structs only before specified date. ex: Date(2020, 3, 10) + - tags [list of strings, default nil]: tags to filter retrieved structs. ex: ["tony", "stark"] + - ids [list of strings, default null]: list of ids to filter retrieved objects. ex: ["5656565656565656", "4545454545454545"] + - status [string, default nil]: filter for status of retrieved structs. ex: "paid" + + ## Return: + - stream of BoletoPayment structs with updated attributes + """ + @spec query(Project.t(), any) :: + ({:cont, {:ok, [BoletoPayment.t()]}} | {:error, [Error.t()]} | {:halt, any} | {:suspend, any}, any -> any) + def query(%Project{} = user, options \\ []) do + Rest.get_list(user, resource(), options |> Checks.check_options(true)) + end + + @doc """ + Same as query(), but it will unwrap the error tuple and raise in case of errors. + """ + @spec query!(Project.t(), any) :: + ({:cont, [BoletoPayment.t()]} | {:halt, any} | {:suspend, any}, any -> any) + def query!(%Project{} = user, options \\ []) do + Rest.get_list!(user, resource(), options |> Checks.check_options(true)) + end + + @doc """ + Delete a BoletoPayment entity previously created in the Stark Bank API + + ## Parameters (required): + - user [Project]: Project struct returned from StarkBank.project(). + - id [string]: BoletoPayment unique id. ex: "5656565656565656" + + ## Return: + - deleted BoletoPayment struct with updated attributes + """ + @spec delete(Project.t(), binary) :: {:ok, BoletoPayment.t()} | {:error, [%Error{}]} + def delete(%Project{} = user, id) do + Rest.delete_id(user, resource(), id) + end + + @doc """ + Same as delete(), but it will unwrap the error tuple and raise in case of errors. + """ + @spec delete!(Project.t(), binary) :: BoletoPayment.t() + def delete!(%Project{} = user, id) do + Rest.delete_id!(user, resource(), id) + end + + @doc false + def resource() do + { + "BoletoPayment", + &resource_maker/1 + } + end + + @doc false + def resource_maker(json) do + %BoletoPayment{ + line: json[:line], + bar_code: json[:bar_code], + tax_id: json[:tax_id], + description: json[:description], + scheduled: json[:scheduled] |> Checks.check_datetime, + tags: json[:tags], + id: json[:id], + status: json[:status], + amount: json[:amount], + fee: json[:fee], + created: json[:created] |> Checks.check_datetime + } + end +end diff --git a/lib/payment/boleto/boleto_payment_log.ex b/lib/payment/boleto/boleto_payment_log.ex new file mode 100644 index 0000000..7e82c97 --- /dev/null +++ b/lib/payment/boleto/boleto_payment_log.ex @@ -0,0 +1,105 @@ +defmodule StarkBank.BoletoPayment.Log do + + alias __MODULE__, as: Log + alias StarkBank.Utils.Rest, as: Rest + alias StarkBank.Utils.Checks, as: Checks + alias StarkBank.Utils.API, as: API + alias StarkBank.BoletoPayment, as: BoletoPayment + alias StarkBank.User.Project, as: Project + alias StarkBank.Error, as: Error + + @moduledoc """ + Groups BoletoPayment.Log related functions + """ + + @doc """ + Every time a BoletoPayment entity is modified, a corresponding BoletoPayment.Log + is generated for the entity. This log is never generated by the + user, but it can be retrieved to check additional information + on the BoletoPayment. + + ## Attributes: + - id [string]: unique id returned when the log is created. ex: "5656565656565656" + - payment [BoletoPayment]: BoletoPayment entity to which the log refers to. + - errors [list of strings]: list of errors linked to this BoletoPayment event. + - type [string]: type of the BoletoPayment event which triggered the log creation. ex: "registered" or "paid" + - created [DateTime]: creation datetime for the payment. ex: ~U[2020-03-26 19:32:35.418698Z] + """ + @enforce_keys [:id, :payment, :errors, :type, :created] + defstruct [:id, :payment, :errors, :type, :created] + + @type t() :: %__MODULE__{} + + @doc """ + Receive a single Log struct previously created by the Stark Bank API by passing its id + + ## Parameters (required): + - user [Project]: Project struct returned from StarkBank.project(). + - id [string]: struct unique id. ex: "5656565656565656" + + ## Return: + - Log struct with updated attributes + """ + @spec get(Project.t(), binary) :: {:ok, Log.t()} | {:error, [%Error{}]} + def get(%Project{} = user, id) do + Rest.get_id(user, resource(), id) + end + + @doc """ + Same as get(), but it will unwrap the error tuple and raise in case of errors. + """ + @spec get!(Project.t(), binary) :: Log.t() + def get!(%Project{} = user, id) do + Rest.get_id!(user, resource(), id) + end + + @doc """ + Receive a stream of Log structs previously created in the Stark Bank API + + ## Parameters (required): + - user [Project]: Project struct returned from StarkBank.project(). + + ## Parameters (optional): + - limit [integer, default nil]: maximum number of entities to be retrieved. Unlimited if nil. ex: 35 + - after [Date, default nil] date filter for entities created only after specified date. ex: Date(2020, 3, 10) + - before [Date, default nil] date filter for entities only before specified date. ex: Date(2020, 3, 10) + - types [list of strings, default nil]: filter retrieved entities by event types. ex: "paid" or "registered" + - payment_ids [list of strings, default nil]: list of BoletoPayment ids to filter retrieved entities. ex: ["5656565656565656", "4545454545454545"] + + ## Return: + - stream of Log structs with updated attributes + """ + @spec query(Project.t(), any) :: + ({:cont, {:ok, [Log.t()]}} | {:error, [Error.t()]} | {:halt, any} | {:suspend, any}, any -> any) + def query(%Project{} = user, options \\ []) do + Rest.get_list(user, resource(), options |> Checks.check_options(true)) + end + + @doc """ + Same as query(), but it will unwrap the error tuple and raise in case of errors. + """ + @spec query!(Project.t(), any) :: + ({:cont, [Log.t()]} | {:halt, any} | {:suspend, any}, any -> any) + def query!(%Project{} = user, options \\ []) do + Rest.get_list!(user, resource(), options |> Checks.check_options(true)) + end + + @doc false + def resource() do + { + "BoletoPaymentLog", + &resource_maker/1 + } + end + + @doc false + def resource_maker(json) do + %Log{ + id: json[:id], + payment: json[:payment] |> API.from_api_json(&BoletoPayment.resource_maker/1), + created: json[:created] |> Checks.check_datetime, + type: json[:type], + errors: json[:errors] + } + end +end diff --git a/lib/payment/utility/utility_payment.ex b/lib/payment/utility/utility_payment.ex new file mode 100644 index 0000000..9e38bc4 --- /dev/null +++ b/lib/payment/utility/utility_payment.ex @@ -0,0 +1,198 @@ +defmodule StarkBank.UtilityPayment do + + alias __MODULE__, as: UtilityPayment + alias StarkBank.Utils.Rest, as: Rest + alias StarkBank.Utils.Checks, as: Checks + alias StarkBank.User.Project, as: Project + alias StarkBank.Error, as: Error + + @moduledoc """ + Groups UtilityPayment related functions + """ + + @doc """ + When you initialize a UtilityPayment, the entity will not be automatically + created in the Stark Bank API. The 'create' function sends the structs + to the Stark Bank API and returns the list of created structs. + + ## Parameters (conditionally required): + - line [string, default nil]: Number sequence that describes the payment. Either 'line' or 'bar_code' parameters are required. If both are sent, they must match. ex: "34191.09008 63571.277308 71444.640008 5 81960000000062" + - bar_code [string, default nil]: Bar code number that describes the payment. Either 'line' or 'barCode' parameters are required. If both are sent, they must match. ex: "34195819600000000621090063571277307144464000" + + ## Parameters (required): + - description [string]: Text to be displayed in your statement (min. 10 characters). ex: "payment ABC" + + ## Parameters (optional): + - scheduled [Date, default today]: payment scheduled date. ex: ~D[2020-03-25] + - tags [list of strings]: list of strings for tagging + + Attributes (return-only): + - id [string, default nil]: unique id returned when payment is created. ex: "5656565656565656" + - status [string, default nil]: current payment status. ex: "registered" or "paid" + - amount [int, default nil]: amount automatically calculated from line or bar_code. ex: 23456 (= R$ 234.56) + - fee [integer, default nil]: fee charged when a utility payment is created. ex: 200 (= R$ 2.00) + - created [DateTime, default nil]: creation datetime for the payment. ex: ~U[2020-03-26 19:32:35.418698Z] + """ + @enforce_keys [:description] + defstruct [:line, :bar_code, :description, :scheduled, :tags, :id, :status, :amount, :fee, :created] + + @type t() :: %__MODULE__{} + + @doc """ + Send a list of UtilityPayment structs for creation in the Stark Bank API + + ## Parameters (required): + - user [Project struct]: Project struct. Not necessary if starkbank.user was set before function call + - payments [list of UtilityPayment structs]: list of UtilityPayment structs to be created in the API + + ## Return: + - list of UtilityPayment structs with updated attributes + """ + @spec create(Project.t(), [UtilityPayment.t()]) :: + {:ok, [UtilityPayment.t()]} | {:error, [Error.t()]} + def create(%Project{} = user, payments) do + Rest.post( + user, + resource(), + payments + ) + end + + @doc """ + Same as create(), but it will unwrap the error tuple and raise in case of errors. + """ + @spec create!(Project.t(), [UtilityPayment.t()]) :: any + def create!(%Project{} = user, payments) do + Rest.post!( + user, + resource(), + payments + ) + end + + @doc """ + Receive a single UtilityPayment struct previously created by the Stark Bank API by passing its id + + ## Parameters (required): + - user [Project struct]: Project struct. Not necessary if starkbank.user was set before function call + - id [string]: struct unique id. ex: "5656565656565656" + + ## Return: + - UtilityPayment struct with updated attributes + """ + @spec get(Project.t(), binary) :: {:ok, UtilityPayment.t()} | {:error, [%Error{}]} + def get(%Project{} = user, id) do + Rest.get_id(user, resource(), id) + end + + @doc """ + Same as get(), but it will unwrap the error tuple and raise in case of errors. + """ + @spec get!(Project.t(), binary) :: UtilityPayment.t() + def get!(%Project{} = user, id) do + Rest.get_id!(user, resource(), id) + end + + @doc """ + Receive a single UtilityPayment pdf file generated in the Stark Bank API by passing its id. + Only valid for utility payments with "success" status. + + ## Parameters (required): + - user [Project struct]: Project struct. Not necessary if starkbank.user was set before function call + - id [string]: struct unique id. ex: "5656565656565656" + + ## Return: + - UtilityPayment pdf file content + """ + @spec pdf(Project.t(), binary) :: {:ok, binary} | {:error, [%Error{}]} + def pdf(%Project{} = user, id) do + Rest.get_pdf(user, resource(), id) + end + + @doc """ + Same as pdf(), but it will unwrap the error tuple and raise in case of errors. + """ + @spec pdf!(Project.t(), binary) :: binary + def pdf!(%Project{} = user, id) do + Rest.get_pdf!(user, resource(), id) + end + + @doc """ + Receive a stream of UtilityPayment structs previously created in the Stark Bank API + + ## Parameters (required): + - user [Project]: Project struct returned from StarkBank.project(). + + ## Parameters (optional): + - limit [integer, default nil]: maximum number of structs to be retrieved. Unlimited if nil. ex: 35 + - after [Date, default nil] date filter for structs created only after specified date. ex: Date(2020, 3, 10) + - before [Date, default nil] date filter for structs only before specified date. ex: Date(2020, 3, 10) + - 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"] + - status [string, default nil]: filter for status of retrieved structs. ex: "paid" + + ## Return: + - stream of UtilityPayment structs with updated attributes + """ + @spec query(Project.t(), any) :: + ({:cont, {:ok, [UtilityPayment.t()]}} | {:error, [Error.t()]} | {:halt, any} | {:suspend, any}, any -> any) + def query(%Project{} = user, options \\ []) do + Rest.get_list(user, resource(), options |> Checks.check_options(true)) + end + + @doc """ + Same as query(), but it will unwrap the error tuple and raise in case of errors. + """ + @spec query!(Project.t(), any) :: + ({:cont, [UtilityPayment.t()]} | {:halt, any} | {:suspend, any}, any -> any) + def query!(%Project{} = user, options \\ []) do + Rest.get_list!(user, resource(), options |> Checks.check_options(true)) + end + + @doc """ + Delete a UtilityPayment entity previously created in the Stark Bank API + + ## Parameters (required): + - user [Project]: Project struct returned from StarkBank.project(). + - id [string]: UtilityPayment unique id. ex: "5656565656565656" + + ## Return: + - deleted UtilityPayment with updated attributes + """ + @spec delete(Project.t(), binary) :: {:ok, UtilityPayment.t()} | {:error, [%Error{}]} + def delete(%Project{} = user, id) do + Rest.delete_id(user, resource(), id) + end + + @doc """ + Same as delete(), but it will unwrap the error tuple and raise in case of errors. + """ + @spec delete!(Project.t(), binary) :: UtilityPayment.t() + def delete!(%Project{} = user, id) do + Rest.delete_id!(user, resource(), id) + end + + @doc false + def resource() do + { + "UtilityPayment", + &resource_maker/1 + } + end + + @doc false + def resource_maker(json) do + %UtilityPayment{ + line: json[:line], + bar_code: json[:bar_code], + description: json[:description], + scheduled: json[:scheduled] |> Checks.check_datetime, + tags: json[:tags], + id: json[:id], + status: json[:status], + amount: json[:amount], + fee: json[:fee], + created: json[:created] |> Checks.check_datetime + } + end +end diff --git a/lib/payment/utility/utility_payment_log.ex b/lib/payment/utility/utility_payment_log.ex new file mode 100644 index 0000000..0f1a4fa --- /dev/null +++ b/lib/payment/utility/utility_payment_log.ex @@ -0,0 +1,104 @@ +defmodule StarkBank.UtilityPayment.Log do + + alias __MODULE__, as: Log + alias StarkBank.Utils.Rest, as: Rest + alias StarkBank.Utils.Checks, as: Checks + alias StarkBank.Utils.API, as: API + alias StarkBank.UtilityPayment, as: UtilityPayment + alias StarkBank.User.Project, as: Project + alias StarkBank.Error, as: Error + + @moduledoc """ + Groups UtilityPayment.Log related functions + """ + + @doc """ + Every time a UtilityPayment entity is modified, a corresponding UtilityPayment.Log + is generated for the entity. This log is never generated by the user, but it can + be retrieved to check additional information on the UtilityPayment. + + ## Attributes: + - id [string]: unique id returned when the log is created. ex: "5656565656565656" + - payment [UtilityPayment]: UtilityPayment entity to which the log refers to. + - errors [list of strings]: list of errors linked to this BoletoPayment event. + - type [string]: type of the UtilityPayment event which triggered the log creation. ex: "registered" or "paid" + - created [DateTime]: creation datetime for the payment. ex: ~U[2020-03-26 19:32:35.418698Z] + """ + @enforce_keys [:id, :payment, :errors, :type, :created] + defstruct [:id, :payment, :errors, :type, :created] + + @type t() :: %__MODULE__{} + + @doc """ + Receive a single Log struct previously created by the Stark Bank API by passing its id + + ## Parameters (required): + - user [Project]: Project struct returned from StarkBank.project(). + - id [string]: struct unique id. ex: "5656565656565656" + + ## Return: + - Log struct with updated attributes + """ + @spec get(Project.t(), binary) :: {:ok, Log.t()} | {:error, [%Error{}]} + def get(%Project{} = user, id) do + Rest.get_id(user, resource(), id) + end + + @doc """ + Same as get(), but it will unwrap the error tuple and raise in case of errors. + """ + @spec get!(Project.t(), binary) :: Log.t() + def get!(%Project{} = user, id) do + Rest.get_id!(user, resource(), id) + end + + @doc """ + Receive a stream of Log structs previously created in the Stark Bank API + + ## Parameters (required): + - user [Project]: Project struct returned from StarkBank.project(). + + ## Parameters (optional): + - limit [integer, default nil]: maximum number of entities to be retrieved. Unlimited if nil. ex: 35 + - after [Date, default nil] date filter for entities created only after specified date. ex: Date(2020, 3, 10) + - before [Date, default nil] date filter for entities only before specified date. ex: Date(2020, 3, 10) + - types [list of strings, default nil]: filter retrieved entities by event types. ex: "paid" or "registered" + - payment_ids [list of strings, default nil]: list of UtilityPayment ids to filter retrieved entities. ex: ["5656565656565656", "4545454545454545"] + + ## Return: + - stream of Log structs with updated attributes + """ + @spec query(Project.t(), any) :: + ({:cont, {:ok, [Log.t()]}} | {:error, [Error.t()]} | {:halt, any} | {:suspend, any}, any -> any) + def query(%Project{} = user, options \\ []) do + Rest.get_list(user, resource(), options |> Checks.check_options(true)) + end + + @doc """ + Same as query(), but it will unwrap the error tuple and raise in case of errors. + """ + @spec query!(Project.t(), any) :: + ({:cont, [Log.t()]} | {:halt, any} | {:suspend, any}, any -> any) + def query!(%Project{} = user, options \\ []) do + Rest.get_list!(user, resource(), options |> Checks.check_options(true)) + end + + @doc false + def resource() do + { + "UtilityPaymentLog", + &resource_maker/1 + } + end + + @doc false + def resource_maker(json) do + %Log{ + id: json[:id], + payment: json[:payment] |> API.from_api_json(&UtilityPayment.resource_maker/1), + created: json[:created] |> Checks.check_datetime, + type: json[:type], + errors: json[:errors] + } + end +end diff --git a/lib/stark_bank.ex b/lib/stark_bank.ex deleted file mode 100644 index ee39bee..0000000 --- a/lib/stark_bank.ex +++ /dev/null @@ -1,9 +0,0 @@ -defmodule StarkBank do - @moduledoc """ - SDK to facilitate Elixir integrations with the Stark Bank API. - - Submodules: - - StarkBank.Auth: Used to manage credentials and login; - - StarkBank.Charge: Used to create and consult charges; - """ -end diff --git a/lib/starkbank.ex b/lib/starkbank.ex new file mode 100644 index 0000000..601c58b --- /dev/null +++ b/lib/starkbank.ex @@ -0,0 +1,30 @@ +defmodule StarkBank do + + @moduledoc """ + SDK to facilitate Elixir integrations with the Stark Bank API v2. + """ + + alias StarkBank.User.Project, as: Project + + @doc """ + The Project struct is the main authentication entity for the SDK. + All requests to the Stark Bank API must be authenticated via a project, + which must have been previously created at the Stark Bank website + [https://sandbox.web.starkbank.com] or [https://web.starkbank.com] + before you can use it in this SDK. Projects may be passed as a parameter on + each request or may be defined as the default user at the start (See README). + + ## Parameters (required): + - environment [string]: environment where the project is being used. ex: "sandbox" or "production" + - id [string]: unique id required to identify project. ex: "5656565656565656" + - private_key [string]: PEM string of the private key linked to the project. ex: "-----BEGIN PUBLIC KEY-----\nMFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEyTIHK6jYuik6ktM9FIF3yCEYzpLjO5X/\ntqDioGM+R2RyW0QEo+1DG8BrUf4UXHSvCjtQ0yLppygz23z0yPZYfw==\n-----END PUBLIC KEY-----" + + ## Attributes (return-only): + - name [string, default ""]: project name. ex: "MyProject" + - allowed_ips [list of strings]: list containing the strings of the ips allowed to make requests on behalf of this project. ex: ["190.190.0.50"] + """ + @spec project(:production | :sandbox, binary, binary, binary, [binary] | nil) :: StarkBank.User.Project.t() + def project(environment, id, private_key, name \\ "", allowed_ips \\ nil) do + Project.validate(environment, id, private_key, name, allowed_ips) + end +end diff --git a/lib/transfer/transfer.ex b/lib/transfer/transfer.ex new file mode 100644 index 0000000..f4dfa66 --- /dev/null +++ b/lib/transfer/transfer.ex @@ -0,0 +1,180 @@ +defmodule StarkBank.Transfer do + + alias __MODULE__, as: Transfer + alias StarkBank.Utils.Rest, as: Rest + alias StarkBank.Utils.Checks, as: Checks + alias StarkBank.User.Project, as: Project + alias StarkBank.Error, as: Error + + @moduledoc """ + Groups Transfer related functions + """ + + @doc """ + When you initialize a Transfer, the entity will not be automatically + created in the Stark Bank API. The 'create' function sends the structs + to the Stark Bank API and returns the list of created structs. + + ## Parameters (required): + - amount [integer]: amount in cents to be transferred. ex: 1234 (= R$ 12.34) + - name [string]: receiver full name. ex: "Anthony Edward Stark" + - tax_id [string]: receiver tax ID (CPF or CNPJ) with or without formatting. ex: "01234567890" or "20.018.183/0001-80" + - bank_code [string]: receiver 1 to 3 digits of the bank institution in Brazil. ex: "200" or "341" + - branch_code [string]: receiver bank account branch. Use '-' in case there is a verifier digit. ex: "1357-9" + - account_number [string]: Receiver Bank Account number. Use '-' before the verifier digit. ex: "876543-2" + + ## Parameters (optional): + - tags [list of strings]: list of strings for reference when searching for transfers. ex: ["employees", "monthly"] + + Attributes (return-only): + - id [string, default nil]: unique id returned when Transfer is created. ex: "5656565656565656" + - fee [integer, default nil]: fee charged when transfer is created. ex: 200 (= R$ 2.00) + - status [string, default nil]: current boleto status. ex: "registered" or "paid" + - transaction_ids [list of strings, default nil]: ledger transaction ids linked to this transfer (if there are two, second is the chargeback). ex: ["19827356981273"] + - created [DateTime, default nil]: creation datetime for the transfer. ex: ~U[2020-03-26 19:32:35.418698Z] + - updated [DateTime, default nil]: latest update datetime for the transfer. ex: ~U[2020-03-26 19:32:35.418698Z] + """ + @enforce_keys [:amount, :name, :tax_id, :bank_code, :branch_code, :account_number] + defstruct [:amount, :name, :tax_id, :bank_code, :branch_code, :account_number, :transaction_ids, :fee, :tags, :status, :id, :created, :updated] + + @type t() :: %__MODULE__{} + + @doc """ + Send a list of Transfer structs for creation in the Stark Bank API + + ## Parameters (required): + - user [Project]: Project struct returned from StarkBank.project(). + - transfers [list of Transfer structs]: list of Transfer structs to be created in the API + + ## Return: + - list of Transfer structs with updated attributes + """ + @spec create(Project.t(), [Transfer.t()]) :: + {:ok, [Transfer.t()]} | {:error, [Error.t()]} + def create(%Project{} = user, transfers) do + Rest.post( + user, + resource(), + transfers + ) + end + + @doc """ + Same as create(), but it will unwrap the error tuple and raise in case of errors. + """ + @spec create!(Project.t(), [Transfer.t()]) :: any + def create!(%Project{} = user, transfers) do + Rest.post!( + user, + resource(), + transfers + ) + end + + @doc """ + Receive a single Transfer struct previously created in the Stark Bank API by passing its id + + ## Parameters (required): + - user [Project]: Project struct returned from StarkBank.project(). + - id [string]: struct unique id. ex: "5656565656565656" + + ## Return: + - Transfer struct with updated attributes + """ + @spec get(Project.t(), binary) :: {:ok, Transfer.t()} | {:error, [%Error{}]} + def get(%Project{} = user, id) do + Rest.get_id(user, resource(), id) + end + + @doc """ + Same as get(), but it will unwrap the error tuple and raise in case of errors. + """ + @spec get!(Project.t(), binary) :: Transfer.t() + def get!(%Project{} = user, id) do + Rest.get_id!(user, resource(), id) + end + + @doc """ + Receive a single Transfer pdf receipt file generated in the Stark Bank API by passing its id. + Only valid for transfers with "processing" or "success" status. + + ## Parameters (required): + - user [Project]: Project struct returned from StarkBank.project(). + - id [string]: struct unique id. ex: "5656565656565656" + + ## Return: + - Transfer pdf file content + """ + @spec pdf(Project.t(), binary) :: {:ok, binary} | {:error, [%Error{}]} + def pdf(%Project{} = user, id) do + Rest.get_pdf(user, resource(), id) + end + + @doc """ + Same as pdf(), but it will unwrap the error tuple and raise in case of errors. + """ + @spec pdf!(Project.t(), binary) :: binary + def pdf!(%Project{} = user, id) do + Rest.get_pdf!(user, resource(), id) + end + + @doc """ + Receive a stream of Transfer structs previously created in the Stark Bank API + + ## Parameters (required): + - user [Project]: Project struct returned from StarkBank.project(). + + ## Parameters (optional): + - limit [integer, default nil]: maximum number of structs to be retrieved. Unlimited if nil. ex: 35 + - after [Date, default nil]: date filter for structs created only after specified date. ex: ~D[2020-03-25] + - before [Date, default nil]: date filter for structs only before specified date. ex: ~D[2020-03-25] + - transaction_ids [list of strings, default nil]: list of transaction IDs linked to the desired transfers. ex: ["5656565656565656", "4545454545454545"] + - status [string, default nil]: filter for status of retrieved structs. ex: "paid" or "registered" + - sort [string, default "-created"]: sort order considered in response. Valid options are "created", "-created", "updated" or "-updated". + - tags [list of strings, default nil]: tags to filter retrieved structs. ex: ["tony", "stark"] + + ## Return: + - stream of Transfer structs with updated attributes + """ + @spec query(Project.t(), any) :: + ({:cont, {:ok, [Transfer.t()]}} | {:error, [Error.t()]} | {:halt, any} | {:suspend, any}, any -> any) + def query(%Project{} = user, options \\ []) do + Rest.get_list(user, resource(), options |> Checks.check_options(true)) + end + + @doc """ + Same as query(), but it will unwrap the error tuple and raise in case of errors. + """ + @spec query!(Project.t(), any) :: + ({:cont, [Transfer.t()]} | {:halt, any} | {:suspend, any}, any -> any) + def query!(%Project{} = user, options \\ []) do + Rest.get_list!(user, resource(), options |> Checks.check_options(true)) + end + + @doc false + def resource() do + { + "Transfer", + &resource_maker/1 + } + end + + @doc false + def resource_maker(json) do + %Transfer{ + amount: json[:amount], + name: json[:name], + tax_id: json[:tax_id], + bank_code: json[:bank_code], + branch_code: json[:branch_code], + account_number: json[:account_number], + transaction_ids: json[:transaction_ids], + fee: json[:fee], + tags: json[:tags], + status: json[:status], + id: json[:id], + created: json[:created] |> Checks.check_datetime, + updated: json[:updated] |> Checks.check_datetime + } + end +end diff --git a/lib/transfer/transfer_log.ex b/lib/transfer/transfer_log.ex new file mode 100644 index 0000000..14eeb02 --- /dev/null +++ b/lib/transfer/transfer_log.ex @@ -0,0 +1,104 @@ +defmodule StarkBank.Transfer.Log do + + alias __MODULE__, as: Log + alias StarkBank.Utils.Rest, as: Rest + alias StarkBank.Utils.Checks, as: Checks + alias StarkBank.Utils.API, as: API + alias StarkBank.Transfer, as: Transfer + alias StarkBank.User.Project, as: Project + alias StarkBank.Error, as: Error + + @moduledoc """ + Groups Transfer.Log related functions + """ + + @doc """ + Every time a Transfer entity is modified, a corresponding Transfer.Log + is generated for the entity. This log is never generated by the + user. + + ## Attributes: + - id [string]: unique id returned when the log is created. ex: "5656565656565656" + - transfer [Transfer]: Transfer entity to which the log refers to. + - errors [list of strings]: list of errors linked to this BoletoPayment event. + - type [string]: type of the Transfer event which triggered the log creation. ex: "processing" or "success" + - created [DateTime]: creation datetime for the transfer. ex: ~U[2020-03-26 19:32:35.418698Z] + """ + @enforce_keys [:id, :transfer, :errors, :type, :created] + defstruct [:id, :transfer, :errors, :type, :created] + + @type t() :: %__MODULE__{} + + @doc """ + Receive a single Log struct previously created by the Stark Bank API by passing its id + + ## Parameters (required): + - user [Project]: Project struct returned from StarkBank.project(). + - id [string]: struct unique id. ex: "5656565656565656" + + ## Return: + - Log struct with updated attributes + """ + @spec get(Project.t(), binary) :: {:ok, Log.t()} | {:error, [%Error{}]} + def get(%Project{} = user, id) do + Rest.get_id(user, resource(), id) + end + + @doc """ + Same as get(), but it will unwrap the error tuple and raise in case of errors. + """ + @spec get!(Project.t(), binary) :: Log.t() + def get!(%Project{} = user, id) do + Rest.get_id!(user, resource(), id) + end + + @doc """ + Receive a stream of Log structs previously created in the Stark Bank API + + ## Parameters (required): + - user [Project]: Project struct returned from StarkBank.project(). + + ## Parameters (optional): + - limit [integer, default nil]: maximum number of structs to be retrieved. Unlimited if nil. ex: 35 + - after [Date, default nil] date filter for structs created only after specified date. ex: Date(2020, 3, 10) + - before [Date, default nil] date filter for structs only before specified date. ex: Date(2020, 3, 10) + - types [list of strings, default nil]: filter retrieved structs by types. ex: "success" or "failed" + - transfer_ids [list of strings, default nil]: list of Transfer ids to filter retrieved structs. ex: ["5656565656565656", "4545454545454545"] + + ## Return: + - stream of Log structs with updated attributes + """ + @spec query(Project.t(), any) :: + ({:cont, {:ok, [Log.t()]}} | {:error, [Error.t()]} | {:halt, any} | {:suspend, any}, any -> any) + def query(%Project{} = user, options \\ []) do + Rest.get_list(user, resource(), options |> Checks.check_options(true)) + end + + @doc """ + Same as query(), but it will unwrap the error tuple and raise in case of errors. + """ + @spec query!(Project.t(), any) :: + ({:cont, [Log.t()]} | {:halt, any} | {:suspend, any}, any -> any) + def query!(%Project{} = user, options \\ []) do + Rest.get_list!(user, resource(), options |> Checks.check_options(true)) + end + + @doc false + def resource() do + { + "TransferLog", + &resource_maker/1 + } + end + + @doc false + def resource_maker(json) do + %Log{ + id: json[:id], + transfer: json[:transfer] |> API.from_api_json(&Transfer.resource_maker/1), + created: json[:created] |> Checks.check_datetime, + type: json[:type], + errors: json[:errors] + } + end +end diff --git a/lib/user/project.ex b/lib/user/project.ex new file mode 100644 index 0000000..4a7d295 --- /dev/null +++ b/lib/user/project.ex @@ -0,0 +1,19 @@ +defmodule StarkBank.User.Project do + + alias __MODULE__, as: Project + alias StarkBank.User, as: User + + @moduledoc false + defstruct [:environment, :access_id, :private_key, :name, :allowed_ips] + + def validate(environment, id, private_key, name \\ "", allowed_ips \\ nil) do + {environment, access_id, private_key} = User.validate("project", id, private_key, environment) + %Project{ + environment: environment, + access_id: access_id, + private_key: private_key, + name: name, + allowed_ips: allowed_ips, + } + end +end diff --git a/lib/user/user.ex b/lib/user/user.ex new file mode 100644 index 0000000..a29d098 --- /dev/null +++ b/lib/user/user.ex @@ -0,0 +1,13 @@ +defmodule StarkBank.User do + @moduledoc false + + alias StarkBank.Utils.Checks, as: Checks + + def validate(kind, id, private_key, environment) do + { + Checks.check_environment(environment), + "#{kind}/#{id}", + Checks.check_private_key(private_key) + } + end +end diff --git a/lib/utils/api.ex b/lib/utils/api.ex new file mode 100644 index 0000000..8efd8be --- /dev/null +++ b/lib/utils/api.ex @@ -0,0 +1,70 @@ +defmodule StarkBank.Utils.API do + @moduledoc false + + alias StarkBank.Utils.Case, as: Case + + def api_json(struct) do + struct + |> Map.from_struct() + |> cast_json_to_api_format + end + + def cast_json_to_api_format(json) do + json + |> Enum.filter(fn {_field, value} -> !is_nil(value) end) + |> Enum.map(fn {field, value} -> {Case.snake_to_camel(to_string(field)), date_to_string(value)} end) + |> Enum.into(%{}) + end + + defp date_to_string(%Date{} = date) do + "#{date.year}-#{date.month}-#{date.day}" + end + + defp date_to_string(%DateTime{} = datetime) do + "#{datetime.year}-#{datetime.month}-#{datetime.day}" + end + + defp date_to_string(data) do + data + end + + def from_api_json(json, resource_maker) do + json + |> Enum.map(fn({field, value}) -> {field |> Case.camel_to_snake() |> String.to_atom(), value} end) + |> resource_maker.() + end + + def endpoint(resource_name) do + resource_name + |> Case.camel_to_kebab + |> String.replace("-log", "/log") + end + + def last_name_plural(resource_name) do + resource_name + |> last_name + |> (fn x -> x <> "s" end).() + end + + def last_name(resource_name) do + resource_name + |> Case.camel_to_kebab + |> String.split("-") + |> List.last + end + + def errors_to_string(errors) do + errors + |> Enum.map(&Map.from_struct/1) + |> Enum.map(&map_to_string/1) + |> to_string + end + + defp map_to_string(map) do + map + |> Map.keys + |> Enum.map(fn key -> "#{key}: #{map[key]}" end) + |> Enum.join(", ") + |> (fn s -> "{#{s}}" end).() + end +end diff --git a/lib/utils/case.ex b/lib/utils/case.ex new file mode 100644 index 0000000..2f1fc28 --- /dev/null +++ b/lib/utils/case.ex @@ -0,0 +1,56 @@ +defmodule StarkBank.Utils.Case do + @moduledoc false + + def camel_to_kebab(string) do + string + |> camel_to_snake + |> String.replace("_", "-") + end + + def camel_to_snake(string) do + string + |> String.graphemes + |> camel_to_snake_graphemes + |> strip_underscore + |> Enum.join + end + + defp camel_to_snake_graphemes([letter | rest]) do + cond do + letter == letter |> String.upcase -> ["_" | [String.downcase(letter) | camel_to_snake_graphemes(rest)]] + :error != Integer.parse(letter) -> ["_" | [letter | camel_to_snake_graphemes(rest)]] + true -> [letter | camel_to_snake_graphemes(rest)] + end + end + + defp camel_to_snake_graphemes([]) do + [] + end + + defp strip_underscore([letter | rest]) when letter == "_" do + rest + end + + defp strip_underscore(string) do + string + end + + def snake_to_camel(string) do + string + |> String.graphemes + |> snake_to_camel_graphemes + |> Enum.join + end + + defp snake_to_camel_graphemes([letter | rest]) when letter == "_" do + snake_to_camel_graphemes([String.upcase(hd(rest)) | tl(rest)]) + end + + defp snake_to_camel_graphemes([letter | rest]) do + [letter | snake_to_camel_graphemes(rest)] + end + + defp snake_to_camel_graphemes([]) do + [] + end +end diff --git a/lib/utils/checks.ex b/lib/utils/checks.ex new file mode 100644 index 0000000..0b729a9 --- /dev/null +++ b/lib/utils/checks.ex @@ -0,0 +1,86 @@ +defmodule StarkBank.Utils.Checks do + @moduledoc false + + alias EllipticCurve.PrivateKey, as: PrivateKey + + def check_environment(environment) do + case environment do + :production -> environment + :sandbox -> environment + end + end + + def check_limit(limit) when is_nil(limit) do + nil + end + + def check_limit(limit) do + min(limit, 100) + end + + def check_datetime(data) when is_nil(data) do + nil + end + + def check_datetime(data) when is_binary(data) do + {:ok, datetime, _utc_offset} = data |> DateTime.from_iso8601 + datetime + end + + def check_date(data) when is_nil(data) do + nil + end + + def check_date(data) when is_binary(data) do + data |> Date.from_iso8601! + end + + def check_date(data = %DateTime{}) do + %Date{year: data.year, month: data.month, day: data.day} + end + + def check_date(data) do + data + end + + def check_private_key(private_key) do + try do + {:ok, parsed_key} = PrivateKey.fromPem(private_key) + :secp256k1 = parsed_key.curve.name + parsed_key + rescue + _e -> raise "Private-key must be valid secp256k1 ECDSA string in pem format" + else + parsed_key -> parsed_key + end + end + + def check_options(options, after_before) when after_before do + options + |> Enum.into(%{}) + |> fill_limit + |> fill_date_field(:after) + |> fill_date_field(:before) + end + + def check_options(options) do + options + |> Enum.into(%{}) + |> fill_limit + end + + defp fill_limit(options) do + if !Map.has_key?(options, :limit) do + Map.put(options, :limit, nil) + end + options + end + + defp fill_date_field(options, field) do + if !Map.has_key?(options, field) do + Map.put(options, field, nil) + else + Map.update!(options, field, &check_date/1) + end + end +end diff --git a/lib/utils/generator.ex b/lib/utils/generator.ex new file mode 100644 index 0000000..b9d2dec --- /dev/null +++ b/lib/utils/generator.ex @@ -0,0 +1,69 @@ +defmodule StarkBank.Utils.QueryGenerator do + @moduledoc false + + alias StarkBank.Utils.Checks, as: Checks + alias StarkBank.Utils.JSON, as: JSON + + def start_query(function, key, query) do + Task.start_link(fn -> yield([], function, key, query, true) end) + end + + def get(pid) do + send(pid, self()) + receive do + :halt -> :halt + {:ok, element} -> {:ok, element} + {:error, errors} -> {:error, errors} + end + end + + defp yield([head | tail], function, key, query) do + receive do + caller -> + send(caller, {:ok, head}) + yield(tail, function, key, query) + end + end + + defp yield([], function, key, query, first \\ false) do + limit = query[:limit] + cursor = query[:cursor] + if (first or !is_nil(cursor)) and (is_nil(limit) or limit > 0) do + case function.(query |> Map.put(:limit, limit |> Checks.check_limit)) do + {:ok, result} -> + decoded = JSON.decode!(result) + yield( + decoded[key], + function, + key, + query |> Map.put(:cursor, decoded["cursor"]) |> Map.put(:limit, iterate_limit(limit)) + ) + {:error, error} -> yield_error(error) + end + else + receive do + caller -> + send(caller, :halt) + end + end + end + + defp yield_error(error) do + receive do + caller -> + send(caller, {:error, error}) + end + receive do + caller -> + send(caller, :halt) + end + end + + defp iterate_limit(limit) when is_nil(limit) do + nil + end + + defp iterate_limit(limit) do + limit - 100 + end +end diff --git a/lib/utils/helpers.ex b/lib/utils/helpers.ex deleted file mode 100644 index 0e4361f..0000000 --- a/lib/utils/helpers.ex +++ /dev/null @@ -1,129 +0,0 @@ -defmodule StarkBank.Utils.Helpers do - @moduledoc false - - @cursor_limit 100 - - def list_to_url_arg(list) when is_nil(list) do - nil - end - - def list_to_url_arg(list) do - Enum.join(list, ",") - end - - def extract_id(id) when is_binary(id) or is_integer(id) or is_nil(id) do - id - end - - def extract_id(struct) do - struct.id - end - - def date_to_string(date) when is_nil(date) do - nil - end - - def date_to_string(date) when is_binary(date) do - date - end - - def date_to_string(date) do - "#{date.year}-#{date.month}-#{date.day}" - end - - def string_to_datetime(string) when is_nil(string) do - nil - end - - def string_to_datetime(string) do - {:ok, datetime, _} = DateTime.from_iso8601(string) - datetime - end - - def get_recursive_limit(limit) when is_nil(limit) do - nil - end - - def get_recursive_limit(limit) do - limit - @cursor_limit - end - - def truncate_limit(limit) when is_nil(limit) or limit > @cursor_limit do - @cursor_limit - end - - def truncate_limit(limit) do - limit - end - - def limit_below_maximum?(limit) do - !is_nil(limit) and limit <= @cursor_limit - end - - def chunk_list_by_max_limit(list) do - Stream.chunk_every(list, @cursor_limit) - end - - def flatten_responses(response_list) do - errors = List.flatten(for {:error, response} <- response_list, do: response) - - if length(errors) > 0 do - {:error, errors} - else - {:ok, List.flatten(for {:ok, response} <- response_list, do: response)} - end - end - - def treat_nullable_id_or_struct_list(id_or_struct_list) when is_nil(id_or_struct_list) do - nil - end - - def treat_nullable_id_or_struct_list(id_or_struct_list) do - list_to_url_arg(for id_or_struct <- id_or_struct_list, do: extract_id(id_or_struct)) - end - - def nullable_fields_match?(nullable_field, _other_field) when is_nil(nullable_field) do - true - end - - def nullable_fields_match?(nullable_field, other_field) do - nullable_field == other_field - end - - def current_microsecond() do - DateTime.utc_now() - |> DateTime.to_unix(:microsecond) - end - - def log_elapsed_time(since) do - IO.puts("time elapsed: " <> to_string((current_microsecond() - since) / 1000_000)) - end - - def lowercase_list_of_strings(list_of_strings) when is_nil(list_of_strings) do - nil - end - - def lowercase_list_of_strings(list_of_strings) do - for(string <- list_of_strings, do: String.downcase(string)) - end - - def snake_to_camel_list_of_strings(list_of_strings) when is_nil(list_of_strings) do - nil - end - - def snake_to_camel_list_of_strings(list_of_strings) do - for string <- list_of_strings, do: Enum.join(snake_to_camel(String.graphemes(string))) - end - - defp snake_to_camel([letter | rest]) when letter == "_" do - snake_to_camel([String.upcase(hd(rest)) | tl(rest)]) - end - - defp snake_to_camel([letter | rest]) do - [letter | snake_to_camel(rest)] - end - - defp snake_to_camel([]) do - [] - end -end diff --git a/lib/utils/json.ex b/lib/utils/json.ex index 3efb244..316adcc 100644 --- a/lib/utils/json.ex +++ b/lib/utils/json.ex @@ -1,11 +1,15 @@ defmodule StarkBank.Utils.JSON do @moduledoc false - def encode(value) do + def encode!(value) when is_nil(value) do + nil + end + + def encode!(value) do Jason.encode!(value) end - def decode(json) do + def decode!(json) do Jason.decode!(json) end end diff --git a/lib/utils/request.ex b/lib/utils/request.ex new file mode 100644 index 0000000..43f6a2f --- /dev/null +++ b/lib/utils/request.ex @@ -0,0 +1,74 @@ +defmodule StarkBank.Utils.Request do + @moduledoc false + + alias StarkBank.Utils.JSON, as: JSON + alias StarkBank.Utils.URL, as: URL + alias StarkBank.Error, as: Error + + def fetch(user, method, path, options \\ []) do + %{payload: payload, query: query, version: version} = + Enum.into(options, %{payload: nil, query: nil, version: 'v2'}) + + request( + user, + method, + URL.get_url(user.environment, version, path, query), + payload + ) |> process_response + end + + defp request(user, method, url, payload) do + Application.ensure_all_started(:inets) + Application.ensure_all_started(:ssl) + + {:ok, {{'HTTP/1.1', status_code, _status_message}, _headers, response_body}} = + :httpc.request( + method, + get_request_params(user, url, JSON.encode!(payload)), + [], + [] + ) + + {status_code, response_body} + end + + defp get_request_params(user, url, body) when is_nil(body) do + { + url, + get_headers(user, "") + } + end + + defp get_request_params(user, url, body) do + { + url, + get_headers(user, body), + 'text/plain', + body + } + end + + defp get_headers(user, body) do + access_time = DateTime.utc_now() |> DateTime.to_unix(:second) + signature = "#{user.access_id}:#{access_time}:#{body}" + |> EllipticCurve.Ecdsa.sign(user.private_key) + |> EllipticCurve.Signature.toBase64() + + [ + {'Access-Id', to_charlist(user.access_id)}, + {'Access-Time', to_charlist(access_time)}, + {'Access-Signature', to_charlist(signature)}, + {'Content-Type', 'application/json'}, + {'User-Agent', 'Elixir-#{System.version}-SDK-#{Mix.Project.config[:version]}'} + ] + end + + defp process_response({status_code, body}) do + cond do + status_code == 500 -> {:error, [%Error{code: "internalServerError", message: "Houston, we have a problem."}]} + status_code == 400 -> {:error, JSON.decode!(body)["errors"] |> Enum.map(fn error -> %Error{code: error["code"], message: error["message"]} end)} + status_code != 200 -> {:error, [%Error{code: "unknownError", message: "Unknown exception encountered: " <> to_string(body)}]} + true -> {:ok, body} + end + end +end diff --git a/lib/utils/requests/api_links.ex b/lib/utils/requests/api_links.ex deleted file mode 100644 index 21f292f..0000000 --- a/lib/utils/requests/api_links.ex +++ /dev/null @@ -1,13 +0,0 @@ -defmodule StarkBank.Utils.Requests.APILinks do - @moduledoc false - - @sandbox_url 'https://sandbox.api.starkbank.com/v1/' - @production_url 'https://api.starkbank.com/v1/' - - def get_url_by_env(env) do - cond do - env == :sandbox -> @sandbox_url - env == :production -> @production_url - end - end -end diff --git a/lib/utils/requests/http_status.ex b/lib/utils/requests/http_status.ex deleted file mode 100644 index 74f2ffd..0000000 --- a/lib/utils/requests/http_status.ex +++ /dev/null @@ -1,14 +0,0 @@ -defmodule StarkBank.Utils.Requests.HTTPStatus do - @moduledoc false - - @ok 200 - @unauthorized 401 - - def unauthorized do - @unauthorized - end - - def ok do - @ok - end -end diff --git a/lib/utils/requests/requests.ex b/lib/utils/requests/requests.ex deleted file mode 100644 index dd469aa..0000000 --- a/lib/utils/requests/requests.ex +++ /dev/null @@ -1,136 +0,0 @@ -defmodule StarkBank.Utils.Requests do - @moduledoc false - - alias StarkBank.Utils.JSON, as: JSON - alias StarkBank.Utils.Requests.APILinks, as: APILinks - alias StarkBank.Utils.Requests.HTTPStatus, as: HTTPStatus - - def get(credentials, endpoint, parameters \\ nil, decode_json \\ true) do - send(credentials, endpoint, :get, nil, parameters, decode_json) - end - - def post(credentials, endpoint, body \\ nil) do - send(credentials, endpoint, :post, body, nil) - end - - def put(credentials, endpoint, body \\ nil) do - send(credentials, endpoint, :put, body, nil) - end - - def delete(credentials, endpoint, parameters \\ nil) do - send(credentials, endpoint, :delete, nil, parameters) - end - - defp send(credentials, endpoint, method, body, parameters, decode_json \\ true) do - Application.ensure_all_started(:inets) - Application.ensure_all_started(:ssl) - - url = get_url(credentials, endpoint, parameters) - - {status_code, body} = make_http_request(method, credentials, url, body) - - if decode_json do - {process_status_code(status_code), JSON.decode(body)} - else - {process_status_code(status_code), body} - end - end - - defp make_http_request(method, credentials, url, body, is_retry \\ false) do - {:ok, {{'HTTP/1.1', status_code, _status_message}, _headers, response_body}} = - :httpc.request( - method, - get_request_params(credentials, url, body), - [], - [] - ) - - if authentication_error?(response_body) do - if is_retry or not StarkBank.Auth.get_auto_refresh(credentials) do - {HTTPStatus.unauthorized(), response_body} - else - StarkBank.Auth.update_access_token(credentials) - make_http_request(method, credentials, url, body, true) - end - else - {status_code, response_body} - end - end - - defp authentication_error?(body) do - if String.contains?(to_string(body), "invalidAccessToken") do - error = JSON.decode(body)["error"] - - cond do - !is_map(error) -> false - error["code"] == "invalidAccessToken" -> true - true -> false - end - else - false - end - end - - defp get_request_params(credentials, url, body) when is_nil(body) do - { - url, - get_headers(credentials) - } - end - - defp get_request_params(credentials, url, body) do - { - url, - get_headers(credentials), - 'text/plain', - JSON.encode(body) - } - end - - defp get_headers(credentials) do - access_token = StarkBank.Auth.get_access_token(credentials) - - cond do - is_nil(access_token) -> - [ - {'Content-Type', 'application/json'} - ] - - true -> - [ - {'Content-Type', 'application/json'}, - {'Access-Token', to_charlist(access_token)} - ] - end - end - - defp get_url(credentials, endpoint, parameters) when is_nil(parameters) do - get_base_url(credentials) ++ endpoint - end - - defp get_url(credentials, endpoint, parameters) do - list = for {k, v} <- parameters, !is_nil(v), do: "#{k}=#{v}" - - if length(list) > 0 do - get_url( - credentials, - endpoint ++ to_charlist("?" <> String.replace(Enum.join(list, "&"), " ", "%20")), - nil - ) - else - get_url(credentials, endpoint, nil) - end - end - - defp get_base_url(credentials) do - StarkBank.Auth.get_env(credentials) - |> APILinks.get_url_by_env() - end - - defp process_status_code(status_code) do - cond do - status_code == HTTPStatus.ok() -> :ok - true -> :error - end - end -end diff --git a/lib/utils/rest.ex b/lib/utils/rest.ex new file mode 100644 index 0000000..f6a4bdd --- /dev/null +++ b/lib/utils/rest.ex @@ -0,0 +1,160 @@ +defmodule StarkBank.Utils.Rest do + @moduledoc false + + alias StarkBank.Utils.Request, as: Request + alias StarkBank.Utils.QueryGenerator, as: QueryGenerator + alias StarkBank.Utils.API, as: API + alias StarkBank.Utils.JSON, as: JSON + + def get_list(user, {resource_name, resource_maker}, query \\ %{}) do + limit = query[:limit] + getter = make_getter(user, resource_name) + + Stream.resource( + fn -> + {:ok, pid} = QueryGenerator.start_query(getter, API.last_name_plural(resource_name), query |> Map.put(:limit, limit)) + pid + end, + fn pid -> + case QueryGenerator.get(pid) do + :halt -> {:halt, pid} + {:ok, element} -> {[{:ok, API.from_api_json(element, resource_maker)}], pid} + {:error, error} -> {[{:error, error}], pid} + end + end, + fn _pid -> nil end + ) + end + + def get_list!(user, {resource_name, resource_maker}, query \\ %{}) do + limit = query[:limit] + getter = make_getter(user, resource_name) + + Stream.resource( + fn -> + {:ok, pid} = QueryGenerator.start_query(getter, API.last_name_plural(resource_name), query |> Map.put(:limit, limit)) + pid + end, + fn pid -> + case QueryGenerator.get(pid) do + :halt -> {:halt, pid} + {:ok, element} -> {[API.from_api_json(element, resource_maker)], pid} + {:error, errors} -> raise API.errors_to_string(errors) + end + end, + fn _pid -> nil end + ) + end + + defp make_getter(user, resource_name) do + fn query -> Request.fetch( + user, + :get, + API.endpoint(resource_name), + query: query + ) + end + end + + def get_id(user, {resource_name, resource_maker}, id) do + case Request.fetch(user, :get, "#{API.endpoint(resource_name)}/#{id}") do + {:ok, response} -> {:ok, process_single_response(response, resource_name, resource_maker)} + {:error, errors} -> {:error, errors} + end + end + + def get_id!(user, {resource_name, resource_maker}, id) do + case get_id(user, {resource_name, resource_maker}, id) do + {:ok, entity} -> entity + {:error, errors} -> raise API.errors_to_string(errors) + end + end + + def get_pdf(user, {resource_name, _resource_maker}, id) do + case Request.fetch(user, :get, "#{API.endpoint(resource_name)}/#{id}/pdf") do + {:ok, pdf} -> {:ok, pdf} + {:error, errors} -> {:error, errors} + end + end + + def get_pdf!(user, {resource_name, _resource_maker}, id) do + case Request.fetch(user, :get, "#{API.endpoint(resource_name)}/#{id}/pdf") do + {:ok, pdf} -> pdf + {:error, errors} -> raise API.errors_to_string(errors) + end + end + + def post(user, {resource_name, resource_maker}, entities) do + case Request.fetch(user, :post, "#{API.endpoint(resource_name)}", payload: prepare_payload(resource_name, entities)) do + {:ok, response} -> {:ok, process_response(resource_name, resource_maker, response)} + {:error, errors} -> {:error, errors} + end + end + + def post!(user, {resource_name, resource_maker}, entities) do + case post(user, {resource_name, resource_maker}, entities) do + {:ok, entities} -> entities + {:error, errors} -> raise API.errors_to_string(errors) + end + end + + def post_single(user, {resource_name, resource_maker}, entity) do + case Request.fetch(user, :post, "#{API.endpoint(resource_name)}", payload: API.api_json(entity)) do + {:ok, response} -> {:ok, process_single_response(response, resource_name, resource_maker)} + {:error, errors} -> {:error, errors} + end + end + + def post_single!(user, {resource_name, resource_maker}, entity) do + case post_single(user, {resource_name, resource_maker}, entity) do + {:ok, entity} -> entity + {:error, errors} -> raise API.errors_to_string(errors) + end + end + + def delete_id(user, {resource_name, resource_maker}, id) do + case Request.fetch(user, :delete, "#{API.endpoint(resource_name)}/#{id}") do + {:ok, response} -> {:ok, process_single_response(response, resource_name, resource_maker)} + {:error, errors} -> {:error, errors} + end + end + + def delete_id!(user, {resource_name, resource_maker}, id) do + case delete_id(user, {resource_name, resource_maker}, id) do + {:ok, entity} -> entity + {:error, errors} -> raise API.errors_to_string(errors) + end + end + + def patch_id(user, {resource_name, resource_maker}, id, payload) do + case Request.fetch(user, :patch, "#{API.endpoint(resource_name)}/#{id}", payload: payload |> API.cast_json_to_api_format) do + {:ok, response} -> {:ok, process_single_response(response, resource_name, resource_maker)} + {:error, errors} -> {:error, errors} + end + end + + def patch_id!(user, {resource_name, resource_maker}, id, payload) do + case patch_id(user, {resource_name, resource_maker}, id, payload) do + {:ok, entity} -> entity + {:error, errors} -> raise API.errors_to_string(errors) + end + end + + defp prepare_payload(resource_name, entities) do + Map.put( + %{}, + API.last_name_plural(resource_name), + Enum.map(entities, &API.api_json/1) + ) + end + + defp process_single_response(response, resource_name, resource_maker) do + JSON.decode!(response)[API.last_name(resource_name)] + |> API.from_api_json(resource_maker) + end + + defp process_response(resource_name, resource_maker, response) do + JSON.decode!(response)[API.last_name_plural(resource_name)] + |> Enum.map(fn json -> API.from_api_json(json, resource_maker) end) + end +end diff --git a/lib/utils/url.ex b/lib/utils/url.ex new file mode 100644 index 0000000..e2154c2 --- /dev/null +++ b/lib/utils/url.ex @@ -0,0 +1,52 @@ +defmodule StarkBank.Utils.URL do + + @moduledoc false + + alias StarkBank.Utils.API, as: API + + def get_url(environment, version, path, query) do + base_url(environment) ++ version ++ '/' + |> add_path(path) + |> add_query(query) + end + + defp base_url(environment) do + case environment do + :production -> 'https://api.starkbank.com/' + :sandbox -> 'https://sandbox.api.starkbank.com/' + end + end + + defp add_path(base_url, path) do + base_url ++ to_charlist(path) + end + + defp add_query(endpoint, query) when is_nil(query) do + endpoint + end + + defp add_query(endpoint, query) do + list = for {k, v} <- query |> API.cast_json_to_api_format, !is_nil(v), do: "#{k |> query_key}=#{v |> query_argument}" + + if length(list) > 0 do + endpoint ++ to_charlist("?" <> String.replace(Enum.join(list, "&"), " ", "%20")) + else + endpoint + end + end + + defp query_key(key) do + key + |> to_string + end + + defp query_argument(value) when is_list(value) or is_tuple(value) do + value + |> Enum.map(fn v -> to_string(v) end) + |> Enum.join(",") + end + + defp query_argument(value) do + value + end +end diff --git a/lib/webhook/event.ex b/lib/webhook/event.ex new file mode 100644 index 0000000..9a32849 --- /dev/null +++ b/lib/webhook/event.ex @@ -0,0 +1,294 @@ +defmodule StarkBank.Event do + + alias __MODULE__, as: Event + alias StarkBank.Utils.Rest, as: Rest + alias StarkBank.Utils.Checks, as: Checks + alias StarkBank.Utils.JSON, as: JSON + alias StarkBank.Utils.API, as: API + alias StarkBank.User.Project, as: Project + alias StarkBank.Error, as: Error + alias StarkBank.Utils.Request, as: Request + alias StarkBank.Boleto.Log, as: BoletoLog + alias StarkBank.Transfer.Log, as: TransferLog + alias StarkBank.BoletoPayment.Log, as: BoletoPaymentLog + alias StarkBank.UtilityPayment.Log, as: UtilityPaymentLog + alias EllipticCurve.Signature, as: Signature + alias EllipticCurve.PublicKey, as: PublicKey + alias EllipticCurve.Ecdsa, as: Ecdsa + + @moduledoc """ + Groups Webhook-Event related functions + """ + + @doc """ + An Event is the notification received from the subscription to the Webhook. + Events cannot be created, but may be retrieved from the Stark Bank API to + list all generated updates on entities. + + ## Attributes: + - id [string]: unique id returned when the log is created. ex: "5656565656565656" + - log [Log]: a Log struct from one the subscription services (Transfer.Log, Boleto.Log, BoletoPayment.log or UtilityPayment.Log) + - created [DateTime]: creation datetime for the notification event. ex: ~U[2020-03-26 19:32:35.418698Z] + - is_delivered [bool]: true if the event has been successfully delivered to the user url. ex: false + - subscription [string]: service that triggered this event. ex: "transfer", "utility-payment" + """ + defstruct [:id, :log, :created, :is_delivered, :subscription] + + @type t() :: %__MODULE__{} + + @doc """ + Receive a single notification Event struct previously created in the Stark Bank API by passing its id + + ## Parameters (required): + - user [Project]: Project struct returned from StarkBank.project(). + - id [string]: struct unique id. ex: "5656565656565656" + + ## Return: + - Event struct with updated attributes + """ + @spec get(Project.t(), binary) :: {:ok, Event.t()} | {:error, [%Error{}]} + def get(%Project{} = user, id) do + Rest.get_id(user, resource(), id) + end + + @doc """ + Same as get(), but it will unwrap the error tuple and raise in case of errors. + """ + @spec get!(Project.t(), binary) :: Event.t() + def get!(%Project{} = user, id) do + Rest.get_id!(user, resource(), id) + end + + @doc """ + Receive a stream of notification Event structs previously created in the Stark Bank API + + ## Parameters (required): + - user [Project]: Project struct returned from StarkBank.project(). + + ## Parameters (optional): + - limit [integer, default nil]: maximum number of structs to be retrieved. Unlimited if nil. ex: 35 + - after [Date, default nil]: date filter for structs created only after specified date. ex: ~D[2020-03-25] + - before [Date, default nil]: date filter for structs only before specified date. ex: ~D[2020-03-25] + - is_delivered [bool, default nil]: filter successfully delivered events. ex: true or false + + ## Return: + - stream of Event structs with updated attributes + """ + @spec query(Project.t(), any) :: + ({:cont, {:ok, [Event.t()]}} | {:error, [Error.t()]} | {:halt, any} | {:suspend, any}, any -> any) + def query(%Project{} = user, options \\ []) do + Rest.get_list(user, resource(), options |> Checks.check_options(true)) + end + + @doc """ + Same as query(), but it will unwrap the error tuple and raise in case of errors. + """ + @spec query!(Project.t(), any) :: + ({:cont, [Event.t()]} | {:halt, any} | {:suspend, any}, any -> any) + def query!(%Project{} = user, options \\ []) do + Rest.get_list!(user, resource(), options |> Checks.check_options(true)) + end + + @doc """ + Delete a list of notification Event entities previously created in the Stark Bank API + + ## Parameters (required): + - user [Project]: Project struct returned from StarkBank.project(). + - id [string]: Event unique id. ex: "5656565656565656" + + ## Return: + - deleted Event struct with updated attributes + """ + @spec delete(Project.t(), binary) :: {:ok, Event.t()} | {:error, [%Error{}]} + def delete(%Project{} = user, id) do + Rest.delete_id(user, resource(), id) + end + + @doc """ + Same as delete(), but it will unwrap the error tuple and raise in case of errors. + """ + @spec delete!(Project.t(), binary) :: Event.t() + def delete!(%Project{} = user, id) do + Rest.delete_id!(user, resource(), id) + end + + @doc """ + Update notification Event by passing id. + If is_delivered is true, the event will no longer be returned on queries with is_delivered=false. + + ## Parameters (required): + - user [Project]: Project struct returned from StarkBank.project(). + - id [list of strings]: Event unique ids. ex: "5656565656565656" + ## Parameters (optional): + - is_delivered [bool]: If true and event hasn't been delivered already, event will be set as delivered. ex: true + + ## Return: + - target Event with updated attributes + """ + @spec update(Project.t(), binary, boolean) :: {:ok, Event.t()} | {:error, [%Error{}]} + def update(%Project{} = user, id, options \\ []) do + Rest.patch_id(user, resource(), id, options |> Enum.into(%{})) + end + + @doc """ + Same as update(), but it will unwrap the error tuple and raise in case of errors. + """ + @spec update!(Project.t(), binary, boolean) :: Event.t() + def update!(%Project{} = user, id, options \\ []) do + Rest.patch_id!(user, resource(), id, options |> Enum.into(%{})) + end + + @doc """ + Create a single Event struct received from event listening at subscribed user endpoint. + If the provided digital signature does not check out with the StarkBank public key, an "invalidSignature" + error will be returned. + + ## Parameters (required): + - user [Project]: Project struct returned from StarkBank.project(). + - content [string]: response content from request received at user endpoint (not parsed) + - signature [string]: base-64 digital signature received at response header "Digital-Signature" + - cache_pid [PID, default nil]: PID of the process that holds the public key cache, returned on previous parses. If not provided, a new cache process will be generated. + + ## Return: + - Event struct with updated attributes + - Cache PID that holds the Stark Bank public key in order to avoid unnecessary requests to the API on future parses + """ + @spec parse(Project.t(), binary, binary, PID.t() | nil) :: + {:ok, {Event.t(), binary}} | {:error, [Error.t()]} + def parse(%Project{} = user, content, signature, cache_pid \\ nil) do + parse(user, content, signature, cache_pid, 0) + end + + @doc """ + Same as parse(), but it will unwrap the error tuple and raise in case of errors. + """ + @spec parse!(Project.t(), binary, binary, PID.t() | nil) :: + {Event.t(), any} + def parse!(%Project{} = user, content, signature, cache_pid \\ nil) do + case parse(user, content, signature, cache_pid, 0) do + {:ok, {event, cache_pid_}} -> {event, cache_pid_} + {:error, errors} -> raise API.errors_to_string(errors) + end + end + + defp parse(user, content, signature, cache_pid, counter) when is_nil(cache_pid) do + {:ok, new_cache_pid} = Agent.start_link(fn -> %{} end) + parse(user, content, signature, new_cache_pid, counter) + end + + defp parse(user, content, signature, cache_pid, counter) do + case verify_signature(user, content, signature, cache_pid, counter) do + {:ok, true} -> {:ok, {content |> parse_content, cache_pid}} + {:ok, false} -> parse(user, content, signature, cache_pid |> update_public_key(nil), counter + 1) + {:error, errors} -> {:error, errors} + end + end + + defp parse_content(content) do + API.from_api_json( + JSON.decode!(content)["event"], + &resource_maker/1 + ) + end + + defp verify_signature(_user, _content, _signature_base_64, _cache_pid, counter) when counter > 1 do + { + :error, + [ + %Error{ + code: "invalidSignature", + message: "The provided signature and content do not match the Stark Bank public key" + } + ] + } + end + + defp verify_signature(user, content, signature_base_64, cache_pid, counter) when is_binary(signature_base_64) and counter <= 1 do + verify_signature(user, content, signature_base_64 |> Signature.fromBase64!, cache_pid, counter) + rescue + _error -> { + :error, + [ + %Error{ + code: "invalidSignature", + message: "The provided signature is not valid" + } + ] + } + end + + defp verify_signature(user, content, signature, cache_pid, _counter) do + case get_starkbank_public_key(user, cache_pid) do + {:ok, public_key} -> { + :ok, + (fn p -> Ecdsa.verify?( + content, + signature, + p |> PublicKey.fromPem! + ) end + ).(public_key) + } + {:error, errors} -> {:error, errors} + end + end + + defp get_starkbank_public_key(user, cache_pid) do + get_public_key(cache_pid) |> fill_public_key(user, cache_pid) + end + + defp fill_public_key(public_key, user, cache_pid) when is_nil(public_key) do + case Request.fetch(user, :get, "public-key", query: %{limit: 1}) do + {:ok, response} -> {:ok, response |> extract_public_key(cache_pid)} + {:error, errors} -> {:error, errors} + end + end + + defp fill_public_key(public_key, _user, _cache_pid) do + {:ok, public_key} + end + + defp extract_public_key(response, cache_pid) do + public_key = JSON.decode!(response)["publicKeys"] + |> hd + |> (fn x -> x["content"] end).() + + update_public_key(cache_pid, public_key) + + public_key + end + + defp get_public_key(cache_pid) do + Agent.get(cache_pid, fn map -> Map.get(map, :starkbank_public_key) end) + end + + defp update_public_key(cache_pid, public_key) do + Agent.update(cache_pid, fn map -> Map.put(map, :starkbank_public_key, public_key) end) + cache_pid + end + + defp resource() do + { + "Event", + &resource_maker/1 + } + end + + defp resource_maker(json) do + %Event{ + id: json[:id], + log: json[:log] |> API.from_api_json(log_maker_by_subscription(json[:subscription])), + created: json[:created] |> Checks.check_datetime, + is_delivered: json[:is_delivered], + subscription: json[:subscription] + } + end + + defp log_maker_by_subscription(subscription) do + case subscription do + "transfer" -> &TransferLog.resource_maker/1 + "boleto" -> &BoletoLog.resource_maker/1 + "boleto-payment" -> &BoletoPaymentLog.resource_maker/1 + "utility-payment" -> &UtilityPaymentLog.resource_maker/1 + end + end +end diff --git a/lib/webhook/webhook.ex b/lib/webhook/webhook.ex new file mode 100644 index 0000000..0953bc0 --- /dev/null +++ b/lib/webhook/webhook.ex @@ -0,0 +1,154 @@ +defmodule StarkBank.Webhook do + + alias __MODULE__, as: Webhook + alias StarkBank.Utils.Rest, as: Rest + alias StarkBank.User.Project, as: Project + alias StarkBank.Error, as: Error + alias StarkBank.Utils.Checks, as: Checks + + @moduledoc """ + Groups Webhook related functions + """ + + @doc """ + A Webhook is used to subscribe to notification events on a user-selected endpoint. + Currently available services for subscription are transfer, boleto, boleto-payment, + and utility-payment + + ## Parameters (required): + - url [string]: Url that will be notified when an event occurs. + - subscriptions [list of strings]: list of any non-empty combination of the available services. ex: ["transfer", "boleto-payment"] + + ## Attributes: + - id [string, default nil]: unique id returned when the log is created. ex: "5656565656565656" + """ + @enforce_keys [:url, :subscriptions] + defstruct [:id, :url, :subscriptions] + + @type t() :: %__MODULE__{} + + @doc """ + Send a single Webhook subscription for creation in the Stark Bank API + + ## Parameters (required): + - user [Project]: Project struct returned from StarkBank.project(). + - url [string]: url to which notification events will be sent to. ex: "https://webhook.site/60e9c18e-4b5c-4369-bda1-ab5fcd8e1b29" + - subscriptions [list of strings]: list of any non-empty combination of the available services. ex: ["transfer", "boleto-payment"] + + ## Return: + - Webhook struct with updated attributes + """ + @spec create(Project.t(), binary, [binary]) :: + {:ok, Webhook.t()} | {:error, [Error.t()]} + def create(%Project{} = user, url, subscriptions) do + webhook = %Webhook{url: url, subscriptions: subscriptions} + Rest.post_single( + user, + resource(), + webhook + ) + end + + @doc """ + Same as create(), but it will unwrap the error tuple and raise in case of errors. + """ + @spec create!(Project.t(), binary, [binary]) :: any + def create!(%Project{} = user, url, subscriptions) do + webhook = %Webhook{url: url, subscriptions: subscriptions} + Rest.post_single!( + user, + resource(), + webhook + ) + end + + @doc """ + Receive a single Webhook subscription struct previously created in the Stark Bank API by passing its id + + ## Parameters (required): + - user [Project]: Project struct returned from StarkBank.project(). + - id [string]: struct unique id. ex: "5656565656565656" + + ## Return: + - Webhook struct with updated attributes + """ + @spec get(Project.t(), binary) :: {:ok, Webhook.t()} | {:error, [%Error{}]} + def get(%Project{} = user, id) do + Rest.get_id(user, resource(), id) + end + + @doc """ + Same as get(), but it will unwrap the error tuple and raise in case of errors. + """ + @spec get!(Project.t(), binary) :: Webhook.t() + def get!(%Project{} = user, id) do + Rest.get_id!(user, resource(), id) + end + + @doc """ + Receive a stream of Webhook subcription structs previously created in the Stark Bank API + + ## Parameters (required): + - user [Project]: Project struct returned from StarkBank.project(). + + ## Parameters (optional): + - limit [integer, default nil]: maximum number of structs to be retrieved. Unlimited if nil. ex: 35 + + ## Return: + - stream of Webhook structs with updated attributes + """ + @spec query(Project.t(), any) :: + ({:cont, {:ok, [Webhook.t()]}} | {:error, [Error.t()]} | {:halt, any} | {:suspend, any}, any -> any) + def query(%Project{} = user, options \\ []) do + Rest.get_list(user, resource(), options |> Checks.check_options) + end + + @doc """ + Same as query(), but it will unwrap the error tuple and raise in case of errors. + """ + @spec query!(Project.t(), any) :: + ({:cont, [Webhook.t()]} | {:halt, any} | {:suspend, any}, any -> any) + def query!(%Project{} = user, options \\ []) do + Rest.get_list!(user, resource(), options |> Checks.check_options) + end + + @doc """ + Delete a Webhook subscription entity previously created in the Stark Bank API + + ## Parameters (required): + - user [Project]: Project struct returned from StarkBank.project(). + - id [string]: Webhook unique id. ex: "5656565656565656" + + ## Return: + - deleted Webhook with updated attributes + """ + @spec delete(Project.t(), binary) :: {:ok, Webhook.t()} | {:error, [%Error{}]} + def delete(%Project{} = user, id) do + Rest.delete_id(user, resource(), id) + end + + @doc """ + Same as delete(), but it will unwrap the error tuple and raise in case of errors. + """ + @spec delete!(Project.t(), binary) :: Webhook.t() + def delete!(%Project{} = user, id) do + Rest.delete_id!(user, resource(), id) + end + + @doc false + def resource() do + { + "Webhook", + &resource_maker/1 + } + end + + @doc false + def resource_maker(json) do + %Webhook{ + id: json[:id], + url: json[:url], + subscriptions: json[:subscriptions] + } + end +end diff --git a/mix.exs b/mix.exs index 0a239c9..2e7a990 100644 --- a/mix.exs +++ b/mix.exs @@ -3,9 +3,9 @@ defmodule StarkBank.MixProject do def project do [ - app: :stark_bank, - name: :stark_bank, - version: "1.1.2", + app: :starkbank, + name: :starkbank, + version: "0.1.0", homepage_url: "https://starkbank.com", source_url: "https://github.com/starkbank/sdk-elixir", description: description(), @@ -37,6 +37,7 @@ defmodule StarkBank.MixProject do defp deps do [ + {:starkbank_ecdsa, "~> 1.0.0"}, {:jason, "~> 1.1"}, {:ex_doc, ">= 0.0.0", only: :dev, runtime: false} ] diff --git a/test/balance_test.exs b/test/balance_test.exs new file mode 100644 index 0000000..5012f36 --- /dev/null +++ b/test/balance_test.exs @@ -0,0 +1,10 @@ +defmodule StarkBankTest.Balance do + use ExUnit.Case + + @tag :balance + test "get balance" do + user = StarkBankTest.Credentials.project() + balance = StarkBank.Balance.get!(user) + assert !is_nil(balance.amount) + end +end diff --git a/test/boleto_log_test.exs b/test/boleto_log_test.exs new file mode 100644 index 0000000..8f49d97 --- /dev/null +++ b/test/boleto_log_test.exs @@ -0,0 +1,50 @@ +defmodule StarkBankTest.BoletoLog do + use ExUnit.Case + + @tag :boleto_log + test "query boleto log" do + user = StarkBankTest.Credentials.project() + StarkBank.Boleto.Log.query(user, limit: 101) + |> Enum.take(200) + |> (fn list -> assert length(list) <= 101 end).() + end + + @tag :boleto_log + test "query! boleto log" do + user = StarkBankTest.Credentials.project() + StarkBank.Boleto.Log.query!(user, limit: 101) + |> Enum.take(200) + |> (fn list -> assert length(list) <= 101 end).() + end + + @tag :boleto_log + test "query! boleto log with filters" do + user = StarkBankTest.Credentials.project() + + boleto = StarkBank.Boleto.query!(user, status: "paid") + |> Enum.take(1) + |> hd() + + StarkBank.Boleto.Log.query!(user, limit: 1, boleto_ids: [boleto.id], types: "paid") + |> Enum.take(5) + |> (fn list -> assert length(list) == 1 end).() + end + + @tag :boleto_log + test "get boleto log" do + user = StarkBankTest.Credentials.project() + log = StarkBank.Boleto.Log.query!(user) + |> Enum.take(1) + |> hd() + {:ok, _log} = StarkBank.Boleto.Log.get(user, log.id) + end + + @tag :boleto_log + test "get! boleto log" do + user = StarkBankTest.Credentials.project() + log = StarkBank.Boleto.Log.query!(user) + |> Enum.take(1) + |> hd() + _log = StarkBank.Boleto.Log.get!(user, log.id) + end +end diff --git a/test/boleto_payment_log_test.exs b/test/boleto_payment_log_test.exs new file mode 100644 index 0000000..01733ea --- /dev/null +++ b/test/boleto_payment_log_test.exs @@ -0,0 +1,50 @@ +defmodule StarkBankTest.BoletoPaymentLog do + use ExUnit.Case + + @tag :boleto_payment_log + test "query boleto payment log" do + user = StarkBankTest.Credentials.project() + StarkBank.BoletoPayment.Log.query(user, limit: 101) + |> Enum.take(200) + |> (fn list -> assert length(list) <= 101 end).() + end + + @tag :boleto_payment_log + test "query! boleto payment log" do + user = StarkBankTest.Credentials.project() + StarkBank.BoletoPayment.Log.query!(user, limit: 101) + |> Enum.take(200) + |> (fn list -> assert length(list) <= 101 end).() + end + + @tag :boleto_payment_log + test "query! boleto payment log with filters" do + user = StarkBankTest.Credentials.project() + + payment = StarkBank.BoletoPayment.query!(user) + |> Enum.take(1) + |> hd() + + StarkBank.BoletoPayment.Log.query!(user, limit: 1, payment_ids: [payment.id]) + |> Enum.take(5) + |> (fn list -> assert length(list) == 1 end).() + end + + @tag :boleto_payment_log + test "get boleto payment log" do + user = StarkBankTest.Credentials.project() + log = StarkBank.BoletoPayment.Log.query!(user) + |> Enum.take(1) + |> hd() + {:ok, _log} = StarkBank.BoletoPayment.Log.get(user, log.id) + end + + @tag :boleto_payment_log + test "get! boleto payment log" do + user = StarkBankTest.Credentials.project() + log = StarkBank.BoletoPayment.Log.query!(user) + |> Enum.take(1) + |> hd() + _log = StarkBank.BoletoPayment.Log.get!(user, log.id) + end +end diff --git a/test/boleto_payment_test.exs b/test/boleto_payment_test.exs new file mode 100644 index 0000000..968ec08 --- /dev/null +++ b/test/boleto_payment_test.exs @@ -0,0 +1,100 @@ +defmodule StarkBankTest.BoletoPayment do + use ExUnit.Case + + @tag :boleto_payment + test "create boleto payment" do + user = StarkBankTest.Credentials.project() + {:ok, payments} = StarkBank.BoletoPayment.create(user, [example_payment()]) + payment = payments |> hd + assert !is_nil(payment) + end + + @tag :boleto_payment + test "create! boleto payment" do + user = StarkBankTest.Credentials.project() + payment = StarkBank.BoletoPayment.create!(user, [example_payment()]) |> hd + assert !is_nil(payment) + end + + @tag :boleto_payment + test "query boleto payment" do + user = StarkBankTest.Credentials.project() + StarkBank.BoletoPayment.query(user, limit: 101) + |> Enum.take(200) + |> (fn list -> assert length(list) <= 101 end).() + end + + @tag :boleto_payment + test "query! boleto payment" do + user = StarkBankTest.Credentials.project() + StarkBank.BoletoPayment.query!(user, limit: 101) + |> Enum.take(200) + |> (fn list -> assert length(list) <= 101 end).() + end + + @tag :boleto_payment + test "get boleto payment" do + user = StarkBankTest.Credentials.project() + payment = StarkBank.BoletoPayment.query!(user) + |> Enum.take(1) + |> hd() + {:ok, _payment} = StarkBank.BoletoPayment.get(user, payment.id) + end + + @tag :boleto_payment + test "get! boleto payment" do + user = StarkBankTest.Credentials.project() + payment = StarkBank.BoletoPayment.query!(user) + |> Enum.take(1) + |> hd() + _payment = StarkBank.BoletoPayment.get!(user, payment.id) + end + + @tag :boleto_payment + test "pdf boleto payment" do + user = StarkBankTest.Credentials.project() + payment = StarkBank.BoletoPayment.query!(user, status: "success") + |> Enum.take(1) + |> hd() + {:ok, _pdf} = StarkBank.BoletoPayment.pdf(user, payment.id) + end + + @tag :boleto_payment + test "pdf! boleto payment" do + user = StarkBankTest.Credentials.project() + payment = StarkBank.BoletoPayment.query!(user, status: "success") + |> Enum.take(1) + |> hd() + pdf = StarkBank.BoletoPayment.pdf!(user, payment.id) + file = File.open!("boleto-payment.pdf", [:write]) + IO.binwrite(file, pdf) + File.close(file) + end + + @tag :boleto_payment + test "delete boleto payment" do + user = StarkBankTest.Credentials.project() + payment = StarkBank.BoletoPayment.create!(user, [example_payment()]) |> hd + {:ok, deleted_payment} = StarkBank.BoletoPayment.delete(user, payment.id) + assert !is_nil(deleted_payment) + end + + @tag :boleto_payment + test "delete! boleto payment" do + user = StarkBankTest.Credentials.project() + payment = StarkBank.BoletoPayment.create!(user, [example_payment()]) |> hd + deleted_payment = StarkBank.BoletoPayment.delete!(user, payment.id) + assert !is_nil(deleted_payment) + end + + defp example_payment() do + user = StarkBankTest.Credentials.project() + boleto = StarkBank.Boleto.create!(user, [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 + } + end +end diff --git a/test/boleto_test.exs b/test/boleto_test.exs new file mode 100644 index 0000000..ca23d56 --- /dev/null +++ b/test/boleto_test.exs @@ -0,0 +1,121 @@ +defmodule StarkBankTest.Boleto do + use ExUnit.Case + + @tag :boleto + test "create boleto" do + user = StarkBankTest.Credentials.project() + {:ok, boletos} = StarkBank.Boleto.create(user, [example_boleto()]) + boleto = boletos |> hd + assert !is_nil(boleto) + end + + @tag :boleto + test "create! boleto" do + user = StarkBankTest.Credentials.project() + boleto = StarkBank.Boleto.create!(user, [example_boleto()]) |> hd + assert !is_nil(boleto) + end + + @tag :boleto + test "query boleto" do + user = StarkBankTest.Credentials.project() + StarkBank.Boleto.query(user, limit: 101, before: DateTime.utc_now()) + |> Enum.take(200) + |> (fn list -> assert length(list) <= 101 end).() + end + + @tag :boleto + test "query! boleto" do + user = StarkBankTest.Credentials.project() + StarkBank.Boleto.query!(user, limit: 101, before: DateTime.utc_now()) + |> Enum.take(200) + |> (fn list -> assert length(list) <= 101 end).() + end + + @tag :boleto + test "get boleto" do + user = StarkBankTest.Credentials.project() + boleto = StarkBank.Boleto.query!(user) + |> Enum.take(1) + |> hd() + {:ok, _boleto} = StarkBank.Boleto.get(user, boleto.id) + end + + @tag :boleto + test "get! boleto" do + user = StarkBankTest.Credentials.project() + boleto = StarkBank.Boleto.query!(user) + |> Enum.take(1) + |> hd() + _boleto = StarkBank.Boleto.get!(user, boleto.id) + end + + @tag :boleto + test "pdf boleto" do + user = StarkBankTest.Credentials.project() + boleto = StarkBank.Boleto.query!(user) + |> Enum.take(1) + |> hd() + {:ok, _pdf} = StarkBank.Boleto.pdf(user, boleto.id) + end + + @tag :boleto + test "pdf! boleto" do + user = StarkBankTest.Credentials.project() + boleto = StarkBank.Boleto.query!(user) + |> Enum.take(1) + |> hd() + pdf = StarkBank.Boleto.pdf!(user, boleto.id) + file = File.open!("boleto.pdf", [:write]) + IO.binwrite(file, pdf) + File.close(file) + end + + @tag :boleto + test "delete boleto" do + user = StarkBankTest.Credentials.project() + boleto = StarkBank.Boleto.create!(user, [example_boleto()]) |> hd + {:ok, deleted_boleto} = StarkBank.Boleto.delete(user, boleto.id) + assert !is_nil(deleted_boleto) + end + + @tag :boleto + test "delete! boleto" do + user = StarkBankTest.Credentials.project() + boleto = StarkBank.Boleto.create!(user, [example_boleto()]) |> hd + deleted_boleto = StarkBank.Boleto.delete!(user, boleto.id) + assert !is_nil(deleted_boleto) + end + + def example_boleto() do + %StarkBank.Boleto{ + amount: 200, + due: Date.utc_today() |> Date.add(5), + name: "Random Company", + street_line_1: "Rua ABC", + street_line_2: "Ap 123", + district: "Jardim Paulista", + city: "São Paulo", + state_code: "SP", + zip_code: "01234-567", + tax_id: "012.345.678-90", + overdue_limit: 10, + fine: 0.00, + interest: 0.00, + descriptions: [ + %{ + text: "product A", + amount: 123 + }, + %{ + text: "product B", + amount: 456 + }, + %{ + text: "product C", + amount: 789 + } + ] + } + end +end diff --git a/test/event_test.exs b/test/event_test.exs new file mode 100644 index 0000000..4b07a28 --- /dev/null +++ b/test/event_test.exs @@ -0,0 +1,109 @@ +defmodule StarkBankTest.WebhookEvent do + use ExUnit.Case + + @content "{\"event\": {\"log\": {\"transfer\": {\"status\": \"processing\", \"updated\": \"2020-04-03T13:20:33.485644+00:00\", \"fee\": 160, \"name\": \"Lawrence James\", \"accountNumber\": \"10000-0\", \"id\": \"5107489032896512\", \"tags\": [], \"taxId\": \"91.642.017/0001-06\", \"created\": \"2020-04-03T13:20:32.530367+00:00\", \"amount\": 2, \"transactionIds\": [\"6547649079541760\"], \"bankCode\": \"01\", \"branchCode\": \"0001\"}, \"errors\": [], \"type\": \"sending\", \"id\": \"5648419829841920\", \"created\": \"2020-04-03T13:20:33.164373+00:00\"}, \"subscription\": \"transfer\", \"id\": \"6234355449987072\", \"created\": \"2020-04-03T13:20:40.784479+00:00\"}}" + @signature "MEYCIQCmFCAn2Z+6qEHmf8paI08Ee5ZJ9+KvLWSS3ddp8+RF3AIhALlK7ltfRvMCXhjS7cy8SPlcSlpQtjBxmhN6ClFC0Tv6" + @bad_signature "MEUCIQDOpo1j+V40DNZK2URL2786UQK/8mDXon9ayEd8U0/l7AIgYXtIZJBTs8zCRR3vmted6Ehz/qfw1GRut/eYyvf1yOk=" + @malformed_signature "something is definitely wrong" + + @tag :event + test "get, update and delete webhook event" do + user = StarkBankTest.Credentials.project() + {:ok, query_event} = StarkBank.Event.query(user, limit: 1) + |> Enum.take(1) + |> hd + {:ok, get_event} = StarkBank.Event.get(user, query_event.id) + {:ok, delivered_event} = StarkBank.Event.update(user, get_event.id, is_delivered: true) + {:ok, delete_event} = StarkBank.Event.delete(user, delivered_event.id) + assert !is_nil(delete_event.id) + end + + @tag :event + test "get!, update! and delete! webhook event" do + user = StarkBankTest.Credentials.project() + query_event = StarkBank.Event.query!(user, limit: 1) + |> Enum.take(1) + |> hd + get_event = StarkBank.Event.get!(user, query_event.id) + assert !get_event.is_delivered + delivered_event = StarkBank.Event.update!(user, get_event.id, is_delivered: true) + assert delivered_event.is_delivered + delete_event = StarkBank.Event.delete!(user, delivered_event.id) + assert !is_nil(delete_event.id) + end + + @tag :event + test "query webhook event" do + user = StarkBankTest.Credentials.project() + StarkBank.Event.query(user, limit: 5) + |> Enum.take(5) + |> (fn list -> assert length(list) <= 5 end).() + end + + @tag :event + test "query! webhook event" do + user = StarkBankTest.Credentials.project() + StarkBank.Event.query!(user, limit: 5) + |> Enum.take(5) + |> (fn list -> assert length(list) <= 5 end).() + end + + @tag :event + test "parse webhook event" do + user = StarkBankTest.Credentials.project() + {:ok, {_event, cache_pid_1}} = StarkBank.Event.parse( + user, + @content, + @signature, + nil + ) + {:ok, {event, cache_pid_2}} = StarkBank.Event.parse( + user, + @content, + @signature, + cache_pid_1 + ) + assert Agent.get(cache_pid_1, fn map -> Map.get(map, :starkbank_public_key) end) == Agent.get(cache_pid_2, fn map -> Map.get(map, :starkbank_public_key) end) + assert !is_nil(event.log) + end + + @tag :event + test "parse! webhook event" do + user = StarkBankTest.Credentials.project() + {_event, cache_pid_1} = StarkBank.Event.parse!( + user, + @content, + @signature + ) + {event, cache_pid_2} = StarkBank.Event.parse!( + user, + @content, + @signature, + cache_pid_1 + ) + assert Agent.get(cache_pid_1, fn map -> Map.get(map, :starkbank_public_key) end) == Agent.get(cache_pid_2, fn map -> Map.get(map, :starkbank_public_key) end) + assert !is_nil(event.log) + end + + @tag :event + test "parse webhook event with invalid signature" do + user = StarkBankTest.Credentials.project() + {:error, [error]} = StarkBank.Event.parse( + user, + @content, + @bad_signature + ) + assert error.code == "invalidSignature" + end + + @tag :event + test "parse webhook event with malformed signature" do + user = StarkBankTest.Credentials.project() + {:error, [error]} = StarkBank.Event.parse( + user, + @content, + @malformed_signature + ) + assert error.code == "invalidSignature" + end +end diff --git a/test/keys_test.exs b/test/keys_test.exs new file mode 100644 index 0000000..830e685 --- /dev/null +++ b/test/keys_test.exs @@ -0,0 +1,16 @@ +defmodule StarkBankTest.Keys do + use ExUnit.Case + + @tag :keys + test "create keys" do + {private_no_path, public_no_path} = StarkBank.Key.create() + + assert is_binary(private_no_path) + assert is_binary(public_no_path) + + {private, public} = StarkBank.Key.create("keys") + + assert is_binary(private) + assert is_binary(public) + end +end diff --git a/test/stark_bank_test.exs b/test/stark_bank_test.exs deleted file mode 100644 index 45aaa9f..0000000 --- a/test/stark_bank_test.exs +++ /dev/null @@ -1,396 +0,0 @@ -defmodule StarkBankTest do - use ExUnit.Case - - @env :sandbox - @username "user" - @email "user@email.com" - @password "password" - - @charge_customer_post_load 2 - @charge_post_load 3 - - test "auth-session" do - {:ok, credentials} = StarkBank.Auth.login(@env, @username, @email, @password) - - assert !is_nil(StarkBank.Auth.get_env(credentials)) - assert !is_nil(StarkBank.Auth.get_workspace(credentials)) - assert !is_nil(StarkBank.Auth.get_email(credentials)) - assert !is_nil(StarkBank.Auth.get_access_token(credentials)) - assert !is_nil(StarkBank.Auth.get_member_id(credentials)) - assert !is_nil(StarkBank.Auth.get_workspace_id(credentials)) - assert !is_nil(StarkBank.Auth.get_name(credentials)) - assert !is_nil(StarkBank.Auth.get_permissions(credentials)) - - {:ok, _response} = StarkBank.Auth.logout(credentials) - end - - test "external-auth-session" do - {:ok, credentials} = - StarkBank.Auth.login(@env, @username, @email, @password, auto_refresh: false) - - auto_refresh = StarkBank.Auth.get_auto_refresh(credentials) - assert !auto_refresh - - access_token = StarkBank.Auth.get_access_token(credentials) - - {:ok, credentials} = - StarkBank.Auth.login(@env, @username, @email, @password, - access_token: access_token, - auto_refresh: false - ) - - {:ok, _response} = StarkBank.Charge.get(credentials, limit: 1) - - # invalidating access token to validate lack of relogin - StarkBank.Auth.insert_external_access_token(credentials, "123") - - {:error, _response} = StarkBank.Charge.get(credentials, limit: 1) - - StarkBank.Auth.insert_external_access_token(credentials, access_token) - - {:ok, _response} = StarkBank.Charge.get(credentials, limit: 1) - - {:ok, _response} = StarkBank.Auth.logout(credentials) - end - - test "auth-relogin" do - {:ok, credentials} = StarkBank.Auth.login(@env, @username, @email, @password) - - # invalidating access token to validate relogin - StarkBank.Auth.insert_external_access_token(credentials, "123") - - {:ok, _response} = StarkBank.Charge.get(credentials, limit: 1) - - {:ok, _response} = StarkBank.Auth.logout(credentials) - end - - test "charge-customer-post" do - {:ok, credentials} = StarkBank.Auth.login(@env, @username, @email, @password) - - customers = - Enum.take( - Stream.cycle([ - %StarkBank.Charge.Structs.CustomerData{ - name: "Arya Stark", - email: "arya.stark@westeros.com", - tax_id: "416.631.524-20", - phone: "(11) 98300-0000", - tags: ["little girl", "no one", "valar morghulis", "Stark", "test"], - address: %StarkBank.Charge.Structs.AddressData{ - street_line_1: "Av. Faria Lima, 1844", - street_line_2: "CJ 13", - district: "Itaim Bibi", - city: "São Paulo", - state_code: "SP", - zip_code: "01500-000" - } - }, - %StarkBank.Charge.Structs.CustomerData{ - name: "Jon Snow", - email: "jon.snow@westeros.com", - tax_id: "012.345.678-90", - phone: "(11) 98300-0001", - tags: ["night`s watch", "lord commander", "knows nothing", "Stark", "test"], - address: %StarkBank.Charge.Structs.AddressData{ - street_line_1: "Av. Faria Lima, 1844", - street_line_2: "CJ 13", - district: "Itaim Bibi", - city: "São Paulo", - state_code: "SP", - zip_code: "01500-000" - } - } - ]), - @charge_customer_post_load - ) - - {:ok, posted_customers} = StarkBank.Charge.Customer.post(credentials, customers) - - assert length(customers) == length(posted_customers) - - {:ok, _response} = StarkBank.Auth.logout(credentials) - end - - test "charge-customer-get" do - {:ok, credentials} = StarkBank.Auth.login(@env, @username, @email, @password) - - {:ok, _all_customers} = StarkBank.Charge.Customer.get(credentials) - - {:ok, _70_customers} = StarkBank.Charge.Customer.get(credentials, tags: ["Stark"], limit: 70) - - {:ok, _110_customers} = - StarkBank.Charge.Customer.get(credentials, tags: ["Stark"], limit: 110) - - {:ok, _test_customers} = - StarkBank.Charge.Customer.get( - credentials, - fields: ["name", "tax_id"], - tags: ["test"], - tax_id: "012.345.678-90" - ) - - {:ok, _response} = StarkBank.Auth.logout(credentials) - end - - test "charge-customer-get_by_id" do - {:ok, credentials} = StarkBank.Auth.login(@env, @username, @email, @password) - - {:ok, one_customer} = - StarkBank.Charge.Customer.get( - credentials, - limit: 1 - ) - - one_customer = hd(one_customer) - - {:ok, customer_from_struct} = StarkBank.Charge.Customer.get_by_id(credentials, one_customer) - {:ok, customer_from_id} = StarkBank.Charge.Customer.get_by_id(credentials, one_customer.id) - - assert one_customer == customer_from_struct - assert one_customer == customer_from_id - - {:ok, _response} = StarkBank.Auth.logout(credentials) - end - - test "charge-customer-put" do - {:ok, credentials} = StarkBank.Auth.login(@env, @username, @email, @password) - - {:ok, one_customer} = - StarkBank.Charge.Customer.get( - credentials, - tags: ["test"], - limit: 1 - ) - - one_customer = hd(one_customer) - - altered_customer = %{one_customer | name: "No One"} - - {:ok, received_altered_customer} = - StarkBank.Charge.Customer.put(credentials, altered_customer) - - assert altered_customer == received_altered_customer - - {:ok, _response} = StarkBank.Auth.logout(credentials) - end - - test "charge-post" do - {:ok, credentials} = StarkBank.Auth.login(@env, @username, @email, @password) - - {:ok, one_customer} = - StarkBank.Charge.Customer.get( - credentials, - tags: ["test"], - limit: 1 - ) - - one_customer = hd(one_customer) - - charges = - Enum.take( - Stream.cycle([ - %StarkBank.Charge.Structs.ChargeData{ - amount: 100_00, - customer: one_customer.id, - tags: ["test"] - }, - %StarkBank.Charge.Structs.ChargeData{ - amount: 1_000_00, - customer: "self", - due_date: Date.utc_today(), - fine: 10, - interest: 15, - overdue_limit: 3, - tags: ["cash-in", "test"], - descriptions: [ - %StarkBank.Charge.Structs.ChargeDescriptionData{ - text: "part-1", - amount: 30_000 - }, - %StarkBank.Charge.Structs.ChargeDescriptionData{ - text: "part-2", - amount: 70_000 - } - ] - }, - %StarkBank.Charge.Structs.ChargeData{ - amount: 32_171_32, - customer: %StarkBank.Charge.Structs.CustomerData{ - name: "Brandon Stark", - email: "bran.builder@westeros.com", - tax_id: "123.456.789-09", - phone: "(11) 98300-0000", - tags: ["builder", "raven", "Stark", "test"], - address: %StarkBank.Charge.Structs.AddressData{ - street_line_1: "Av. Faria Lima, 1844", - street_line_2: "CJ 13", - district: "Itaim Bibi", - city: "São Paulo", - state_code: "SP", - zip_code: "01500-000" - } - }, - discount: 5, - discount_date: Date.add(Date.utc_today(), 2), - due_date: Date.add(Date.utc_today(), 3), - tags: ["test"] - } - ]), - @charge_post_load - ) - - {:ok, posted_charges} = StarkBank.Charge.post(credentials, charges) - - assert length(charges) == length(posted_charges) - - put_test_customer = %StarkBank.Charge.Structs.ChargeData{ - amount: 32_171_32, - customer: %StarkBank.Charge.Structs.CustomerData{ - name: "Brandon Stark 2", - email: "bran.builder@westeros.com", - tax_id: "123.456.789-09", - phone: "(11) 98300-0000", - tags: ["builder", "raven", "Stark", "test"], - address: %StarkBank.Charge.Structs.AddressData{ - street_line_1: "Av. Faria Lima, 1844", - street_line_2: "CJ 13", - district: "Itaim Bibi", - city: "São Paulo", - state_code: "SP", - zip_code: "01500-000" - } - }, - tags: ["test"] - } - - {:ok, _posted_charges} = - StarkBank.Charge.post(credentials, [put_test_customer], overwrite_customer_on_mismatch: true) - - {:ok, _response} = StarkBank.Auth.logout(credentials) - end - - test "charge-get" do - {:ok, credentials} = StarkBank.Auth.login(@env, @username, @email, @password) - - {:ok, all_charges} = StarkBank.Charge.get(credentials) - - {:ok, _filtered_charges} = - StarkBank.Charge.get( - credentials, - status: "registered", - tags: ["cash-in"], - ids: [hd(all_charges).id], - fields: ["id", "tax_id"], - filter_after: Date.add(Date.utc_today(), -1), - filter_before: Date.add(Date.utc_today(), 1), - limit: 50 - ) - - {:ok, _response} = StarkBank.Auth.logout(credentials) - end - - test "charge-get-pdf" do - {:ok, credentials} = StarkBank.Auth.login(@env, @username, @email, @password) - - {:ok, one_charge} = - StarkBank.Charge.get( - credentials, - status: "registered", - tags: ["test"], - fields: ["id"], - limit: 1 - ) - - {:ok, pdf} = - StarkBank.Charge.get_pdf( - credentials, - hd(one_charge).id - ) - - {:ok, file} = File.open("test/charge.pdf", [:write]) - IO.binwrite(file, pdf) - File.close(file) - - {:ok, _response} = StarkBank.Auth.logout(credentials) - end - - test "charge-delete" do - {:ok, credentials} = StarkBank.Auth.login(@env, @username, @email, @password) - - {:ok, test_charges} = - StarkBank.Charge.get( - credentials, - status: "registered", - tags: ["test"] - ) - - {:ok, _deleted_charges} = - StarkBank.Charge.delete( - credentials, - test_charges - ) - - {:ok, _response} = StarkBank.Auth.logout(credentials) - end - - test "charge-customer-delete" do - {:ok, credentials} = StarkBank.Auth.login(@env, @username, @email, @password) - - {:ok, test_customers} = - StarkBank.Charge.Customer.get( - credentials, - fields: ["id"], - tags: ["test"] - ) - - {:ok, _response} = StarkBank.Charge.Customer.delete(credentials, test_customers) - - {:ok, _response} = StarkBank.Auth.logout(credentials) - end - - test "charge-log-get" do - {:ok, credentials} = StarkBank.Auth.login(@env, @username, @email, @password) - - {:ok, one_charge} = - StarkBank.Charge.get( - credentials, - tags: ["test"], - fields: ["id"], - limit: 1 - ) - - {:ok, _response} = StarkBank.Charge.Log.get(credentials, [hd(one_charge)]) - - {:ok, _charge_logs} = - StarkBank.Charge.Log.get(credentials, [hd(one_charge).id], - events: ["registered", "cancel"], - limit: 30 - ) - - {:ok, _charge_logs} = - StarkBank.Charge.Log.get(credentials, [hd(one_charge).id], - events: ["registered", "cancel"], - limit: 130 - ) - - {:ok, _response} = StarkBank.Auth.logout(credentials) - end - - test "charge-log-get_by_id" do - {:ok, credentials} = StarkBank.Auth.login(@env, @username, @email, @password) - - {:ok, one_charge} = - StarkBank.Charge.get( - credentials, - tags: ["test"], - fields: ["id"], - limit: 1 - ) - - {:ok, charge_logs} = StarkBank.Charge.Log.get(credentials, [hd(one_charge)]) - - {:ok, _charge_log} = StarkBank.Charge.Log.get_by_id(credentials, hd(charge_logs).id) - - {:ok, _response} = StarkBank.Auth.logout(credentials) - end -end diff --git a/test/test_helper.exs b/test/test_helper.exs index cdcdd88..bfa9a3c 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,2 +1,27 @@ -ExUnit.start() -ExUnit.configure(seed: 0) +# remove excluded tags to run specific module tests +ExUnit.start(exclude: [ + :balance, + :boleto_log, + :boleto_payment_log, + :boleto_payment, + :boleto, + :keys, + :transaction, + :transfer_log, + :transfer, + :utility_payment_log, + :utility_payment, + :webhook, + :event +]) + + +defmodule StarkBankTest.Credentials do + + @project_id "9999999999999999" + @private_key "-----BEGIN EC PRIVATE KEY-----\nMHQCAQEEIBEcEJZLk/DyuXVsEjz0w4vrE7plPXhQxODvcG1Jc0WToAcGBSuBBAAK\noUQDQgAE6t4OGx1XYktOzH/7HV6FBukxq0Xs2As6oeN6re1Ttso2fwrh5BJXDq75\nmSYHeclthCRgU8zl6H1lFQ4BKZ5RCQ==\n-----END EC PRIVATE KEY-----\n" + + def project() do + StarkBank.project(:sandbox, @project_id, @private_key) + end +end diff --git a/test/transaction_test.exs b/test/transaction_test.exs new file mode 100644 index 0000000..8f6766e --- /dev/null +++ b/test/transaction_test.exs @@ -0,0 +1,61 @@ +defmodule StarkBankTest.Transaction do + use ExUnit.Case + + @tag :transaction + test "create transaction" do + user = StarkBankTest.Credentials.project() + {:ok, transactions} = StarkBank.Transaction.create(user, [example_transaction()]) + transaction = transactions |> hd + assert transaction.amount < 0 + end + + @tag :transaction + test "create! transaction" do + user = StarkBankTest.Credentials.project() + transaction = StarkBank.Transaction.create!(user, [example_transaction()]) |> hd + assert transaction.amount < 0 + end + + @tag :transaction + test "query transaction" do + user = StarkBankTest.Credentials.project() + StarkBank.Transaction.query(user, limit: 101) + |> Enum.take(200) + |> (fn list -> assert length(list) <= 101 end).() + end + + @tag :transaction + test "query! transaction" do + user = StarkBankTest.Credentials.project() + StarkBank.Transaction.query!(user, limit: 101) + |> Enum.take(200) + |> (fn list -> assert length(list) <= 101 end).() + end + + @tag :transaction + test "get transaction" do + user = StarkBankTest.Credentials.project() + transaction = StarkBank.Transaction.query!(user) + |> Enum.take(1) + |> hd() + {:ok, _transaction} = StarkBank.Transaction.get(user, transaction.id) + end + + @tag :transaction + test "get! transaction" do + user = StarkBankTest.Credentials.project() + transaction = StarkBank.Transaction.query!(user) + |> Enum.take(1) + |> hd() + _transaction = StarkBank.Transaction.get!(user, transaction.id) + end + + defp example_transaction() do + %StarkBank.Transaction{ + amount: 1, + receiver_id: "5768064935133184", + external_id: :crypto.strong_rand_bytes(30) |> Base.url_encode64 |> binary_part(0, 30), + description: "Transferencia para Workspace aleatorio" + } + end +end diff --git a/test/transfer_log_test.exs b/test/transfer_log_test.exs new file mode 100644 index 0000000..25e25e5 --- /dev/null +++ b/test/transfer_log_test.exs @@ -0,0 +1,50 @@ +defmodule StarkBankTest.TransferLog do + use ExUnit.Case + + @tag :transfer_log + test "query transfer log" do + user = StarkBankTest.Credentials.project() + StarkBank.Transfer.Log.query(user, limit: 101) + |> Enum.take(200) + |> (fn list -> assert length(list) <= 101 end).() + end + + @tag :transfer_log + test "query! transfer log" do + user = StarkBankTest.Credentials.project() + StarkBank.Transfer.Log.query!(user, limit: 101) + |> Enum.take(200) + |> (fn list -> assert length(list) <= 101 end).() + end + + @tag :transfer_log + test "query! transfer log with filters" do + user = StarkBankTest.Credentials.project() + + transfer = StarkBank.Transfer.query!(user, status: "success") + |> Enum.take(1) + |> hd() + + StarkBank.Transfer.Log.query!(user, limit: 1, transfer_ids: [transfer.id], types: "success") + |> Enum.take(5) + |> (fn list -> assert length(list) == 1 end).() + end + + @tag :transfer_log + test "get transfer log" do + user = StarkBankTest.Credentials.project() + log = StarkBank.Transfer.Log.query!(user) + |> Enum.take(1) + |> hd() + {:ok, _log} = StarkBank.Transfer.Log.get(user, log.id) + end + + @tag :transfer_log + test "get! transfer log" do + user = StarkBankTest.Credentials.project() + log = StarkBank.Transfer.Log.query!(user) + |> Enum.take(1) + |> hd() + _log = StarkBank.Transfer.Log.get!(user, log.id) + end +end diff --git a/test/transfer_test.exs b/test/transfer_test.exs new file mode 100644 index 0000000..bfc0dde --- /dev/null +++ b/test/transfer_test.exs @@ -0,0 +1,89 @@ +defmodule StarkBankTest.Transfer do + use ExUnit.Case + + @tag :transfer + test "create transfer" do + user = StarkBankTest.Credentials.project() + {:ok, transfers} = StarkBank.Transfer.create(user, [example_transfer()]) + transfer = transfers |> hd + assert !is_nil(transfer) + end + + @tag :transfer + test "create! transfer" do + user = StarkBankTest.Credentials.project() + transfer = StarkBank.Transfer.create!(user, [example_transfer()]) |> hd + assert !is_nil(transfer) + end + + @tag :transfer + test "query transfer" do + user = StarkBankTest.Credentials.project() + StarkBank.Transfer.query(user, limit: 101) + |> Enum.take(200) + |> (fn list -> assert length(list) <= 101 end).() + end + + @tag :transfer + test "query! transfer" do + user = StarkBankTest.Credentials.project() + StarkBank.Transfer.query!(user, limit: 101) + |> Enum.take(200) + |> (fn list -> assert length(list) <= 101 end).() + end + + @tag :transfer + test "get transfer" do + user = StarkBankTest.Credentials.project() + transfer = StarkBank.Transfer.query!(user) + |> Enum.take(1) + |> hd() + {:ok, _transfer} = StarkBank.Transfer.get(user, transfer.id) + end + + @tag :transfer + test "get! transfer" do + user = StarkBankTest.Credentials.project() + transfer = StarkBank.Transfer.query!(user) + |> Enum.take(1) + |> hd() + _transfer = StarkBank.Transfer.get!(user, transfer.id) + end + + @tag :transfer + test "pdf transfer" do + user = StarkBankTest.Credentials.project() + transfer = StarkBank.Transfer.query!(user, status: "success") + |> Enum.take(1) + |> hd() + {:ok, _pdf} = StarkBank.Transfer.pdf(user, transfer.id) + end + + @tag :transfer + test "pdf! transfer" do + user = StarkBankTest.Credentials.project() + transfer = StarkBank.Transfer.query!(user, status: "success") + |> Enum.take(1) + |> hd() + pdf = StarkBank.Transfer.pdf!(user, transfer.id) + file = File.open!("transfer.pdf", [:write]) + IO.binwrite(file, pdf) + File.close(file) + end + + defp example_transfer() do + %StarkBank.Transfer{ + amount: 10, + name: "João", + tax_id: "01234567890", + bank_code: "01", + branch_code: :crypto.rand_uniform(0, 9999) + |> to_string + |> String.pad_leading(4, "0"), + account_number: :crypto.rand_uniform(0, 99999) + |> to_string + |> String.pad_leading(5, "0") + |> (fn s -> s <> "-#{:crypto.rand_uniform(0, 9)}" end).() + } + end +end diff --git a/test/utility_payment_log_test.exs b/test/utility_payment_log_test.exs new file mode 100644 index 0000000..747bd3c --- /dev/null +++ b/test/utility_payment_log_test.exs @@ -0,0 +1,50 @@ +defmodule StarkBankTest.UtilityPaymentLog do + use ExUnit.Case + + @tag :utility_payment_log + test "query utility payment log" do + user = StarkBankTest.Credentials.project() + StarkBank.UtilityPayment.Log.query(user, limit: 101) + |> Enum.take(200) + |> (fn list -> assert length(list) <= 101 end).() + end + + @tag :utility_payment_log + test "query! utility payment log" do + user = StarkBankTest.Credentials.project() + StarkBank.UtilityPayment.Log.query!(user, limit: 101) + |> Enum.take(200) + |> (fn list -> assert length(list) <= 101 end).() + end + + @tag :utility_payment_log + test "query! utility payment log with filters" do + user = StarkBankTest.Credentials.project() + + payment = StarkBank.UtilityPayment.query!(user, status: "failed") + |> Enum.take(1) + |> hd() + + StarkBank.UtilityPayment.Log.query!(user, limit: 1, payment_ids: [payment.id], types: "failed") + |> Enum.take(5) + |> (fn list -> assert length(list) == 1 end).() + end + + @tag :utility_payment_log + test "get utility payment log" do + user = StarkBankTest.Credentials.project() + log = StarkBank.UtilityPayment.Log.query!(user) + |> Enum.take(1) + |> hd() + {:ok, _log} = StarkBank.UtilityPayment.Log.get(user, log.id) + end + + @tag :utility_payment_log + test "get! utility payment log" do + user = StarkBankTest.Credentials.project() + log = StarkBank.UtilityPayment.Log.query!(user) + |> Enum.take(1) + |> hd() + _log = StarkBank.UtilityPayment.Log.get!(user, log.id) + end +end diff --git a/test/utility_payment_test.exs b/test/utility_payment_test.exs new file mode 100644 index 0000000..0f16a2c --- /dev/null +++ b/test/utility_payment_test.exs @@ -0,0 +1,102 @@ +defmodule StarkBankTest.UtilityPayment do + use ExUnit.Case + + @tag :utility_payment + test "create utility payment" do + user = StarkBankTest.Credentials.project() + {:ok, payments} = StarkBank.UtilityPayment.create(user, [example_payment()]) + payment = payments |> hd + assert !is_nil(payment) + end + + @tag :utility_payment + test "create! utility payment" do + user = StarkBankTest.Credentials.project() + payment = StarkBank.UtilityPayment.create!(user, [example_payment()]) |> hd + assert !is_nil(payment) + end + + @tag :utility_payment + test "query utility payment" do + user = StarkBankTest.Credentials.project() + StarkBank.UtilityPayment.query(user, limit: 101) + |> Enum.take(200) + |> (fn list -> assert length(list) <= 101 end).() + end + + @tag :utility_payment + test "query! utility payment" do + user = StarkBankTest.Credentials.project() + StarkBank.UtilityPayment.query!(user, limit: 101) + |> Enum.take(200) + |> (fn list -> assert length(list) <= 101 end).() + end + + @tag :utility_payment + test "get utility payment" do + user = StarkBankTest.Credentials.project() + payment = StarkBank.UtilityPayment.query!(user) + |> Enum.take(1) + |> hd() + {:ok, _payment} = StarkBank.UtilityPayment.get(user, payment.id) + end + + @tag :utility_payment + test "get! utility payment" do + user = StarkBankTest.Credentials.project() + payment = StarkBank.UtilityPayment.query!(user) + |> Enum.take(1) + |> hd() + _payment = StarkBank.UtilityPayment.get!(user, payment.id) + end + + @tag :utility_payment + test "pdf utility payment" do + user = StarkBankTest.Credentials.project() + payment = StarkBank.UtilityPayment.query!(user, status: "success") + |> Enum.take(1) + |> hd() + {:ok, _pdf} = StarkBank.UtilityPayment.pdf(user, payment.id) + end + + @tag :utility_payment + test "pdf! utility payment" do + user = StarkBankTest.Credentials.project() + payment = StarkBank.UtilityPayment.query!(user, status: "success") + |> Enum.take(1) + |> hd() + pdf = StarkBank.UtilityPayment.pdf!(user, payment.id) + file = File.open!("utility-payment.pdf", [:write]) + IO.binwrite(file, pdf) + File.close(file) + end + + @tag :utility_payment + test "delete utility payment" do + user = StarkBankTest.Credentials.project() + payment = StarkBank.UtilityPayment.create!(user, [example_payment(2)]) |> hd + {:ok, deleted_payment} = StarkBank.UtilityPayment.delete(user, payment.id) + assert !is_nil(deleted_payment) + end + + @tag :utility_payment + test "delete! utility payment" do + user = StarkBankTest.Credentials.project() + payment = StarkBank.UtilityPayment.create!(user, [example_payment(2)]) |> hd + deleted_payment = StarkBank.UtilityPayment.delete!(user, payment.id) + assert !is_nil(deleted_payment) + end + + defp example_payment(schedule \\ 0) do + bar_code_core = :crypto.rand_uniform(100, 100000) + |> to_string + |> String.pad_leading(11, "0") + + %StarkBank.UtilityPayment{ + bar_code: "8366" <> bar_code_core <> "01380074119002551100010601813", + scheduled: Date.utc_today() |> Date.add(schedule), + description: "pagando a conta", + tags: ["my", "precious", "tags"], + } + end +end diff --git a/test/webhook_test.exs b/test/webhook_test.exs new file mode 100644 index 0000000..ca05c18 --- /dev/null +++ b/test/webhook_test.exs @@ -0,0 +1,45 @@ +defmodule StarkBankTest.Webhook do + use ExUnit.Case + + @tag :webhook + test "create, get and delete webhook" do + user = StarkBankTest.Credentials.project() + {:ok, create_webhook} = StarkBank.Webhook.create( + user, + "https://webhook.site/60e9c18e-4b5c-4369-bda1-ab5fcd8e1b29", + ["transfer", "boleto", "boleto-payment", "utility-payment"] + ) + {:ok, get_webhook} = StarkBank.Webhook.get(user, create_webhook.id) + {:ok, delete_webhook} = StarkBank.Webhook.delete(user, get_webhook.id) + assert !is_nil(delete_webhook) + end + + @tag :webhook + test "create!, get! and delete! webhook" do + user = StarkBankTest.Credentials.project() + create_webhook = StarkBank.Webhook.create!( + user, + "https://webhook.site/60e9c18e-4b5c-4369-bda1-ab5fcd8e1b29", + ["transfer", "boleto", "boleto-payment", "utility-payment"] + ) + get_webhook = StarkBank.Webhook.get!(user, create_webhook.id) + delete_webhook = StarkBank.Webhook.delete!(user, get_webhook.id) + assert !is_nil(delete_webhook) + end + + @tag :webhook + test "query webhook" do + user = StarkBankTest.Credentials.project() + StarkBank.Webhook.query(user, limit: 5) + |> Enum.take(5) + |> (fn list -> assert length(list) <= 5 end).() + end + + @tag :webhook + test "query! webhook" do + user = StarkBankTest.Credentials.project() + StarkBank.Webhook.query!(user, limit: 5) + |> Enum.take(5) + |> (fn list -> assert length(list) <= 5 end).() + end +end