boutique 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +1 -6
- data/boutique +1 -0
- data/config.ru +6 -4
- data/lib/boutique.rb +51 -2
- data/test/app_test.rb +28 -5
- data/test/config_test.rb +1 -0
- data/test/helper.rb +14 -1
- data/test/model_test.rb +16 -4
- metadata +12 -12
data/README.md
CHANGED
@@ -24,6 +24,7 @@ can configure what products you sell here:
|
|
24
24
|
require 'boutique'
|
25
25
|
|
26
26
|
Boutique.configure do |c|
|
27
|
+
c.dev_email 'dev@mailinator.com'
|
27
28
|
c.pem_private '/path/to/private.pem'
|
28
29
|
c.pem_public '/path/to/public.pem'
|
29
30
|
c.pem_paypal '/path/to/paypal.pem'
|
@@ -82,12 +83,6 @@ Development
|
|
82
83
|
Tests are setup to run individually via `ruby test/*_test.rb` or run them all
|
83
84
|
via `rake`.
|
84
85
|
|
85
|
-
TODO
|
86
|
-
====
|
87
|
-
|
88
|
-
* email customer + recover action
|
89
|
-
* email exceptions to developer
|
90
|
-
|
91
86
|
License
|
92
87
|
=======
|
93
88
|
|
data/boutique
CHANGED
data/config.ru
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require File.expand_path('lib/boutique', File.dirname(__FILE__))
|
2
2
|
|
3
3
|
Boutique.configure(!ENV['BOUTIQUE_CMD'].nil?) do |c|
|
4
|
+
c.dev_email 'dev@localhost'
|
4
5
|
c.pem_private File.expand_path('certs/private.pem', File.dirname(__FILE__))
|
5
6
|
c.pem_public File.expand_path('certs/public.pem', File.dirname(__FILE__))
|
6
7
|
c.pem_paypal File.expand_path('certs/paypal.pem', File.dirname(__FILE__))
|
@@ -16,10 +17,11 @@ Boutique.configure(!ENV['BOUTIQUE_CMD'].nil?) do |c|
|
|
16
17
|
end
|
17
18
|
|
18
19
|
Boutique.product('readme') do |p|
|
19
|
-
p.name
|
20
|
-
p.files
|
21
|
-
p.price
|
22
|
-
p.return_url
|
20
|
+
p.name 'README document'
|
21
|
+
p.files File.expand_path('README.md', File.dirname(__FILE__))
|
22
|
+
p.price 1.5
|
23
|
+
p.return_url 'http://localhost'
|
24
|
+
p.support_email 'support@localhost'
|
23
25
|
end
|
24
26
|
|
25
27
|
run Boutique::App if !ENV['BOUTIQUE_CMD']
|
data/lib/boutique.rb
CHANGED
@@ -8,9 +8,10 @@ require 'date'
|
|
8
8
|
require 'digest/sha1'
|
9
9
|
require 'json'
|
10
10
|
require 'openssl'
|
11
|
+
require 'pony'
|
11
12
|
|
12
13
|
module Boutique
|
13
|
-
VERSION = '0.0.
|
14
|
+
VERSION = '0.0.4'
|
14
15
|
|
15
16
|
class << self
|
16
17
|
def configure(setup_db=true)
|
@@ -39,6 +40,11 @@ module Boutique
|
|
39
40
|
end
|
40
41
|
|
41
42
|
class Config
|
43
|
+
def self.dev_email(value=nil)
|
44
|
+
@dev_email = value if !value.nil?
|
45
|
+
@dev_email
|
46
|
+
end
|
47
|
+
|
42
48
|
def self.pem_private(value=nil)
|
43
49
|
@pem_private = value if !value.nil?
|
44
50
|
@pem_private
|
@@ -126,12 +132,18 @@ module Boutique
|
|
126
132
|
@return_url
|
127
133
|
end
|
128
134
|
|
135
|
+
def support_email(value=nil)
|
136
|
+
@support_email = value if !value.nil?
|
137
|
+
@support_email
|
138
|
+
end
|
139
|
+
|
129
140
|
def to_hash
|
130
141
|
{:code => @code,
|
131
142
|
:name => @name,
|
132
143
|
:files => @files,
|
133
144
|
:price => @price,
|
134
|
-
:return_url => @return_url
|
145
|
+
:return_url => @return_url,
|
146
|
+
:support_email => @support_email}
|
135
147
|
end
|
136
148
|
end
|
137
149
|
|
@@ -144,6 +156,7 @@ module Boutique
|
|
144
156
|
property :files, CommaSeparatedList, :required => true
|
145
157
|
property :price, Decimal, :required => true
|
146
158
|
property :return_url, String, :required => true
|
159
|
+
property :support_email, String, :required => true
|
147
160
|
|
148
161
|
has n, :purchases
|
149
162
|
end
|
@@ -212,6 +225,19 @@ module Boutique
|
|
212
225
|
Digest::SHA1.hexdigest("#{DateTime.now}#{rand}")[0..9]
|
213
226
|
end
|
214
227
|
|
228
|
+
def send_mail
|
229
|
+
raise 'Cannot send link to incomplete purchase' if !completed?
|
230
|
+
Pony.mail(
|
231
|
+
:to => self.email,
|
232
|
+
:from => self.product.support_email,
|
233
|
+
:subject => "#{self.product.name} Receipt",
|
234
|
+
:body => "Thanks for purchasing #{self.product.name}! " +
|
235
|
+
"To download it, follow this link:\n\n" +
|
236
|
+
" #{self.product.return_url}?b=#{boutique_id}\n\n" +
|
237
|
+
"Let us know if you have any troubles.\n"
|
238
|
+
)
|
239
|
+
end
|
240
|
+
|
215
241
|
def to_json
|
216
242
|
{
|
217
243
|
:id => id,
|
@@ -258,6 +284,18 @@ module Boutique
|
|
258
284
|
DataMapper.finalize
|
259
285
|
|
260
286
|
class App < Sinatra::Base
|
287
|
+
set :raise_errors, false
|
288
|
+
set :show_exceptions, false
|
289
|
+
|
290
|
+
error do
|
291
|
+
Pony.mail(
|
292
|
+
:to => Boutique.config.dev_email,
|
293
|
+
:from => "boutique@#{Boutique.config.dev_email.split('@')[1..-1]}",
|
294
|
+
:subject => 'Production Exception',
|
295
|
+
:body => request.env['sinatra.error'].to_s
|
296
|
+
) if Boutique.config.dev_email
|
297
|
+
end
|
298
|
+
|
261
299
|
post '/boutique/buy/:code' do
|
262
300
|
product = Boutique::Product.first(:code => params[:code])
|
263
301
|
if product.nil?
|
@@ -283,11 +321,22 @@ module Boutique
|
|
283
321
|
params['payment_status'] &&
|
284
322
|
params['receiver_email'] == Boutique.config.pp_email
|
285
323
|
purchase.complete(params['txn_id'], params['payer_email'], params['first_name'])
|
324
|
+
purchase.send_mail
|
286
325
|
purchase.save
|
287
326
|
end
|
288
327
|
''
|
289
328
|
end
|
290
329
|
|
330
|
+
post '/boutique/recover/:code' do
|
331
|
+
product = Boutique::Product.first(:code => params[:code])
|
332
|
+
purchase = product.purchases.first(:email => params['email']) if product
|
333
|
+
if product.nil? || purchase.nil? || !purchase.completed?
|
334
|
+
halt(404, "purchase #{params[:code]}/#{params['email']} not found")
|
335
|
+
end
|
336
|
+
purchase.send_mail
|
337
|
+
purchase.boutique_id
|
338
|
+
end
|
339
|
+
|
291
340
|
get '/boutique/record/:boutique_id' do
|
292
341
|
purchase = get_purchase(params[:boutique_id])
|
293
342
|
purchase.maybe_refresh_downloads!
|
data/test/app_test.rb
CHANGED
@@ -23,12 +23,14 @@ class AppTest < BoutiqueTest
|
|
23
23
|
product.purchases << purchase
|
24
24
|
product.save
|
25
25
|
refute(purchase.completed?)
|
26
|
+
assert_nil(Pony.last_mail)
|
26
27
|
|
27
28
|
post "/boutique/notify/#{purchase.boutique_id}?payment_status=Completed&txn_id=1337&receiver_email=#{Boutique.config.pp_email}"
|
28
29
|
assert(last_response.ok?)
|
29
30
|
|
30
31
|
purchase.reload
|
31
32
|
assert(purchase.completed?)
|
33
|
+
refute_nil(Pony.last_mail)
|
32
34
|
assert_equal('1337', purchase.transaction_id)
|
33
35
|
end
|
34
36
|
|
@@ -37,12 +39,25 @@ class AppTest < BoutiqueTest
|
|
37
39
|
assert(last_response.not_found?)
|
38
40
|
end
|
39
41
|
|
40
|
-
def
|
41
|
-
|
42
|
-
purchase
|
43
|
-
|
44
|
-
|
42
|
+
def test_recover
|
43
|
+
purchase = ebook_purchase
|
44
|
+
purchase.complete('1337', 'john@mailinator.com', 'John')
|
45
|
+
purchase.save
|
46
|
+
assert_nil(Pony.last_mail)
|
47
|
+
|
48
|
+
post "/boutique/recover/ebook", 'email' => 'john@mailinator.com'
|
49
|
+
assert(last_response.ok?)
|
50
|
+
assert_equal(purchase.boutique_id, last_response.body)
|
51
|
+
refute_nil(Pony.last_mail)
|
52
|
+
end
|
53
|
+
|
54
|
+
def test_recover_not_found
|
55
|
+
post "/boutique/recover/99-notfound"
|
56
|
+
assert(last_response.not_found?)
|
57
|
+
end
|
45
58
|
|
59
|
+
def test_record
|
60
|
+
purchase = ebook_purchase
|
46
61
|
get "/boutique/record/#{purchase.boutique_id}"
|
47
62
|
assert(last_response.ok?)
|
48
63
|
|
@@ -60,6 +75,14 @@ class AppTest < BoutiqueTest
|
|
60
75
|
end
|
61
76
|
|
62
77
|
private
|
78
|
+
def ebook_purchase
|
79
|
+
product = ebook_product
|
80
|
+
purchase = Boutique::Purchase.new
|
81
|
+
product.purchases << purchase
|
82
|
+
product.save
|
83
|
+
purchase
|
84
|
+
end
|
85
|
+
|
63
86
|
def app
|
64
87
|
@app ||= Rack::Server.new.app
|
65
88
|
end
|
data/test/config_test.rb
CHANGED
@@ -2,6 +2,7 @@ require File.expand_path('helper', File.dirname(__FILE__))
|
|
2
2
|
|
3
3
|
class ConfigTest < BoutiqueTest
|
4
4
|
def test_db
|
5
|
+
assert_equal('dev@localhost', Boutique.config.dev_email)
|
5
6
|
assert_equal(File.expand_path('../certs/private.pem', File.dirname(__FILE__)), Boutique.config.pem_private)
|
6
7
|
assert_equal(File.expand_path('../certs/public.pem', File.dirname(__FILE__)), Boutique.config.pem_public)
|
7
8
|
assert_equal(File.expand_path('../certs/private.pem', File.dirname(__FILE__)), Boutique.config.pem_private)
|
data/test/helper.rb
CHANGED
@@ -9,11 +9,22 @@ require 'fileutils'
|
|
9
9
|
DataMapper.setup(:default, 'sqlite::memory:')
|
10
10
|
DataMapper.auto_migrate!
|
11
11
|
|
12
|
+
module Pony
|
13
|
+
def self.mail(fields)
|
14
|
+
@last_mail = fields
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.last_mail
|
18
|
+
@last_mail
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
12
22
|
class BoutiqueTest < MiniTest::Unit::TestCase
|
13
23
|
def setup
|
14
24
|
Boutique::Purchase.all.destroy
|
15
25
|
Boutique::Product.all.destroy
|
16
26
|
Boutique.configure(false) do |c|
|
27
|
+
c.dev_email 'dev@localhost'
|
17
28
|
c.pem_private File.expand_path('../certs/private.pem', File.dirname(__FILE__))
|
18
29
|
c.pem_public File.expand_path('../certs/public.pem', File.dirname(__FILE__))
|
19
30
|
c.pem_paypal File.expand_path('../certs/paypal.pem', File.dirname(__FILE__))
|
@@ -27,6 +38,7 @@ class BoutiqueTest < MiniTest::Unit::TestCase
|
|
27
38
|
c.pp_email 'paypal_biz@mailinator.com'
|
28
39
|
c.pp_url 'http://localhost'
|
29
40
|
end
|
41
|
+
Pony.mail(nil)
|
30
42
|
end
|
31
43
|
|
32
44
|
def teardown
|
@@ -40,6 +52,7 @@ class BoutiqueTest < MiniTest::Unit::TestCase
|
|
40
52
|
:name => 'Ebook',
|
41
53
|
:files => [File.expand_path('../README.md', File.dirname(__FILE__))],
|
42
54
|
:price => 10.5,
|
43
|
-
:return_url => 'http://zincmade.com'
|
55
|
+
:return_url => 'http://zincmade.com',
|
56
|
+
:support_email => 'support@zincmade.com')
|
44
57
|
end
|
45
58
|
end
|
data/test/model_test.rb
CHANGED
@@ -39,6 +39,16 @@ class ModelTest < BoutiqueTest
|
|
39
39
|
assert_equal('_s-xclick', form['cmd'])
|
40
40
|
refute_nil(form['encrypted'])
|
41
41
|
|
42
|
+
purchase.send_mail
|
43
|
+
assert_equal({
|
44
|
+
:to => 'john@mailinator.com',
|
45
|
+
:from => 'support@zincmade.com',
|
46
|
+
:subject => 'Ebook Receipt',
|
47
|
+
:body => "Thanks for purchasing Ebook! To download it, follow this link:\n\n" +
|
48
|
+
" http://zincmade.com?b=#{purchase.boutique_id}\n\n" +
|
49
|
+
"Let us know if you have any troubles.\n"
|
50
|
+
}, Pony.last_mail)
|
51
|
+
|
42
52
|
json = JSON.parse(purchase.to_json)
|
43
53
|
assert_equal(purchase.id, json['id'])
|
44
54
|
assert_equal(2, json['counter'])
|
@@ -51,10 +61,11 @@ class ModelTest < BoutiqueTest
|
|
51
61
|
def test_product_create
|
52
62
|
count = Boutique::Product.count
|
53
63
|
Boutique.product('icon-set') do |p|
|
54
|
-
p.name
|
55
|
-
p.files
|
56
|
-
p.price
|
57
|
-
p.return_url
|
64
|
+
p.name 'Icon Set'
|
65
|
+
p.files [File.expand_path('../README.md', File.dirname(__FILE__))]
|
66
|
+
p.price 10.5
|
67
|
+
p.return_url 'http://zincmade.com'
|
68
|
+
p.support_email 'support@zincmade.com'
|
58
69
|
end
|
59
70
|
assert_equal(count + 1, Boutique::Product.count)
|
60
71
|
|
@@ -63,6 +74,7 @@ class ModelTest < BoutiqueTest
|
|
63
74
|
assert_equal(File.expand_path('../README.md', File.dirname(__FILE__)), set.files[0])
|
64
75
|
assert_equal(10.5, set.price)
|
65
76
|
assert_equal('http://zincmade.com', set.return_url)
|
77
|
+
assert_equal('support@zincmade.com', set.support_email)
|
66
78
|
assert_equal(0, set.purchases.size)
|
67
79
|
end
|
68
80
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: boutique
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.4
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: .
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-01-
|
12
|
+
date: 2012-01-30 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: sinatra
|
16
|
-
requirement: &
|
16
|
+
requirement: &70093300944320 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: '0'
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *70093300944320
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: data_mapper
|
27
|
-
requirement: &
|
27
|
+
requirement: &70093300943640 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ! '>='
|
@@ -32,10 +32,10 @@ dependencies:
|
|
32
32
|
version: '0'
|
33
33
|
type: :runtime
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *70093300943640
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: pony
|
38
|
-
requirement: &
|
38
|
+
requirement: &70093300942280 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
40
40
|
requirements:
|
41
41
|
- - ! '>='
|
@@ -43,10 +43,10 @@ dependencies:
|
|
43
43
|
version: '0'
|
44
44
|
type: :runtime
|
45
45
|
prerelease: false
|
46
|
-
version_requirements: *
|
46
|
+
version_requirements: *70093300942280
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: minitest
|
49
|
-
requirement: &
|
49
|
+
requirement: &70093300861840 !ruby/object:Gem::Requirement
|
50
50
|
none: false
|
51
51
|
requirements:
|
52
52
|
- - ! '>='
|
@@ -54,10 +54,10 @@ dependencies:
|
|
54
54
|
version: '0'
|
55
55
|
type: :development
|
56
56
|
prerelease: false
|
57
|
-
version_requirements: *
|
57
|
+
version_requirements: *70093300861840
|
58
58
|
- !ruby/object:Gem::Dependency
|
59
59
|
name: shotgun
|
60
|
-
requirement: &
|
60
|
+
requirement: &70093300881600 !ruby/object:Gem::Requirement
|
61
61
|
none: false
|
62
62
|
requirements:
|
63
63
|
- - ! '>='
|
@@ -65,7 +65,7 @@ dependencies:
|
|
65
65
|
version: '0'
|
66
66
|
type: :development
|
67
67
|
prerelease: false
|
68
|
-
version_requirements: *
|
68
|
+
version_requirements: *70093300881600
|
69
69
|
description: A Sinatra module which accepts payments via PayPal and gives customers
|
70
70
|
a secret URL to download your product.
|
71
71
|
email:
|