cash_out 0.0.2 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 89d3b16a65640bfe86c9c9b08db3415d802b5daa
4
- data.tar.gz: e7264ed0558c1dd2ff12a1741ca08b1f6627c03a
3
+ metadata.gz: e35c084e12b884895299c46ecc486f3e55fde8af
4
+ data.tar.gz: 716d1175f5fdf7d85ff0b065c1acae93c4ebc179
5
5
  SHA512:
6
- metadata.gz: ba3d8f16518bd1b28926a4a1d28cd8a70f867e5c7060f6c684429ef0191eef143e5b02ac558b1a0bb23ccbaea1cda1c7d57b08b49d7f701f83706bbbbb5745de
7
- data.tar.gz: fa92ab005a40e0e61ef6540bacc53f11f6789dee1f3b7f0a2637676e6d496389fec00a47ad6ea08dcfcc170c8d3bae3614b80386fa1f16d7e7a40431019ab94d
6
+ metadata.gz: a511c1848565781927821a57abc2c45f485965f42a7ed97cb73d65772e8d0dd5851d8c84ed3f8c5231742c743b7742751330910a40910be4ed3d6407a8660096
7
+ data.tar.gz: a7d2aace28c3984608cd74131798dc4fddd2b1a919574d50d2894bc542cd2441e78ecc53f408fc1ef3dcce19974fc8974cfa97806a038123cc39e902d95f4545
data/.gitignore CHANGED
@@ -1 +1,2 @@
1
1
  spec/examples.txt
2
+ assets/title-image.psd
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- cash_out (0.0.1)
4
+ cash_out (0.0.3)
5
5
  active_interaction (~> 3.6)
6
6
  railties (< 6.0)
7
7
  stripe (~> 3.21)
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Cash Out
1
+ ![Cash Out](assets/title-image.jpg)
2
2
 
3
3
  A gem developed with the express purpose of reducing development time when integrating
4
4
  with Stripe payments.
@@ -6,13 +6,42 @@ with Stripe payments.
6
6
  This gem is targeted at API development and assumes your frontend is able to generate
7
7
  and send tokenized payment data to your endpoints.
8
8
 
