flowcommerce-solidus 0.1.11
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.version +1 -0
- data/bin/flowcommerce-solidus +121 -0
- data/lib/flowcommerce-solidus.rb +7 -0
- data/static/app/flow/README.md +77 -0
- data/static/app/flow/SOLIDUS_FLOW.md +127 -0
- data/static/app/flow/decorators/admin_decorators.rb +34 -0
- data/static/app/flow/decorators/localized_coupon_code_decorator.rb +49 -0
- data/static/app/flow/decorators/spree_credit_card_decorator.rb +35 -0
- data/static/app/flow/decorators/spree_order_decorator.rb +128 -0
- data/static/app/flow/decorators/spree_product_decorator.rb +16 -0
- data/static/app/flow/decorators/spree_user_decorator.rb +16 -0
- data/static/app/flow/decorators/spree_variant_decorator.rb +124 -0
- data/static/app/flow/flow.rb +67 -0
- data/static/app/flow/flow/error.rb +46 -0
- data/static/app/flow/flow/experience.rb +51 -0
- data/static/app/flow/flow/order.rb +267 -0
- data/static/app/flow/flow/pay_pal.rb +27 -0
- data/static/app/flow/flow/session.rb +77 -0
- data/static/app/flow/flow/simple_crypt.rb +30 -0
- data/static/app/flow/flow/simple_gateway.rb +123 -0
- data/static/app/flow/flow/webhook.rb +62 -0
- data/static/app/flow/lib/flow_api_refresh.rb +89 -0
- data/static/app/flow/lib/spree_flow_gateway.rb +86 -0
- data/static/app/flow/lib/spree_stripe_gateway.rb +142 -0
- data/static/app/views/spree/admin/payments/index.html.erb +37 -0
- data/static/app/views/spree/admin/promotions/edit.html.erb +59 -0
- data/static/app/views/spree/admin/shared/_order_summary.html.erb +58 -0
- data/static/app/views/spree/admin/shared/_order_summary_flow.html.erb +13 -0
- data/static/app/views/spree/order_mailer/confirm_email.html.erb +85 -0
- data/static/app/views/spree/order_mailer/confirm_email.text.erb +41 -0
- data/static/lib/tasks/flow.rake +248 -0
- metadata +160 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c0101a0d46954d016cd3dac32a96708081c02d77
|
4
|
+
data.tar.gz: 7311c515495ab8d4f004c641619e590c17c70a2d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e6a81592b490a7ea0b8efe4f1a9673eb1e3a606798d81c862a7cf8eb66595bf03be586504dc2986bce2cc184fe9aeaacb0016a3b02306d52be5ae72d67d34747
|
7
|
+
data.tar.gz: 7c2ba0914d7c8d5e39435e9711b1daf15dec210eca659d244caae85fa0719b1a9968c61064107281f694d90a83373833b979120b27c51af94fb980e45fe8173d
|
data/.version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.11
|
@@ -0,0 +1,121 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'thor'
|
4
|
+
require 'fileutils'
|
5
|
+
require 'colorize'
|
6
|
+
|
7
|
+
module Helper
|
8
|
+
extend self
|
9
|
+
|
10
|
+
def copy_with_path src, destionation
|
11
|
+
target_dir = File.dirname(destionation)
|
12
|
+
|
13
|
+
FileUtils.mkdir_p(target_dir) unless Dir.exists?(target_dir)
|
14
|
+
FileUtils.cp src, destionation
|
15
|
+
end
|
16
|
+
|
17
|
+
def die text
|
18
|
+
puts text.red
|
19
|
+
exit
|
20
|
+
end
|
21
|
+
|
22
|
+
def glob_files root_folders
|
23
|
+
Cli.die './app not found, not in Rails root' unless Dir.exists?('%s/app' % Dir.pwd)
|
24
|
+
|
25
|
+
root_folder = `gem which flowcommerce-solidus`.chomp.sub('/lib/flowcommerce-solidus.rb', '/static')
|
26
|
+
|
27
|
+
files = root_folders.inject([]) do |total, folder|
|
28
|
+
total += Dir['%s/%s/**/*' % [root_folder, folder]]
|
29
|
+
end
|
30
|
+
|
31
|
+
files = files
|
32
|
+
.map { |f| f.sub('%s/' % root_folder, '') }
|
33
|
+
.select{ |f| f =~ /\.\w+$/ }
|
34
|
+
|
35
|
+
has_different_size_file = false
|
36
|
+
|
37
|
+
files.each do |file|
|
38
|
+
source = [root_folder, file].join('/')
|
39
|
+
target = [Dir.pwd, file].join('/')
|
40
|
+
|
41
|
+
do_copy = if File.exists?(target)
|
42
|
+
if File.size(target) == File.size(source)
|
43
|
+
print 'same size'.rjust(15).green
|
44
|
+
else
|
45
|
+
print 'different size'.rjust(15).blue
|
46
|
+
has_different_size_file = true
|
47
|
+
end
|
48
|
+
|
49
|
+
false
|
50
|
+
else
|
51
|
+
print 'copied'.rjust(15).green
|
52
|
+
true
|
53
|
+
end
|
54
|
+
|
55
|
+
Helper.copy_with_path source, target if do_copy
|
56
|
+
|
57
|
+
puts ' %s' % file
|
58
|
+
end
|
59
|
+
|
60
|
+
puts
|
61
|
+
puts ' '*14+'* files with different sizes you have to first deleted or renamed'.yellow if has_different_size_file
|
62
|
+
puts ' '*14+'* checked %d files'.yellow % files.length
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
###
|
67
|
+
|
68
|
+
class FlowcommerceSolidus < Thor
|
69
|
+
desc 'install', 'Installs Flow Solidus libs in current installation, if missing.'
|
70
|
+
def install
|
71
|
+
Helper.glob_files ['app/flow', 'app/views/spree/shared', 'lib']
|
72
|
+
end
|
73
|
+
|
74
|
+
desc 'install_admin_views', 'Installs views (erb templates) in ./app/views/admin'
|
75
|
+
def install_admin_views
|
76
|
+
Helper.glob_files ['app/views/spree/admin']
|
77
|
+
end
|
78
|
+
|
79
|
+
desc 'install_email_views', 'Installs custom Flow views (erb templates) for email localization'
|
80
|
+
def install_email_views
|
81
|
+
Helper.glob_files ['app/views/spree/order_mailer']
|
82
|
+
end
|
83
|
+
|
84
|
+
desc 'dev_copy [folder]','Copy files from local solids to gem', :hide => true
|
85
|
+
def copy_to_gem solidus_folder=nil
|
86
|
+
Cli.die 'Solidus target folder not defined' unless solidus_folder
|
87
|
+
Cli.die './app not found' unless Dir.exists?('%s/app' % solidus_folder)
|
88
|
+
Cli.die 'Not executed in gem root folder' unless $0.include('/flowcommerce-solidus/bin')
|
89
|
+
|
90
|
+
`rm -rf ./static`
|
91
|
+
`mkdir ./static`
|
92
|
+
|
93
|
+
# add flow folder
|
94
|
+
files = Dir['%s/app/flow/**/*' % solidus_folder]
|
95
|
+
|
96
|
+
# admin view changes
|
97
|
+
files += Dir['%s/app/views/admin/*' % solidus_folder]
|
98
|
+
|
99
|
+
# add rake tasks
|
100
|
+
files += ['%s/lib/tasks/flow.rake' % solidus_folder]
|
101
|
+
|
102
|
+
# relativise paths and remove folders
|
103
|
+
files = files
|
104
|
+
.map { |f| f.sub(solidus_folder+'/', '') }
|
105
|
+
.select{ |f| f =~ /\.\w+$/ }
|
106
|
+
|
107
|
+
# copy file to ./static dir in gem home
|
108
|
+
files.each do |file|
|
109
|
+
source = [solidus_folder, file].join('/')
|
110
|
+
target = './static/%s' % file
|
111
|
+
|
112
|
+
Helper.copy_with_path source, target
|
113
|
+
end
|
114
|
+
|
115
|
+
puts files
|
116
|
+
|
117
|
+
puts 'Copied %d files to ./static'.green % files.length
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
FlowcommerceSolidus.start ARGV
|
@@ -0,0 +1,77 @@
|
|
1
|
+
<img align="right" src="http://i.imgur.com/tov8bTw.png">
|
2
|
+
|
3
|
+
# Flow.io adapter Solidus/Spree
|
4
|
+
|
5
|
+
Work in progress. This will be converted to gem.
|
6
|
+
|
7
|
+
All flow libs are located in ./app/flow folder with exception of two controllers
|
8
|
+
ApplicationController and FlowController that are present in ./app/controllers folder.
|
9
|
+
|
10
|
+
|
11
|
+
## Instalation
|
12
|
+
|
13
|
+
Define this additional ENV variables. You will find them in [Flow console](https://console.flow.io)
|
14
|
+
|
15
|
+
```
|
16
|
+
FLOW_API_KEY='SUPERsecretTOKEN'
|
17
|
+
FLOW_ORGANIZATION='solidus-app-sandbox'
|
18
|
+
FLOW_BASE_COUNTRY='usa'
|
19
|
+
```
|
20
|
+
|
21
|
+
In ```./config/application.rb``` this is the only peace of code that is needed to
|
22
|
+
init complete flow app
|
23
|
+
|
24
|
+
```
|
25
|
+
config.to_prepare do
|
26
|
+
# add all flow libs
|
27
|
+
overload = Dir.glob('./app/flow/**/*.rb')
|
28
|
+
overload.reverse.each { |c| require(c) }
|
29
|
+
end
|
30
|
+
|
31
|
+
config.after_initialize do |app|
|
32
|
+
# init Flow payments as an option
|
33
|
+
app.config.spree.payment_methods << Spree::Gateway::Flow
|
34
|
+
|
35
|
+
# define defaults
|
36
|
+
Flow.organization = ENV.fetch('FLOW_ORGANIZATION')
|
37
|
+
Flow.base_country = ENV.fetch('FLOW_BASE_COUNTRY')
|
38
|
+
Flow.api_key = ENV.fetch('FLOW_API_KEY')
|
39
|
+
end
|
40
|
+
```
|
41
|
+
|
42
|
+
in ./config/application.rb to enable payments with Flow.
|
43
|
+
|
44
|
+
## Flow API specific
|
45
|
+
|
46
|
+
Classes that begin with Flow are responsible for comunicating with flow API.
|
47
|
+
|
48
|
+
### Flow
|
49
|
+
|
50
|
+
Helper class that offeres low level flow api access and few helper methods.
|
51
|
+
|
52
|
+
### Flow::Experience
|
53
|
+
|
54
|
+
Responsible for selecting current experience. You have to define available experiences in flow console.
|
55
|
+
|
56
|
+
### Flow::Order
|
57
|
+
|
58
|
+
Maintain and synchronizes Spree::Order with Flow API.
|
59
|
+
|
60
|
+
### Flow::Session
|
61
|
+
|
62
|
+
Every shop user has a session. This class helps in creating and maintaining session with Flow.
|
63
|
+
|
64
|
+
## Decorators
|
65
|
+
|
66
|
+
Decorators are found in ./app/flow/decorators folders and they decorate Solidus/Spree models with Flow specific methods.
|
67
|
+
|
68
|
+
All methods are prefixed with ```flow_```.
|
69
|
+
|
70
|
+
## Helper lib
|
71
|
+
|
72
|
+
### Spree::Flow::Gateway
|
73
|
+
|
74
|
+
Adapter for Solidus/Spree, that allows using [Flow.io](https://www.flow.io) as payment gateway. Flow is PCI compliant payment processor.
|
75
|
+
|
76
|
+
|
77
|
+
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# Flow, ActiveMerchant and Solidus integration
|
2
|
+
|
3
|
+
Integration of Solidus with Flow, how it is done.
|
4
|
+
|
5
|
+
I plan to be concise as possible, but cover all important topics.
|
6
|
+
|
7
|
+
## Instalation
|
8
|
+
|
9
|
+
In ```./config/application.rb``` this is the only peace of code that is needed to
|
10
|
+
init complete flow app.
|
11
|
+
|
12
|
+
```
|
13
|
+
config.to_prepare do
|
14
|
+
# add all flow libs
|
15
|
+
overload = Dir.glob('./app/flow/**/*.rb')
|
16
|
+
overload.reverse.each { |c| require(c) }
|
17
|
+
end
|
18
|
+
|
19
|
+
config.after_initialize do |app|
|
20
|
+
# init Flow payments as an option
|
21
|
+
app.config.spree.payment_methods << Spree::Gateway::Flow
|
22
|
+
|
23
|
+
# define defaults
|
24
|
+
Flow.organization = ENV.fetch('FLOW_ORGANIZATION')
|
25
|
+
Flow.base_country = ENV.fetch('FLOW_BASE_COUNTRY')
|
26
|
+
Flow.api_key = ENV.fetch('FLOW_API_KEY')
|
27
|
+
end
|
28
|
+
```
|
29
|
+
|
30
|
+
## Things to take into account
|
31
|
+
|
32
|
+
ActiveMerchent is not supporting sessions and orders, natively. If one wants
|
33
|
+
to maintain sessions and orders in Flow, you have to do it outside the ActiveMerchant
|
34
|
+
terminology which focuses around purchases, voids and refunds.
|
35
|
+
|
36
|
+
Another thing to have in mind is that Solidus/Spree can't work with ActiveMerchent directly, it has to have
|
37
|
+
an adapter. Adapter can be "stupid" and light, and can forward all the "heavy lifting" to ActiveMerchant gem
|
38
|
+
but it can also have all the logic localy.
|
39
|
+
|
40
|
+
In http://guides.spreecommerce.org/developer/payments.html at the bottom of the page Spree authors say
|
41
|
+
|
42
|
+
"better_spree_paypal_express and spree-adyen are good examples of standalone
|
43
|
+
custom gateways. No dependency on spree_gateway or activemerchant required."
|
44
|
+
|
45
|
+
Reading that we can see this is even considered good approach. For us, this is a possibility
|
46
|
+
but we consume ActiveMerchatFlow gem.
|
47
|
+
|
48
|
+
## ActiveMerchant gem into more detail
|
49
|
+
|
50
|
+
https://github.com/flowcommerce/active_merchant
|
51
|
+
|
52
|
+
Sopporst stanard public ActiveMerchant actions which are
|
53
|
+
purchase, authorize, capture, void, store and refund.
|
54
|
+
|
55
|
+
It depends on following gems
|
56
|
+
|
57
|
+
* flowcommerce - api calls
|
58
|
+
* flow-reference - we use currency validations
|
59
|
+
|
60
|
+
It is not aware of Solidus or any other shopping lib or framework.
|
61
|
+
|
62
|
+
### ActiveMerchant::Flow supported actions in detail
|
63
|
+
|
64
|
+
* purchase - shortcut for authorize and then capture
|
65
|
+
* authorize - authorize the cc and funds.
|
66
|
+
* capture - capture the funds
|
67
|
+
* void - cancel the transaction
|
68
|
+
* store - store credit card (gets credit card flow token)
|
69
|
+
* refund - refund the funds
|
70
|
+
|
71
|
+
## Solidus/Spree Implementation in more detail
|
72
|
+
|
73
|
+
Not present as standalone gem, yet. I will do that once we agree on implementation details.
|
74
|
+
|
75
|
+
From product list to purchase, complete chain v1
|
76
|
+
|
77
|
+
1. customer has to prepare data, migrate db and connect to Flow. In general
|
78
|
+
* create experiences in Flow console, add tiers, shipping methods, etc.
|
79
|
+
* add flow_data (jsonb) fields to this models
|
80
|
+
* Spree::Variant - we cache localized product prices
|
81
|
+
* Spree::Order - we cache flow order state details, shipping method
|
82
|
+
* create and sync product catalog via rake tasks
|
83
|
+
1. now site users can browse prooducts and add them to cart.
|
84
|
+
1. when user comes to shop, FlowSession is created
|
85
|
+
1. once product is in cart
|
86
|
+
* spree order is created and linked to Experience that we get from FlowSession
|
87
|
+
* it is captured and synced with flow, realtime
|
88
|
+
* we do this all the time because we want to have 100% accurate prices.
|
89
|
+
Product prices that are shown in cart come directly from Flow API.
|
90
|
+
* in checkout, when customer address is added or shipping method defined,
|
91
|
+
all is synced with flow order.
|
92
|
+
* when order is complete, we trigger flow-cc-authorize or flow-cc-capture directly
|
93
|
+
on Spree::Order object instance. This is good because all gateway actions
|
94
|
+
are functions of the order object anyway.
|
95
|
+
* flow-cc-authorize or flow-cc-capture use ActiveMerchantFlow to execute this actions
|
96
|
+
* ActiveMerchantFlow included flow-reference gem
|
97
|
+
|
98
|
+
## What can be better
|
99
|
+
|
100
|
+
We need a way to access the order in Rails. Access it after it is created in
|
101
|
+
controller but before it hits the render.
|
102
|
+
Current implementation is -> "overload" ApplicationController render
|
103
|
+
If we detect @spree_order object or debug flags, we react.
|
104
|
+
|
105
|
+
* good - elegant solution, all need code is in one file in few lines of code
|
106
|
+
* bad - somehow intrusive, we overload render, somw people will not like that.
|
107
|
+
* alternatives: gem that allows before_render bethod, call explicitly when needed
|
108
|
+
|
109
|
+
## Aditional notes - view and frontend
|
110
|
+
|
111
|
+
I see many Solidus/Spree merchant gems carry frontend code, js, coffe, views etc.
|
112
|
+
I thing that this is bad practise and that shop frontend has to be 100% customer code.
|
113
|
+
|
114
|
+
What I did not see but thing is great idea is to have custom light Flow admin present at
|
115
|
+
|
116
|
+
/admin/flow
|
117
|
+
|
118
|
+
that will ease the way of working with Flow. Code can be made to be Rails 4 and Rails 5 compatibile.
|
119
|
+
Part of that is allready done as can be seen here [Flow admin screenshot](https://i.imgur.com/FXbPrwK.png)
|
120
|
+
|
121
|
+
By default Flow Admin (on /admin/flow) is anybody that is Solidus admin.
|
122
|
+
|
123
|
+
This way we provide good frontend info, some integration notes in realtime as opposed to running
|
124
|
+
rake tests to check for integrity of Flow integration.
|
125
|
+
|
126
|
+
I suggest we make it part of flow-solidus gem.
|
127
|
+
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# Flow (2017)
|
2
|
+
# Enable this modifications if you want to display flow localized line item
|
3
|
+
# and total prices beside Solidus/Spree default
|
4
|
+
# Example: https://i.imgur.com/7v2ix2G.png
|
5
|
+
|
6
|
+
Spree::LineItem.class_eval do
|
7
|
+
# admin show line item price
|
8
|
+
def single_money
|
9
|
+
price = display_price.to_s
|
10
|
+
price += ' (%s)' % order.flow_line_item_price(self) if order.flow_order
|
11
|
+
price
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
###
|
16
|
+
|
17
|
+
Spree::Order.class_eval do
|
18
|
+
def display_total
|
19
|
+
price = Flow.format_default_price total
|
20
|
+
price += ' (%s)' % flow_total if flow_order
|
21
|
+
price.html_safe
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
###
|
26
|
+
|
27
|
+
module Spree::Admin::OrdersHelper
|
28
|
+
# admin show line item total price
|
29
|
+
def line_item_shipment_price(line_item, quantity)
|
30
|
+
price = Spree::Money.new(line_item.price * quantity, { currency: line_item.currency }).to_s
|
31
|
+
price += ' (%s)' % @order.flow_line_item_price(line_item, quantity) if @order.flow_order
|
32
|
+
price.html_safe
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# Flow (2017)
|
2
|
+
|
3
|
+
Spree::Api::OrdersController.class_eval do
|
4
|
+
|
5
|
+
# /flow/promotion_set_option?id=3&type=experience&name=canada&value=1
|
6
|
+
# alias :apply_coupon_code_pointer :apply_coupon_code
|
7
|
+
def apply_coupon_code
|
8
|
+
# find promotion code
|
9
|
+
coupon_code = params[:coupon_code]
|
10
|
+
promotion_code = Spree::PromotionCode.find_by value: coupon_code
|
11
|
+
|
12
|
+
if promotion_code
|
13
|
+
# promotion code found
|
14
|
+
promotion = Spree::Promotion.find promotion_code.promotion_id
|
15
|
+
|
16
|
+
# get experience key from session
|
17
|
+
experience_data = JSON.load(session[ApplicationController::FLOW_SESSION_KEY] || '{}')
|
18
|
+
|
19
|
+
experience_key = experience_data.dig 'local', 'experience', 'key'
|
20
|
+
forbiden_keys = promotion.flow_data.dig 'filter', 'experience'
|
21
|
+
|
22
|
+
# Rails.logger.error 'experience_key: %s' % experience_key
|
23
|
+
# Rails.logger.error 'forbiden_keys: %s' % forbiden_keys.join(', ')
|
24
|
+
|
25
|
+
allowed = true
|
26
|
+
allowed = false if experience_key && forbiden_keys.is_a?(Array) && !forbiden_keys.include?(experience_key)
|
27
|
+
|
28
|
+
return render(status: 400, json: {
|
29
|
+
successful: false,
|
30
|
+
success: nil,
|
31
|
+
status_code: 'coupon_code_not_found',
|
32
|
+
error: 'Promotion is not available in current country'
|
33
|
+
}) unless allowed
|
34
|
+
end
|
35
|
+
|
36
|
+
# call original coupon handler
|
37
|
+
# apply_coupon_code_pointer
|
38
|
+
|
39
|
+
authorize! :update, @order, order_token
|
40
|
+
@order.coupon_code = params[:coupon_code]
|
41
|
+
@handler = Spree::PromotionHandler::Coupon.new(@order).apply
|
42
|
+
if @handler.successful?
|
43
|
+
render "spree/api/promotions/handler", status: 200
|
44
|
+
else
|
45
|
+
logger.error("apply_coupon_code_error=#{@handler.error.inspect}")
|
46
|
+
render "spree/api/promotions/handler", status: 422
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# added flow specific methods to Spree::CreditCard
|
2
|
+
|
3
|
+
Spree::CreditCard.class_eval do
|
4
|
+
|
5
|
+
validate :flow_fetch_cc_token
|
6
|
+
|
7
|
+
def flow_fetch_cc_token
|
8
|
+
return false unless respond_to?(:flow_data) # Flow not installed
|
9
|
+
return false if flow_data['cc_token']
|
10
|
+
return false unless number
|
11
|
+
return errors.add(:verification_value, 'CVV verification value is required') unless verification_value.present?
|
12
|
+
|
13
|
+
# build cc hash
|
14
|
+
data = {}
|
15
|
+
data[:number] = number
|
16
|
+
data[:name] = name
|
17
|
+
data[:cvv] = verification_value
|
18
|
+
data[:expiration_year] = year.to_i
|
19
|
+
data[:expiration_month] = month.to_i
|
20
|
+
|
21
|
+
card_form = ::Io::Flow::V0::Models::CardForm.new(data)
|
22
|
+
result = FlowCommerce.instance.cards.post(Flow.organization, card_form)
|
23
|
+
|
24
|
+
# cache the result
|
25
|
+
flow_data['cc_token'] = result.token
|
26
|
+
|
27
|
+
true
|
28
|
+
rescue Io::Flow::V0::HttpClient::ServerError
|
29
|
+
puts $!.message
|
30
|
+
flow_error = $!.message.split(':', 2).first
|
31
|
+
errors.add(:base, flow_error)
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|