moneytree-rails 0.1.3 β†’ 0.1.4

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
  SHA256:
3
- metadata.gz: 2b651472c44c921358698e5469bd1fee526bcf70987e63ca516b2bf04fa11aa8
4
- data.tar.gz: 2f2838ef7ee618326b1cf2b1e4fb1ebded13a051450f673193236da3a630a955
3
+ metadata.gz: 779e0ff0793ee2a7c983a518fddc541d947789a1389e272348b011de846fa6ef
4
+ data.tar.gz: d6a4598c886d4f650aef78f1bfb44d0e1d6372f887e6610d219c9e0a9bd16d4e
5
5
  SHA512:
6
- metadata.gz: 78f099d2dbb4f5c7bafc75376befdfdf1b1d55edfc356a02a3a18116288eeddb0515cd8bf3d277cfb43ac458e9497a160c6e7286df2c9f715dc8dd62bec1f169
7
- data.tar.gz: a3324a522e1c3350a2740684a48917bd3d637850281d49198992269719151372ae9c050a8075c02b9fccc24703b22a3cde687af0e3931915eb26ddf14e44c9ac
6
+ metadata.gz: 871ff1ec60cc065d4508e2a8e3aa3fd82b9cac5abb955975ecdfdfd79619520317daab2ee69df13f6b122861181e516dcc192bf55f0bd5049c5166249715911c
7
+ data.tar.gz: bd4a338cec1216e8e6cfad8a861d0b21344cfc5fa2d14eb34baeccf109b6569493a002fa19fa31e38632bbf3d3c7419b4417f3d2093eaed91d6b0b036910a197
data/README.md CHANGED
@@ -1,9 +1,5 @@
1
1
  # 🚧 WORK IN PROGRESS 🚧
2
2
 
3
- Currently only supports:
4
-
5
- - Oauth flow for stripe
6
-
7
3
  # Moneytree πŸ’΅ 🌴
8
4
 
