venice 0.2.0 → 0.3.0
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 +4 -4
- data/Gemfile.lock +26 -22
- data/LICENSE +1 -1
- data/README.md +60 -25
- data/Rakefile +1 -2
- data/bin/iap +13 -0
- data/coverage/assets/0.10.0/application.css +799 -0
- data/coverage/assets/0.10.0/application.js +1707 -0
- data/coverage/assets/0.10.0/colorbox/border.png +0 -0
- data/coverage/assets/0.10.0/colorbox/controls.png +0 -0
- data/coverage/assets/0.10.0/colorbox/loading.gif +0 -0
- data/coverage/assets/0.10.0/colorbox/loading_background.png +0 -0
- data/coverage/assets/0.10.0/favicon_green.png +0 -0
- data/coverage/assets/0.10.0/favicon_red.png +0 -0
- data/coverage/assets/0.10.0/favicon_yellow.png +0 -0
- data/coverage/assets/0.10.0/loading.gif +0 -0
- data/coverage/assets/0.10.0/magnify.png +0 -0
- data/coverage/assets/0.10.0/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
- data/coverage/assets/0.10.0/smoothness/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
- data/coverage/assets/0.10.0/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
- data/coverage/assets/0.10.0/smoothness/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
- data/coverage/assets/0.10.0/smoothness/images/ui-bg_glass_75_dadada_1x400.png +0 -0
- data/coverage/assets/0.10.0/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
- data/coverage/assets/0.10.0/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
- data/coverage/assets/0.10.0/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
- data/coverage/assets/0.10.0/smoothness/images/ui-icons_222222_256x240.png +0 -0
- data/coverage/assets/0.10.0/smoothness/images/ui-icons_2e83ff_256x240.png +0 -0
- data/coverage/assets/0.10.0/smoothness/images/ui-icons_454545_256x240.png +0 -0
- data/coverage/assets/0.10.0/smoothness/images/ui-icons_888888_256x240.png +0 -0
- data/coverage/assets/0.10.0/smoothness/images/ui-icons_cd0a0a_256x240.png +0 -0
- data/coverage/index.html +2152 -0
- data/lib/venice.rb +1 -0
- data/lib/venice/client.rb +16 -8
- data/lib/venice/in_app_receipt.rb +92 -0
- data/lib/venice/receipt.rb +62 -58
- data/lib/venice/version.rb +1 -1
- data/spec/client_spec.rb +73 -6
- data/spec/in_app_receipt_spec.rb +58 -0
- data/spec/receipt_spec.rb +47 -70
- data/spec/spec_helper.rb +9 -1
- data/venice.gemspec +2 -2
- metadata +59 -35
- data/spec/receipt_verification_spec.rb +0 -19
- data/venice-0.1.0.gem +0 -0
- data/venice-0.1.1.gem +0 -0
data/lib/venice.rb
CHANGED
data/lib/venice/client.rb
CHANGED
@@ -29,24 +29,32 @@ module Venice
|
|
29
29
|
end
|
30
30
|
|
31
31
|
def verify!(data, options = {})
|
32
|
+
@verification_url ||= ITUNES_DEVELOPMENT_RECEIPT_VERIFICATION_ENDPOINT
|
33
|
+
@shared_secret = options[:shared_secret] if options[:shared_secret]
|
34
|
+
|
32
35
|
json = json_response_from_verifying_data(data)
|
33
36
|
status, receipt_attributes = json['status'].to_i, json['receipt']
|
37
|
+
receipt_attributes['original_json_response'] = json if receipt_attributes
|
34
38
|
|
35
39
|
case status
|
36
40
|
when 0, 21006
|
37
41
|
receipt = Receipt.new(receipt_attributes)
|
38
42
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
receipt.
|
43
|
+
# From Apple docs:
|
44
|
+
# > Only returned for iOS 6 style transaction receipts for auto-renewable subscriptions.
|
45
|
+
# > The JSON representation of the receipt for the most recent renewal
|
46
|
+
if latest_receipt_info_attributes = json['latest_receipt_info']
|
47
|
+
# AppStore returns 'latest_receipt_info' even if we use over iOS 6. Besides, its format is an Array.
|
48
|
+
receipt.latest_receipt_info = []
|
49
|
+
latest_receipt_info_attributes.each do |latest_receipt_info_attribute|
|
50
|
+
# latest_receipt_info format is identical with in_app
|
51
|
+
receipt.latest_receipt_info << InAppReceipt.new(latest_receipt_info_attribute)
|
52
|
+
end
|
45
53
|
end
|
46
54
|
|
47
55
|
return receipt
|
48
56
|
else
|
49
|
-
raise Receipt::VerificationError.new(status)
|
57
|
+
raise Receipt::VerificationError.new(status, receipt)
|
50
58
|
end
|
51
59
|
end
|
52
60
|
|
@@ -62,7 +70,7 @@ module Venice
|
|
62
70
|
uri = URI(@verification_url)
|
63
71
|
http = Net::HTTP.new(uri.host, uri.port)
|
64
72
|
http.use_ssl = true
|
65
|
-
http.verify_mode = OpenSSL::SSL::
|
73
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
66
74
|
|
67
75
|
request = Net::HTTP::Post.new(uri.request_uri)
|
68
76
|
request['Accept'] = "application/json"
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
module Venice
|
4
|
+
class InAppReceipt
|
5
|
+
# For detailed explanations on these keys/values, see
|
6
|
+
# https://developer.apple.com/library/ios/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html#//apple_ref/doc/uid/TP40010573-CH106-SW12
|
7
|
+
|
8
|
+
# The number of items purchased. This value corresponds to the quantity property of
|
9
|
+
# the SKPayment object stored in the transaction’s payment property.
|
10
|
+
attr_reader :quantity
|
11
|
+
|
12
|
+
# The product identifier of the item that was purchased. This value corresponds to
|
13
|
+
# the productIdentifier property of the SKPayment object stored in the transaction’s
|
14
|
+
# payment property.
|
15
|
+
attr_reader :product_id
|
16
|
+
|
17
|
+
# The transaction identifier of the item that was purchased. This value corresponds
|
18
|
+
# to the transaction’s transactionIdentifier property.
|
19
|
+
attr_reader :transaction_id
|
20
|
+
|
21
|
+
# The date and time this transaction occurred. This value corresponds to the
|
22
|
+
# transaction’s transactionDate property.
|
23
|
+
attr_reader :purchased_at
|
24
|
+
|
25
|
+
# A string that the App Store uses to uniquely identify the application that created
|
26
|
+
# the payment transaction. If your server supports multiple applications, you can use
|
27
|
+
# this value to differentiate between them. Applications that are executing in the
|
28
|
+
# sandbox do not yet have an app-item-id assigned to them, so this key is missing from
|
29
|
+
# receipts created by the sandbox.
|
30
|
+
attr_reader :app_item_id
|
31
|
+
|
32
|
+
# An arbitrary number that uniquely identifies a revision of your application. This key
|
33
|
+
# is missing in receipts created by the sandbox.
|
34
|
+
attr_reader :version_external_identifier
|
35
|
+
|
36
|
+
# For a transaction that restores a previous transaction, this is the original receipt
|
37
|
+
attr_accessor :original
|
38
|
+
|
39
|
+
# For auto-renewable subscriptions, returns the date the subscription will expire
|
40
|
+
attr_reader :expires_at
|
41
|
+
|
42
|
+
# For a transaction that was canceled by Apple customer support, the time and date of the cancellation.
|
43
|
+
attr_reader :cancellation_at
|
44
|
+
|
45
|
+
|
46
|
+
def initialize(attributes = {})
|
47
|
+
@quantity = Integer(attributes['quantity']) if attributes['quantity']
|
48
|
+
@product_id = attributes['product_id']
|
49
|
+
@transaction_id = attributes['transaction_id']
|
50
|
+
@purchased_at = DateTime.parse(attributes['purchase_date']) if attributes['purchase_date']
|
51
|
+
@app_item_id = attributes['app_item_id']
|
52
|
+
@version_external_identifier = attributes['version_external_identifier']
|
53
|
+
|
54
|
+
# expires_date is in ms since the Epoch, Time.at expects seconds
|
55
|
+
@expires_at = Time.at(attributes['expires_date_ms'].to_i / 1000) if attributes['expires_date_ms']
|
56
|
+
|
57
|
+
# cancellation_date is in ms since the Epoch, Time.at expects seconds
|
58
|
+
@cancellation_date = Time.at(attributes['cancellation_date'].to_i / 1000) if attributes['cancellation_date']
|
59
|
+
|
60
|
+
if attributes['original_transaction_id'] || attributes['original_purchase_date']
|
61
|
+
original_attributes = {
|
62
|
+
'transaction_id' => attributes['original_transaction_id'],
|
63
|
+
'purchase_date' => attributes['original_purchase_date']
|
64
|
+
}
|
65
|
+
|
66
|
+
self.original = InAppReceipt.new(original_attributes)
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
def to_hash
|
72
|
+
{
|
73
|
+
:quantity => @quantity,
|
74
|
+
:product_id => @product_id,
|
75
|
+
:transaction_id => @transaction_id,
|
76
|
+
:purchase_date => (@purchased_at.httpdate rescue nil),
|
77
|
+
:original_transaction_id => (@original.transaction_id rescue nil),
|
78
|
+
:original_purchase_date => (@original.purchased_at.httpdate rescue nil),
|
79
|
+
:app_item_id => @app_item_id,
|
80
|
+
:version_external_identifier => @version_external_identifier,
|
81
|
+
:expires_at => (@expires_at.httpdate rescue nil),
|
82
|
+
:cancellation_at => (@cancellation_at.httpdate rescue nil)
|
83
|
+
}
|
84
|
+
end
|
85
|
+
alias_method :to_h, :to_hash
|
86
|
+
|
87
|
+
def to_json
|
88
|
+
self.to_hash.to_json
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
end
|
data/lib/venice/receipt.rb
CHANGED
@@ -2,83 +2,85 @@ require 'time'
|
|
2
2
|
|
3
3
|
module Venice
|
4
4
|
class Receipt
|
5
|
-
#
|
6
|
-
|
5
|
+
# For detailed explanations on these keys/values, see
|
6
|
+
# https://developer.apple.com/library/ios/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html#//apple_ref/doc/uid/TP40010573-CH106-SW1
|
7
7
|
|
8
|
-
# The
|
9
|
-
attr_reader :
|
8
|
+
# The app’s bundle identifier.
|
9
|
+
attr_reader :bundle_id
|
10
10
|
|
11
|
-
# The
|
12
|
-
attr_reader :
|
11
|
+
# The app’s version number.
|
12
|
+
attr_reader :application_version
|
13
13
|
|
14
|
-
# The
|
15
|
-
attr_reader :
|
14
|
+
# The receipt for an in-app purchase.
|
15
|
+
attr_reader :in_app
|
16
16
|
|
17
|
-
#
|
18
|
-
attr_reader :
|
17
|
+
# The version of the app that was originally purchased.
|
18
|
+
attr_reader :original_application_version
|
19
19
|
|
20
|
-
#
|
21
|
-
attr_reader :
|
20
|
+
# The original purchase date
|
21
|
+
attr_reader :original_purchase_date
|
22
22
|
|
23
|
-
# The
|
24
|
-
attr_reader :
|
25
|
-
|
26
|
-
# A version number for the application.
|
27
|
-
attr_reader :bvrs
|
23
|
+
# The date that the app receipt expires.
|
24
|
+
attr_reader :expires_at
|
28
25
|
|
29
|
-
#
|
30
|
-
|
26
|
+
# Non-Documented receipt keys/values
|
27
|
+
attr_reader :receipt_type
|
28
|
+
attr_reader :adam_id
|
29
|
+
attr_reader :download_id
|
30
|
+
attr_reader :requested_at
|
31
31
|
|
32
|
-
#
|
33
|
-
|
32
|
+
# Original json response from AppStore
|
33
|
+
attr_reader :original_json_response
|
34
34
|
|
35
|
-
# For an expired auto-renewable subscription, this contains the receipt details for the latest expired receipt
|
36
|
-
attr_accessor :latest_expired
|
37
35
|
|
38
|
-
|
39
|
-
attr_reader :expires_at
|
36
|
+
attr_accessor :latest_receipt_info
|
40
37
|
|
41
38
|
def initialize(attributes = {})
|
42
|
-
@
|
43
|
-
|
44
|
-
@
|
45
|
-
@
|
46
|
-
@
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
@expires_at = Time.at(attributes['expires_date'].to_i / 1000) if attributes['expires_date']
|
53
|
-
|
54
|
-
if attributes['original_transaction_id'] || attributes['original_purchase_date']
|
55
|
-
original_attributes = {
|
56
|
-
'transaction_id' => attributes['original_transaction_id'],
|
57
|
-
'purchase_date' => attributes['original_purchase_date']
|
58
|
-
}
|
59
|
-
|
60
|
-
self.original = Receipt.new(original_attributes)
|
39
|
+
@original_json_response = attributes['original_json_response']
|
40
|
+
|
41
|
+
@bundle_id = attributes['bundle_id']
|
42
|
+
@application_version = attributes['application_version']
|
43
|
+
@original_application_version = attributes['original_application_version']
|
44
|
+
if attributes['original_purchase_date']
|
45
|
+
@original_purchase_date = DateTime.parse(attributes['original_purchase_date'])
|
46
|
+
end
|
47
|
+
if attributes['expiration_date']
|
48
|
+
@expires_at = Time.at(attributes['expiration_date'].to_i / 1000).to_datetime
|
61
49
|
end
|
50
|
+
|
51
|
+
@receipt_type = attributes['receipt_type']
|
52
|
+
@adam_id = attributes['adam_id']
|
53
|
+
@download_id = attributes['download_id']
|
54
|
+
@requested_at = DateTime.parse(attributes['request_date']) if attributes['request_date']
|
55
|
+
|
56
|
+
@in_app = []
|
57
|
+
if attributes['in_app']
|
58
|
+
attributes['in_app'].each do |in_app_purchase_attributes|
|
59
|
+
@in_app << InAppReceipt.new(in_app_purchase_attributes)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
62
63
|
end
|
63
64
|
|
64
|
-
def
|
65
|
+
def to_hash
|
65
66
|
{
|
66
|
-
:
|
67
|
-
:
|
68
|
-
:
|
69
|
-
:
|
70
|
-
:
|
71
|
-
:
|
72
|
-
:
|
73
|
-
:
|
74
|
-
:
|
75
|
-
:
|
76
|
-
:
|
67
|
+
:bundle_id => @bundle_id,
|
68
|
+
:application_version => @application_version,
|
69
|
+
:original_application_version => @original_application_version,
|
70
|
+
:original_purchase_date => (@original_purchase_date.httpdate rescue nil),
|
71
|
+
:expires_at => (@expires_at.httpdate rescue nil),
|
72
|
+
:receipt_type => @receipt_type,
|
73
|
+
:adam_id => @adam_id,
|
74
|
+
:download_id => @download_id,
|
75
|
+
:requested_at => (@requested_at.httpdate rescue nil),
|
76
|
+
:in_app => @in_app.map{|iap| iap.to_h },
|
77
|
+
:latest_receipt_info => @latest_receipt_info
|
77
78
|
}
|
78
79
|
end
|
80
|
+
alias_method :to_h, :to_hash
|
79
81
|
|
80
82
|
def to_json
|
81
|
-
self.
|
83
|
+
self.to_hash.to_json
|
82
84
|
end
|
83
85
|
|
84
86
|
class << self
|
@@ -111,9 +113,11 @@ module Venice
|
|
111
113
|
|
112
114
|
class VerificationError < StandardError
|
113
115
|
attr_accessor :code
|
116
|
+
attr_accessor :receipt
|
114
117
|
|
115
|
-
def initialize(code)
|
118
|
+
def initialize(code, receipt)
|
116
119
|
@code = Integer(code)
|
120
|
+
@receipt = receipt
|
117
121
|
end
|
118
122
|
|
119
123
|
def message
|
data/lib/venice/version.rb
CHANGED
data/spec/client_spec.rb
CHANGED
@@ -8,10 +8,14 @@ describe Venice::Client do
|
|
8
8
|
context "no shared_secret" do
|
9
9
|
before do
|
10
10
|
client.shared_secret = nil
|
11
|
+
Venice::Receipt.stub :new
|
11
12
|
end
|
12
13
|
|
13
14
|
it "should only include the receipt_data" do
|
14
|
-
|
15
|
+
Net::HTTP.any_instance.should_receive(:request) do |post|
|
16
|
+
post.body.should eq({'receipt-data' => receipt_data}.to_json)
|
17
|
+
post
|
18
|
+
end
|
15
19
|
client.verify! receipt_data
|
16
20
|
end
|
17
21
|
end
|
@@ -20,14 +24,77 @@ describe Venice::Client do
|
|
20
24
|
let(:secret) { "shhhhhh" }
|
21
25
|
|
22
26
|
before do
|
23
|
-
|
27
|
+
Venice::Receipt.stub :new
|
24
28
|
end
|
25
29
|
|
26
|
-
|
27
|
-
|
28
|
-
|
30
|
+
context "set secret manually" do
|
31
|
+
before do
|
32
|
+
client.shared_secret = secret
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should include the secret in the post" do
|
36
|
+
Net::HTTP.any_instance.should_receive(:request) do |post|
|
37
|
+
post.body.should eq({'receipt-data' => receipt_data, 'password' => secret}.to_json)
|
38
|
+
post
|
39
|
+
end
|
40
|
+
client.verify! receipt_data
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context "set secret when verification" do
|
45
|
+
let(:options) { {shared_secret: secret} }
|
46
|
+
|
47
|
+
it "should include the secret in the post" do
|
48
|
+
Net::HTTP.any_instance.should_receive(:request) do |post|
|
49
|
+
post.body.should eq({'receipt-data' => receipt_data, 'password' => secret}.to_json)
|
50
|
+
post
|
51
|
+
end
|
52
|
+
client.verify! receipt_data, options
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
context "with a latest receipt info attribute" do
|
59
|
+
before do
|
60
|
+
client.stub(:json_response_from_verifying_data).and_return(response)
|
61
|
+
end
|
62
|
+
|
63
|
+
let(:response) do
|
64
|
+
{
|
65
|
+
'status' => 0,
|
66
|
+
'receipt' => {},
|
67
|
+
'latest_receipt' => "<encoded string>",
|
68
|
+
'latest_receipt_info' => [
|
69
|
+
{
|
70
|
+
"original_purchase_date_pst" => "2012-12-30 09:39:24 America/Los_Angeles",
|
71
|
+
"unique_identifier" => "0000b01147b8",
|
72
|
+
"original_transaction_id" => "1000000061051565",
|
73
|
+
"expires_date" => "1365114731000",
|
74
|
+
"transaction_id" => "1000000070104252",
|
75
|
+
"quantity" => "1",
|
76
|
+
"product_id" => "com.ficklebits.nsscreencast.monthly_sub",
|
77
|
+
"original_purchase_date_ms" => "1356889164000",
|
78
|
+
"bid" => "com.ficklebits.nsscreencast",
|
79
|
+
"web_order_line_item_id" => "1000000026812043",
|
80
|
+
"bvrs" => "0.1",
|
81
|
+
"expires_date_formatted" => "2013-04-04 22:32:11 Etc/GMT",
|
82
|
+
"purchase_date" => "2013-04-04 22:27:11 Etc/GMT",
|
83
|
+
"purchase_date_ms" => "1365114431000",
|
84
|
+
"expires_date_formatted_pst" => "2013-04-04 15:32:11 America/Los_Angeles",
|
85
|
+
"purchase_date_pst" => "2013-04-04 15:27:11 America/Los_Angeles",
|
86
|
+
"original_purchase_date" => "2012-12-30 17:39:24 Etc/GMT",
|
87
|
+
"item_id" => "590265423"
|
88
|
+
}
|
89
|
+
]
|
90
|
+
}
|
91
|
+
end
|
92
|
+
|
93
|
+
it "should create a latest receipt" do
|
94
|
+
receipt = client.verify! 'asdf'
|
95
|
+
receipt.latest_receipt_info.should_not be_nil
|
29
96
|
end
|
30
97
|
end
|
98
|
+
|
31
99
|
end
|
32
100
|
end
|
33
|
-
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'spec_helper.rb'
|
2
|
+
|
3
|
+
describe Venice::InAppReceipt do
|
4
|
+
|
5
|
+
describe ".new" do
|
6
|
+
|
7
|
+
let :attributes do
|
8
|
+
{
|
9
|
+
"quantity" => 1,
|
10
|
+
"product_id" => "com.foo.product1",
|
11
|
+
"transaction_id" => "1000000070107235",
|
12
|
+
"purchase_date" => "2014-05-28 14:47:53 Etc/GMT",
|
13
|
+
"purchase_date_ms" => "1401288473000",
|
14
|
+
"purchase_date_pst" => "2014-05-28 07:47:53 America/Los_Angeles",
|
15
|
+
"original_transaction_id" => "140xxx867509",
|
16
|
+
"original_purchase_date" => "2014-05-28 14:47:53 Etc/GMT",
|
17
|
+
"original_purchase_date_ms" => "1401288473000",
|
18
|
+
"original_purchase_date_pst" => "2014-05-28 07:47:53 America/Los_Angeles",
|
19
|
+
"is_trial_period" => false,
|
20
|
+
"version_external_identifier" => "123",
|
21
|
+
"app_item_id" => 'com.foo.app1',
|
22
|
+
"expires_date" => "2014-06-28 07:47:53 America/Los_Angeles",
|
23
|
+
"expires_date_ms" => "1403941673000"
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
subject(:in_app_receipt) do
|
28
|
+
Venice::InAppReceipt.new attributes
|
29
|
+
end
|
30
|
+
|
31
|
+
its(:quantity) { 1 }
|
32
|
+
its(:product_id) { "com.foo.product1" }
|
33
|
+
its(:transaction_id) { "1000000070107235" }
|
34
|
+
its(:purchased_at) { should be_instance_of DateTime }
|
35
|
+
its(:app_item_id) { 'com.foo.app1' }
|
36
|
+
its(:version_external_identifier) { "123" }
|
37
|
+
its(:original) { should be_instance_of Venice::InAppReceipt }
|
38
|
+
its(:expires_at) { should be_instance_of Time }
|
39
|
+
|
40
|
+
it "should parse the 'original' attributes" do
|
41
|
+
subject.original.should be_instance_of Venice::InAppReceipt
|
42
|
+
subject.original.transaction_id.should == "140xxx867509"
|
43
|
+
subject.original.purchased_at.should be_instance_of DateTime
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should output a hash with attributes" do
|
47
|
+
in_app_receipt.to_h.should include(:quantity => 1,
|
48
|
+
:product_id => "com.foo.product1",
|
49
|
+
:transaction_id => "1000000070107235",
|
50
|
+
:purchase_date => "Wed, 28 May 2014 14:47:53 GMT",
|
51
|
+
:original_purchase_date => "Wed, 28 May 2014 14:47:53 GMT"
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|