diff --git a/.gitignore b/.gitignore index e3200e0..7695d8c 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,8 @@ /test/tmp/ /test/version_tmp/ /tmp/ +/bin/ +/keys/ # Used by dotenv library to load environment variables. # .env @@ -54,3 +56,5 @@ build-iPhoneSimulator/ # Used by RuboCop. Remote config files pulled in from inherit_from directive. # .rubocop-https?--* +.rubocop_todo.yml +.rubocop.yml diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..c99d2e7 --- /dev/null +++ b/.rspec @@ -0,0 +1 @@ +--require spec_helper 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/Gemfile b/Gemfile new file mode 100644 index 0000000..6bb01b0 --- /dev/null +++ b/Gemfile @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +source('https://rubygems.org') + +git_source(:github) { |repo_name| 'https://github.com/starkbank/sdk-ruby' } + +# test framework +gem('rspec', '~> 3') +# StarkBank ECDSA lib +gem('starkbank-ecdsa', '~> 0.0.2') +# Linter +gem('rubocop', require: false) diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..cf685df --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,46 @@ +GEM + remote: https://rubygems.org/ + specs: + ast (2.4.0) + diff-lcs (1.3) + jaro_winkler (1.5.4) + parallel (1.19.1) + parser (2.7.1.0) + ast (~> 2.4.0) + rainbow (3.0.0) + rexml (3.2.4) + rspec (3.9.0) + rspec-core (~> 3.9.0) + rspec-expectations (~> 3.9.0) + rspec-mocks (~> 3.9.0) + rspec-core (3.9.1) + rspec-support (~> 3.9.1) + rspec-expectations (3.9.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.9.0) + rspec-mocks (3.9.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.9.0) + rspec-support (3.9.2) + rubocop (0.81.0) + jaro_winkler (~> 1.5.1) + parallel (~> 1.10) + parser (>= 2.7.0.1) + rainbow (>= 2.2.2, < 4.0) + rexml + ruby-progressbar (~> 1.7) + unicode-display_width (>= 1.4.0, < 2.0) + ruby-progressbar (1.10.1) + starkbank-ecdsa (0.0.2) + unicode-display_width (1.7.0) + +PLATFORMS + ruby + +DEPENDENCIES + rspec (~> 3) + rubocop + starkbank-ecdsa (~> 0.0.2) + +BUNDLED WITH + 1.17.3 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9f8a782 --- /dev/null +++ b/LICENSE @@ -0,0 +1,16 @@ +MIT License + +Copyright 2020 Stark Bank S.A. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index ae84a3a..557f765 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,866 @@ -# sdk-ruby -SDK to facilitate Ruby integrations with the Stark Bank API +# Stark Bank Ruby SDK Beta + +Welcome to the Stark Bank Ruby SDK! This tool is made for Ruby +developers who want to easily integrate with our API. +This SDK version is compatible with the Stark Bank API v2. + +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! + +## Supported Ruby Versions + +This library supports the following Ruby versions: + +* Ruby 2.3+ + +## 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 + +1.1 To install the package with gem, run: + +```sh +gem install starkbank +``` + +1.2 Or just add this to your Gemfile: + +```sh +gem('starkbank', '~> 0.1.0') +``` + +### 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: + +```ruby +require('starkbank') + +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: + +```ruby +require('starkbank') + +# 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----- +' + +project = StarkBank::Project.new( + environment: 'sandbox', + id: '5656565656565656', + private_key: private_key_content +) +``` + +NOTE 1: Never hard-code your private key. Get it from an environment variable or an encrypted database. + +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. + +There are two ways to inform the user to the SDK: + +4.1 Passing the user as argument in all functions: + +```ruby +require('starkbank') + +balance = StarkBank::Balance.get(user: project) +``` + +4.2 Set it as a default user in the SDK: + +```ruby +require('starkbank') + +StarkBank.user = project + +balance = StarkBank::Balance.get() +``` + +Just select the way of passing the project user that is more convenient to you. +On all following examples we will assume a default user has been set. + +## 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, check out +the function or class docstring to get more info or go straight to our [API docs]. + +### Get balance + +To know how much money you have in your workspace, run: + +```ruby +require('starkbank') + +balance = StarkBank::Balance.get() + +puts balance +``` + +### Create boletos + +You can create boletos to charge customers or to receive money from accounts +you have in other banks. + +```ruby +require('starkbank') + +boletos = StarkBank::Boleto.create( + [ + StarkBank::Boleto.new( + 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: Time.now + 24 * 3600, + fine: 5, # 5% + interest: 2.5 # 2.5% per month + ) + ] +) + +boletos.each do |boleto| + puts boleto +end +``` + +### Get boleto + +After its creation, information on a boleto may be retrieved by passing its id. +Its status indicates whether it's been paid. + +```ruby +require('starkbank') + +boleto = StarkBank::Boleto.get('6365512502083584') + +puts boleto +``` + +### Get boleto PDF + +After its creation, a boleto PDF may be retrieved by passing its id. + +```ruby +require('starkbank') + +pdf = StarkBank::Boleto.pdf('6365512502083584') + +File.open('boleto.pdf', 'w') { |file| file.write(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 + +You can also cancel a boleto by its id. +Note that this is not possible if it has been processed already. + +```ruby +require('starkbank') + +boleto = StarkBank::Boleto.delete('5155165527080960') + +puts boleto +``` + +### Query boletos + +You can get a list of created boletos given some filters. + +```ruby +require('starkbank') +require('date') + +boletos = StarkBank::Boleto.query( + after: '2020-01-01', + before: Date.today - 1 +) + +boletos.each do |boleto| + puts boleto +end +``` + +### Query boleto logs + +Logs are pretty important to understand the life cycle of a boleto. + +```ruby +require('starkbank') + +logs = StarkBank::Boleto::Log.query(limit: 150) + +logs.each do |log| + puts log +end +``` + +### Get a boleto log + +You can get a single log by its id. + +```ruby +require('starkbank') + +log = StarkBank::Boleto::Log.get('5155165527080960') + +puts log +``` + +### Create transfers + +You can also create transfers in the SDK (TED/DOC). + +```ruby +require('starkbank') + +transfers = StarkBank::Transfer.create( + [ + StarkBank::Transfer.new( + amount: 100, + bank_code: '033', + branch_code: '0001', + account_number: '10000-0', + tax_id: '012.345.678-90', + name: 'Tony Stark', + tags: %w[iron suit] + ), + StarkBank::Transfer.new( + amount: 200, + bank_code: '341', + branch_code: '1234', + account_number: '123456-7', + tax_id: '012.345.678-90', + name: 'Jon Snow', + tags: [] + ) + ] +) + +transfers.each do |transfer| + puts transfer +end +``` + +### Query transfers + +You can query multiple transfers according to filters. + +```ruby +require('starkbank') + +transfers = StarkBank::Transfer.query( + after: '2020-01-01', + before: '2020-04-01' +) + +transfers.each do |transfer| + puts transfer.name +end +``` + +### Get transfer + +To get a single transfer by its id, run: + +```ruby +require('starkbank') + +transfer = StarkBank::Transfer.get('4804196796727296') + +puts transfer +``` + +### Get transfer PDF + +A transfer PDF may also be retrieved by passing its id. +This operation is only valid if the transfer status is "processing" or "success". + +```ruby +require('starkbank') + +pdf = StarkBank::Transfer.pdf('4832343898456064') + +File.open('transfer.pdf', 'w') { |file| file.write(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. + +### Query transfer logs + +You can query transfer logs to better understand transfer life cycles. + +```ruby +require('starkbank') + +logs = StarkBank::Transfer::Log.query(limit: 50) + +logs.each do |log| + puts log +end +``` + +### Get a transfer log + +You can also get a specific log by its id. + +```ruby +require('starkbank') + +log = StarkBank::Transfer::Log.get('5554732936462336') + +puts log +``` + +### Pay a boleto + +Paying a boleto is also simple. + +```ruby +require('starkbank') + +payments = StarkBank::BoletoPayment.create( + [ + StarkBank::BoletoPayment.new( + line: '34191.09008 64694.197308 71444.640008 1 97230000028900', + tax_id: '012.345.678-90', + scheduled: Time.now, + description: 'take my money', + tags: %w[take my money] + ), + StarkBank::BoletoPayment.new( + bar_code: '34191966100000145001090064694017307144464000', + tax_id: '012.345.678-90', + scheduled: Time.now + 24 * 3600, + description: 'take my money one more time', + tags: %w[again] + ) + ] +) + +payments.each do |payment| + puts payment +end +``` + +### Get boleto payment + +To get a single boleto payment by its id, run: + +```ruby +require('starkbank') + +payment = StarkBank::BoletoPayment.get('6591161082839040') + +puts payment +``` + +### Get boleto payment PDF + +After its creation, a boleto payment PDF may be retrieved by passing its id. + +```ruby +require('starkbank') + +pdf = StarkBank::BoletoPayment.pdf('6591161082839040') + +File.open('boleto_payment.pdf', 'w') { |file| file.write(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. + +```ruby +require('starkbank') + +payment = StarkBank::BoletoPayment.delete('5155165527080960') + +puts payment +``` + +### Query boleto payments + +You can search for boleto payments using filters. + +```ruby +require('starkbank') + +payments = StarkBank::BoletoPayment.query( + tags: %w[company_1 company_2] +) + +payments.each do |payment| + puts payment +end +``` + +### Query boleto payment logs + +Searches are also possible with boleto payment logs: + +```ruby +require('starkbank') + +logs = StarkBank::BoletoPayment::Log.query( + payment_ids: %w[5391730421530624 6324396973096960] +) + +logs.each do |log| + puts log +end +``` + + +### Get boleto payment log + +You can also get a boleto payment log by specifying its id. + +```ruby +require('starkbank') + +log = StarkBank::BoletoPayment::Log.get('5155165527080960') + +puts log +``` + +### Create utility payment + +It's also simple to pay utility bills (such as electricity and water bills) in the SDK. + +```ruby +require('starkbank') + +payments = StarkBank::UtilityPayment.create( + [ + StarkBank::UtilityPayment.new( + line: '83680000001 7 08430138003 0 71070987611 8 00041351685 7', + scheduled: Time.now, + description: 'take my money', + tags: %w[take my money], + ), + StarkBank::UtilityPayment.new( + bar_code: '83600000001522801380037107172881100021296561', + scheduled: Time.now + 3 * 24 * 3600, + description: 'take my money one more time', + tags: %w[again], + ) + ] +) + +payments.each do |payment| + puts payment +end +``` + +### Query utility payments + +To search for utility payments using filters, run: + +```ruby +require('starkbank') + +payments = StarkBank::UtilityPayment.query( + tags: %w[electricity gas] +) + +payments.each do |payment| + puts payment +end +``` + +### Get utility payment + +You can get a specific bill by its id: + +```ruby +require('starkbank') + +payment = StarkBank::UtilityPayment.get('6258964706623488') + +puts payment +``` + +### Get utility payment PDF + +After its creation, a utility payment PDF may also be retrieved by passing its id. + +```ruby +require('starkbank') + +pdf = StarkBank::UtilityPayment.pdf('5155165527080960') + +File.open('electricity_payment.pdf', 'w') { |file| file.write(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 utility payment + +You can also cancel a utility payment by its id. +Note that this is not possible if it has been processed already. + +```ruby +require('starkbank') + +payment = StarkBank::UtilityPayment.delete('6258964706623489') + +puts payment +``` + +### Query utility payment logs + +You can search for payments by specifying filters. Use this to understand the +bills life cycles. + +```ruby +require('starkbank') + +logs = StarkBank::UtilityPayment::Log.query( + payment_ids: %w[102893710982379182 92837912873981273] +) + +logs.each do |log| + puts log +end +``` + +### Get utility bill payment log + +If you want to get a specific payment log by its id, just run: + +```ruby +require('starkbank') + +log = StarkBank::UtilityPayment::Log.get('4922041111150592') + +puts log +``` + +### Create transactions + +To send money between Stark Bank accounts, you can create transactions: + +```ruby +require('starkbank') + +transactions = StarkBank::Transaction.create( + [ + StarkBank::Transaction.new( + amount: 100, # (R$ 1.00) + receiver_id: '5083989094170624', + description: 'Transaction to dear provider', + external_id: '123456', # so we can block anything you send twice by mistake + tags: %w[provider] + ), + StarkBank::Transaction.new( + amount: 234, # (R$ 2.34) + receiver_id: '5083989094170624', + description: 'Transaction to the other provider', + external_id: '123457', # so we can block anything you send twice by mistake + tags: %w[provider] + ) + ] +) + +transactions.each do |transaction| + puts transaction +end +``` + +### 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. + +```ruby +require('starkbank') + +transactions = StarkBank::Transaction.query( + after: '2020-01-01', + before: '2020-03-01' +) + +transactions.each do |transaction| + puts transaction +end +``` + +### Get transaction + +You can get a specific transaction by its id: + +```ruby +require('starkbank') + +transaction = StarkBank::Transaction.get('5764045667827712') + +puts transaction +``` + +### Create webhook subscription + +To create a webhook subscription and be notified whenever an event occurs, run: + +```ruby +require('starkbank') + +webhook = StarkBank::Webhook.create( + url: 'https://webhook.site/dd784f26-1d6a-4ca6-81cb-fda0267761ec', + subscriptions: %w[transfer boleto boleto-payment utility-payment] +) + +puts webhook +``` + +### Query webhooks + +To search for registered webhooks, run: + +```ruby +require('starkbank') + +webhooks = StarkBank::Webhook.query() + +webhooks.each do |webhook| + puts webhook +end +``` + +### Get webhook + +You can get a specific webhook by its id. + +```ruby +require('starkbank') + +webhook = StarkBank::Webhook.get('10827361982368179') + +puts webhook +``` + +### Delete webhook + +You can also delete a specific webhook by its id. + +```ruby +require('starkbank') + +webhook = StarkBank::Webhook.delete('10827361982368179') + +puts webhook +``` + +### Process webhook events + +It's easy to process events that arrived in your webhook. Remember to pass the +signature header so the SDK can make sure it's really StarkBank that sent you +the event. + +```ruby +require('starkbank') + +response = listen() # this is the method you made to get the events posted to your webhook + +event = StarkBank::Event.parse(content: response.content, signature: response.headers['Digital-Signature']) + +if event.subscription == 'transfer' + puts event.log.transfer +elsif event.subscription == 'boleto' + puts event.log.boleto +elsif event.subscription == 'boleto-payment' + puts event.log.payment +elsif event.subscription == 'utility-payment' + puts event.log.payment +end +``` + +### Query webhook events + +To search for webhook events, run: + +```ruby +require('starkbank') + +events = StarkBank::Event.query(after: '2020-03-20', is_delivered: false) + +events.each do |event| + puts event +end +``` + +### Get webhook event + +You can get a specific webhook event by its id. + +```ruby +require('starkbank') + +event = StarkBank::Event.get('4828869076975616') + +puts event +``` + +### Delete webhook event + +You can also delete a specific webhook event by its id. + +```ruby +require('starkbank') + +event = StarkBank::Event.delete('4828869076975616') + +puts event +``` + +### 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`. + +```ruby +require('starkbank') + +event = StarkBank::Event.update('5892075044208640', is_delivered: true) + +puts event +``` + +## Handling errors + +The SDK may raise one of four types of errors: __InputErrors__, __InternalServerError__, __UnknownError__, __InvalidSignatureError__ + +__InputErrors__ will be raised whenever the API detects an error in your request (status code 400). +If you catch such an error, you can get its elements to verify each of the +individual errors that were detected in your request by the API. +For example: + +```ruby +require('starkbank') + +begin + transactions = StarkBank::Transaction.create( + [ + StarkBank::Transaction.new( + amount: 99999999999999, + receiver_id: '1029378109327810', + description: '.', + external_id: '12345', + tags: %w[provider] + ) + ] + ) +rescue StarkBank::Error::InputErrors => e + e.errors.each do |error| + puts error.code + puts error.message + end +end +``` + +__InternalServerError__ will be raised if the API runs 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. + +__UnknownError__ will be raised if a request encounters an error that is +neither __InputErrors__ nor an __InternalServerError__, such as connectivity problems. + +__InvalidSignatureError__ will be raised specifically by StarkBank::Event.parse() +when the provided content and signature do not check out with the Stark Bank public +key. diff --git a/lib/boleto/boleto.rb b/lib/boleto/boleto.rb new file mode 100644 index 0000000..805df7a --- /dev/null +++ b/lib/boleto/boleto.rb @@ -0,0 +1,195 @@ +# frozen_string_literal: true + +require_relative('../utils/resource') +require_relative('../utils/rest') +require_relative('../utils/checks') + +module StarkBank + # # Boleto object + # + # When you initialize a Boleto, the entity will not be automatically + # sent to the Stark Bank API. The 'create' function sends the objects + # to the Stark Bank API and returns the list of created objects. + # + # ## Parameters (required): + # - amount [integer]: Boleto value in cents. Minimum = 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 dictionaries, default nil]: list of dictionaries with "text":string and (optional) "amount":int 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: DateTime.new(2020, 3, 10, 10, 30, 0, 0) + class Boleto < StarkBank::Utils::Resource + attr_reader :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 + def initialize( + amount:, name:, tax_id:, street_line_1:, street_line_2:, district:, city:, state_code:, zip_code:, + due: nil, fine: nil, interest: nil, overdue_limit: nil, tags: nil, descriptions: nil, id: nil, fee: nil, line: nil, + bar_code: nil, status: nil, created: nil + ) + super(id) + @amount = amount + @name = name + @tax_id = tax_id + @street_line_1 = street_line_1 + @street_line_2 = street_line_2 + @district = district + @city = city + @state_code = state_code + @zip_code = zip_code + @due = due + @fine = fine + @interest = interest + @overdue_limit = overdue_limit + @tags = tags + @descriptions = descriptions + @fee = fee + @line = line + @bar_code = bar_code + @status = status + @created = StarkBank::Utils::Checks.check_datetime(created) + end + + # # Create Boletos + # + # Send a list of Boleto objects for creation in the Stark Bank API + # + # ## Parameters (required): + # - boletos [list of Boleto objects]: list of Boleto objects to be created in the API + # + # ## Parameters (optional): + # - user [Project object]: Project object. Not necessary if StarkBank.user was set before function call + # + # ## Return: + # - list of Boleto objects with updated attributes + def self.create(boletos, user: nil) + StarkBank::Utils::Rest.post(entities: boletos, user: user, **resource) + end + + # # Retrieve a specific Boleto + # + # Receive a single Boleto object previously created in the Stark Bank API by passing its id + # + # ## Parameters (required): + # - id [string]: object unique id. ex: "5656565656565656" + # + # ## Parameters (optional): + # - user [Project object]: Project object. Not necessary if StarkBank.user was set before function call + # + # ## Return: + # - Boleto object with updated attributes + def self.get(id, user: nil) + StarkBank::Utils::Rest.get_id(id: id, user: user, **resource) + end + + # # Retrieve a specific Boleto pdf file + # + # Receive a single Boleto pdf file generated in the Stark Bank API by passing its id. + # + # ## Parameters (required): + # - id [string]: object unique id. ex: "5656565656565656" + # + # ## Parameters (optional): + # - user [Project object]: Project object. Not necessary if StarkBank.user was set before function call + # + # ## Return: + # - Boleto pdf file + def self.pdf(id, user: nil) + StarkBank::Utils::Rest.get_pdf(id: id, user: user, **resource) + end + + # # Retrieve Boletos + # + # Receive a generator of Boleto objects previously created in the Stark Bank API + # + # ## Parameters (optional): + # - limit [integer, default nil]: maximum number of objects to be retrieved. Unlimited if nil. ex: 35 + # - after [Date, default nil] date filter for objects created only after specified date. ex: Date.new(2020, 3, 10) + # - before [Date, default nil] date filter for objects only before specified date. ex: Date.new(2020, 3, 10) + # - status [string, default nil]: filter for status of retrieved objects. ex: 'paid' or 'registered' + # - tags [list of strings, default nil]: tags to filter retrieved objects. ex: ['tony', 'stark'] + # - ids [list of strings, default nil]: list of ids to filter retrieved objects. ex: ['5656565656565656', '4545454545454545'] + # - user [Project object, default nil]: Project object. Not necessary if StarkBank.user was set before function call + # + # ## Return: + # - generator of Boleto objects with updated attributes + def self.query(limit: nil, after: nil, before: nil, status: nil, tags: nil, ids: nil, user: nil) + after = StarkBank::Utils::Checks.check_date(after) + before = StarkBank::Utils::Checks.check_date(before) + StarkBank::Utils::Rest.get_list( + limit: limit, + after: after, + before: before, + status: status, + tags: tags, + ids: ids, + user: user, + **resource + ) + end + + # # Delete a Boleto entity + # + # Delete a Boleto entity previously created in the Stark Bank API + # + # ## Parameters (required): + # - id [string]: Boleto unique id. ex: "5656565656565656" + # + # ## Parameters (optional): + # - user [Project object]: Project object. Not necessary if StarkBank.user was set before function call + # + # ## Return: + # - deleted Boleto with updated attributes + def self.delete(id, user: nil) + StarkBank::Utils::Rest.delete_id(id: id, user: user, **resource) + end + + def self.resource + { + resource_name: 'Boleto', + resource_maker: proc { |json| + Boleto.new( + 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'], + 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'] + ) + } + } + end + end +end diff --git a/lib/boleto/log.rb b/lib/boleto/log.rb new file mode 100644 index 0000000..5b4c364 --- /dev/null +++ b/lib/boleto/log.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +require_relative('../utils/resource') +require_relative('../utils/rest') +require_relative('../utils/checks') +require_relative('boleto') + +module StarkBank + class Boleto + # # Boleto::Log object + # + # 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: DateTime.new(2020, 3, 10, 10, 30, 0, 0) + class Log < StarkBank::Utils::Resource + attr_reader :id, :created, :type, :errors, :boleto + def initialize(id:, created:, type:, errors:, boleto:) + super(id) + @type = type + @errors = errors + @boleto = boleto + @created = StarkBank::Utils::Checks.check_datetime(created) + end + + # # Retrieve a specific Log + # + # Receive a single Log object previously created by the Stark Bank API by passing its id + # + # ## Parameters (required): + # - id [string]: object unique id. ex: "5656565656565656" + # + # ## Parameters (optional): + # - user [Project object]: Project object. Not necessary if StarkBank.user was set before function call + # + # ## Return: + # - Log object with updated attributes + def self.get(id, user: nil) + StarkBank::Utils::Rest.get_id(id: id, user: user, **resource) + end + + # # Retrieve Logs + # + # Receive a generator of Log objects previously created in the Stark Bank API + # + # ## Parameters (optional): + # - limit [integer, default nil]: maximum number of objects to be retrieved. Unlimited if nil. ex: 35 + # - after [Date, default nil] date filter for objects created only after specified date. ex: Date.new(2020, 3, 10) + # - before [Date, default nil] date filter for objects only before specified date. ex: Date.new(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'] + # - user [Project object, default nil]: Project object. Not necessary if StarkBank.user was set before function call + # + # ## Return: + # - list of Log objects with updated attributes + def self.query(limit: nil, after: nil, before: nil, types: nil, boleto_ids: nil, user: nil) + after = StarkBank::Utils::Checks.check_date(after) + before = StarkBank::Utils::Checks.check_date(before) + StarkBank::Utils::Rest.get_list( + limit: limit, + after: after, + before: before, + types: types, + boleto_ids: boleto_ids, + user: user, + **resource + ) + end + + def self.resource + boleto_maker = StarkBank::Boleto.resource[:resource_maker] + { + resource_name: 'BoletoLog', + resource_maker: proc { |json| + Log.new( + id: json['id'], + created: json['created'], + type: json['type'], + errors: json['errors'], + boleto: StarkBank::Utils::API.from_api_json(boleto_maker, json['boleto']) + ) + } + } + end + end + end +end diff --git a/lib/error.rb b/lib/error.rb new file mode 100644 index 0000000..c50ee4c --- /dev/null +++ b/lib/error.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require('json') + +module StarkBank + module Error + class Error < StandardError + attr_reader :code, :message + def initialize(code, message) + @code = code + @message = message + super("#{code}: #{message}") + end + end + + class InputErrors < StandardError + attr_reader :errors + def initialize(content) + errors = [] + content.each do |error| + errors << Error.new(error['code'], error['message']) + end + @errors = errors + + super(content.to_json) + end + end + + class InternalServerError < StandardError + def initialize(message = 'Houston, we have a problem.') + super(message) + end + end + + class UnknownError < StandardError + def initialize(message) + super("Unknown exception encountered: #{message}") + end + end + + class InvalidSignatureError < StandardError + end + end +end diff --git a/lib/key.rb b/lib/key.rb new file mode 100644 index 0000000..be96596 --- /dev/null +++ b/lib/key.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require('fileutils') +require('starkbank-ecdsa') + +module StarkBank + module Key + # # Generate a new key pair + # Generates a secp256k1 ECDSA private/public key pair to be used in the API + # authentications + # + # ## Parameters (optional): + # - path [string]: path to save the keys .pem files. No files will be saved if this parameter isn't provided + # + # ## Return: + # - private and public key pems + def self.create(path = nil) + private_key = PrivateKey.new + public_key = private_key.publicKey + + private_key_pem = private_key.toPem + public_key_pem = public_key.toPem + + unless path.nil? + FileUtils.mkdir_p(path) + File.write(File.join(path, 'private.pem'), private_key_pem) + File.write(File.join(path, 'public.pem'), public_key_pem) + end + + [private_key_pem, public_key_pem] + end + end +end diff --git a/lib/ledger/balance.rb b/lib/ledger/balance.rb new file mode 100644 index 0000000..e5998c9 --- /dev/null +++ b/lib/ledger/balance.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require_relative('../utils/resource') +require_relative('../utils/rest') +require_relative('../utils/checks') + +module StarkBank + # # Balance object + # + # The Balance object 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 available information. + # + # ## 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: DateTime.new(2020, 3, 10, 10, 30, 0, 0) + class Balance < StarkBank::Utils::Resource + attr_reader :amount, :currency, :updated + def initialize(amount:, currency:, updated:, id:) + super(id) + @amount = amount + @currency = currency + @updated = StarkBank::Utils::Checks.check_datetime(updated) + end + + # # Retrieve the Balance object + # + # Receive the Balance object linked to your workspace in the Stark Bank API + # + # ## Parameters (optional): + # - user [Project object]: Project object. Not necessary if StarkBank.user was set before function call + # + # ## Return: + # - Balance object with updated attributes + def self.get(user: nil) + StarkBank::Utils::Rest.get_list(user: user, **resource).next + end + + class << self + private + + def resource + { + resource_name: 'Balance', + resource_maker: proc { |json| + Balance.new( + amount: json['amount'], + currency: json['currency'], + updated: json['updated'], + id: json['id'] + ) + } + } + end + end + end +end diff --git a/lib/ledger/transaction.rb b/lib/ledger/transaction.rb new file mode 100644 index 0000000..1bb9254 --- /dev/null +++ b/lib/ledger/transaction.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +require_relative('../utils/resource') +require_relative('../utils/rest') +require_relative('../utils/checks') + +module StarkBank + # # Transaction object + # + # 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 objects + # to the Stark Bank API and returns the list of created objects. + # + # ## 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): + # - sender_id [string]: unique id of the sending workspace. ex: '5656565656565656' + # - source [string, default nil]: locator of the entity that generated the transaction. ex: 'charge/1827351876292', 'transfer/92873912873/chargeback' + # - id [string, default nil]: unique id returned when Transaction is created. ex: '7656565656565656' + # - fee [integer, default nil]: fee charged when transfer is created. ex: 200 (= R$ 2.00) + # - created [DateTime, default nil]: creation datetime for the boleto. ex: DateTime.new(2020, 3, 10, 10, 30, 0, 0) + class Transaction < StarkBank::Utils::Resource + attr_reader :amount, :description, :external_id, :receiver_id, :sender_id, :tags, :id, :fee, :created, :source + def initialize(amount:, description:, external_id:, receiver_id:, sender_id: nil, tags: nil, id: nil, fee: nil, created: nil, source: nil) + super(id) + @amount = amount + @description = description + @external_id = external_id + @receiver_id = receiver_id + @sender_id = sender_id + @tags = tags + @fee = fee + @source = source + @created = StarkBank::Utils::Checks.check_datetime(created) + end + + # # Create Transactions + # + # Send a list of Transaction objects for creation in the Stark Bank API + # + # ## Parameters (required): + # - transactions [list of Transaction objects]: list of Transaction objects to be created in the API + # + # ## Parameters (optional): + # - user [Project object]: Project object. Not necessary if StarkBank.user was set before function call + # + # ## Return: + # - list of Transaction objects with updated attributes + def self.create(transactions, user: nil) + StarkBank::Utils::Rest.post(entities: transactions, user: user, **resource) + end + + # # Retrieve a specific Transaction + # + # Receive a single Transaction object previously created in the Stark Bank API by passing its id + # + # ## Parameters (required): + # - id [string]: object unique id. ex: "5656565656565656" + # + # ## Parameters (optional): + # - user [Project object]: Project object. Not necessary if StarkBank.user was set before function call + # + # ## Return: + # - Transaction object with updated attributes + def self.get(id, user: nil) + StarkBank::Utils::Rest.get_id(id: id, user: user, **resource) + end + + # # Retrieve Transactions + # + # Receive a generator of Transaction objects previously created in the Stark Bank API + # + # ## Parameters (optional): + # - limit [integer, default nil]: maximum number of objects to be retrieved. Unlimited if nil. ex: 35 + # - after [Date, default nil] date filter for objects created only after specified date. ex: Date.new(2020, 3, 10) + # - before [Date, default nil] date filter for objects created only before specified date. ex: Date.new(2020, 3, 10) + # - external_ids [list of strings, default nil]: list of external ids to filter retrieved objects. ex: ['5656565656565656', '4545454545454545'] + # - user [Project object, default nil]: Project object. Not necessary if StarkBank.user was set before function call + # + # ## Return: + # - generator of Transaction objects with updated attributes + def self.query(limit: nil, after: nil, before: nil, external_ids: nil, user: nil) + after = StarkBank::Utils::Checks.check_date(after) + before = StarkBank::Utils::Checks.check_date(before) + StarkBank::Utils::Rest.get_list(user: user, limit: limit, after: after, before: before, external_ids: external_ids, **resource) + end + + def self.resource + { + resource_name: 'Transaction', + resource_maker: proc { |json| + Transaction.new( + 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 +end diff --git a/lib/payment/boleto/boleto.rb b/lib/payment/boleto/boleto.rb new file mode 100644 index 0000000..44c4b28 --- /dev/null +++ b/lib/payment/boleto/boleto.rb @@ -0,0 +1,162 @@ +# frozen_string_literal: true + +require_relative('../../utils/resource') +require_relative('../../utils/rest') +require_relative('../../utils/checks') + +module StarkBank + # # BoletoPayment object + # + # When you initialize a BoletoPayment, the entity will not be automatically + # created in the Stark Bank API. The 'create' function sends the objects + # to the Stark Bank API and returns the list of created objects. + # + # ## 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: Date.new(2020, 3, 10) + # - 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 boleto payment is created. ex: 200 (= R$ 2.00) + # - created [DateTime, default nil]: creation datetime for the payment. ex: DateTime.new(2020, 3, 10, 10, 30, 0, 0) + class BoletoPayment < StarkBank::Utils::Resource + attr_reader :tax_id, :description, :line, :bar_code, :scheduled, :tags, :id, :status, :amount, :fee, :created + def initialize(tax_id:, description:, line: nil, bar_code: nil, scheduled: nil, tags: nil, id: nil, status: nil, amount: nil, fee: nil, created: nil) + super(id) + @tax_id = tax_id + @description = description + @line = line + @bar_code = bar_code + @scheduled = StarkBank::Utils::Checks.check_datetime(scheduled) + @tags = tags + @status = status + @amount = amount + @fee = fee + @created = StarkBank::Utils::Checks.check_datetime(created) + end + + # # Create BoletoPayments + # + # Send a list of BoletoPayment objects for creation in the Stark Bank API + # + # ## Parameters (required): + # - payments [list of BoletoPayment objects]: list of BoletoPayment objects to be created in the API + # + # ## Parameters (optional): + # - user [Project object]: Project object. Not necessary if StarkBank.user was set before function call + # + # ## Return: + # - list of BoletoPayment objects with updated attributes + def self.create(payments, user: nil) + StarkBank::Utils::Rest.post(entities: payments, user: user, **resource) + end + + # # Retrieve a specific BoletoPayment + # + # Receive a single BoletoPayment object previously created by the Stark Bank API by passing its id + # + # ## Parameters (required): + # - id [string]: object unique id. ex: "5656565656565656" + # + # ## Parameters (optional): + # - user [Project object]: Project object. Not necessary if StarkBank.user was set before function call + # + # ## Return: + # - BoletoPayment object with updated attributes + def self.get(id, user: nil) + StarkBank::Utils::Rest.get_id(id: id, user: user, **resource) + end + + # # Retrieve a specific BoletoPayment pdf file + # + # 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): + # - id [string]: object unique id. ex: "5656565656565656" + # + # ## Parameters (optional): + # - user [Project object]: Project object. Not necessary if StarkBank.user was set before function call + # + # ## Return: + # - BoletoPayment pdf file + def self.pdf(id, user: nil) + StarkBank::Utils::Rest.get_pdf(id: id, user: user, **resource) + end + + # # Retrieve BoletoPayments + # + # Receive a generator of BoletoPayment objects previously created in the Stark Bank API + # + # ## Parameters (optional): + # - limit [integer, default nil]: maximum number of objects to be retrieved. Unlimited if nil. ex: 35 + # - after [Date, default nil] date filter for objects created only after specified date. ex: Date.new(2020, 3, 10) + # - before [Date, default nil] date filter for objects only before specified date. ex: Date.new(2020, 3, 10) + # - tags [list of strings, default nil]: tags to filter retrieved objects. ex: ['tony', 'stark'] + # - ids [list of strings, default nil]: list of strings to get specific entities by ids. ex: ['12376517623', '1928367198236'] + # - status [string, default nil]: filter for status of retrieved objects. ex: 'paid' + # - user [Project object, default nil]: Project object. Not necessary if StarkBank.user was set before function call + # + # ## Return: + # - generator of BoletoPayment objects with updated attributes + def self.query(limit: nil, after: nil, before: nil, tags: nil, ids: nil, status: nil, user: nil) + after = StarkBank::Utils::Checks.check_date(after) + before = StarkBank::Utils::Checks.check_date(before) + StarkBank::Utils::Rest.get_list( + user: user, + limit: limit, + after: after, + before: before, + tags: tags, + ids: ids, + status: status, + **resource + ) + end + + # # Delete a BoletoPayment entity + # + # Delete a BoletoPayment entity previously created in the Stark Bank API + # + # Parameters (required): + # - id [string]: BoletoPayment unique id. ex: "5656565656565656" + # Parameters (optional): + # - user [Project object]: Project object. Not necessary if StarkBank.user was set before function call + # Return: + # - deleted BoletoPayment with updated attributes + def self.delete(id, user: nil) + StarkBank::Utils::Rest.delete_id(id: id, user: user, **resource) + end + + def self.resource + { + resource_name: 'BoletoPayment', + resource_maker: proc { |json| + BoletoPayment.new( + id: json['id'], + tax_id: json['tax_id'], + description: json['description'], + line: json['line'], + bar_code: json['bar_code'], + scheduled: json['scheduled'], + tags: json['tags'], + status: json['status'], + amount: json['amount'], + fee: json['fee'], + created: json['created'] + ) + } + } + end + end +end diff --git a/lib/payment/boleto/log.rb b/lib/payment/boleto/log.rb new file mode 100644 index 0000000..3eec286 --- /dev/null +++ b/lib/payment/boleto/log.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +require_relative('../../utils/resource') +require_relative('../../utils/rest') +require_relative('../../utils/checks') +require_relative('boleto') + +module StarkBank + class BoletoPayment + # # BoletoPayment::Log object + # + # 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: DateTime.new(2020, 3, 10, 10, 30, 0, 0) + class Log < StarkBank::Utils::Resource + attr_reader :id, :created, :type, :errors, :payment + def initialize(id:, created:, type:, errors:, payment:) + super(id) + @type = type + @errors = errors + @payment = payment + @created = StarkBank::Utils::Checks.check_datetime(created) + end + + # # Retrieve a specific Log + # + # Receive a single Log object previously created by the Stark Bank API by passing its id + # + # ## Parameters (required): + # - id [string]: object unique id. ex: "5656565656565656" + # + # ## Parameters (optional): + # - user [Project object]: Project object. Not necessary if StarkBank.user was set before function call + # + # ## Return: + # - Log object with updated attributes + def self.get(id, user: nil) + StarkBank::Utils::Rest.get_id(id: id, user: user, **resource) + end + + # # Retrieve Logs + # + # Receive a generator of Log objects previously created in the Stark Bank API + # + # ## Parameters (optional): + # - limit [integer, default nil]: maximum number of objects to be retrieved. Unlimited if nil. ex: 35 + # - after [Date, default nil] date filter for objects created only after specified date. ex: Date.new(2020, 3, 10) + # - before [Date, default nil] date filter for objects only before specified date. ex: Date.new(2020, 3, 10) + # - types [list of strings, default nil]: filter retrieved objects by event types. ex: 'paid' or 'registered' + # - payment_ids [list of strings, default nil]: list of BoletoPayment ids to filter retrieved objects. ex: ['5656565656565656', '4545454545454545'] + # - user [Project object, default nil]: Project object. Not necessary if StarkBank.user was set before function call + # + # ## Return: + # - list of Log objects with updated attributes + def self.query(limit: nil, after: nil, before: nil, types: nil, payment_ids: nil, user: nil) + after = StarkBank::Utils::Checks.check_date(after) + before = StarkBank::Utils::Checks.check_date(before) + StarkBank::Utils::Rest.get_list( + limit: limit, + after: after, + before: before, + types: types, + payment_ids: payment_ids, + user: user, + **resource + ) + end + + def self.resource + payment_maker = StarkBank::BoletoPayment.resource[:resource_maker] + { + resource_name: 'BoletoPaymentLog', + resource_maker: proc { |json| + Log.new( + id: json['id'], + created: json['created'], + type: json['type'], + errors: json['errors'], + payment: StarkBank::Utils::API.from_api_json(payment_maker, json['payment']) + ) + } + } + end + end + end +end diff --git a/lib/payment/utility/log.rb b/lib/payment/utility/log.rb new file mode 100644 index 0000000..01fb0c5 --- /dev/null +++ b/lib/payment/utility/log.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +require_relative('../../utils/resource') +require_relative('../../utils/rest') +require_relative('../../utils/checks') +require_relative('utility') + +module StarkBank + class UtilityPayment + # # UtilityPayment::Log object + # + # 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: DateTime.new(2020, 3, 10, 10, 30, 0, 0) + class Log < StarkBank::Utils::Resource + attr_reader :id, :created, :type, :errors, :payment + def initialize(id:, created:, type:, errors:, payment:) + super(id) + @type = type + @errors = errors + @payment = payment + @created = StarkBank::Utils::Checks.check_datetime(created) + end + + # # Retrieve a specific Log + # + # Receive a single Log object previously created by the Stark Bank API by passing its id + # + # ## Parameters (required): + # - id [string]: object unique id. ex: "5656565656565656" + # + # ## Parameters (optional): + # - user [Project object]: Project object. Not necessary if StarkBank.user was set before function call + # + # ## Return: + # - Log object with updated attributes + def self.get(id, user: nil) + StarkBank::Utils::Rest.get_id(id: id, user: user, **resource) + end + + # # Retrieve Logs + # + # Receive a generator of Log objects previously created in the Stark Bank API + # + # ## Parameters (optional): + # - limit [integer, default nil]: maximum number of objects to be retrieved. Unlimited if nil. ex: 35 + # - after [Date, default nil] date filter for objects created only after specified date. ex: Date.new(2020, 3, 10) + # - before [Date, default nil] date filter for objects only before specified date. ex: Date.new(2020, 3, 10) + # - types [list of strings, default nil]: filter retrieved objects by event types. ex: 'paid' or 'registered' + # - payment_ids [list of strings, default nil]: list of UtilityPayment ids to filter retrieved objects. ex: ['5656565656565656', '4545454545454545'] + # - user [Project object, default nil]: Project object. Not necessary if StarkBank.user was set before function call + # + # ## Return: + # - list of Log objects with updated attributes + def self.query(limit: nil, after: nil, before: nil, types: nil, payment_ids: nil, user: nil) + after = StarkBank::Utils::Checks.check_date(after) + before = StarkBank::Utils::Checks.check_date(before) + StarkBank::Utils::Rest.get_list( + user: user, + limit: limit, + after: after, + before: before, + types: types, + payment_ids: payment_ids, + **resource + ) + end + + def self.resource + payment_maker = StarkBank::UtilityPayment.resource[:resource_maker] + { + resource_name: 'UtilityPaymentLog', + resource_maker: proc { |json| + Log.new( + id: json['id'], + created: json['created'], + type: json['type'], + errors: json['errors'], + payment: StarkBank::Utils::API.from_api_json(payment_maker, json['payment']) + ) + } + } + end + end + end +end diff --git a/lib/payment/utility/utility.rb b/lib/payment/utility/utility.rb new file mode 100644 index 0000000..de6cf94 --- /dev/null +++ b/lib/payment/utility/utility.rb @@ -0,0 +1,161 @@ +# frozen_string_literal: true + +require_relative('../../utils/resource') +require_relative('../../utils/rest') +require_relative('../../utils/checks') + +module StarkBank + # # UtilityPayment object + # + # When you initialize a UtilityPayment, the entity will not be automatically + # created in the Stark Bank API. The 'create' function sends the objects + # to the Stark Bank API and returns the list of created objects. + # + # ## 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: Date.new(2020, 3, 10) + # - 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 utility payment is created. ex: 200 (= R$ 2.00) + # - created [DateTime, default nil]: creation datetime for the payment. ex: DateTime.new(2020, 3, 10, 10, 30, 0, 0) + class UtilityPayment < StarkBank::Utils::Resource + attr_reader :description, :line, :bar_code, :tags, :scheduled, :id, :amount, :fee, :status, :created + def initialize(description:, line: nil, bar_code: nil, tags: nil, scheduled: nil, id: nil, amount: nil, fee: nil, status: nil, created: nil) + super(id) + @description = description + @line = line + @bar_code = bar_code + @tags = tags + @scheduled = StarkBank::Utils::Checks.check_datetime(scheduled) + @amount = amount + @fee = fee + @status = status + @created = StarkBank::Utils::Checks.check_datetime(created) + end + + # # Create UtilityPayments + # + # Send a list of UtilityPayment objects for creation in the Stark Bank API + # + # ## Parameters (required): + # - payments [list of UtilityPayment objects]: list of UtilityPayment objects to be created in the API + # + # ## Parameters (optional): + # - user [Project object]: Project object. Not necessary if StarkBank.user was set before function call + # + # ## Return: + # - list of UtilityPayment objects with updated attributes + def self.create(payments, user: nil) + StarkBank::Utils::Rest.post(entities: payments, user: user, **resource) + end + + # # Retrieve a specific UtilityPayment + # + # Receive a single UtilityPayment object previously created by the Stark Bank API by passing its id + # + # ## Parameters (required): + # - id [string]: object unique id. ex: "5656565656565656" + # + # ## Parameters (optional): + # - user [Project object]: Project object. Not necessary if StarkBank.user was set before function call + # + # ## Return: + # - UtilityPayment object with updated attributes + def self.get(id, user: nil) + StarkBank::Utils::Rest.get_id(id: id, user: user, **resource) + end + + # # Retrieve a specific UtilityPayment pdf file + # + # 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): + # - id [string]: object unique id. ex: "5656565656565656" + # + # ## Parameters (optional): + # - user [Project object]: Project object. Not necessary if StarkBank.user was set before function call + # + # ## Return: + # - UtilityPayment pdf file + def self.pdf(id, user: nil) + StarkBank::Utils::Rest.get_pdf(id: id, user: user, **resource) + end + + # # Retrieve UtilityPayments + # + # Receive a generator of UtilityPayment objects previously created in the Stark Bank API + # + # ## Parameters (optional): + # - limit [integer, default nil]: maximum number of objects to be retrieved. Unlimited if nil. ex: 35 + # - after [Date, default nil] date filter for objects created only after specified date. ex: Date.new(2020, 3, 10) + # - before [Date, default nil] date filter for objects only before specified date. ex: Date.new(2020, 3, 10) + # - tags [list of strings, default nil]: tags to filter retrieved objects. ex: ['tony', 'stark'] + # - ids [list of strings, default nil]: list of ids to filter retrieved objects. ex: ['5656565656565656', '4545454545454545'] + # - status [string, default nil]: filter for status of retrieved objects. ex: 'paid' + # - user [Project object, default nil]: Project object. Not necessary if StarkBank.user was set before function call + # + # ## Return: + # - generator of UtilityPayment objects with updated attributes + def self.query(limit: nil, after: nil, before: nil, tags: nil, ids: nil, status: nil, user: nil) + after = StarkBank::Utils::Checks.check_date(after) + before = StarkBank::Utils::Checks.check_date(before) + StarkBank::Utils::Rest.get_list( + user: user, + limit: limit, + after: after, + before: before, + tags: tags, + ids: ids, + status: status, + **resource + ) + end + + # # Delete a UtilityPayment entity + # + # Delete a UtilityPayment entity previously created in the Stark Bank API + # + # ## Parameters (required): + # - id [string]: UtilityPayment unique id. ex: "5656565656565656" + # + # ## Parameters (optional): + # - user [Project object]: Project object. Not necessary if StarkBank.user was set before function call + # + # ## Return: + # - deleted UtilityPayment with updated attributes + def self.delete(id, user: nil) + StarkBank::Utils::Rest.delete_id(id: id, user: user, **resource) + end + + def self.resource + { + resource_name: 'UtilityPayment', + resource_maker: proc { |json| + UtilityPayment.new( + id: json['id'], + description: json['description'], + line: json['line'], + bar_code: json['bar_code'], + tags: json['tags'], + scheduled: json['scheduled'], + amount: json['amount'], + fee: json['fee'], + status: json['status'], + created: json['created'] + ) + } + } + end + end +end diff --git a/lib/starkbank.rb b/lib/starkbank.rb new file mode 100644 index 0000000..656ce9e --- /dev/null +++ b/lib/starkbank.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require_relative('key') +require_relative('user/project') +require_relative('ledger/balance') +require_relative('ledger/transaction') +require_relative('boleto/boleto') +require_relative('boleto/log') +require_relative('transfer/transfer') +require_relative('transfer/log') +require_relative('payment/boleto/boleto') +require_relative('payment/boleto/log') +require_relative('payment/utility/utility') +require_relative('payment/utility/log') +require_relative('webhook/webhook') +require_relative('webhook/event') + +# SDK to facilitate Ruby integrations with Stark Bank +module StarkBank + @user = nil + class << self; attr_accessor :user; end +end diff --git a/lib/transfer/log.rb b/lib/transfer/log.rb new file mode 100644 index 0000000..606e73b --- /dev/null +++ b/lib/transfer/log.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +require_relative('../utils/resource') +require_relative('../utils/rest') +require_relative('../utils/checks') +require_relative('transfer') + +module StarkBank + class Transfer + # # Transfer::Log object + # + # 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: DateTime.new(2020, 3, 10, 10, 30, 0, 0) + class Log < StarkBank::Utils::Resource + attr_reader :id, :created, :type, :errors, :transfer + def initialize(id:, created:, type:, errors:, transfer:) + super(id) + @type = type + @errors = errors + @transfer = transfer + @created = StarkBank::Utils::Checks.check_datetime(created) + end + + # # Retrieve a specific Log + # + # Receive a single Log object previously created by the Stark Bank API by passing its id + # + # ## Parameters (required): + # - id [string]: object unique id. ex: "5656565656565656" + # + # ## Parameters (optional): + # - user [Project object]: Project object. Not necessary if StarkBank.user was set before function call + # + # ## Return: + # - Log object with updated attributes + def self.get(id, user: nil) + StarkBank::Utils::Rest.get_id(id: id, user: user, **resource) + end + + # # Retrieve Logs + # + # Receive a generator of Log objects previously created in the Stark Bank API + # + # ## Parameters (optional): + # - limit [integer, default nil]: maximum number of objects to be retrieved. Unlimited if nil. ex: 35 + # - after [Date, default nil] date filter for objects created only after specified date. ex: Date.new(2020, 3, 10) + # - before [Date, default nil] date filter for objects only before specified date. ex: Date.new(2020, 3, 10) + # - types [list of strings, default nil]: filter retrieved objects by types. ex: 'success' or 'failed' + # - transfer_ids [list of strings, default nil]: list of Transfer ids to filter retrieved objects. ex: ['5656565656565656', '4545454545454545'] + # - user [Project object, default nil]: Project object. Not necessary if StarkBank.user was set before function call + # + # ## Return: + # - list of Log objects with updated attributes + def self.query(limit: nil, after: nil, before: nil, types: nil, transfer_ids: nil, user: nil) + after = StarkBank::Utils::Checks.check_date(after) + before = StarkBank::Utils::Checks.check_date(before) + StarkBank::Utils::Rest.get_list( + limit: limit, + after: after, + before: before, + types: types, + transfer_ids: transfer_ids, + user: user, + **resource + ) + end + + def self.resource + transfer_maker = StarkBank::Transfer.resource[:resource_maker] + { + resource_name: 'TransferLog', + resource_maker: proc { |json| + Log.new( + id: json['id'], + created: json['created'], + type: json['type'], + errors: json['errors'], + transfer: StarkBank::Utils::API.from_api_json(transfer_maker, json['transfer']) + ) + } + } + end + end + end +end diff --git a/lib/transfer/transfer.rb b/lib/transfer/transfer.rb new file mode 100644 index 0000000..15cd53d --- /dev/null +++ b/lib/transfer/transfer.rb @@ -0,0 +1,153 @@ +# frozen_string_literal: true + +require_relative('../utils/resource') +require_relative('../utils/rest') +require_relative('../utils/checks') + +module StarkBank + # # Transfer object + # + # When you initialize a Transfer, the entity will not be automatically + # created in the Stark Bank API. The 'create' function sends the objects + # to the Stark Bank API and returns the list of created objects. + # + # ## 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: DateTime.new(2020, 3, 10, 10, 30, 0, 0) + # - updated [DateTime, default nil]: latest update datetime for the transfer. ex: DateTime.new(2020, 3, 10, 10, 30, 0, 0) + class Transfer < StarkBank::Utils::Resource + attr_reader :amount, :name, :tax_id, :bank_code, :branch_code, :account_number, :transaction_ids, :fee, :tags, :status, :id, :created, :updated + def initialize(amount:, name:, tax_id:, bank_code:, branch_code:, account_number:, transaction_ids: nil, fee: nil, tags: nil, status: nil, id: nil, created: nil, updated: nil) + super(id) + @amount = amount + @name = name + @tax_id = tax_id + @bank_code = bank_code + @branch_code = branch_code + @account_number = account_number + @transaction_ids = transaction_ids + @fee = fee + @tags = tags + @status = status + @created = StarkBank::Utils::Checks.check_datetime(created) + @updated = StarkBank::Utils::Checks.check_datetime(updated) + end + + # # Create Transfers + # + # Send a list of Transfer objects for creation in the Stark Bank API + # + # ## Parameters (required): + # - transfers [list of Transfer objects]: list of Transfer objects to be created in the API + # + # ## Parameters (optional): + # - user [Project object]: Project object. Not necessary if StarkBank.user was set before function call + # + # ## Return: + # - list of Transfer objects with updated attributes + def self.create(transfers, user: nil) + StarkBank::Utils::Rest.post(entities: transfers, user: user, **resource) + end + + # # Retrieve a specific Transfer + # + # Receive a single Transfer object previously created in the Stark Bank API by passing its id + # + # ## Parameters (required): + # - id [string]: object unique id. ex: "5656565656565656" + # + # ## Parameters (optional): + # - user [Project object]: Project object. Not necessary if StarkBank.user was set before function call + # + # ## Return: + # - Transfer object with updated attributes + def self.get(id, user: nil) + StarkBank::Utils::Rest.get_id(id: id, user: user, **resource) + end + + # # Retrieve a specific Transfer pdf file + # + # Receive a single Transfer pdf receipt file generated in the Stark Bank API by passing its id. + # Only valid for transfers with "processing" and "success" status. + # + # ## Parameters (required): + # - id [string]: object unique id. ex: "5656565656565656" + # + # ## Parameters (optional): + # - user [Project object]: Project object. Not necessary if StarkBank.user was set before function call + # + # ## Return: + # - Transfer pdf file + def self.pdf(id, user: nil) + StarkBank::Utils::Rest.get_pdf(id: id, user: user, **resource) + end + + # # Retrieve Transfers + # + # Receive a generator of Transfer objects previously created in the Stark Bank API + # + # ## Parameters (optional): + # - limit [integer, default nil]: maximum number of objects to be retrieved. Unlimited if nil. ex: 35 + # - after [Date, default nil] date filter for objects created only after specified date. ex: Date.new(2020, 3, 10) + # - before [Date, default nil] date filter for objects only before specified date. ex: Date.new(2020, 3, 10) + # - transactionIds [list of strings, default nil]: list of ids to filter retrieved objects. ex: ['5656565656565656', '4545454545454545'] + # - status [string, default nil]: filter for status of retrieved objects. ex: 'paid' or 'registered' + # - tags [list of strings, default nil]: tags to filter retrieved objects. ex: ['tony', 'stark'] + # - user [Project object, default nil]: Project object. Not necessary if StarkBank.user was set before function call + # + # ## Return: + # - generator of Transfer objects with updated attributes + def self.query(limit: nil, after: nil, before: nil, transaction_ids: nil, status: nil, sort: nil, tags: nil, user: nil) + after = StarkBank::Utils::Checks.check_date(after) + before = StarkBank::Utils::Checks.check_date(before) + StarkBank::Utils::Rest.get_list( + limit: limit, + after: after, + before: before, + transaction_ids: transaction_ids, + status: status, + sort: sort, + tags: tags, + user: user, + **resource + ) + end + + def self.resource + { + resource_name: 'Transfer', + resource_maker: proc { |json| + Transfer.new( + id: json['id'], + 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'], + created: json['created'], + updated: json['updated'] + ) + } + } + end + end +end diff --git a/lib/user/project.rb b/lib/user/project.rb new file mode 100644 index 0000000..865bbc7 --- /dev/null +++ b/lib/user/project.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require_relative('user') + +module StarkBank + # # Project object + # + # The Project object 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): + # - id [string]: unique id required to identify project. ex: '5656565656565656' + # - private_key [EllipticCurve.Project()]: PEM string of the private key linked to the project. ex: '-----BEGIN PUBLIC KEY-----\nMFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEyTIHK6jYuik6ktM9FIF3yCEYzpLjO5X/\ntqDioGM+R2RyW0QEo+1DG8BrUf4UXHSvCjtQ0yLppygz23z0yPZYfw==\n-----END PUBLIC KEY-----' + # - environment [string]: environment where the project is being used. ex: 'sandbox' or 'production' + # + # ## 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'] + # - pem [string]: private key in pem format. ex: '-----BEGIN PUBLIC KEY-----\nMFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEyTIHK6jYuik6ktM9FIF3yCEYzpLjO5X/\ntqDioGM+R2RyW0QEo+1DG8BrUf4UXHSvCjtQ0yLppygz23z0yPZYfw==\n-----END PUBLIC KEY-----' + class Project < StarkBank::User + attr_reader :name, :allowed_ips + def initialize(environment:, id:, private_key:, name: '', allowed_ips: nil) + super(environment, id, private_key) + @name = name + @allowed_ips = allowed_ips + end + end +end diff --git a/lib/user/user.rb b/lib/user/user.rb new file mode 100644 index 0000000..a7b71b7 --- /dev/null +++ b/lib/user/user.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require('starkbank-ecdsa') +require_relative('../utils/resource') +require_relative('../utils/checks') + +module StarkBank + class User < StarkBank::Utils::Resource + attr_reader :pem, :environment + def initialize(environment, id, private_key) + super(id) + @pem = StarkBank::Utils::Checks.check_private_key(private_key) + @environment = StarkBank::Utils::Checks.check_environment(environment) + end + + def access_id + "#{self.class.name.split('::').last.downcase}/#{@id}" + end + + def private_key + EllipticCurve::PrivateKey.fromPem(@pem) + end + end +end diff --git a/lib/utils/api.rb b/lib/utils/api.rb new file mode 100644 index 0000000..61f19a3 --- /dev/null +++ b/lib/utils/api.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require_relative('case') + +module StarkBank + module Utils + module API + def self.api_json(entity) + entity_hash = {} + entity.instance_variables.each do |key| + entity_hash[key[1..-1]] = entity.instance_variable_get(key) + end + cast_json_to_api_format(entity_hash) + end + + def self.cast_json_to_api_format(hash) + entity_hash = {} + hash.each do |key, value| + next if value.nil? + + value = value.is_a?(Date) || value.is_a?(DateTime) || value.is_a?(Time) ? value.strftime('%Y-%m-%d') : value + entity_hash[StarkBank::Utils::Case.snake_to_camel(key)] = value + end + entity_hash + end + + def self.from_api_json(resource_maker, json) + snakes = {} + json.each do |key, value| + snakes[StarkBank::Utils::Case.camel_to_snake(key)] = value + end + resource_maker.call(snakes) + end + + def self.endpoint(resource_name) + kebab = StarkBank::Utils::Case.camel_to_kebab(resource_name) + kebab.sub!('-log', '/log') + kebab + end + + def self.last_name_plural(resource_name) + "#{last_name(resource_name)}s" + end + + def self.last_name(resource_name) + StarkBank::Utils::Case.camel_to_kebab(resource_name).split('-').last + end + end + end +end diff --git a/lib/utils/cache.rb b/lib/utils/cache.rb new file mode 100644 index 0000000..4674253 --- /dev/null +++ b/lib/utils/cache.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module StarkBank + module Utils + module Cache + @starkbank_public_key = nil + class << self; attr_accessor :starkbank_public_key; end + end + end +end diff --git a/lib/utils/case.rb b/lib/utils/case.rb new file mode 100644 index 0000000..f5fbdf5 --- /dev/null +++ b/lib/utils/case.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module StarkBank + module Utils + module Case + def self.camel_to_snake(camel) + camel.to_s.gsub(/([a-z])([A-Z\d])/, '\1_\2').downcase + end + + def self.snake_to_camel(snake) + camel = snake.to_s.split('_').map(&:capitalize).join + camel[0] = camel[0].downcase + camel + end + + def self.camel_to_kebab(camel) + camel_to_snake(camel).tr('_', '-') + end + end + end +end diff --git a/lib/utils/checks.rb b/lib/utils/checks.rb new file mode 100644 index 0000000..a28ad26 --- /dev/null +++ b/lib/utils/checks.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +require('starkbank-ecdsa') +require('date') +require_relative('../user/user') +require_relative('environment') + +module StarkBank + module Utils + class Checks + def self.check_user(user) + return user if user.is_a?(StarkBank::User) + + user = user.nil? ? StarkBank.user : user + raise(ArgumentError, 'A user is required to access our API. Check our README: https://github.com/starkbank/sdk-ruby/') if user.nil? + + user + end + + def self.check_environment(environment) + environments = StarkBank::Utils::Environment.constants(false).map { |c| StarkBank::Utils::Environment.const_get(c) } + raise(ArgumentError, "Select a valid environment: #{environments.join(', ')}") unless environments.include?(environment) + + environment + end + + def self.check_private_key(pem) + EllipticCurve::PrivateKey.fromPem(pem) + pem + rescue + raise(ArgumentError, 'Private-key must be a valid secp256k1 ECDSA string in pem format') + end + + def self.check_datetime(data) + return if data.nil? + + return data if data.is_a?(Time) || data.is_a?(DateTime) + + return Time.new(data.year, data.month, data.day) if data.is_a?(Date) + + check_datetime_string(data) + end + + def self.check_date(data) + return if data.nil? + + return Date.new(data.year, data.month, data.day) if data.is_a?(Time) || data.is_a?(DateTime) + + return data if data.is_a?(Date) + + data = check_datetime_string(data) + + Date.new(data.year, data.month, data.day) + end + + class << self + private + + def check_datetime_string(data) + data = data.to_s + + begin + return DateTime.strptime(data, '%Y-%m-%dT%H:%M:%S.%L+00:00') + rescue ArgumentError + end + + begin + return DateTime.strptime(data, '%Y-%m-%dT%H:%M:%S+00:00') + rescue ArgumentError + end + + begin + return DateTime.strptime(data, '%Y-%m-%d') + rescue ArgumentError + raise(ArgumentError, 'invalid datetime string ' + data) + end + end + end + end + end +end diff --git a/lib/utils/environment.rb b/lib/utils/environment.rb new file mode 100644 index 0000000..1c80ca6 --- /dev/null +++ b/lib/utils/environment.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module StarkBank + module Utils + class Environment + PRODUCTION = 'production' + public_constant :PRODUCTION + + SANDBOX = 'sandbox' + public_constant :SANDBOX + end + end +end diff --git a/lib/utils/request.rb b/lib/utils/request.rb new file mode 100644 index 0000000..951f5de --- /dev/null +++ b/lib/utils/request.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require('json') +require('starkbank-ecdsa') +require('net/http') +require_relative('url') +require_relative('checks') +require_relative('../error') + +module StarkBank + module Utils + module Request + class Response + attr_reader :status, :content + def initialize(status, content) + @status = status + @content = content + end + + def json + JSON.parse(@content) + end + end + + def self.fetch(method:, path:, payload: nil, query: nil, user: nil) + user = Checks.check_user(user) + + base_url = { + Environment::PRODUCTION => 'https://api.starkbank.com/', + Environment::SANDBOX => 'https://sandbox.api.starkbank.com/' + }[user.environment] + 'v2' + + url = "#{base_url}/#{path}#{StarkBank::Utils::URL.urlencode(query)}" + uri = URI(url) + + access_time = Time.now.to_i + body = payload.nil? ? '' : payload.to_json + message = "#{user.access_id}:#{access_time}:#{body}" + signature = EllipticCurve::Ecdsa.sign(message, user.private_key).toBase64 + + case method + when 'GET' + req = Net::HTTP::Get.new(uri) + when 'DELETE' + req = Net::HTTP::Delete.new(uri) + when 'POST' + req = Net::HTTP::Post.new(uri) + req.body = body + when 'PATCH' + req = Net::HTTP::Patch.new(uri) + req.body = body + when 'PUT' + req = Net::HTTP::Put.new(uri) + req.body = body + else + raise(ArgumentError, 'unknown HTTP method ' + method) + end + + req['Access-Id'] = user.access_id + req['Access-Time'] = access_time + req['Access-Signature'] = signature + req['Content-Type'] = 'application/json' + req['User-Agent'] = "Ruby-#{RUBY_VERSION}-SDK-0.1.0" + + request = Net::HTTP.start(uri.hostname, use_ssl: true) { |http| http.request(req) } + + response = Response.new(Integer(request.code, 10), request.body) + + raise(StarkBank::Error::InternalServerError) if response.status == 500 + raise(StarkBank::Error::InputErrors, response.json['errors']) if response.status == 400 + raise(StarkBank::Error::UnknownError, response.content) unless response.status == 200 + + response + end + end + end +end diff --git a/lib/utils/resource.rb b/lib/utils/resource.rb new file mode 100644 index 0000000..599fc59 --- /dev/null +++ b/lib/utils/resource.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module StarkBank + module Utils + class Resource + attr_reader :id + def initialize(id = nil) + @id = id + end + + def to_s + string_vars = [] + instance_variables.each do |key| + value = instance_variable_get(key).to_s.lines.map(&:chomp).join("\n ") + string_vars << "#{key[1..-1]}: #{value}" + end + fields = string_vars.join(",\n ") + "#{class_name}(\n #{fields}\n)" + end + + def inspect + "#{class_name}[#{@id}]" + end + + private + + def class_name + self.class.name.split('::').last.downcase + end + end + end +end diff --git a/lib/utils/rest.rb b/lib/utils/rest.rb new file mode 100644 index 0000000..6182642 --- /dev/null +++ b/lib/utils/rest.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +require_relative('request') +require_relative('api') + +module StarkBank + module Utils + module Rest + def self.get_list(resource_name:, resource_maker:, user: nil, **query) + limit = query[:limit] + query[:limit] = limit.nil? ? limit : [limit, 100].min + + Enumerator.new do |enum| + loop do + json = StarkBank::Utils::Request.fetch( + method: 'GET', + path: StarkBank::Utils::API.endpoint(resource_name), + query: query, + user: user + ).json + entities = json[StarkBank::Utils::API.last_name_plural(resource_name)] + + entities.each do |entity| + enum << StarkBank::Utils::API.from_api_json(resource_maker, entity) + end + + unless limit.nil? + limit -= 100 + query[:limit] = [limit, 100].min + end + + cursor = json['cursor'] + query['cursor'] = cursor + + break if cursor.nil? || (!limit.nil? && limit <= 0) + end + end + end + + def self.get_id(resource_name:, resource_maker:, id:, user: nil) + json = StarkBank::Utils::Request.fetch( + method: 'GET', + path: "#{StarkBank::Utils::API.endpoint(resource_name)}/#{id}", + user: user + ).json + entity = json[StarkBank::Utils::API.last_name(resource_name)] + StarkBank::Utils::API.from_api_json(resource_maker, entity) + end + + def self.get_pdf(resource_name:, resource_maker:, id:, user: nil) + StarkBank::Utils::Request.fetch( + method: 'GET', + path: "#{StarkBank::Utils::API.endpoint(resource_name)}/#{id}/pdf", + user: user + ).content + end + + def self.post(resource_name:, resource_maker:, entities:, user: nil) + jsons = [] + entities.each do |entity| + jsons << StarkBank::Utils::API.api_json(entity) + end + payload = { StarkBank::Utils::API.last_name_plural(resource_name) => jsons } + json = StarkBank::Utils::Request.fetch( + method: 'POST', + path: StarkBank::Utils::API.endpoint(resource_name), + payload: payload, + user: user + ).json + returned_jsons = json[StarkBank::Utils::API.last_name_plural(resource_name)] + entities = [] + returned_jsons.each do |returned_json| + entities << StarkBank::Utils::API.from_api_json(resource_maker, returned_json) + end + entities + end + + def self.post_single(resource_name:, resource_maker:, entity:, user: nil) + json = StarkBank::Utils::Request.fetch( + method: 'POST', + path: StarkBank::Utils::API.endpoint(resource_name), + payload: StarkBank::Utils::API.api_json(entity), + user: user + ).json + entity_json = json[StarkBank::Utils::API.last_name(resource_name)] + StarkBank::Utils::API.from_api_json(resource_maker, entity_json) + end + + def self.delete_id(resource_name:, resource_maker:, id:, user: nil) + json = StarkBank::Utils::Request.fetch( + method: 'DELETE', + path: "#{StarkBank::Utils::API.endpoint(resource_name)}/#{id}", + user: user + ).json + entity = json[StarkBank::Utils::API.last_name(resource_name)] + StarkBank::Utils::API.from_api_json(resource_maker, entity) + end + + def self.patch_id(resource_name:, resource_maker:, id:, user: nil, **payload) + json = StarkBank::Utils::Request.fetch( + method: 'PATCH', + path: "#{StarkBank::Utils::API.endpoint(resource_name)}/#{id}", + user: user, + payload: StarkBank::Utils::API.cast_json_to_api_format(payload) + ).json + entity = json[StarkBank::Utils::API.last_name(resource_name)] + StarkBank::Utils::API.from_api_json(resource_maker, entity) + end + end + end +end diff --git a/lib/utils/url.rb b/lib/utils/url.rb new file mode 100644 index 0000000..7766347 --- /dev/null +++ b/lib/utils/url.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module StarkBank + module Utils + module URL + # generates query string from hash + def self.urlencode(params) + return '' if params.nil? + + params = StarkBank::Utils::API.cast_json_to_api_format(params) + return '' if params.empty? + + string_params = {} + params.each do |key, value| + string_params[key] = value.is_a?(Array) ? value.join(',') : value + end + + query_list = [] + string_params.each do |key, value| + query_list << "#{key}=#{value}" + end + '?' + query_list.join('&') + end + end + end +end diff --git a/lib/webhook/event.rb b/lib/webhook/event.rb new file mode 100644 index 0000000..742e431 --- /dev/null +++ b/lib/webhook/event.rb @@ -0,0 +1,186 @@ +# frozen_string_literal: true + +require('json') +require('starkbank-ecdsa') +require_relative('../utils/resource') +require_relative('../utils/rest') +require_relative('../utils/checks') +require_relative('../utils/cache') +require_relative('../error') +require_relative('../boleto/log') +require_relative('../transfer/log') +require_relative('../payment/boleto/log') +require_relative('../payment/utility/log') + +module StarkBank + # # Webhook Event object + # + # 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 object from one the subscription services (TransferLog, BoletoLog, BoletoPaymentlog or UtilityPaymentLog) + # - created [DateTime]: creation datetime for the notification event. ex: DateTime.new(2020, 3, 10, 10, 30, 0, 0) + # - 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" + class Event < StarkBank::Utils::Resource + attr_reader :id, :log, :created, :is_delivered, :subscription + def initialize(id:, log:, created:, is_delivered:, subscription:) + super(id) + @created = StarkBank::Utils::Checks.check_datetime(created) + @is_delivered = is_delivered + @subscription = subscription + + maker = { + 'transfer': StarkBank::Transfer::Log.resource, + 'boleto': StarkBank::Boleto::Log.resource, + 'boleto-payment': StarkBank::BoletoPayment::Log.resource, + 'utility-payment': StarkBank::UtilityPayment::Log.resource + }[subscription.to_sym][:resource_maker] + + @log = StarkBank::Utils::API.from_api_json(maker, log) + end + + # # Retrieve a specific notification Event + # + # Receive a single notification Event object previously created in the Stark Bank API by passing its id + # + # ## Parameters (required): + # - id [string]: object unique id. ex: "5656565656565656" + # + # ## Parameters (optional): + # - user [Project object]: Project object. Not necessary if StarkBank.user was set before function call + # + # ## Return: + # - Event object with updated attributes + def self.get(id, user: nil) + StarkBank::Utils::Rest.get_id(id: id, user: user, **resource) + end + + # # Retrieve notification Events + # + # Receive a generator of notification Event objects previously created in the Stark Bank API + # + # ## Parameters (optional): + # - limit [integer, default nil]: maximum number of objects to be retrieved. Unlimited if nil. ex: 35 + # - after [Date, default nil]: date filter for objects created only after specified date. ex: Date.new(2020, 3, 10) + # - before [Date, default nil]: date filter for objects only before specified date. ex: Date.new(2020, 3, 10) + # - is_delivered [bool, default nil]: bool to filter successfully delivered events. ex: True or False + # - user [Project object, default nil]: Project object. Not necessary if StarkBank.user was set before function call + # + # ## Return: + # - generator of Event objects with updated attributes + def self.query(limit: nil, after: nil, before: nil, is_delivered: nil, user: nil) + after = StarkBank::Utils::Checks.check_date(after) + before = StarkBank::Utils::Checks.check_date(before) + StarkBank::Utils::Rest.get_list( + user: user, + limit: limit, + after: after, + before: before, + is_delivered: is_delivered, + **resource + ) + end + + # # Delete a notification Event + # + # Delete a of notification Event entity previously created in the Stark Bank API by its ID + # + # ## Parameters (required): + # - id [string]: Event unique id. ex: "5656565656565656" + # + # ## Parameters (optional): + # - user [Project object]: Project object. Not necessary if StarkBank.user was set before function call + # + # ## Return: + # - deleted Event with updated attributes + def self.delete(id, user: nil) + StarkBank::Utils::Rest.delete_id(id: id, user: user, **resource) + end + + # # Update notification Event entity + # + # 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): + # - id [list of strings]: Event unique ids. ex: "5656565656565656" + # - is_delivered [bool]: If True and event hasn't been delivered already, event will be set as delivered. ex: True + # + # ## Parameters (optional): + # - user [Project object]: Project object. Not necessary if StarkBank.user was set before function call + # + # ## Return: + # - target Event with updated attributes + def self.update(id, is_delivered:, user: nil) + StarkBank::Utils::Rest.patch_id(id: id, user: user, is_delivered: is_delivered, **resource) + end + + # # Create single notification Event from a content string + # + # Create a single Event object received from event listening at subscribed user endpoint. + # If the provided digital signature does not check out with the StarkBank public key, a + # starkbank.exception.InvalidSignatureException will be raised. + # + # ## Parameters (required): + # - content [string]: response content from request received at user endpoint (not parsed) + # - signature [string]: base-64 digital signature received at response header "Digital-Signature" + # + # ## Parameters (optional): + # - user [Project object]: Project object. Not necessary if StarkBank.user was set before function call + # + # ## Return: + # - Parsed Event object + def self.parse(content:, signature:, user: nil) + event = StarkBank::Utils::API.from_api_json(resource[:resource_maker], JSON.parse(content)['event']) + + begin + signature = EllipticCurve::Signature.fromBase64(signature) + rescue + raise(StarkBank::Error::InvalidSignatureError, 'The provided signature is not valid') + end + + return event if verify_signature(content: content, signature: signature, user: user) + + return event if verify_signature(content: content, signature: signature, user: user, refresh: true) + + raise(StarkBank::Error::InvalidSignatureError, 'The provided signature and content do not match the Stark Bank public key') + end + + class << self + private + + def verify_signature(content:, signature:, user:, refresh: false) + public_key = StarkBank::Utils::Cache.starkbank_public_key + if public_key.nil? || refresh + pem = get_public_key_pem(user) + public_key = EllipticCurve::PublicKey.fromPem(pem) + StarkBank::Utils::Cache.starkbank_public_key = public_key + end + EllipticCurve::Ecdsa.verify(content, signature, public_key) + end + + def get_public_key_pem(user) + StarkBank::Utils::Request.fetch(method: 'GET', path: 'public-key', query: { limit: 1 }, user: user).json['publicKeys'][0]['content'] + end + + def resource + { + resource_name: 'Event', + resource_maker: proc { |json| + Event.new( + id: json['id'], + log: json['log'], + created: json['created'], + is_delivered: json['is_delivered'], + subscription: json['subscription'] + ) + } + } + end + end + end +end diff --git a/lib/webhook/webhook.rb b/lib/webhook/webhook.rb new file mode 100644 index 0000000..17063a8 --- /dev/null +++ b/lib/webhook/webhook.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +require_relative('../utils/resource') +require_relative('../utils/rest') +require_relative('../utils/checks') + +module StarkBank + # # Webhook subscription object + # + # 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' + class Webhook < StarkBank::Utils::Resource + attr_reader :url, :subscriptions, :id + def initialize(url:, subscriptions:, id: nil) + super(id) + @url = url + @subscriptions = subscriptions + end + + # # Create Webhook subscription + # + # Send a single Webhook subscription for creation in the Stark Bank API + # + # ## Parameters (required): + # - 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'] + # + # ## Parameters (optional): + # - user [Project object]: Project object. Not necessary if StarkBank.user was set before function call + # + # ## Return: + # - Webhook object with updated attributes + def self.create(url:, subscriptions:, id: nil, user: nil) + StarkBank::Utils::Rest.post_single(entity: Webhook.new(url: url, subscriptions: subscriptions), user: user, **resource) + end + + # # Retrieve a specific Webhook subscription + # + # Receive a single Webhook subscription object previously created in the Stark Bank API by passing its id + # + # ## Parameters (required): + # - id [string]: object unique id. ex: '5656565656565656' + # + # ## Parameters (optional): + # - user [Project object]: Project object. Not necessary if StarkBank.user was set before function call + # + # ## Return: + # - Webhook object with updated attributes + def self.get(id, user: nil) + StarkBank::Utils::Rest.get_id(id: id, user: user, **resource) + end + + # # Retrieve Webhook subcriptions + # + # Receive a generator of Webhook subcription objects previously created in the Stark Bank API + # + # ## Parameters (optional): + # - limit [integer, default nil]: maximum number of objects to be retrieved. Unlimited if nil. ex: 35 + # - user [Project object, default nil]: Project object. Not necessary if StarkBank.user was set before function call + # + # ## Return: + # - generator of Webhook objects with updated attributes + def self.query(limit: nil, user: nil) + StarkBank::Utils::Rest.get_list(user: user, limit: limit, **resource) + end + + # # Delete a Webhook entity + # + # Delete a Webhook entity previously created in the Stark Bank API + # + # ## Parameters (required): + # - id [string]: Webhook unique id. ex: '5656565656565656' + # + # ## Parameters (optional): + # - user [Project object]: Project object. Not necessary if StarkBank.user was set before function call + # + # ## Return: + # - deleted Webhook with updated attributes + def self.delete(id, user: nil) + StarkBank::Utils::Rest.delete_id(id: id, user: user, **resource) + end + + def self.resource + { + resource_name: 'Webhook', + resource_maker: proc { |json| + Webhook.new( + id: json['id'], + url: json['url'], + subscriptions: json['subscriptions'] + ) + } + } + end + end +end diff --git a/spec/balance_spec.rb b/spec/balance_spec.rb new file mode 100644 index 0000000..b1adcbc --- /dev/null +++ b/spec/balance_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require('starkbank') +require('user') + +RSpec.describe(StarkBank::Balance, '#balance#') do + context 'no requirements' do + it 'get balance' do + balance = StarkBank::Balance.get + expect(balance.id).not_to(be_nil) + expect(balance.amount).not_to(be_nil) + expect(balance.currency).not_to(be_nil) + expect(balance.updated).not_to(be_nil) + end + end +end diff --git a/spec/boleto_log_spec.rb b/spec/boleto_log_spec.rb new file mode 100644 index 0000000..b42f865 --- /dev/null +++ b/spec/boleto_log_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require('starkbank') +require('user') + +RSpec.describe(StarkBank::Boleto::Log, '#boleto/log#') do + context 'at least 10 paid boletos' do + it 'query logs' do + logs = StarkBank::Boleto::Log.query(limit: 10, types: 'paid').to_a + expect(logs.length).to(eq(10)) + logs.each do |log| + expect(log.id).not_to(be_nil) + expect(log.type).to(eq('paid')) + expect(log.boleto.status).to(eq('paid')) + end + end + end + + context 'at least one created boleto' do + it 'query and get' do + log = StarkBank::Boleto::Log.query(limit: 1).to_a[0] + get_log = StarkBank::Boleto::Log.get(log.id) + expect(log.id).to(eq(get_log.id)) + end + end +end diff --git a/spec/boleto_payment_log_spec.rb b/spec/boleto_payment_log_spec.rb new file mode 100644 index 0000000..2cc6970 --- /dev/null +++ b/spec/boleto_payment_log_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require('starkbank') +require('user') + +RSpec.describe(StarkBank::BoletoPayment::Log, '#boleto-payment/log#') do + context 'at least 10 successful boleto payments' do + it 'query logs' do + logs = StarkBank::BoletoPayment::Log.query(limit: 10, types: 'success').to_a + expect(logs.length).to(eq(10)) + logs.each do |log| + expect(log.id).not_to(be_nil) + expect(log.type).to(eq('success')) + expect(log.payment.status).to(eq('success')) + end + end + end + + context 'at least one created boleto payment' do + it 'query and get' do + log = StarkBank::BoletoPayment::Log.query(limit: 1).to_a[0] + get_log = StarkBank::BoletoPayment::Log.get(log.id) + expect(log.id).to(eq(get_log.id)) + end + end +end diff --git a/spec/boleto_payment_spec.rb b/spec/boleto_payment_spec.rb new file mode 100644 index 0000000..48627b3 --- /dev/null +++ b/spec/boleto_payment_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require('date') +require('starkbank') +require('user') + +RSpec.describe(StarkBank::BoletoPayment, '#boleto-payment#') do + context 'at least 10 successful boleto payments' do + it 'query' do + payments = StarkBank::BoletoPayment.query(limit: 10, status: 'success').to_a + expect(payments.length).to(eq(10)) + payments.each do |payment| + expect(payment.id).not_to(be_nil) + expect(payment.status).to(eq('success')) + end + end + end + + context 'payment of example line is not yet registered' do + it 'create, get, get_pdf and delete' do + payment = StarkBank::BoletoPayment.create([example])[0] + get_payment = StarkBank::BoletoPayment.get(payment.id) + expect(payment.id).to(eq(get_payment.id)) + pdf = StarkBank::BoletoPayment.pdf(payment.id) + File.open('boleto_payment.pdf', 'w') { |file| file.write(pdf) } + delete_payment = StarkBank::BoletoPayment.delete(payment.id) + expect(payment.id).to(eq(delete_payment.id)) + end + end + + def example + StarkBank::BoletoPayment.new( + line: '34191.09008 61713.957308 71444.640008 2 83430000984732', + scheduled: Date.today + 2, + description: 'loading a random account', + tax_id: '20.018.183/0001-80' + ) + end +end diff --git a/spec/boleto_spec.rb b/spec/boleto_spec.rb new file mode 100644 index 0000000..2b1a2d2 --- /dev/null +++ b/spec/boleto_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require('starkbank') +require('user') + +RSpec.describe(StarkBank::Boleto, '#boleto#') do + context 'at least 10 paid boletos' do + it 'query' do + boletos = StarkBank::Boleto.query(limit: 10, status: 'paid', before: DateTime.now).to_a + expect(boletos.length).to(eq(10)) + boletos.each do |boleto| + expect(boleto.id).not_to(be_nil) + expect(boleto.status).to(eq('paid')) + end + end + end + + context 'no requirements' do + it 'create, get, get_pdf and delete' do + boleto = StarkBank::Boleto.create([example])[0] + get_boleto = StarkBank::Boleto.get(boleto.id) + expect(boleto.id).to(eq(get_boleto.id)) + pdf = StarkBank::Boleto.pdf(boleto.id) + File.open('boleto.pdf', 'w') { |file| file.write(pdf) } + delete_boleto = StarkBank::Boleto.delete(boleto.id) + expect(boleto.id).to(eq(delete_boleto.id)) + end + end + + def example + StarkBank::Boleto.new( + amount: 100_000, + due: Time.now + 24 * 3600, + 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/spec/event_spec.rb b/spec/event_spec.rb new file mode 100644 index 0000000..dac9f26 --- /dev/null +++ b/spec/event_spec.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require('starkbank') +require('user') + +RSpec.describe(StarkBank::Event, '#event#') do + context 'at least 101 webhook events' do + it 'query' do + events = StarkBank::Event.query(limit: 101).to_a + expect(events.length).to(eq(101)) + events.each do |event| + expect(event.id).not_to(be_nil) + expect(event.log).not_to(be_nil) + end + end + end + + context 'at least one undelivered event' do + it 'query, get, update and delete' do + event = StarkBank::Event.query(limit: 1, is_delivered: false).to_a[0] + expect(event.is_delivered).to(be(false)) + get_event = StarkBank::Event.get(event.id) + expect(event.id).to(eq(get_event.id)) + update_event = StarkBank::Event.update(event.id, is_delivered: true) + expect(update_event.id).to(eq(event.id)) + expect(update_event.is_delivered).to(be(true)) + delete_event = StarkBank::Event.delete(event.id) + expect(delete_event.id).to(eq(event.id)) + end + end + + context 'no requirements' do + it 'parse with right signature' do + _no_cache_event = StarkBank::Event.parse( + 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' + ) + event = StarkBank::Event.parse( + 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' + ) + expect(event.log).not_to(be_nil) + end + + it 'parse with wrong signature' do + begin + StarkBank::Event.parse( + 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: 'MEUCIQDOpo1j+V40DNZK2URL2786UQK/8mDXon9ayEd8U0/l7AIgYXtIZJBTs8zCRR3vmted6Ehz/qfw1GRut/eYyvf1yOk=' + ) + rescue StarkBank::Error::InvalidSignatureError + else + raise(StandardError, 'invalid signature was not detected') + end + end + + it 'parse with malformed signature' do + begin + StarkBank::Event.parse( + 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: 'something is definitely wrong' + ) + rescue StarkBank::Error::InvalidSignatureError + else + raise(StandardError, 'malformed signature was not detected') + end + end + end +end diff --git a/spec/key_spec.rb b/spec/key_spec.rb new file mode 100644 index 0000000..b3617cc --- /dev/null +++ b/spec/key_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require('starkbank') + +RSpec.describe(StarkBank::Key, '#key#') do + context 'no requirements' do + it 'generates new random ECDSA key pair' do + private_key, public_key = StarkBank::Key.create + expect(private_key).not_to(be_empty) + expect(public_key).not_to(be_empty) + end + + it 'generates new random ECDSA key pair and saves to folder keys' do + private_key, public_key = StarkBank::Key.create('keys') + expect(private_key).not_to(be_empty) + expect(public_key).not_to(be_empty) + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..251aa51 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,100 @@ +# This file was generated by the `rspec --init` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# The generated `.rspec` file contains `--require spec_helper` which will cause +# this file to always be loaded, without a need to explicitly require it in any +# files. +# +# Given that it is always loaded, you are encouraged to keep this file as +# light-weight as possible. Requiring heavyweight dependencies from this file +# will add to the boot time of your test suite on EVERY test run, even for an +# individual file that may not need all of that loaded. Instead, consider making +# a separate helper file that requires the additional dependencies and performs +# the additional setup, and require it from the spec files that actually need +# it. +# +# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +RSpec.configure do |config| + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + + # This option will default to `:apply_to_host_groups` in RSpec 4 (and will + # have no way to turn it off -- the option exists only for backwards + # compatibility in RSpec 3). It causes shared context metadata to be + # inherited by the metadata hash of host groups and examples, rather than + # triggering implicit auto-inclusion in groups with matching metadata. + config.shared_context_metadata_behavior = :apply_to_host_groups + +# The settings below are suggested to provide a good initial experience +# with RSpec, but feel free to customize to your heart's content. +=begin + # This allows you to limit a spec run to individual examples or groups + # you care about by tagging them with `:focus` metadata. When nothing + # is tagged with `:focus`, all examples get run. RSpec also provides + # aliases for `it`, `describe`, and `context` that include `:focus` + # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + config.filter_run_when_matching :focus + + # Allows RSpec to persist some state between runs in order to support + # the `--only-failures` and `--next-failure` CLI options. We recommend + # you configure your source control system to ignore this file. + config.example_status_persistence_file_path = "spec/examples.txt" + + # Limits the available syntax to the non-monkey patched syntax that is + # recommended. For more details, see: + # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ + # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ + # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode + config.disable_monkey_patching! + + # This setting enables warnings. It's recommended, but in some cases may + # be too noisy due to issues in dependencies. + config.warnings = true + + # Many RSpec users commonly either run the entire suite or an individual + # file, and it's useful to allow more verbose output when running an + # individual spec file. + if config.files_to_run.one? + # Use the documentation formatter for detailed output, + # unless a formatter has already been configured + # (e.g. via a command-line flag). + config.default_formatter = "doc" + end + + # Print the 10 slowest examples and example groups at the + # end of the spec run, to help surface which specs are running + # particularly slow. + config.profile_examples = 10 + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = :random + + # Seed global randomization in this process using the `--seed` CLI option. + # Setting this allows you to use `--seed` to deterministically reproduce + # test failures related to randomization by passing the same `--seed` value + # as the one that triggered the failure. + Kernel.srand config.seed +=end +end diff --git a/spec/transaction_spec.rb b/spec/transaction_spec.rb new file mode 100644 index 0000000..ef26132 --- /dev/null +++ b/spec/transaction_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require('securerandom') +require('date') +require('starkbank') +require('user') + +RSpec.describe(StarkBank::Transaction, '#transaction#') do + context 'at least 10 transactions created on past 30 days' do + it 'query' do + after = Date.today - 30 + transactions = StarkBank::Transaction.query(limit: 10, after: after).to_a + expect(transactions.length).to(eq(10)) + transactions.each do |transaction| + expect(transaction.id).not_to(be_nil) + expect(transaction.created).to(be >= after) + end + end + end + + context 'no requirements' do + it 'create and get' do + transaction = example + create_transaction = StarkBank::Transaction.create([transaction])[0] + expect(-transaction.amount).to(eq(create_transaction.amount)) + get_transaction = StarkBank::Transaction.get(create_transaction.id) + expect(create_transaction.id).to(eq(get_transaction.id)) + expect(create_transaction.amount).to(eq(get_transaction.amount)) + end + end + + def example + StarkBank::Transaction.new( + amount: 50, + receiver_id: '5768064935133184', + external_id: SecureRandom.base64, + description: 'Transferência para Workspace aleatório' + ) + end +end diff --git a/spec/transfer_log_spec.rb b/spec/transfer_log_spec.rb new file mode 100644 index 0000000..45c1003 --- /dev/null +++ b/spec/transfer_log_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require('starkbank') +require('user') + +RSpec.describe(StarkBank::Transfer::Log, '#transfer/log#') do + context 'at least 10 success transfers' do + it 'query logs' do + logs = StarkBank::Transfer::Log.query(limit: 10, types: 'success').to_a + expect(logs.length).to(eq(10)) + logs.each do |log| + expect(log.id).not_to(be_nil) + expect(log.type).to(eq('success')) + expect(log.transfer.status).to(eq('success')) + end + end + end + + context 'at least one created transfer' do + it 'query and get' do + log = StarkBank::Transfer::Log.query(limit: 1).to_a[0] + get_log = StarkBank::Transfer::Log.get(log.id) + expect(log.id).to(eq(get_log.id)) + end + end +end diff --git a/spec/transfer_spec.rb b/spec/transfer_spec.rb new file mode 100644 index 0000000..3b41a78 --- /dev/null +++ b/spec/transfer_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require('starkbank') +require('user') + +RSpec.describe(StarkBank::Transfer, '#transfer#') do + context 'at least 10 successful transfers' do + it 'query' do + transfers = StarkBank::Transfer.query(limit: 10, status: 'success').to_a + expect(transfers.length).to(eq(10)) + transfers.each do |transfer| + expect(transfer.id).not_to(be_nil) + expect(transfer.status).to(eq('success')) + end + end + end + + context 'no requirements' do + it 'create, get and get_pdf' do + transfer = StarkBank::Transfer.create([example])[0] + get_transfer = StarkBank::Transfer.get(transfer.id) + expect(transfer.id).to(eq(get_transfer.id)) + pdf = StarkBank::Transfer.pdf(transfer.id) + File.open('transfer.pdf', 'w') { |file| file.write(pdf) } + end + end + + def example + StarkBank::Transfer.new( + amount: rand(1000), + name: 'João', + tax_id: '01234567890', + bank_code: '01', + branch_code: '0001', + account_number: '10000-0' + ) + end +end diff --git a/spec/user.rb b/spec/user.rb new file mode 100644 index 0000000..19493fb --- /dev/null +++ b/spec/user.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require('starkbank') + +StarkBank.user = StarkBank::Project.new( + environment: 'sandbox', + id: '9999999999999999', + private_key: '-----BEGIN EC PRIVATE KEY----- +MHQCAQEEIBEcEJZLk/DyuXVsEjz0w4vrE7plPXhQxODvcG1Jc0WToAcGBSuBBAAK +oUQDQgAE6t4OGx1XYktOzH/7HV6FBukxq0Xs2As6oeN6re1Ttso2fwrh5BJXDq75 +mSYHeclthCRgU8zl6H1lFQ4BKZ5RCQ== +-----END EC PRIVATE KEY-----' +) diff --git a/spec/utility_payment_log_spec.rb b/spec/utility_payment_log_spec.rb new file mode 100644 index 0000000..6980ab0 --- /dev/null +++ b/spec/utility_payment_log_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require('starkbank') +require('user') + +RSpec.describe(StarkBank::UtilityPayment::Log, '#utility-payment/log#') do + context 'at least 10 successful utility payments' do + it 'query logs' do + logs = StarkBank::UtilityPayment::Log.query(limit: 10, types: 'success').to_a + expect(logs.length).to(eq(10)) + logs.each do |log| + expect(log.id).not_to(be_nil) + expect(log.type).to(eq('success')) + expect(log.payment.status).to(eq('success')) + end + end + end + + context 'at least one created utility payment' do + it 'query and get' do + log = StarkBank::UtilityPayment::Log.query(limit: 1).to_a[0] + get_log = StarkBank::UtilityPayment::Log.get(log.id) + expect(log.id).to(eq(get_log.id)) + end + end +end diff --git a/spec/utility_payment_spec.rb b/spec/utility_payment_spec.rb new file mode 100644 index 0000000..bdbb345 --- /dev/null +++ b/spec/utility_payment_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require('date') +require('starkbank') +require('user') + +RSpec.describe(StarkBank::UtilityPayment, '#utility-payment#') do + context 'at least 10 successful utility payments' do + it 'query' do + payments = StarkBank::UtilityPayment.query(limit: 10, status: 'success').to_a + expect(payments.length).to(eq(10)) + payments.each do |payment| + expect(payment.id).not_to(be_nil) + expect(payment.status).to(eq('success')) + end + end + end + + context 'payment of example line is not yet registered' do + it 'create, get, get_pdf and delete' do + payment = StarkBank::UtilityPayment.create([example])[0] + get_payment = StarkBank::UtilityPayment.get(payment.id) + expect(payment.id).to(eq(get_payment.id)) + pdf = StarkBank::UtilityPayment.pdf(payment.id) + File.open('utility_payment.pdf', 'w') { |file| file.write(pdf) } + delete_payment = StarkBank::UtilityPayment.delete(payment.id) + expect(payment.id).to(eq(delete_payment.id)) + end + end + + def example + StarkBank::UtilityPayment.new( + bar_code: '83660000001084301380074119002551100010601813', + scheduled: Date.today + 2, + description: 'pagando a conta' + ) + end +end diff --git a/spec/webhook_spec.rb b/spec/webhook_spec.rb new file mode 100644 index 0000000..e70ad1d --- /dev/null +++ b/spec/webhook_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require('starkbank') +require('user') + +RSpec.describe(StarkBank::Webhook, '#webhook#') do + context 'at least 1 webhook' do + it 'query' do + webhooks = StarkBank::Webhook.query.to_a + webhooks.each do |webhook| + expect(webhook.id).not_to(be_nil) + end + end + end + + context 'no requirements' do + it 'create, get and delete' do + webhook = example + webhook = StarkBank::Webhook.create(url: webhook.url, subscriptions: webhook.subscriptions) + get_webhook = StarkBank::Webhook.get(webhook.id) + expect(webhook.id).to(eq(get_webhook.id)) + delete_webhook = StarkBank::Webhook.delete(webhook.id) + expect(webhook.id).to(eq(delete_webhook.id)) + end + end + + def example + StarkBank::Webhook.new( + url: 'https://webhook.site/60e9c18e-4b5c-4369-bda1-ab5fcd8e1b29', + subscriptions: %w[transfer boleto boleto-payment utility-payment] + ) + end +end diff --git a/starkbank.gemspec b/starkbank.gemspec new file mode 100644 index 0000000..805a7f2 --- /dev/null +++ b/starkbank.gemspec @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +Gem::Specification.new do |s| + s.name = 'starkbank' + s.version = '0.1.0' + s.date = '2020-04-06' + s.summary = 'SDK to facilitate Ruby integrations with Stark Bank' + s.authors = 'starkbank' + s.homepage = 'https://github.com/starkbank/sdk-ruby' + s.files = Dir['lib/**/*.rb'] + s.licenses = 'MIT' + s.required_ruby_version = '>= 2.3' + s.add_dependency('starkbank-ecdsa', '~> 0.0.2') + s.add_development_dependency('rspec', '~> 3.0') +end