cash_out 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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