sailplay 0.1.1

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.
data/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ .idea
7
+ .ruby-version
8
+ Gemfile.lock
9
+ InstalledFiles
10
+ _yardoc
11
+ coverage
12
+ doc/
13
+ lib/bundler/man
14
+ pkg
15
+ rdoc
16
+ spec/reports
17
+ test/tmp
18
+ test/version_tmp
19
+ tmp
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.8.7
4
+ - 1.9.3
5
+ - 2.0.0
6
+
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ ## v0.0.1
2
+
3
+ * initial release
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source 'https://rubygems.org'
2
+
3
+
4
+ gem 'json'
5
+ gem 'guard-bundler'
6
+ gem 'guard-rspec'
7
+ gem 'rb-fsevent'
8
+ gem 'terminal-notifier-guard'
9
+
10
+ # Specify your gem's dependencies in sailplay.gemspec
11
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,10 @@
1
+ guard 'rspec', :cli => '--color' do
2
+ watch(%r{^spec/.+_spec\.rb$})
3
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
4
+ watch('spec/spec_helper.rb') { 'spec' }
5
+ end
6
+
7
+ guard 'bundler' do
8
+ watch('Gemfile')
9
+ watch(/^.+\.gemspec/)
10
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Sergey Nebolsin
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # Sailplay for Rails
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/sailplay.png)](http://badge.fury.io/rb/sailplay)
4
+ [![Build Status](https://travis-ci.org/nebolsin/sailplay.png?branch=master)](https://travis-ci.org/nebolsin/sailplay)
5
+ [![Coverage Status](https://coveralls.io/repos/nebolsin/sailplay/badge.png)](https://coveralls.io/r/nebolsin/sailplay)
6
+ [![Code Climate](https://codeclimate.com/github/nebolsin/sailplay.png)](https://codeclimate.com/github/nebolsin/sailplay)
7
+
8
+ Integrate your Rails-based store with [Sailplay](http://sailplay.ru) to give reward points to consumers for making purchases.
9
+
10
+ This gem provides Ruby bindings for [Sailplay API](https://docs.google.com/document/d/1WZggz8i5EYbkAhbF790UB2M7qTzQq33AHBbq_7BNQcc/edit)
11
+ and helpers for Rails integration.
12
+
13
+ Sponsored by [Fotoshkola.net](http://fotoshkola.net).
14
+
15
+ ## Installation
16
+
17
+ Add this line to your application's Gemfile:
18
+
19
+ gem 'sailplay'
20
+
21
+ And then execute:
22
+
23
+ $ bundle
24
+
25
+ Or install it yourself as:
26
+
27
+ $ gem install sailplay
28
+
29
+ ## Usage
30
+
31
+ TODO: describe `sailplay_client(options)` method
32
+
33
+ ## Contributing
34
+
35
+ 1. Fork it
36
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
37
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
38
+ 4. Push to the branch (`git push origin my-new-feature`)
39
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new
5
+
6
+ task :default => :spec
7
+ task :test => :spec
data/lib/sailplay.rb ADDED
@@ -0,0 +1,109 @@
1
+ require 'sailplay/version'
2
+
3
+ require 'sailplay/client'
4
+ require 'sailplay/error'
5
+ require 'sailplay/configuration'
6
+ require 'sailplay/response'
7
+
8
+ require 'sailplay/api/base'
9
+ require 'sailplay/api/user'
10
+ require 'sailplay/api/gift'
11
+ require 'sailplay/api/purchase'
12
+
13
+ module Sailplay
14
+ class << self
15
+ # The client object is responsible for communication with Sailplay API server.
16
+ attr_writer :client
17
+
18
+ # A Sailplay configuration object.
19
+ attr_writer :configuration
20
+
21
+ # Call this method to modify defaults in your initializers.
22
+ #
23
+ # @example
24
+ # Sailplay.configure do |config|
25
+ # config.store_id = '123'
26
+ # config.store_key = '4567890'
27
+ # config.store_pin = '3131'
28
+ # config.secure = true
29
+ # end
30
+ def configure
31
+ yield(configuration)
32
+ @client = Client.new(configuration)
33
+ end
34
+
35
+ # The configuration object.
36
+ # @see Sailplay.configure
37
+ def configuration
38
+ @configuration ||= Configuration.new
39
+ end
40
+
41
+ def client
42
+ @client ||= Client.new(configuration)
43
+ end
44
+
45
+ def reset!
46
+ @client = nil
47
+ @configuration = nil
48
+ end
49
+
50
+ def logger
51
+ self.configuration.logger
52
+ end
53
+
54
+ # @param [String] phone
55
+ # @param [Hash] options
56
+ #
57
+ # @option options [true|false] :auth — authenticate user
58
+ #
59
+ # @return [Sailplay::User]
60
+ def create_user(phone, options = {})
61
+ self.client.create_user(phone, options)
62
+ end
63
+
64
+ # @param [String] phone
65
+ # @param [Hash] options
66
+ #
67
+ # @option options [true|false] :auth — authenticate user
68
+ #
69
+ # @return [Sailplay::User]
70
+ def find_user(phone, options = {})
71
+ self.client.find_user(phone, options)
72
+ end
73
+
74
+ # @param [String|Integer] phone_or_user_id
75
+ # @param [BigDecimal] price
76
+ # @param [Hash] options
77
+ #
78
+ # @option options [Integer] :order_id — ID заказа
79
+ # @option options [Decimal] :points_rate — коэффициент конвертации рублей в баллы. Может принимать значение из полуинтервала (0,1].
80
+ # При отсутствии данного параметра, используется значение, указанное в настройках.
81
+ # Формат points_rate=0.45
82
+ # @option options [true|false] :force_complete — если true, транзакция считается подтвержденной несмотря на флаг в настройках.
83
+ # Данный аттрибут может быть использован, например, в случае когда часть оплат
84
+ # необходимо подтверждать, а про остальные известно что они уже подтверждены.
85
+ #
86
+ # @return [Sailplay::Purchase]
87
+ def create_purchase(phone_or_user_id, price, options = {})
88
+ self.client.create_purchase(phone_or_user_id, price, options)
89
+ end
90
+
91
+ # @param [Integer] order_id
92
+ # @param [Hash] options
93
+ #
94
+ # @option options [BigDecimal] :price
95
+ def confirm_purchase(order_id, options = {})
96
+ self.client.confirm_purchase(order_id, options)
97
+ end
98
+
99
+
100
+ # @param [String] gift_public_key
101
+ def confirm_gift(gift_public_key)
102
+ self.client.confirm_gift(gift_public_key)
103
+ end
104
+
105
+ def request(method, url, params)
106
+ self.client.request(method, url, params)
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,4 @@
1
+ module Sailplay
2
+ class Base
3
+ end
4
+ end
@@ -0,0 +1,34 @@
1
+ require 'sailplay/api/base'
2
+
3
+ module Sailplay
4
+ class Gift < Base
5
+ attr_accessor :id, :sku, :name, :pic, :view_url, :pick_url, :points
6
+
7
+ #{
8
+ # sku: 5,
9
+ # name: "Подарок 1",
10
+ # pic: "gifts/gift/b6e011188b74d3e0d838fbbace84de92.jpeg",
11
+ # pick_url: "http://sailplay.ru/api/v1/ecommerce/gifts/pick/?gift_id=15&user_phone=79266054612...,
12
+ # points: 55,
13
+ # id: 25
14
+ #}
15
+ def self.parse(json)
16
+ Sailplay::Gift.new(
17
+ :id => json[:id],
18
+ :sku => json[:sku],
19
+ :name => json[:name],
20
+ :pic => json[:pic],
21
+ :view_url => json[:view_url],
22
+ :pick_url => json[:pick_url],
23
+ :points => json[:points]
24
+ )
25
+ end
26
+
27
+ def initialize(attrs = {})
28
+ [:id, :sku, :name, :pic, :view_url, :pick_url, :points].each do |attr|
29
+ instance_variable_set("@#{attr}", attrs[attr])
30
+ end
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,40 @@
1
+ require 'sailplay/api/base'
2
+
3
+ module Sailplay
4
+ class Purchase < Base
5
+ attr_accessor :id, :order_id, :price, :points_delta, :complete_date, :public_key
6
+ attr_accessor :user
7
+
8
+ #{
9
+ # "complete_date":"2013-01-25T00:31:42.642",
10
+ # "price":"10",
11
+ # "id":8,
12
+ # "points_delta":4,
13
+ # "public_key":";ao08rj3tj09fu9jkwer20393urjflshg54h",
14
+ # "order_num":87573,
15
+ #}
16
+ def self.parse(json)
17
+ purchase_json = json[:purchase]
18
+ purchase = Sailplay::Purchase.new(
19
+ :id => purchase_json[:id],
20
+ :order_id => purchase_json[:order_num],
21
+ :price => purchase_json[:price],
22
+ :points_delta => purchase_json[:points_delta],
23
+ :complete_date => purchase_json[:complete_date],
24
+ :public_key => purchase_json[:public_key]
25
+ )
26
+
27
+ if user_json = json[:user]
28
+ purchase.user = User.parse(user_json)
29
+ end
30
+
31
+ purchase
32
+ end
33
+
34
+ def initialize(options = {})
35
+ [:id, :order_id, :price, :points_delta, :complete_date, :public_key].each do |attr|
36
+ instance_variable_set("@#{attr}", options[attr])
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,52 @@
1
+ require 'sailplay/api/base'
2
+ require 'sailplay/api/gift'
3
+
4
+ module Sailplay
5
+ class User < Base
6
+ attr_accessor :id, :phone, :full_name, :points, :media_url, :available_gifts, :unavailable_gifts, :auth_hash
7
+
8
+ # {
9
+ # user_phone: "79266054612",
10
+ # auth_hash: "1a159580bc111be0c288eb90afbce6f42ee48bba",
11
+ # user_points: 189,
12
+ # media_url: "http://d3257v5wstjx8h.cloudfront.net/media/"
13
+ # available_gifts: [
14
+ # {
15
+ # sku: 5,
16
+ # name: "Подарок 1",
17
+ # pic: "gifts/gift/b6e011188b74d3e0d838fbbace84de92.jpeg",
18
+ # pick_url: "http://sailplay.ru/api/v1/ecommerce/gifts/pick/?gift_id=15&user_phone=79266054612&token=239b3282621115d2e71bc844d546b7dea4385326&store_department_id=19",
19
+ # points: 55,
20
+ # id: 25
21
+ # }
22
+ # ],
23
+ # over_user_points_gifts: [
24
+ # {
25
+ # sku: 1,
26
+ # name: "Подарок 2",
27
+ # view_url: "http://sailplay.ru/gifts/view/97/",
28
+ # pic: "gifts/gift/83dd4abd6f13495f222113416103b716.jpg",
29
+ # points: 200,
30
+ # id: 27
31
+ # }
32
+ # ]
33
+ # }
34
+ def self.parse(json)
35
+ Sailplay::User.new(
36
+ :phone => json[:user_phone] || json[:phone],
37
+ :points => json[:user_points] || json[:points],
38
+ :full_name => json[:full_name],
39
+ :media_url => json[:media_url],
40
+ :auth_hash => json[:auth_hash],
41
+ :available_gifts => (json[:available_gifts] || []).map {|gift_json| Gift.parse(gift_json)},
42
+ :unavailable_gifts => (json[:over_user_points_gifts] || []).map {|gift_json| Gift.parse(gift_json)}
43
+ )
44
+ end
45
+
46
+ def initialize(attrs = {})
47
+ [:id, :phone, :full_name, :points, :media_url, :auth_hash, :available_gifts, :unavailable_gifts].each do |attr|
48
+ instance_variable_set("@#{attr}", attrs[attr])
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,255 @@
1
+ require 'rest-client'
2
+ require 'multi_json'
3
+ require 'sailplay/configuration'
4
+ require 'sailplay/error'
5
+ require 'sailplay/response'
6
+
7
+ module Sailplay
8
+ class Client
9
+ attr_reader :protocol,
10
+ :host,
11
+ :port,
12
+ :endpoint,
13
+ :secure,
14
+ :connection_options,
15
+ :store_id,
16
+ :store_key,
17
+ :store_pin
18
+
19
+ alias_method :secure?, :secure
20
+
21
+ def initialize(options = {})
22
+ [ :protocol,
23
+ :host,
24
+ :port,
25
+ :endpoint,
26
+ :secure,
27
+ :connection_options,
28
+ :store_id,
29
+ :store_key,
30
+ :store_pin
31
+ ].each do |option|
32
+ instance_variable_set("@#{option}", options[option])
33
+ end
34
+ end
35
+
36
+ # @param [String] phone
37
+ # @param [Hash] options
38
+ #
39
+ # @option options [true|false] :auth — authenticate user
40
+ #
41
+ # @return [Sailplay::User]
42
+ def create_user(phone, options = {})
43
+ params = {:user_phone => phone}
44
+ params[:extra_fields] = 'auth_hash' if options[:auth]
45
+
46
+ response = request(:get, '/users/reg', :user_phone => phone)
47
+ if response.success?
48
+ User.parse(response.data)
49
+ else
50
+ raise APIError, "Cannot create user '#{phone}': #{response.error_message}"
51
+ end
52
+ end
53
+
54
+ # @param [String] phone
55
+ # @param [Hash] options
56
+ #
57
+ # @option options [true|false] :auth — authenticate user
58
+ #
59
+ # @return [Sailplay::User]
60
+ def find_user(phone, options = {})
61
+ params = {:user_phone => phone}
62
+ params[:extra_fields] = 'auth_hash' if options[:auth]
63
+
64
+ response = Sailplay.request(:get, '/users/points-info', params)
65
+ if response.success?
66
+ User.parse(response.data)
67
+ else
68
+ raise APIError, "Cannot find a user '#{phone}': #{response.error_message}"
69
+ end
70
+ end
71
+
72
+ # options[:points_rate] — коэффициент конвертации рублей в баллы. Может принимать значение из полуинтервала (0,1].
73
+ # При отсутствии данного параметра, используется значение, указанное в настройках.
74
+ # Формат points_rate=0.45
75
+ # options[:force_complete] — если true, транзакция считается подтвержденной несмотря на флаг в настройках.
76
+ # Данный аттрибут может быть использован, например, в случае когда часть оплат
77
+ # необходимо подтверждать, а про остальные известно что они уже подтверждены.
78
+ # options[:order_id] — ID заказа
79
+ #
80
+ # @return [Sailplay::Purchase]
81
+ def create_purchase(user_id, price, options = {})
82
+ params = {:price => price, :origin_user_id => user_id}
83
+
84
+ params[:user_phone] = options[:phone] if options[:phone]
85
+ params[:points_rate] = options[:points_rate] if options[:points_rate]
86
+ params[:force_complete] = options[:force_complete] if options[:force_complete]
87
+ params[:order_num] = options[:order_id] if options[:order_id]
88
+
89
+ params[:fields] = [:public_key, options[:order_id] && :order_num].compact.join(',')
90
+
91
+ response = Sailplay.request(:get, '/purchases/new', params)
92
+
93
+ if response.success?
94
+ Purchase.parse(response.data)
95
+ else
96
+ raise APIError, "Cannot create a purchase: #{response.error_message}"
97
+ end
98
+ end
99
+
100
+ # @param [Integer] order_id
101
+ # @param [Hash] options
102
+ #
103
+ # @option options [BigDecimal] :price
104
+ def confirm_purchase(order_id, options = {})
105
+ params = {:order_num => order_id}
106
+ params[:new_price] = options[:price] if options[:price]
107
+
108
+ response = request(:get, '/purchases/confirm', params)
109
+
110
+ if response.success?
111
+ Purchase.parse(response.data)
112
+ else
113
+ raise APIError, "Cannot confirm a purchase: #{response.error_message}"
114
+ end
115
+ end
116
+
117
+ # @param [String] gift_public_key
118
+ def confirm_gift(gift_public_key)
119
+ params = {:gift_public_key => gift_public_key}
120
+ response = request(:get, '/ecommerce/gifts/commit-transaction', params)
121
+
122
+ if response.success?
123
+ else
124
+ raise APIError, "Cannot confirm a gift: #{response.error_message}"
125
+ end
126
+ end
127
+
128
+ def request(method, url, params = {})
129
+ execute_request(method, url, auth_params.merge(params))
130
+ end
131
+
132
+ def logger
133
+ Sailplay.logger
134
+ end
135
+
136
+ private
137
+
138
+ def auth_params
139
+ {:store_department_id => store_id, :token => api_token}
140
+ end
141
+
142
+ def api_url(url = '')
143
+ URI.parse("#{protocol}://#{host}:#{port}").merge("#{endpoint}/#{url}").to_s
144
+ end
145
+
146
+ def api_token
147
+ @api_token ||= login
148
+ end
149
+
150
+ def login
151
+ raise ConfigurationError, 'Missing client configuration: ' +
152
+ 'please check that store_id, store_key and pin_code are ' +
153
+ 'configured' unless credentials?
154
+
155
+ response = execute_request(:get, 'login', credentials)
156
+
157
+ if response.success?
158
+ response.data[:token]
159
+ else
160
+ raise AuthenticationError.new("Cannot authenticate on Sailplay. Check your config. (Response: #{response})")
161
+ end
162
+ end
163
+
164
+ def credentials
165
+ {
166
+ :store_department_id => @store_id,
167
+ :store_department_key => @store_key,
168
+ :pin_code => @store_pin
169
+ }
170
+ end
171
+
172
+ def credentials?
173
+ credentials.values.all?
174
+ end
175
+
176
+ def execute_request(method, url, params = {})
177
+ logger.debug(self.class) {"Starting #{method} request to #{url} with #{params.inspect}"}
178
+ url = api_url(url)
179
+ headers = @connection_options[:headers].merge(:params => params)
180
+
181
+ request_opts = {:method => method, :url => url, :headers => headers}
182
+
183
+ begin
184
+ response = RestClient::Request.execute(request_opts)
185
+ rescue RestClient::ExceptionWithResponse => e
186
+ if e.http_code && e.http_body
187
+ handle_api_error(e.http_code, e.http_body)
188
+ else
189
+ handle_restclient_error(e)
190
+ end
191
+ rescue RestClient::Exception, SocketError, Errno::ECONNREFUSED => e
192
+ handle_restclient_error(e)
193
+ end
194
+
195
+
196
+ http_code, http_body = response.code, response.body
197
+
198
+ logger.debug(self.class) {"\t HTTP Code -> #{http_code}"}
199
+ #logger.debug(self.class) {"\t HTTP Body -> #{http_body}"}
200
+
201
+ begin
202
+ json_body = MultiJson.load(http_body, :symbolize_keys => true)
203
+ rescue MultiJson::DecodeError
204
+ raise APIError.new("Invalid response object from API: #{http_body.inspect} (HTTP response code was #{http_code})", http_code, http_body)
205
+ end
206
+
207
+ #logger.debug(self.class) {"\t JSON Body -> #{json_body}"}
208
+
209
+ response = Response.new(json_body)
210
+
211
+ logger.debug(self.class) {"\t Valid -> #{response.success?}"}
212
+ logger.debug(self.class) {"\t Payload -> #{response.data}"}
213
+
214
+ response
215
+ end
216
+
217
+ def handle_restclient_error(e)
218
+ case e
219
+ when RestClient::ServerBrokeConnection, RestClient::RequestTimeout
220
+ message = "Could not connect to Sailplay (#{endpoint}). " +
221
+ "Please check your internet connection and try again. " +
222
+ "If this problem persists, let us know at support@sailplay.ru."
223
+ when SocketError
224
+ message = "Unexpected error communicating when trying to connect to Sailplay. " +
225
+ "HINT: You may be seeing this message because your DNS is not working. " +
226
+ "To check, try running 'host sailplay.ru' from the command line."
227
+ else
228
+ message = "Unexpected error communicating with Sailplay. " +
229
+ "If this problem persists, let us know at support@sailplay.ru."
230
+ end
231
+ message += "\n\n(Network error: #{e.message})"
232
+ raise APIConnectionError, message
233
+ end
234
+
235
+ def handle_api_error(http_code, http_body)
236
+ begin
237
+ error_obj = MultiJson.load(http_body, :symbolize_keys => true)
238
+ message = error_obj[:message]
239
+ rescue MultiJson::DecodeError
240
+ message = "Invalid response object from API: #{http_body.inspect} (HTTP response code was #{http_code})"
241
+ raise APIError.new(message, http_code, http_body)
242
+ end
243
+
244
+ case http_code
245
+ when 400, 404 then
246
+ raise InvalidRequestError.new(message, http_code, http_body, error_obj)
247
+ when 401
248
+ raise AuthenticationError.new(message, http_code, http_body, error_obj)
249
+ else
250
+ raise APIError.new(message, http_code, http_body, error_obj)
251
+ end
252
+ end
253
+
254
+ end
255
+ end