flowcommerce-solidus 0.1.11
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 +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
|
+
|