venice 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|