ledger_sync-netsuite 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.
Files changed (140) hide show
  1. checksums.yaml +7 -0
  2. data/.env.test +14 -0
  3. data/.github/workflows/gem-workflow.yml +63 -0
  4. data/.gitignore +20 -0
  5. data/.overcommit.yml +29 -0
  6. data/.rubocop.yml +78 -0
  7. data/.rubocop_todo.yml +25 -0
  8. data/Gemfile +8 -0
  9. data/Gemfile.lock +203 -0
  10. data/README.md +19 -0
  11. data/Rakefile +8 -0
  12. data/bin/console +15 -0
  13. data/bin/setup +8 -0
  14. data/ledger_sync-netsuite.gemspec +44 -0
  15. data/lib/ledger_sync/netsuite.rb +13 -0
  16. data/lib/ledger_sync/netsuite/account/deserializer.rb +21 -0
  17. data/lib/ledger_sync/netsuite/account/operations/create.rb +26 -0
  18. data/lib/ledger_sync/netsuite/account/operations/find.rb +26 -0
  19. data/lib/ledger_sync/netsuite/account/searcher.rb +10 -0
  20. data/lib/ledger_sync/netsuite/account/searcher_deserializer.rb +27 -0
  21. data/lib/ledger_sync/netsuite/account/serializer.rb +19 -0
  22. data/lib/ledger_sync/netsuite/check/deserializer.rb +27 -0
  23. data/lib/ledger_sync/netsuite/check/operations/create.rb +25 -0
  24. data/lib/ledger_sync/netsuite/check/operations/delete.rb +25 -0
  25. data/lib/ledger_sync/netsuite/check/operations/find.rb +25 -0
  26. data/lib/ledger_sync/netsuite/check/operations/update.rb +25 -0
  27. data/lib/ledger_sync/netsuite/check/searcher.rb +13 -0
  28. data/lib/ledger_sync/netsuite/check/searcher_deserializer.rb +14 -0
  29. data/lib/ledger_sync/netsuite/check/serializer.rb +49 -0
  30. data/lib/ledger_sync/netsuite/check_line_item/deserializer.rb +16 -0
  31. data/lib/ledger_sync/netsuite/check_line_item/serializer.rb +24 -0
  32. data/lib/ledger_sync/netsuite/client.rb +186 -0
  33. data/lib/ledger_sync/netsuite/config.rb +12 -0
  34. data/lib/ledger_sync/netsuite/currency/deserializer.rb +21 -0
  35. data/lib/ledger_sync/netsuite/currency/operations/create.rb +23 -0
  36. data/lib/ledger_sync/netsuite/currency/operations/delete.rb +21 -0
  37. data/lib/ledger_sync/netsuite/currency/operations/find.rb +25 -0
  38. data/lib/ledger_sync/netsuite/currency/operations/update.rb +21 -0
  39. data/lib/ledger_sync/netsuite/currency/serializer.rb +19 -0
  40. data/lib/ledger_sync/netsuite/customer/deserializer.rb +20 -0
  41. data/lib/ledger_sync/netsuite/customer/operations/create.rb +24 -0
  42. data/lib/ledger_sync/netsuite/customer/operations/delete.rb +24 -0
  43. data/lib/ledger_sync/netsuite/customer/operations/find.rb +24 -0
  44. data/lib/ledger_sync/netsuite/customer/operations/update.rb +24 -0
  45. data/lib/ledger_sync/netsuite/customer/searcher.rb +10 -0
  46. data/lib/ledger_sync/netsuite/customer/searcher_deserializer.rb +16 -0
  47. data/lib/ledger_sync/netsuite/customer/serializer.rb +27 -0
  48. data/lib/ledger_sync/netsuite/customer_deposit/deserializer.rb +18 -0
  49. data/lib/ledger_sync/netsuite/customer_deposit/operations/create.rb +22 -0
  50. data/lib/ledger_sync/netsuite/customer_deposit/operations/delete.rb +22 -0
  51. data/lib/ledger_sync/netsuite/customer_deposit/operations/find.rb +22 -0
  52. data/lib/ledger_sync/netsuite/customer_deposit/operations/update.rb +22 -0
  53. data/lib/ledger_sync/netsuite/customer_deposit/searcher.rb +10 -0
  54. data/lib/ledger_sync/netsuite/customer_deposit/searcher_deserializer.rb +14 -0
  55. data/lib/ledger_sync/netsuite/customer_deposit/serializer.rb +23 -0
  56. data/lib/ledger_sync/netsuite/dashboard_url_helper.rb +26 -0
  57. data/lib/ledger_sync/netsuite/department/deserializer.rb +13 -0
  58. data/lib/ledger_sync/netsuite/department/operations/create.rb +23 -0
  59. data/lib/ledger_sync/netsuite/department/operations/delete.rb +23 -0
  60. data/lib/ledger_sync/netsuite/department/operations/find.rb +23 -0
  61. data/lib/ledger_sync/netsuite/department/operations/update.rb +23 -0
  62. data/lib/ledger_sync/netsuite/department/searcher.rb +10 -0
  63. data/lib/ledger_sync/netsuite/department/searcher_deserializer.rb +17 -0
  64. data/lib/ledger_sync/netsuite/department/serializer.rb +11 -0
  65. data/lib/ledger_sync/netsuite/deserializer.rb +16 -0
  66. data/lib/ledger_sync/netsuite/error.rb +17 -0
  67. data/lib/ledger_sync/netsuite/journal_entry/deserializer.rb +29 -0
  68. data/lib/ledger_sync/netsuite/journal_entry/operations/create.rb +24 -0
  69. data/lib/ledger_sync/netsuite/journal_entry/operations/delete.rb +24 -0
  70. data/lib/ledger_sync/netsuite/journal_entry/operations/find.rb +24 -0
  71. data/lib/ledger_sync/netsuite/journal_entry/operations/update.rb +28 -0
  72. data/lib/ledger_sync/netsuite/journal_entry/serializer.rb +34 -0
  73. data/lib/ledger_sync/netsuite/journal_entry_line_item/deserializer.rb +18 -0
  74. data/lib/ledger_sync/netsuite/journal_entry_line_item/serializer.rb +18 -0
  75. data/lib/ledger_sync/netsuite/ledger_class/deserializer.rb +13 -0
  76. data/lib/ledger_sync/netsuite/ledger_class/operations/create.rb +23 -0
  77. data/lib/ledger_sync/netsuite/ledger_class/operations/delete.rb +23 -0
  78. data/lib/ledger_sync/netsuite/ledger_class/operations/find.rb +23 -0
  79. data/lib/ledger_sync/netsuite/ledger_class/operations/update.rb +23 -0
  80. data/lib/ledger_sync/netsuite/ledger_class/searcher.rb +10 -0
  81. data/lib/ledger_sync/netsuite/ledger_class/searcher_deserializer.rb +17 -0
  82. data/lib/ledger_sync/netsuite/ledger_class/serializer.rb +11 -0
  83. data/lib/ledger_sync/netsuite/location/deserializer.rb +13 -0
  84. data/lib/ledger_sync/netsuite/location/operations/create.rb +19 -0
  85. data/lib/ledger_sync/netsuite/location/operations/delete.rb +19 -0
  86. data/lib/ledger_sync/netsuite/location/operations/find.rb +19 -0
  87. data/lib/ledger_sync/netsuite/location/operations/update.rb +19 -0
  88. data/lib/ledger_sync/netsuite/location/searcher.rb +10 -0
  89. data/lib/ledger_sync/netsuite/location/serializer.rb +11 -0
  90. data/lib/ledger_sync/netsuite/operation.rb +38 -0
  91. data/lib/ledger_sync/netsuite/operation/create.rb +56 -0
  92. data/lib/ledger_sync/netsuite/operation/delete.rb +47 -0
  93. data/lib/ledger_sync/netsuite/operation/find.rb +68 -0
  94. data/lib/ledger_sync/netsuite/operation/update.rb +59 -0
  95. data/lib/ledger_sync/netsuite/record/http_method.rb +31 -0
  96. data/lib/ledger_sync/netsuite/record/metadata.rb +107 -0
  97. data/lib/ledger_sync/netsuite/record/parameter.rb +18 -0
  98. data/lib/ledger_sync/netsuite/record/property.rb +20 -0
  99. data/lib/ledger_sync/netsuite/reference/serializer.rb +11 -0
  100. data/lib/ledger_sync/netsuite/resource.rb +8 -0
  101. data/lib/ledger_sync/netsuite/resources/account.rb +41 -0
  102. data/lib/ledger_sync/netsuite/resources/check.rb +21 -0
  103. data/lib/ledger_sync/netsuite/resources/check_line_item.rb +13 -0
  104. data/lib/ledger_sync/netsuite/resources/currency.rb +11 -0
  105. data/lib/ledger_sync/netsuite/resources/customer.rb +17 -0
  106. data/lib/ledger_sync/netsuite/resources/customer_deposit.rb +13 -0
  107. data/lib/ledger_sync/netsuite/resources/department.rb +14 -0
  108. data/lib/ledger_sync/netsuite/resources/deposit.rb +26 -0
  109. data/lib/ledger_sync/netsuite/resources/deposit_line_item.rb +23 -0
  110. data/lib/ledger_sync/netsuite/resources/invoice.rb +26 -0
  111. data/lib/ledger_sync/netsuite/resources/invoice_line_item.rb +19 -0
  112. data/lib/ledger_sync/netsuite/resources/item.rb +9 -0
  113. data/lib/ledger_sync/netsuite/resources/journal_entry.rb +24 -0
  114. data/lib/ledger_sync/netsuite/resources/journal_entry_line_item.rb +22 -0
  115. data/lib/ledger_sync/netsuite/resources/ledger_class.rb +14 -0
  116. data/lib/ledger_sync/netsuite/resources/location.rb +9 -0
  117. data/lib/ledger_sync/netsuite/resources/subsidiary.rb +10 -0
  118. data/lib/ledger_sync/netsuite/resources/vendor.rb +21 -0
  119. data/lib/ledger_sync/netsuite/searcher.rb +64 -0
  120. data/lib/ledger_sync/netsuite/serializer.rb +11 -0
  121. data/lib/ledger_sync/netsuite/subsidiary/deserializer.rb +15 -0
  122. data/lib/ledger_sync/netsuite/subsidiary/searcher.rb +10 -0
  123. data/lib/ledger_sync/netsuite/subsidiary/searcher_deserializer.rb +13 -0
  124. data/lib/ledger_sync/netsuite/subsidiary/serializer.rb +13 -0
  125. data/lib/ledger_sync/netsuite/token.rb +156 -0
  126. data/lib/ledger_sync/netsuite/type/deserializer_active_type.rb +30 -0
  127. data/lib/ledger_sync/netsuite/type/deserializer_customer_type.rb +33 -0
  128. data/lib/ledger_sync/netsuite/type/deserializer_entity_type.rb +28 -0
  129. data/lib/ledger_sync/netsuite/type/deserializer_subsidiary_type.rb +33 -0
  130. data/lib/ledger_sync/netsuite/vendor/deserializer.rb +28 -0
  131. data/lib/ledger_sync/netsuite/vendor/operations/create.rb +25 -0
  132. data/lib/ledger_sync/netsuite/vendor/operations/delete.rb +25 -0
  133. data/lib/ledger_sync/netsuite/vendor/operations/find.rb +25 -0
  134. data/lib/ledger_sync/netsuite/vendor/operations/update.rb +25 -0
  135. data/lib/ledger_sync/netsuite/vendor/searcher.rb +10 -0
  136. data/lib/ledger_sync/netsuite/vendor/searcher_deserializer.rb +19 -0
  137. data/lib/ledger_sync/netsuite/vendor/serializer.rb +28 -0
  138. data/lib/ledger_sync/netsuite/version.rb +17 -0
  139. data/license.txt +4 -0
  140. metadata +392 -0
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LedgerSync
4
+ module NetSuite
5
+ class CheckLineItem
6
+ class Deserializer < NetSuite::Deserializer
7
+ attribute :amount
8
+ attribute :memo
9
+ references_one :account
10
+ references_one :ledger_class,
11
+ hash_attribute: 'class'
12
+ references_one :department
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../reference/serializer'
4
+
5
+ module LedgerSync
6
+ module NetSuite
7
+ class CheckLineItem
8
+ class Serializer < NetSuite::Serializer
9
+ attribute :amount
10
+ attribute :memo
11
+
12
+ references_one :account,
13
+ serializer: Reference::Serializer
14
+
15
+ references_one :department,
16
+ serializer: Reference::Serializer
17
+
18
+ references_one :class,
19
+ resource_attribute: :ledger_class,
20
+ serializer: Reference::Serializer
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'oauth2'
4
+
5
+ module LedgerSync
6
+ module NetSuite
7
+ class Client
8
+ include Ledgers::Client::Mixin
9
+
10
+ HEADERS = {
11
+ # 'Accept' => 'application/schema+json'
12
+ }.freeze
13
+
14
+ WRITE_HEADERS = {
15
+ 'Accept' => '*/*',
16
+ 'Content-Type' => 'application/json',
17
+ 'prefer' => 'transient'
18
+ }.freeze
19
+
20
+ attr_reader :account_id,
21
+ :consumer_key,
22
+ :consumer_secret,
23
+ :token_id,
24
+ :token_secret
25
+
26
+ def initialize(
27
+ account_id:,
28
+ consumer_key:,
29
+ consumer_secret:,
30
+ token_id:,
31
+ token_secret:
32
+ )
33
+ @account_id = account_id
34
+ @consumer_key = consumer_key
35
+ @consumer_secret = consumer_secret
36
+ @token_id = token_id
37
+ @token_secret = token_secret
38
+
39
+ super()
40
+ end
41
+
42
+ def account_id_for_oauth
43
+ account_id.gsub(/-/, '_').upcase
44
+ end
45
+
46
+ def account_id_for_url
47
+ account_id.gsub(/_/, '-').downcase
48
+ end
49
+
50
+ def api_base_url
51
+ @api_base_url ||= "https://#{api_host}/services/rest/record/v1"
52
+ end
53
+
54
+ def api_host
55
+ @api_host ||= "#{account_id_for_url}.suitetalk.api.netsuite.com"
56
+ end
57
+
58
+ def delete(**keywords)
59
+ request(keywords.merge(method: :delete))
60
+ end
61
+
62
+ def get(**keywords)
63
+ request(keywords.merge(method: :get))
64
+ end
65
+
66
+ def ledger_resource_path(args = {})
67
+ resource = args.fetch(:resource, nil)
68
+ params = args.fetch(:params, {})
69
+
70
+ ret = self.class.ledger_resource_type_for(resource_class: resource.class)
71
+ ret += "/#{resource.ledger_id}" if resource.ledger_id.present? && args.fetch(:id, true)
72
+ Util::URLHelpers.merge_params_in_path(
73
+ path: ret,
74
+ params: params
75
+ )
76
+ end
77
+
78
+ def metadata_for(record:)
79
+ Record::Metadata.new(
80
+ client: self,
81
+ record: record
82
+ )
83
+ end
84
+
85
+ def patch(headers: {}, **keywords)
86
+ request(
87
+ keywords.merge(
88
+ headers: WRITE_HEADERS.merge(headers),
89
+ method: :patch
90
+ )
91
+ )
92
+ end
93
+
94
+ def post(headers: {}, **keywords)
95
+ request(
96
+ keywords.merge(
97
+ headers: WRITE_HEADERS.merge(headers),
98
+ method: :post
99
+ )
100
+ )
101
+ end
102
+
103
+ def self.ledger_attributes_to_save
104
+ %i[]
105
+ end
106
+
107
+ def self.new_from_env(**override)
108
+ new(
109
+ {
110
+ account_id: ENV.fetch('NETSUITE_ACCOUNT_ID', nil),
111
+ consumer_key: ENV.fetch('NETSUITE_CONSUMER_KEY', nil),
112
+ consumer_secret: ENV.fetch('NETSUITE_CONSUMER_SECRET', nil),
113
+ token_id: ENV.fetch('NETSUITE_TOKEN_ID', nil),
114
+ token_secret: ENV.fetch('NETSUITE_TOKEN_SECRET', nil)
115
+ }.merge(override)
116
+ )
117
+ end
118
+
119
+ def url_for(resource:)
120
+ DashboardURLHelper.new(
121
+ resource: resource,
122
+ base_url: "https://#{account_id_for_url}.app.netsuite.com"
123
+ ).url
124
+ end
125
+
126
+ def self.ledger_resource_type_overrides
127
+ {
128
+ CustomerDeposit => 'customerdeposit',
129
+ LedgerClass => 'classification',
130
+ JournalEntry => 'journalEntry'
131
+ }
132
+ end
133
+
134
+ private
135
+
136
+ def new_token(args = {})
137
+ Token.new(
138
+ args.merge(
139
+ consumer_key: consumer_key,
140
+ consumer_secret: consumer_secret,
141
+ token_id: token_id,
142
+ token_secret: token_secret
143
+ )
144
+ )
145
+ end
146
+
147
+ def request(args = {})
148
+ body = args.fetch(:body, nil)
149
+ headers = args.fetch(:headers, {})
150
+ method = args.fetch(:method)
151
+ path = args.fetch(:path, nil)
152
+ request_url = args.fetch(:request_url, url_from_path(path: path))
153
+
154
+ token = new_token(
155
+ body: body,
156
+ method: method,
157
+ url: request_url
158
+ )
159
+
160
+ request = LedgerSync::Ledgers::Request.new(
161
+ body: body,
162
+ headers: headers
163
+ .merge(HEADERS)
164
+ .merge(
165
+ token.headers(
166
+ realm: account_id_for_oauth
167
+ )
168
+ )
169
+ .merge(
170
+ 'Host' => api_host
171
+ ),
172
+ method: method,
173
+ url: request_url
174
+ )
175
+
176
+ request.perform
177
+ end
178
+
179
+ def url_from_path(path:)
180
+ request_url = api_base_url
181
+ request_url += '/' unless path.to_s.start_with?('/')
182
+ request_url + path.to_s
183
+ end
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'client'
4
+
5
+ args = {
6
+ base_module: LedgerSync::NetSuite,
7
+ root_path: 'ledger_sync/netsuite'
8
+ }
9
+
10
+ LedgerSync.register_ledger(:netsuite, args) do |config|
11
+ config.name = 'NetSuite REST'
12
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LedgerSync
4
+ module NetSuite
5
+ class Currency
6
+ class Deserializer < NetSuite::Deserializer
7
+ id
8
+
9
+ attribute :name
10
+
11
+ attribute :external_id,
12
+ hash_attribute: :externalid
13
+
14
+ attribute :symbol
15
+
16
+ attribute :exchange_rate,
17
+ hash_attribute: :exchangerate
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # require_relative '../../operation/create'
4
+
5
+ module LedgerSync
6
+ module NetSuite
7
+ class Currency
8
+ module Operations
9
+ class Create < NetSuite::Operation::Create
10
+ class Contract < LedgerSync::Ledgers::Contract
11
+ params do
12
+ required(:external_id).filled(:string)
13
+ required(:ledger_id).value(:nil)
14
+ required(:exchange_rate).filled(:float)
15
+ required(:name).filled(:string)
16
+ required(:symbol).filled(:string)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LedgerSync
4
+ module NetSuite
5
+ class Currency
6
+ module Operations
7
+ class Delete < NetSuite::Operation::Delete
8
+ class Contract < LedgerSync::Ledgers::Contract
9
+ params do
10
+ required(:external_id).maybe(:string)
11
+ required(:ledger_id).filled(:string)
12
+ required(:exchange_rate).maybe(:float)
13
+ required(:name).maybe(:string)
14
+ required(:symbol).maybe(:string)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LedgerSync
4
+ module NetSuite
5
+ class Currency
6
+ module Operations
7
+ class Find < NetSuite::Operation::Find
8
+ class Contract < LedgerSync::Ledgers::Contract
9
+ params do
10
+ required(:external_id).maybe(:string)
11
+ required(:ledger_id).filled(:string)
12
+ required(:name).maybe(:string)
13
+ required(:symbol).maybe(:string)
14
+ required(:exchange_rate).maybe(:float)
15
+ end
16
+ end
17
+
18
+ def self.expand_sub_resources?
19
+ false
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LedgerSync
4
+ module NetSuite
5
+ class Currency
6
+ module Operations
7
+ class Update < NetSuite::Operation::Update
8
+ class Contract < LedgerSync::Ledgers::Contract
9
+ params do
10
+ required(:external_id).maybe(:string)
11
+ required(:ledger_id).filled(:string)
12
+ required(:exchange_rate).maybe(:float)
13
+ required(:name).filled(:string)
14
+ required(:symbol).filled(:string)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LedgerSync
4
+ module NetSuite
5
+ class Currency
6
+ class Serializer < NetSuite::Serializer
7
+ attribute :name
8
+
9
+ attribute :externalid,
10
+ resource_attribute: :external_id
11
+
12
+ attribute :symbol
13
+
14
+ attribute :exchangerate,
15
+ resource_attribute: :exchange_rate
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LedgerSync
4
+ module NetSuite
5
+ class Customer
6
+ class Deserializer < NetSuite::Deserializer
7
+ id
8
+
9
+ attribute :companyName
10
+ attribute :firstName
11
+ attribute :lastName
12
+ attribute :email
13
+ attribute :phone
14
+
15
+ attribute :subsidiary,
16
+ type: Type::DeserializerSubsidiaryType.new(subsidiary_class: Subsidiary)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LedgerSync
4
+ module NetSuite
5
+ class Customer
6
+ module Operations
7
+ class Create < NetSuite::Operation::Create
8
+ class Contract < LedgerSync::Ledgers::Contract
9
+ params do
10
+ required(:external_id).filled(:string)
11
+ required(:ledger_id).value(:nil)
12
+ required(:email).maybe(:string)
13
+ required(:companyName).filled(:string)
14
+ required(:phone).maybe(:string)
15
+ required(:firstName).maybe(:string)
16
+ required(:lastName).maybe(:string)
17
+ required(:subsidiary).filled(:hash, Types::Reference)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LedgerSync
4
+ module NetSuite
5
+ class Customer
6
+ module Operations
7
+ class Delete < NetSuite::Operation::Delete
8
+ class Contract < LedgerSync::Ledgers::Contract
9
+ params do
10
+ required(:external_id).maybe(:string)
11
+ required(:ledger_id).filled(:string)
12
+ required(:email).maybe(:string)
13
+ required(:companyName).maybe(:string)
14
+ required(:firstName).maybe(:string)
15
+ required(:lastName).maybe(:string)
16
+ required(:phone).maybe(:string)
17
+ required(:subsidiary).maybe(:hash, Types::Reference)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end