9
+ ## Contents
10
+ 1. [Installation](#installation)
11
+ 1. [Configuration](#configuration)
12
+ 1. [Usage](#usage)
13
+ 1. [Handling Credit Cards](#creating-a-stripe-customer)
14
+ 1. [Creating a Stripe Customer](#creating-a-stripe-customer)
15
+ 1. [Deleting a Stripe Customer](#deleting-a-stripe-customer)
16
+ 1. [Handling Bank Accounts](#creating-a-stripe-connect-custom-account)
17
+ 1. [Creating a Stripe Connect Custom Account](#creating-a-stripe-connect-custom-account)
18
+ 1. [Further Reading](#further-reading)
19
+ 1. [Deleting a Stripe Connect Custom Account](#deleting-a-stripe-connect-custom-account)
20
+ 1. [Creating a Charge](#creating-a-charge)
21
+ 1. [Without a Payout](#without-a-payout)
22
+ 1. [With a Payout](#with-a-payout)
23
+ 1. [Creating a Transfer](#creating-a-transfer)
24
+
25
+
9
26
  ## Installation
10
27
 
11
- 1. Add `gem cash_out` to your Gemfile
28
+ 1. Add `gem cash_out` to your Gemfile.
12
29
  1. `bundle install`
13
30
  1. `rails generate cash_out:install`
14
- 1. Update the items in `config/initializers/cash_out.rb` with your Stripe api keys
15
- 1. Create a migration to add a `stripe_id` field to `User` or whichever model will hold the Stripe token
31
+ 1. Update the items in `config/initializers/cash_out.rb` with your Stripe API keys.
32
+ 1. Create a migration to add a `stripe_id` (String) field to `User` or whichever model will hold the Stripe token.
33
+ 1. If using Stripe Connect (for making payouts), add `date_of_birth`(Date) to your `User` as well.
34
+
35
+ ## Configuration
36
+ CashOut needs to be configured with your Stripe API keys. You can find the config file
37
+ in `config/initializers/cash_out.rb`. If it's not there, run `rails generate cash_out:install`.
38
+
39
+ ```ruby
40
+ CashOut.configure do |config|
41
+ config.stripe_publishable_key = 'your_stripe_public_key'
42
+ config.stripe_secret_key = 'your_stripe_secret_key'
43
+ end
44
+ ```
16
45
 
17
46
  ## Usage
18
47
 
@@ -24,7 +53,7 @@ and will return an appropriate service object.
24
53
  Stripe Customer objects are used for making credit card purchases. You can create a
25
54
  Stripe Customer and store the token on your User with the following code.
26
55
 
27
- Inside your controller, add:
56
+ Sample Code:
28
57
  ```ruby
29
58
  def create
30
59
  CashOut::Payments::Customer::Create.run(customer_params)
@@ -39,12 +68,154 @@ end
39
68
 
40
69
  To remove payment information from your user, run the appropriate delete service
41
70
 
42
- Inside your controller, add:
71
+ Sample Code:
43
72
  ```ruby
44
73
  def destroy
45
74
  CashOut::Payments::Customer::Delete.run(user: current_user)
46
75
  end
47
76
  ```
48
77
 
49
- Note: This currently does not delete the payment information from Stripe. Support
50
- for this will be added in a future release.
78
+ Note: This currently deletes the entire Customer account. Support for deleting
79
+ individual cards will be added in a future release.
80
+
81
+ ### Creating a Stripe Connect Custom Account
82
+
83
+ A Stripe Connect Custom Account is used when your app needs to pay independent
84
+ contractors. This will connect the user's bank account with a Stripe account
85
+ you create for them. This will allow you to make payments out to the user's account
86
+ and make transfers from the user's bank account to settle any outstanding balance.
87
+
88
+ Setting up a Connect Account requires several params. `user` can be any object
89
+ that responds to `stripe_id`, `valid?`, `save`, `date_of_birth`, `first_name`, and `last_name`.
90
+
91
+ | Param | Description |
92
+ :--------------|:------------:|
93
+ | **user** | The user receiving the account. It must respond to `stripe_id`, `valid?`, `save`, `date_of_birth`, `first_name`, and `last_name`
94
+ | **external_account_token** | Tokenized bank account info, generated via frontend app.
95
+ | **ip_address** | The user's current ip address. Can be accessed via `request.remote_ip`
96
+ | **legal_entity_type** | Must be `"individual"` or `"company"`
97
+ | **ssn_last_4** | The last 4 digits of the user's SSN
98
+ | **stripe_terms_accepted** | The frontend must link to the [Stripe Connected Account Agreement](https://stripe.com/connect-account/legal), and the user must accept it.
99
+ | **legal_entity_address** | The address of the individual user or the user's business. Must contain `line1`, `city`, `state`, `country`, and `postal_code`. `line2` is optional.
100
+
101
+ Sample Code:
102
+ ```ruby
103
+ def create
104
+ CashOut::Connect::Account::Create.run(account_params)
105
+ end
106
+
107
+ def account_params
108
+ {
109
+ user: current_user,
110
+ external_account_token: params[:external_account_token],
111
+ ip_address: ,
112
+ legal_entity_type: params[:legal_entity_type],
113
+ ssn_last_4: params[:ssn_last_4],
114
+ stripe_terms_accepted: params[:stripe_terms_accepted],
115
+ legal_entity_address: legal_entity_params
116
+ }
117
+ end
118
+
119
+ def legal_entity_params
120
+ params.require(:address).permit(
121
+ :line1,
122
+ :line2,
123
+ :city,
124
+ :state,
125
+ :country,
126
+ :postal_code
127
+ )
128
+ end
129
+ ```
130
+
131
+ #### Further Reading
132
+ Further documentation regarding Connect accounts may be found on
133
+ [Stripe's Official Connect Documentation](https://stripe.com/docs/api#account)
134
+
135
+ ### Deleting a Stripe Connect Custom Account
136
+
137
+ To remove payment information from your user, run the appropriate delete service
138
+
139
+ Sample Code:
140
+ ```ruby
141
+ def destroy
142
+ CashOut::Connect::Account::Delete.run(user: current_user)
143
+ end
144
+ ```
145
+
146
+ ### Creating a Charge
147
+
148
+ #### Without a Payout
149
+ When creating a charge without a payout, you can send just 2 params
150
+
151
+ | Param | Description |
152
+ :--------------|:------------:|
153
+ | **payor** | The user being charged. It must respond to `stripe_id`. |
154
+ | **amount_to_charge** | The amount of money in cents to be charged. Must be a positive integer. |
155
+
156
+ **Example:**
157
+
158
+ ```ruby
159
+ CashOut::Charge::Create.run(charge_params)
160
+
161
+ def charge_params
162
+ { payor: paying_user, amount_to_charge: charge_in_cents }
163
+ end
164
+ ```
165
+
166
+ #### With a Payout
167
+ Making a charge with a payout is a little more complex, but CashOut aims to handle
168
+ the difficult parts for you. There are 4 params that need to be sent:
169
+
170
+ | Param | Description |
171
+ :--------------|:------------:|
172
+ | **payor** | The user being charged. It must respond to `stripe_id`. |
173
+ | **amount_to_charge** | The amount of money in cents to be charged. Must be a positive integer. |
174
+ | **payee** | The user receiving payment. It also must respond to `stripe_id` |
175
+ | **amount_to_payout** | The amount of money in cents to be paid out. Must be an integer. |
176
+
177
+ **Example:**
178
+
179
+ ```ruby
180
+ CashOut::Charge::Create.run(charge_params)
181
+
182
+ def charge_params
183
+ {
184
+ payor: paying_user,
185
+ amount_to_charge: charge_in_cents,
186
+ payee: user_getting_paid,
187
+ amount_to_payout: payout_in_cents
188
+ }
189
+ end
190
+ ```
191
+
192
+ It's important to know that `amount_to_payout` can be either positive or negative.
193
+
194
+ In the case of a positive payout, the user will receive the `amount_to_payout` in their Stripe account.
195
+ For negative payouts, the `amount_to_payout` will be transfered from the user to the platform (business owned)
196
+ Stripe account.
197
+
198
+ ### Creating a Transfer
199
+ CashOut attempts to handle transfers automatically. However, should the case arise that you need to create
200
+ one manually, you can do so with the following.
201
+
202
+
203
+ | Param | Description |
204
+ :--------------|:------------:|
205
+ | **payee** | The user receiving payment. It also must respond to `stripe_id` |
206
+ | **amount_to_payout** | The amount of money in cents to be paid out. Must be an integer. |
207
+
208
+ ```ruby
209
+ CashOut::Connect::Transfer::Create.run(transfer_params)
210
+
211
+ def transfer_params
212
+ {
213
+ payee: user_getting_paid,
214
+ amount_to_payout: payout_in_cents
215
+ }
216
+ end
217
+ ```
218
+
219
+ **Important Note** When creating a transfer without a corresponding payment, or when the transfer
220
+ amount exceeds the amount of associated charges, the platform Stripe account **MUST** have enough
221
+ available funds to cover the transfer, or the transfer will fail.
Binary file
@@ -1,5 +1,10 @@
1
1
  en:
2
2
  cash_out:
3
+ connect:
4
+ is_required: "is required"
5
+ account_already_exists: "User already has a Stripe account"
6
+ charge:
7
+ invalid_charge_amount: "The charge amount must be a positive integer"
3
8
  customer:
4
9
  account_already_exists: "User already has a Stripe account"
5
10
  invalid_card: "The card entered is not valid"
@@ -1,10 +1,20 @@
1
1
  # frozen_string_literal: true
2
+ # Config
2
3
  require 'cash_out/configuration'
3
4
 
5
+ # External dependencies
4
6
  require 'active_interaction'
5
7
  require 'stripe'
8
+
9
+ # Core Files
6
10
  require 'cash_out/base'
7
11
  require 'cash_out/service_base'
12
+
13
+ # Other Services
14
+ require 'cash_out/charge/create'
15
+ require 'cash_out/connect/account/create'
16
+ require 'cash_out/connect/account/delete'
17
+ require 'cash_out/connect/transfer/create'
8
18
  require 'cash_out/payments/customer/create'
9
19
  require 'cash_out/payments/customer/delete'
10
20
 
@@ -0,0 +1,87 @@
1
+ module CashOut
2
+ module Charge
3
+ class Create < ::CashOut::ServiceBase
4
+ interface :payor, methods: %i(stripe_id)
5
+ interface :payee, methods: %i(stripe_id), default: nil
6
+ integer :amount_to_charge
7
+ integer :amount_to_payout, default: 0
8
+
9
+ validate :charges_are_positive
10
+
11
+ def execute
12
+ create_charge
13
+ end
14
+
15
+ private
16
+
17
+ def create_charge
18
+ # Stripe won't let you create a destination charge if the payout exceeds the charges
19
+ payout_less_than_charge? ? destination_charge : standalone_charge
20
+ end
21
+
22
+ def payout_less_than_charge?
23
+ amount_to_payout.positive? && amount_to_payout < amount_to_charge
24
+ end
25
+
26
+ def destination_charge
27
+ # This is what we should do in most cases
28
+ # amount_to_charge is charged to payor
29
+ # amount_to_payout goes to payee
30
+ # Remainder is kept in company Stripe account
31
+ # https://stripe.com/docs/connect/destination-charges#collecting-platform-fees
32
+ return errors.add(:stripe, I18n.t('cash_out.charge.no_payee')) unless payee
33
+ charge_payor(destination_charge_params)
34
+ end
35
+
36
+ def standalone_charge
37
+ # When dealing with a negative payout, we must create the charge and transfer separately
38
+ charge_payor(standalone_charge_params)
39
+ initiate_transfer
40
+ end
41
+
42
+ def initiate_transfer
43
+ CashOut::Connect::Transfer::Create.run(payee: payee, amount_to_payout: amount_to_payout)
44
+ end
45
+
46
+ def charge_payor(*args)
47
+ Stripe::Charge.create(*args)
48
+ rescue *STRIPE_ERRORS => e
49
+ errors.add(:stripe, e.to_s)
50
+ e.json_body
51
+ end
52
+
53
+ def standalone_charge_params
54
+ # Charges the payor the total amount owed
55
+ {
56
+ amount: amount_to_charge,
57
+ currency: "usd",
58
+ description: "",
59
+ customer: payor.stripe_id
60
+ }
61
+ end
62
+
63
+ def destination_charge_params
64
+ # Charges the payor the total amount owed
65
+ # Sends payout amount to payee
66
+ {
67
+ amount: amount_to_charge,
68
+ currency: "usd",
69
+ description: "",
70
+ customer: payor.stripe_id,
71
+ destination: {
72
+ account: payee.stripe_id,
73
+ amount: amount_to_payout
74
+ }
75
+ }
76
+ end
77
+
78
+ def charges_are_positive
79
+ # Charge will fail if balance due is $0.00
80
+ # Charge should never be negative
81
+ unless amount_to_charge.positive?
82
+ errors.add(:stripe,I18n.t('cash_out.charge.invalid_charge_amount'))
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,93 @@
1
+ module CashOut
2
+ module Connect
3
+ module Account
4
+ class Create < ::CashOut::ServiceBase
5
+ interface :user, methods: %i(stripe_id valid? save date_of_birth first_name last_name)
6
+ string :external_account_token
7
+ string :ip_address
8
+ string :legal_entity_type
9
+ string :ssn_last_4
10
+ boolean :stripe_terms_accepted, default: false
11
+ hash :legal_entity_address do
12
+ string :line1
13
+ string :line2, default: ""
14
+ string :city
15
+ string :state
16
+ string :country
17
+ string :postal_code
18
+ end
19
+
20
+ validates :stripe_terms_accepted, presence: true
21
+ validate :no_existing_stripe_account, :legal_entity_must_be_present
22
+
23
+ def execute
24
+ account = create_stripe_account
25
+ user.stripe_id = account["id"] if account.is_a?(Stripe::Account)
26
+ validate_and_save(user)
27
+ end
28
+
29
+ private
30
+
31
+ def create_stripe_account
32
+ Stripe::Account.create(stripe_account_params)
33
+ rescue *STRIPE_ERRORS => e
34
+ errors.add(:stripe, e.to_s)
35
+ end
36
+
37
+ def stripe_account_params
38
+ {
39
+ type: "custom",
40
+ country: "US",
41
+ tos_acceptance: {
42
+ date: Time.current.to_i,
43
+ ip: ip_address,
44
+ },
45
+ payout_schedule: {
46
+ delay_days: 2,
47
+ interval: "daily"
48
+ },
49
+ debit_negative_balances: true,
50
+ legal_entity: legal_entity_params,
51
+ external_account: external_account_token
52
+ }
53
+ end
54
+
55
+ def legal_entity_params
56
+ {
57
+ type: legal_entity_type,
58
+ first_name: user.first_name,
59
+ last_name: user.last_name,
60
+ ssn_last_4: ssn_last_4,
61
+ dob: {
62
+ day: user.date_of_birth.day,
63
+ month: user.date_of_birth.month,
64
+ year: user.date_of_birth.year,
65
+ },
66
+ address: {
67
+ line1: legal_entity_address[:line1],
68
+ line2: legal_entity_address[:line2],
69
+ city: legal_entity_address[:city],
70
+ state: legal_entity_address[:state],
71
+ country: legal_entity_address[:country],
72
+ postal_code: legal_entity_address[:postal_code]
73
+ }
74
+ }
75
+ end
76
+
77
+ def no_existing_stripe_account
78
+ errors.add(:stripe, I18n.t('cash_out.connect.account_already_exists')) if user.stripe_id.present?
79
+ end
80
+
81
+ def legal_entity_must_be_present
82
+ missing_fields.each { |mf| errors.add(mf, I18n.t('cash_out.connect.is_required')) }
83
+ end
84
+
85
+ def missing_fields
86
+ legal_entity_address.except(:line2).map do |key, value|
87
+ key.to_sym if value.empty?
88
+ end.compact
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,23 @@
1
+ module CashOut
2
+ module Connect
3
+ module Account
4
+ class Delete < ::CashOut::ServiceBase
5
+ interface :user, methods: %i(stripe_id save valid?)
6
+
7
+ def execute
8
+ delete_account
9
+ user.stripe_id = nil
10
+ validate_and_save(user)
11
+ end
12
+
13
+ private
14
+
15
+ def delete_account
16
+ Stripe::Account.retrieve(user.stripe_id).delete
17
+ rescue *STRIPE_ERRORS => e
18
+ errors.add(:stripe, e.to_s)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,69 @@
1
+ module CashOut
2
+ module Connect
3
+ module Transfer
4
+ class Create < ::CashOut::ServiceBase
5
+ interface :payee, methods: %i(stripe_id)
6
+ integer :amount_to_payout
7
+
8
+ def execute
9
+ initiate_transfer
10
+ end
11
+
12
+ private
13
+
14
+ def initiate_transfer
15
+ Stripe::Transfer.create(*params)
16
+ rescue *STRIPE_ERRORS => e
17
+ errors.add(:stripe, e.to_s)
18
+ e.json_body
19
+ end
20
+
21
+ def params
22
+ # Stripe does not allow negative transfers, so we must change the params accordingly
23
+ amount_to_payout.positive? ? positive_payout_params : negative_payout_params
24
+ end
25
+
26
+ def negative_payout_params
27
+ # Makes a transfer from the payee to the platform account for amount owed
28
+ # https://stripe.com/docs/connect/account-debits#transferring-from-a-connected-account
29
+ [
30
+ {
31
+ amount: amount_to_payout.abs,
32
+ currency: "usd",
33
+ description: "",
34
+ destination: platform_stripe_account.id
35
+ },
36
+ {
37
+ stripe_account: payee.stripe_id
38
+ }
39
+ ]
40
+ end
41
+
42
+ def positive_payout_params
43
+ # Only runs when payout is positive but payout amount > charges
44
+ # Makes a transfer from platform account to payee
45
+ # Platform Stripe Account balance MUST be greater than transfer amount
46
+ [
47
+ {
48
+ amount: amount_to_payout,
49
+ currency: "usd",
50
+ description: "",
51
+ destination: payee.stripe_id
52
+ },
53
+ {
54
+ stripe_account: platform_stripe_account.id,
55
+ }
56
+ ]
57
+ end
58
+
59
+ def platform_stripe_account
60
+ # Calling retrieve without a param retrieves the platform account
61
+ # In test mode, create charges with 4000 0000 0000 0077 to add funds
62
+ # https://stripe.com/docs/testing#cards-responses
63
+ @platform_stripe_account ||= Stripe::Account.retrieve
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+
@@ -2,7 +2,7 @@ module CashOut
2
2
  module Payments
3
3
  module Customer
4
4
  class Create < ::CashOut::ServiceBase
5
- interface :user, methods: [:stripe_id]
5
+ interface :user, methods: %i(stripe_id valid? save)
6
6
  string :stripe_token, default: nil
7
7
 
8
8
  validate :customer_is_nil
@@ -1,4 +1,4 @@
1
1
  module CashOut
2
- VERSION = "0.0.2"
2
+ VERSION = "0.1.0"
3
3
  end
4
4
 
@@ -1,5 +1,4 @@
1
1
  CashOut.configure do |config|
2
2
  config.stripe_publishable_key = ''
3
3
  config.stripe_secret_key = ''
4
- config.stripe_api_key = ''
5
4
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cash_out
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tyler Rockwell
@@ -149,13 +149,18 @@ files:
149
149
  - LICENSE.txt
150
150
  - README.md
151
151
  - Rakefile
152
+ - assets/title-image.jpg
152
153
  - bin/console
153
154
  - bin/setup
154
155
  - cash_out.gemspec
155
156
  - config/locales/en.yml
156
157
  - lib/cash_out.rb
157
158
  - lib/cash_out/base.rb
159
+ - lib/cash_out/charge/create.rb
158
160
  - lib/cash_out/configuration.rb
161
+ - lib/cash_out/connect/account/create.rb
162
+ - lib/cash_out/connect/account/delete.rb
163
+ - lib/cash_out/connect/transfer/create.rb
159
164
  - lib/cash_out/payments/customer/create.rb
160
165
  - lib/cash_out/payments/customer/delete.rb
161
166
  - lib/cash_out/service_base.rb