9
5
  [![Actions Status](https://github.com/kieranklaassen/moneytree/workflows/build/badge.svg)](https://github.com/kieranklaassen/moneytree/actions)
@@ -16,16 +12,16 @@ functionality with almost no work on your end:
16
12
 
17
13
  - πŸ’΅πŸ’ΆπŸ’·πŸ’΄ Multi-currency
18
14
  - πŸ”‘ OAuth to link your PSP account
19
- - πŸ‘©β€πŸ’»PSP account creation, (with commission)
20
- - βš™οΈ Webhooks
21
- - πŸ’³ PCI compliance with Javascript libraries
15
+ - πŸ‘©β€πŸ’» PSP account creation, (with commission)
16
+ - βš™οΈ ~~Webhooks~~ comming soon
17
+ - πŸ’³ ~~PCI compliance with Javascript libraries~~ comming soon
22
18
  - 🧲 Platform fees a.k.a. Market Places
23
19
 
24
20
  Currently we support the following PSP's:
25
21
 
26
- - ~~Square~~
22
+ - ~~Square~~ comming soon
27
23
  - Stripe
28
- - ~~Braintree~~
24
+ - ~~Braintree~~ comming soon
29
25
 
30
26
  But if you want to add more PSP's, we make it easy to do so. Read our
31
27
  [Contributing](https://github.com/kieranklaassen/moneytree#contributing) section to learn more.
@@ -53,6 +49,8 @@ $ rails g db:migrate
53
49
 
54
50
  ## Configuration
55
51
 
52
+ ### Initializer
53
+
56
54
  Do you need to make some changes to how Moneytree is used? You can create an initializer
57
55
  `config/initializers/moneytree.rb`
58
56
 
@@ -64,18 +62,23 @@ Moneytree.setup do |config|
64
62
  client_id: ENV['STRIPE_CLIENT_ID']
65
63
  }
66
64
  config.oauth_redirect = '/welcome_back'
65
+ config.refund_application_fee = true # false by default
67
66
  end
68
67
  ```
69
68
 
70
- Add to your routes and authenticate if needed:
69
+ ### Routes
70
+
71
+ Add to your routes and authenticate if needed to make sure only admins can integrate with OAuth.
71
72
 
72
73
  ```ruby
73
- authenticate :user, ->(u) { u.owner? } do
74
+ authenticate :user, ->(u) { u.admin? } do
74
75
  mount Moneytree::Engine => '/moneytree'
75
76
  end
76
77
  ```
77
78
 
78
- Include account concern into your model and make sure the following attributes work:
79
+ ### Models
80
+
81
+ Include account concern into your models and make sure the following attributes work:
79
82
 
80
83
  ```ruby
81
84
  class Merchant < ApplicationRecord
@@ -95,6 +98,15 @@ class Merchant < ApplicationRecord
95
98
  end
96
99
  ```
97
100
 
101
+ And add `Moneytree::Order` concern to the model that will be the parent for all the transactions. In most cases this
102
+ will be an order. This model will keep a balance and you can add multiple payments and refunds to it.
103
+
104
+ ```ruby
105
+ class Order < ApplicationRecord
106
+ include Moneytree::Order
107
+ end
108
+ ```
109
+
98
110
  ## Usage
99
111
 
100
112
  ### The Primitives
@@ -126,28 +138,9 @@ intended to be a safe, welcoming space for collaboration, and contributors are e
126
138
 
127
139
  ## License
128
140
 
129
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
141
+ The gem is available as open-source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
130
142
 
131
143
  ## Code of Conduct
132
144
 
133
145
  Everyone interacting in the Moneytree project's codebases, issue trackers, chat rooms and mailing lists is expected to
134
146
  follow the [code of conduct](https://github.com/kieranklaassen/moneytree/blob/master/CODE_OF_CONDUCT.md).
135
-
136
- ```
137
- rails g model payment_gateway psp_credentials:text moneytree_psp:integer account:references{polymorphic}
138
-
139
- owner t.string :name t.text :psp_credentials t.integer :moneytree_psp
140
-
141
- rails g model orders t.string :description t.string :remote_identifier t.references :customer t.references :account
142
-
143
- rails g model transactions t.decimal :amount t.decimal :app_fee_amount t.integer :status t.integer :type t.string
144
- :remote_identifier t.string :currency_code t.string :psp_error t.integer :moneytree_psp t.references :account
145
- t.references :order t.references :card
146
-
147
- rails g model customers t.string :first_name t.string :last_name t.string :email t.string :remote_identifier t.integer
148
- :moneytree_psp t.references :account
149
-
150
- rails g model cards t.string :card_brand t.string :last_4 t.integer :expiration_month t.integer :expiration_year
151
- t.string :cardholder_name t.string :fingerprint t.integer :moneytree_psp t.references :customer t.references :account
152
-
153
- ```
@@ -27,7 +27,7 @@ module Moneytree
27
27
  response_type: :code,
28
28
  client_id: Moneytree.stripe_credentials[:client_id],
29
29
  scope: PaymentProvider::Stripe::PERMISSION,
30
- redirect_uri: oauth_stripe_callback_url, # FIXME: use rails url helper and add host
30
+ redirect_uri: oauth_stripe_callback_url,
31
31
  'stripe_user[email]': current_account.email,
32
32
  'stripe_user[url]': current_account.website,
33
33
  'stripe_user[currency]': current_account.currency_code
@@ -1,8 +1,66 @@
1
1
  module Moneytree
2
2
  module Webhooks
3
3
  class StripeController < ApplicationController
4
+ skip_before_action :verify_authenticity_token
5
+
4
6
  def create
5
- # Do some callback magic here
7
+ case webhook_params.type
8
+ when 'charge.succeeded'
9
+ process_charge!
10
+ when 'charge.refunded'
11
+ process_refund!
12
+ else
13
+ puts "Unhandled event type: #{webhook_params.type}"
14
+ end
15
+
16
+ head :ok
17
+ end
18
+
19
+ private
20
+
21
+ def webhook_params
22
+ @webhook_params ||=
23
+ ::Stripe::Event.construct_from(
24
+ JSON.parse(request.body.read, symbolize_names: true)
25
+ )
26
+ end
27
+
28
+ def process_charge!
29
+ return if transaction.completed?
30
+
31
+ transaction.process_response(
32
+ TransactionResponse.new(:success, '', { charge_id: stripe_object.id })
33
+ )
34
+
35
+ if Moneytree.order_status_trigger_method
36
+ transaction.order.send(Moneytree.order_status_trigger_method, transaction)
37
+ end
38
+ end
39
+
40
+ def process_refund!
41
+ stripe_object.refunds.data.each do |stripe_refund_object|
42
+ # TODO: Create refund transaction in db for PSP-initiated refunds
43
+ next if stripe_refund_object.metadata[:moneytree_transaction_id].blank?
44
+
45
+ refund = transaction.refunds.find(stripe_refund_object.metadata[:moneytree_transaction_id])
46
+
47
+ next if refund.completed?
48
+
49
+ refund.process_response(
50
+ TransactionResponse.new(:success, '')
51
+ )
52
+ if Moneytree.order_status_trigger_method
53
+ transaction.order.send(Moneytree.order_status_trigger_method, transaction)
54
+ end
55
+ end
56
+ end
57
+
58
+ def transaction
59
+ @transaction ||= Transaction.find(stripe_object.metadata[:moneytree_transaction_id])
60
+ end
61
+
62
+ def stripe_object
63
+ @stripe_object ||= webhook_params.data.object
6
64
  end
7
65
  end
8
66
  end
@@ -0,0 +1,23 @@
1
+ module Moneytree
2
+ class Payment < Transaction
3
+ has_many :refunds, class_name: 'Refund'
4
+
5
+ validates_absence_of :payment_id
6
+
7
+ validates_numericality_of :amount, greater_than: 0
8
+ validates_numericality_of :app_fee_amount, greater_than_or_equal_to: 0
9
+
10
+ private
11
+
12
+ def execute_transaction(metadata: {})
13
+ process_response(
14
+ payment_gateway.charge(
15
+ amount,
16
+ details,
17
+ app_fee_amount: app_fee_amount,
18
+ metadata: metadata.merge(moneytree_transaction_id: id)
19
+ )
20
+ )
21
+ end
22
+ end
23
+ end
@@ -1,14 +1,12 @@
1
1
  module Moneytree
2
2
  class PaymentGateway < ApplicationRecord
3
- include Moneytree::Account
4
-
5
3
  belongs_to :account, polymorphic: true
6
4
 
7
5
  enum psp: Moneytree::PSPS
8
6
  serialize :psp_credentials
9
7
  # encrypts :psp_credentials
10
8
  # FIXME: enable https://github.com/ankane/lockbox
11
- delegate :oauth_link, :scope_correct?, to: :payment_provider
9
+ delegate :oauth_link, :scope_correct?, :charge, :refund, to: :payment_provider
12
10
 
13
11
  # has_many :orders
14
12
  # has_many :transactions
@@ -31,16 +29,13 @@ module Moneytree
31
29
  psp_credentials[:scope] == payment_provider.scope
32
30
  end
33
31
 
34
- def charge; end
35
-
36
- def refund; end
37
-
38
32
  private
39
33
 
40
34
  def payment_provider
41
35
  @payment_provider ||=
42
36
  case psp
43
37
  when 'stripe'
38
+ # TODO: see if we only need to pass credentials
44
39
  Moneytree::PaymentProvider::Stripe.new(self)
45
40
  # when 'square'
46
41
  # Moneytree::PaymentProvider::Square.new(self)
@@ -0,0 +1,46 @@
1
+ module Moneytree
2
+ class Refund < Transaction
3
+ belongs_to :payment, class_name: 'Moneytree::Payment'
4
+
5
+ before_validation :set_order, :set_payment_gateway
6
+
7
+ validates_presence_of :payment
8
+
9
+ validates_numericality_of :amount, less_than: 0
10
+ validates_numericality_of :app_fee_amount, less_than_or_equal_to: 0
11
+
12
+ validate :order_matches_payment, :gateway_matches_payment
13
+
14
+ private
15
+
16
+ def execute_transaction(metadata: {})
17
+ process_response(
18
+ payment_gateway.refund(
19
+ amount,
20
+ payment.details,
21
+ metadata: metadata.merge(moneytree_transaction_id: id)
22
+ )
23
+ )
24
+ end
25
+
26
+ # validates_presence_of :payment
27
+ def set_order
28
+ self.order ||= payment&.order
29
+ end
30
+
31
+ # validates_presence_of :payment
32
+ def set_payment_gateway
33
+ self.payment_gateway ||= payment&.payment_gateway
34
+ end
35
+
36
+ # validates
37
+ def order_matches_payment
38
+ errors.add(:order_id, :mismatch) if order_id != payment&.order_id
39
+ end
40
+
41
+ # validates
42
+ def gateway_matches_payment
43
+ errors.add(:payment_gateway_id, :mismatch) if payment_gateway_id != payment&.payment_gateway_id
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,30 @@
1
+ module Moneytree
2
+ class Transaction < ApplicationRecord
3
+ belongs_to :payment_gateway
4
+ belongs_to :order, polymorphic: true
5
+
6
+ validates_presence_of :psp_error, if: :failed?
7
+
8
+ enum status: %i[initialized pending completed failed]
9
+
10
+ serialize :details
11
+
12
+ after_create_commit :execute_transaction
13
+
14
+ def process_response(response)
15
+ if response.success?
16
+ update!(
17
+ status: :completed,
18
+ psp_error: response.message,
19
+ details: (details || {}).merge(response.body)
20
+ )
21
+ else
22
+ # FIXME: pending state
23
+ update!(
24
+ status: :failed,
25
+ psp_error: response.message
26
+ )
27
+ end
28
+ end
29
+ end
30
+ end
@@ -1,4 +1,8 @@
1
1
  Moneytree::Engine.routes.draw do
2
2
  get 'oauth/stripe/new', to: 'oauth/stripe#new'
3
3
  get 'oauth/stripe/callback', to: 'oauth/stripe#callback'
4
+
5
+ namespace :webhooks do
6
+ resources :stripe, only: :create
7
+ end
4
8
  end
@@ -0,0 +1,18 @@
1
+ class CreateMoneytreeTransactions < ActiveRecord::Migration[6.0]
2
+ def change
3
+ create_table :moneytree_transactions do |t|
4
+ t.decimal :amount, null: false, default: 0
5
+ t.decimal :app_fee_amount, null: false, default: 0
6
+ t.integer :status, null: false, default: 0
7
+ t.string :type, null: false, default: 'Moneytree::Payment'
8
+ t.references :order, polymorphic: true, null: false
9
+ t.references :payment_gateway, null: false
10
+ t.references :payment, foreign_key: { to_table: :moneytree_transactions }
11
+ t.text :psp_error
12
+ t.text :details
13
+ t.text :refund_reason
14
+
15
+ t.timestamps
16
+ end
17
+ end
18
+ end
@@ -5,7 +5,9 @@
5
5
  # modules
6
6
  # FIXME: autoload instead? https :/ / github.com / excid3 / noticed / blob / master / lib / noticed.rb
7
7
  require 'moneytree/version'
8
+ require 'moneytree/transaction_response'
8
9
  require 'moneytree/account'
10
+ require 'moneytree/order'
9
11
  require 'moneytree/payment_provider/base'
10
12
  require 'moneytree/payment_provider/stripe'
11
13
  require 'moneytree/engine'
@@ -17,10 +19,13 @@ module Moneytree
17
19
  mattr_accessor :stripe_credentials
18
20
  mattr_accessor :current_account
19
21
  mattr_accessor :oauth_redirect
22
+ mattr_accessor :refund_application_fee
23
+ mattr_accessor :order_status_trigger_method
20
24
 
21
25
  @@enabled_psps = PSPS
22
26
  @@current_account = :current_account
23
27
  @@oauth_redirect = '/'
28
+ @@refund_application_fee = false
24
29
 
25
30
  def self.setup
26
31
  yield self
@@ -3,9 +3,10 @@ require 'active_support/concern'
3
3
  module Moneytree
4
4
  module Account
5
5
  extend ActiveSupport::Concern
6
+ # FIXME: see if we can remove config.current_account = :current_merchant and set it from here
6
7
 
7
8
  included do
8
- has_one :moneytree_payment_gateway, class_name: 'Moneytree::PaymentGateway', foreign_key: 'account_id'
9
+ has_one :moneytree_payment_gateway, class_name: 'Moneytree::PaymentGateway', foreign_key: 'account_id', inverse_of: :account, as: :account
9
10
  end
10
11
  end
11
12
  end
@@ -0,0 +1,15 @@
1
+ require 'active_support/concern'
2
+
3
+ module Moneytree
4
+ module Order
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ has_many :moneytree_transactions, class_name: 'Moneytree::Transaction', foreign_key: 'order_id', inverse_of: :order, as: :order
9
+ end
10
+
11
+ def new_payment(*args)
12
+ moneytree_transactions << Payment.new(*args)
13
+ end
14
+ end
15
+ end
@@ -14,16 +14,62 @@ module Moneytree
14
14
 
15
15
  def get_access_token(params)
16
16
  # FIXME: add error handling
17
- ::Stripe::OAuth.token({
18
- grant_type: 'authorization_code',
19
- code: params[:code]
20
- }).to_hash
17
+ ::Stripe::OAuth.token(
18
+ {
19
+ grant_type: 'authorization_code',
20
+ code: params[:code]
21
+ }
22
+ ).to_hash
21
23
  end
22
24
 
23
25
  def scope
24
26
  PERMISSION.to_s
25
27
  end
26
28
 
29
+ def charge(amount, details, app_fee_amount: 0, description: "Charge for #{account.name}", metadata:)
30
+ # `source` is obtained with Stripe.js; see https://stripe.com/docs/payments/accept-a-payment-charges#web-create-token
31
+ response = ::Stripe::Charge.create(
32
+ {
33
+ amount: (amount * 100).to_i,
34
+ currency: account.currency_code,
35
+ source: details[:card_token],
36
+ description: description,
37
+ metadata: metadata,
38
+ application_fee_amount: (app_fee_amount * 100).to_i
39
+ },
40
+ stripe_account: payment_gateway.psp_credentials[:stripe_user_id]
41
+ )
42
+ # succeeded, pending, or failed
43
+ TransactionResponse.new(
44
+ { succeeded: :success, pending: :pending, failed: :failed }[response[:status].to_sym],
45
+ response[:failure_message],
46
+ { charge_id: response[:id] }
47
+ )
48
+ rescue ::Stripe::StripeError => e
49
+ TransactionResponse.new(:failed, e.message)
50
+ end
51
+
52
+ def refund(amount, details, metadata:)
53
+ response = ::Stripe::Refund.create(
54
+ {
55
+ charge: details[:charge_id],
56
+ amount: (-amount * 100).to_i,
57
+ metadata: metadata,
58
+ refund_application_fee: Moneytree.refund_application_fee
59
+ },
60
+ stripe_account: payment_gateway.psp_credentials[:stripe_user_id]
61
+ )
62
+
63
+ # succeeded, pending, or failed
64
+ TransactionResponse.new(
65
+ { succeeded: :success, pending: :pending, failed: :failed }[response[:status].to_sym],
66
+ response[:failure_message],
67
+ { refund_id: response[:id] }
68
+ )
69
+ rescue ::Stripe::StripeError => e
70
+ TransactionResponse.new(:failed, e.message)
71
+ end
72
+
27
73
  private
28
74
 
29
75
  def credentitals
@@ -0,0 +1,15 @@
1
+ module Moneytree
2
+ class TransactionResponse
3
+ attr_reader :message, :status, :body
4
+
5
+ def initialize(status, message, body = {})
6
+ @status = status
7
+ @message = message
8
+ @body = body
9
+ end
10
+
11
+ def success?
12
+ @status == :success
13
+ end
14
+ end
15
+ end
@@ -1,3 +1,3 @@
1
1
  module Moneytree
2
- VERSION = '0.1.3'.freeze
2
+ VERSION = '0.1.4'.freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: moneytree-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kieran Klaassen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-10-08 00:00:00.000000000 Z
11
+ date: 2020-11-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -56,22 +56,26 @@ files:
56
56
  - app/controllers/moneytree/oauth/stripe_controller.rb
57
57
  - app/controllers/moneytree/webhooks/square_controller.rb
58
58
  - app/controllers/moneytree/webhooks/stripe_controller.rb
59
- - app/helpers/moneytree/application_helper.rb
60
- - app/helpers/moneytree/oauth/stripe_helper.rb
61
59
  - app/jobs/moneytree/application_job.rb
62
60
  - app/mailers/moneytree/application_mailer.rb
63
61
  - app/models/moneytree/application_record.rb
62
+ - app/models/moneytree/payment.rb
64
63
  - app/models/moneytree/payment_gateway.rb
64
+ - app/models/moneytree/refund.rb
65
+ - app/models/moneytree/transaction.rb
65
66
  - app/views/layouts/moneytree/application.html.erb
66
67
  - config/routes.rb
67
68
  - db/migrate/20200914151648_create_moneytree_payment_gateways.rb
69
+ - db/migrate/20201008161617_create_moneytree_transactions.rb
68
70
  - lib/moneytree-rails.rb
69
71
  - lib/moneytree.rb
70
72
  - lib/moneytree/account.rb
71
73
  - lib/moneytree/engine.rb
74
+ - lib/moneytree/order.rb
72
75
  - lib/moneytree/payment_provider/base.rb
73
76
  - lib/moneytree/payment_provider/square.rb
74
77
  - lib/moneytree/payment_provider/stripe.rb
78
+ - lib/moneytree/transaction_response.rb
75
79
  - lib/moneytree/version.rb
76
80
  - lib/tasks/moneytree_tasks.rake
77
81
  homepage: https://github.com/kieranklaassen/moneytree
@@ -1,4 +0,0 @@
1
- module Moneytree
2
- module ApplicationHelper
3
- end
4
- end
@@ -1,6 +0,0 @@
1
- module Moneytree
2
- module Oauth
3
- module StripeHelper
4
- end
5
- end
6
- end