mangopay 3.0.30 → 3.0.34

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +6 -1
  3. data/CHANGELOG.md +35 -0
  4. data/README.md +5 -5
  5. data/lib/generators/mangopay/install_generator.rb +1 -1
  6. data/lib/generators/templates/mangopay.rb.erb +1 -1
  7. data/lib/mangopay.rb +19 -9
  8. data/lib/mangopay/authorization_token.rb +2 -2
  9. data/lib/mangopay/bank_account.rb +11 -0
  10. data/lib/mangopay/card.rb +15 -0
  11. data/lib/mangopay/filter_parameters.rb +6 -1
  12. data/lib/mangopay/http_calls.rb +7 -7
  13. data/lib/mangopay/legal_user.rb +0 -8
  14. data/lib/mangopay/mandate.rb +10 -0
  15. data/lib/mangopay/pay_in.rb +31 -0
  16. data/lib/mangopay/pay_out.rb +11 -0
  17. data/lib/mangopay/refund.rb +11 -0
  18. data/lib/mangopay/transfer.rb +11 -0
  19. data/lib/mangopay/ubo.rb +26 -0
  20. data/lib/mangopay/ubo_declaration.rb +22 -11
  21. data/lib/mangopay/user.rb +24 -6
  22. data/lib/mangopay/version.rb +1 -1
  23. data/spec/mangopay/bank_account_spec.rb +8 -0
  24. data/spec/mangopay/card_registration_spec.rb +7 -0
  25. data/spec/mangopay/client_spec.rb +17 -6
  26. data/spec/mangopay/configuration_spec.rb +53 -1
  27. data/spec/mangopay/event_spec.rb +12 -10
  28. data/spec/mangopay/mandate_spec.rb +7 -0
  29. data/spec/mangopay/payin_applepay_direct_spec.rb +20 -0
  30. data/spec/mangopay/payin_bankwire_external_instruction_spec.rb +43 -4
  31. data/spec/mangopay/payin_card_web_spec.rb +17 -0
  32. data/spec/mangopay/payin_paypal_web_spec.rb +13 -1
  33. data/spec/mangopay/payout_bankwire_spec.rb +8 -0
  34. data/spec/mangopay/preauthorization_spec.rb +9 -0
  35. data/spec/mangopay/refund_spec.rb +13 -0
  36. data/spec/mangopay/shared_resources.rb +346 -240
  37. data/spec/mangopay/transfer_spec.rb +8 -0
  38. data/spec/mangopay/ubo_declaration_spec.rb +13 -17
  39. data/spec/mangopay/ubo_spec.rb +39 -0
  40. data/spec/mangopay/user_spec.rb +51 -16
  41. data/spec/spec_helper.rb +1 -1
  42. metadata +9 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6c038a492f0096820e24da22def17585dc1b5a13aa58eca6d18820bba16607bb
4
- data.tar.gz: db6237ec1d2c88f544e975d6edc0b13944a5fdb202089e08ce2984a56176f966
3
+ metadata.gz: cb254e350875d634519586fc4e451a4c83148886522287f266ed1306131235b4
4
+ data.tar.gz: ef04474a28bf48dbef09cb20cf48375fdac3183546d30631b1f443aa0f9f21b8
5
5
  SHA512:
6
- metadata.gz: 63fe20ad687cfbb42d4e31f7d7cf0c65b66e87e82487489d0e4bfd45486e30e99360a6c0396dc38ec44f1a7a1652b5bdb8c423b88b586e30c556efb824c5d637
7
- data.tar.gz: c9f7cbe0843ccaf8153b2741d5da3587a3d6bf83f25884f342c1a60233b55101b0ed840c67214ceb3e7a48a4b31ee5bd08f455cf2317031aba63efa6a7c8887f
6
+ metadata.gz: c8ddc8e015f6ffe95c3947f063a2736a9863b19cf24a02d389fa00d9b9f623c27e083468e7c6eb9d2fcde31a974d3cb73da2674e76b09f53a14b80aaf6c59ca4
7
+ data.tar.gz: 49ce455ede60281e5c6b430c2c722d7c0c0f3abcc15204beaedd8c3954621d8f0057f05493ebd47df84f02cb91ea0d63db4e247b984f28091edcb525326b7158
@@ -1,6 +1,11 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 2.0.0
3
+ - 2.0.0
4
+ - 2.1
5
+ - 2.2
6
+ - 2.3
7
+ - 2.4
8
+ - 2.5
4
9
  script: bundle exec rspec
5
10
  deploy:
6
11
  provider: rubygems
@@ -0,0 +1,35 @@
1
+ ## [Unreleased]
2
+ - New v4 SDK version (available as a beta version here: https://rubygems.org/gems/mangopay-v4) - will be shortly merged on master.
3
+
4
+ ## [3.0.34] - 2020-06-25
5
+ ### Added
6
+ - This SDK is now GooglePay-ready ! Feel free to ask our lovely support for more infos about its activation.
7
+ - `UBODeclaration` is now directly available through its ID.
8
+ - If a bankwire is done from a UK bankaccount on one of your `BankingAlias`, you could find its `AccountNumber` on `GET /payins/` response
9
+ - You can now send a `Culture` parameter for Paypal PayIns. Thanks to it, payment page can be displayed in various languages.
10
+
11
+ ### Changed
12
+ - `PAYLINEV2`value for Payin Web has been added on `TemplateURLOptions` object. You now should use it instead of `PAYLINE` for page customization.
13
+
14
+ ### Fixed
15
+ - Missing filters parameters have been added
16
+ - You can now send headers in update requests
17
+ - Loggers have been enhanced
18
+
19
+ ## [3.0.33] - 2019-09-23
20
+ ### Added
21
+ - ApplePay `Payin` functions are now available. More info about activation to come in the following weeks...
22
+ ### Changed
23
+ - GET EMoney method now supports year and month parameters. More info on our [docs](https://docs.mangopay.com/endpoints/v2.01/user-emoney#e895_view-a-users-emoney)
24
+
25
+ ## [v3.0.32] - 2019-06-19
26
+ ### Added
27
+ - New UBO Declaration system (more info [here](https://docs.mangopay.com/endpoints/v2.01/ubo-declarations#e1024_the-ubo-declaration-object))
28
+ ### Changed
29
+ - Paypal buyer account email that has been used is now available for Payin Paypal
30
+ - Your `HeadquartersPhoneNumber` can now be updated for your client account directly from our API
31
+ ### BREAKING
32
+ - `APIKey` is now replacing `passphrase` property for credentials. You must update it by updating to 3.0.32 SDK version and upper ones.
33
+
34
+
35
+
data/README.md CHANGED
@@ -4,7 +4,7 @@ The gem for interacting with the version 2 of the Mangopay API.
4
4
  See the [API documentation](http://docs.mangopay.com/api-references/)
5
5
  for more details on the API.
6
6
 
7
- Tested on the following versions of Ruby: 1.9.2, 1.9.3, 2.0.0
7
+ Tested on the following versions of Ruby: 1.9.2, 1.9.3, 2.0.0 and 2.x up to 2.5
8
8
 
9
9
  ## NEWS
10
10
 
@@ -37,7 +37,7 @@ require 'mangopay'
37
37
  MangoPay.configure do |c|
38
38
  c.preproduction = true
39
39
  c.client_id = 'YOUR_CLIENT_ID'
40
- c.client_passphrase = 'YOUR_CLIENT_PASSWORD'
40
+ c.client_apiKey = 'YOUR_CLIENT_API_KEY'
41
41
  c.log_file = File.join('mypath', 'mangopay.log')
42
42
  c.http_timeout = 10000
43
43
  end
@@ -96,13 +96,13 @@ Along with each request, the rate limiting headers are automatically updated in
96
96
  MangoPay.ratelimit
97
97
 
98
98
  {
99
- :limit=>["74", "74", "75", "908"],
100
- :remaining=>["2226", "4426", "8725", "104692"],
99
+ :limit=>["74", "74", "75", "908"],
100
+ :remaining=>["2226", "4426", "8725", "104692"],
101
101
  :reset=>["1495615620", "1495616520", "1495618320", "1495701060"]
102
102
  }
103
103
  ```
104
104
 
105
- Read more about rate limiting on [our documetiation](https://docs.mangopay.com/guide/rate-limiting).
105
+ Read more about rate limiting on [our documentation](https://docs.mangopay.com/guide/rate-limiting).
106
106
 
107
107
  ### Log requests and responses
108
108
  You can easily enable logs by setting the ```log_file``` configuration option (see the section **configuration** above). If you don't want logs, remove the ```log_file``` line.
@@ -17,7 +17,7 @@ module Mangopay
17
17
  client = client_id_valid?
18
18
  remove_file 'config/initializers/mangopay.rb'
19
19
  @client_id = client_id
20
- @client_passphrase = client['Passphrase']
20
+ @client_apiKey = client['APIKey']
21
21
  template 'mangopay.rb.erb', 'config/initializers/mangopay.rb'
22
22
  rescue => e
23
23
  puts e.message
@@ -1,5 +1,5 @@
1
1
  MangoPay.configure do |c|
2
2
  c.preproduction = <%= options[:preproduction] %>
3
3
  c.client_id = '<%= @client_id %>'
4
- c.client_passphrase = '<%= @client_passphrase %>'
4
+ c.client_apiKey = '<%= @client_apiKey %>'
5
5
  end
@@ -41,14 +41,16 @@ module MangoPay
41
41
  autoload :BankingAliases, 'mangopay/bankingaliases'
42
42
  autoload :BankingAliasesIBAN, 'mangopay/bankingaliases_iban'
43
43
  autoload :UboDeclaration, 'mangopay/ubo_declaration'
44
+ autoload :Ubo, 'mangopay/ubo'
44
45
 
45
46
  # temporary
46
47
  autoload :Temp, 'mangopay/temp'
47
48
 
48
49
  class Configuration
49
50
  attr_accessor :preproduction, :root_url,
50
- :client_id, :client_passphrase,
51
- :temp_dir, :log_file, :http_timeout
51
+ :client_id, :client_apiKey,
52
+ :temp_dir, :log_file, :http_timeout,
53
+ :logger
52
54
 
53
55
  def preproduction
54
56
  @preproduction || false
@@ -157,7 +159,7 @@ module MangoPay
157
159
  limit: res['x-ratelimit'].split(", "),
158
160
  remaining: res['x-ratelimit-remaining'].split(", "),
159
161
  reset: res['x-ratelimit-reset'].split(", ")
160
- }
162
+ }
161
163
  end
162
164
 
163
165
  data
@@ -203,10 +205,10 @@ module MangoPay
203
205
  end
204
206
 
205
207
  def do_request(http, req, uri)
206
- if configuration.log_file.nil?
207
- do_request_without_log(http, req)
208
- else
208
+ if logs_required?
209
209
  do_request_with_log(http, req, uri)
210
+ else
211
+ do_request_without_log(http, req)
210
212
  end
211
213
  end
212
214
 
@@ -235,15 +237,23 @@ module MangoPay
235
237
  end
236
238
 
237
239
  def logger
238
- raise NotImplementedError if configuration.log_file.nil?
239
- if @logger.nil?
240
+ raise NotImplementedError unless logs_required?
241
+ return @logger if @logger
242
+
243
+ if !configuration.logger.nil?
244
+ @logger = configuration.logger
245
+ elsif !configuration.log_file.nil?
240
246
  @logger = Logger.new(configuration.log_file)
241
- @logger.formatter = proc do |severity, datetime, progname, msg|
247
+ @logger.formatter = proc do |_, _, _, msg|
242
248
  "#{msg}\n"
243
249
  end
244
250
  end
251
+
245
252
  @logger
246
253
  end
247
254
 
255
+ def logs_required?
256
+ !configuration.log_file.nil? || !configuration.logger.nil?
257
+ end
248
258
  end
249
259
  end
@@ -19,7 +19,7 @@ module MangoPay
19
19
  if token.nil? || token['timestamp'].nil? || token['timestamp'] <= Time.now || token['environment_key'] != env_key
20
20
  token = MangoPay.request(:post, "/#{MangoPay.version_code}/oauth/token", {}, {}, {}, Proc.new do |req|
21
21
  cfg = MangoPay.configuration
22
- req.basic_auth cfg.client_id, cfg.client_passphrase
22
+ req.basic_auth cfg.client_id, cfg.client_apiKey
23
23
  req.body = 'grant_type=client_credentials'
24
24
  req.add_field('Content-Type', 'application/x-www-form-urlencoded')
25
25
  end)
@@ -32,7 +32,7 @@ module MangoPay
32
32
 
33
33
  def get_environment_key_for_token
34
34
  cfg = MangoPay.configuration
35
- key = "#{cfg.root_url}|#{cfg.client_id}|#{cfg.client_passphrase}"
35
+ key = "#{cfg.root_url}|#{cfg.client_id}|#{cfg.client_apiKey}"
36
36
  key = Digest::MD5.hexdigest(key)
37
37
  key
38
38
  end
@@ -26,6 +26,17 @@ module MangoPay
26
26
  MangoPay.request(:put, url(user_id, bank_account_id), params)
27
27
  end
28
28
 
29
+ # Fetches list of transactions belonging to given +bank_account_id+.
30
+ #
31
+ # Optional +filters+ is a hash accepting following keys:
32
+ # - +page+, +per_page+, +sort+: pagination and sorting params (see MangoPay::HTTPCalls::Fetch::ClassMethods#fetch)
33
+ # - +Status+: TransactionStatus {CREATED, SUCCEEDED, FAILED}
34
+ # - +ResultCode+: string representing the transaction result
35
+ def transactions(bank_account_id, filters = {})
36
+ url = "#{MangoPay.api_path}/bankaccounts/#{bank_account_id}/transactions"
37
+ MangoPay.request(:get, url, {}, filters)
38
+ end
39
+
29
40
  def url(user_id, bank_account_id = nil)
30
41
  if bank_account_id
31
42
  "#{MangoPay.api_path}/users/#{CGI.escape(user_id.to_s)}/bankaccounts/#{CGI.escape(bank_account_id.to_s)}"
@@ -17,9 +17,24 @@ module MangoPay
17
17
  MangoPay.request(:get, fingerprint_url(fingerprint), {}, filters)
18
18
  end
19
19
 
20
+ # Retrieves a list of transactions belonging to given +card_id+.
21
+ #
22
+ # Optional +filters+ is a hash accepting following keys:
23
+ # - +page+, +per_page+, +sort+: pagination and sorting params (see MangoPay::HTTPCalls::Fetch::ClassMethods#fetch)
24
+ # - +Status+: TransactionStatus {CREATED, SUCCEEDED, FAILED}
25
+ # - +ResultCode+: string representing the transaction result
26
+ def transactions(card_id, filters = {})
27
+ url = url(card_id) + '/transactions'
28
+ MangoPay.request(:get, url, {}, filters)
29
+ end
30
+
20
31
  def fingerprint_url(fingerprint)
21
32
  "#{MangoPay.api_path}/cards/fingerprints/#{fingerprint}"
22
33
  end
34
+
35
+ def get_pre_authorizations(card_id, filters = {})
36
+ MangoPay.request(:get, "#{MangoPay.api_path}/cards/#{card_id}/preauthorizations")
37
+ end
23
38
  end
24
39
  end
25
40
  end
@@ -13,7 +13,12 @@ module MangoPay
13
13
 
14
14
  def self.response(body)
15
15
  return '' if body.to_s.empty?
16
- body = JSON.load(body)
16
+
17
+ begin
18
+ body = JSON.load(body)
19
+ rescue MultiJson::LoadError => e
20
+ return body
21
+ end
17
22
  filter_hash(body, res_confidential_params)
18
23
  JSON.dump(body)
19
24
  end
@@ -21,8 +21,8 @@ module MangoPay
21
21
 
22
22
  module Update
23
23
  module ClassMethods
24
- def update(id = nil, params = {})
25
- MangoPay.request(:put, url(id), params)
24
+ def update(id = nil, params = {}, idempotency_key = nil)
25
+ MangoPay.request(:put, url(id), params, {}, idempotency_key)
26
26
  end
27
27
  end
28
28
 
@@ -35,20 +35,20 @@ module MangoPay
35
35
  module ClassMethods
36
36
 
37
37
  # - Fetching _single_entity_ by +id+:
38
- #
38
+ #
39
39
  # MangoPay::User.fetch("user-id") # => {"FirstName"=>"Mango", "LastName"=>"Pay", ...}
40
- #
40
+ #
41
41
  # - or fetching _multiple_entities_ with _optional_ +filters+ hash,
42
42
  # including _pagination_ and _sorting_ params
43
43
  # +page+, +per_page+, +sort+ (see http://docs.mangopay.com/api-references/pagination/):
44
- #
44
+ #
45
45
  # MangoPay::User.fetch() # => [{...}, ...]: list of user data hashes (10 per page by default)
46
46
  # MangoPay::User.fetch({'page' => 2, 'per_page' => 3}) # => list of 3 hashes from 2nd page
47
47
  # MangoPay::BankAccount.fetch(user_id, {'sort' => 'CreationDate:desc'}) # => bank accounts by user, sorting by date descending (with default pagination)
48
48
  # MangoPay::BankAccount.fetch(user_id, {'sort' => 'CreationDate:desc', 'page' => 2, 'per_page' => 3}) # both sorting and pagination params provided
49
- #
49
+ #
50
50
  # - For paginated queries the +filters+ param will be supplemented by +total_pages+ and +total_items+ info:
51
- #
51
+ #
52
52
  # MangoPay::User.fetch(filter = {'page' => 2, 'per_page' => 3})
53
53
  # filter # => {"page"=>2, "per_page"=>3, "total_pages"=>1969, "total_items"=>5905}
54
54
  #
@@ -13,14 +13,6 @@ module MangoPay
13
13
  end
14
14
 
15
15
  class << self
16
-
17
- # Create a UBO declaration.
18
- # @param +user_id+ ID of the legal user owning the declaration
19
- # @param +ubo_declaration+ Object containing UBO declaration data
20
- # @return Newly-created UBO declaration entity data
21
- def create_ubo_declaration(user_id, ubo_declaration)
22
- MangoPay.request(:post, "#{url(user_id)}/ubodeclarations", ubo_declaration)
23
- end
24
16
  end
25
17
  end
26
18
  end
@@ -27,6 +27,16 @@ module MangoPay
27
27
  MangoPay.request(:get, url, {}, filters)
28
28
  end
29
29
 
30
+ # Fetches list of transactions belonging to given +mandate_id+.
31
+ #
32
+ # Optional +filters+ is a hash accepting following keys:
33
+ # - +page+, +per_page+, +sort+: pagination and sorting params (see MangoPay::HTTPCalls::Fetch::ClassMethods#fetch)
34
+ # - +Status+: TransactionStatus {CREATED, SUCCEEDED, FAILED}
35
+ # - +ResultCode+: string representing the transaction result
36
+ def transactions(mandate_id, filters = {})
37
+ url = url(mandate_id) + '/transactions'
38
+ MangoPay.request(:get, url, {}, filters)
39
+ end
30
40
  end
31
41
  end
32
42
  end
@@ -5,6 +5,17 @@ module MangoPay
5
5
  include HTTPCalls::Fetch
6
6
  include HTTPCalls::Refund
7
7
 
8
+ # Fetches list of refunds belonging to the given +pay_in_id+.
9
+ #
10
+ # Optional +filters+ is a hash accepting following keys:
11
+ # - +page+, +per_page+, +sort+: pagination and sorting params (see MangoPay::HTTPCalls::Fetch::ClassMethods#fetch)
12
+ # - +Status+: TransactionStatus {CREATED, SUCCEEDED, FAILED}
13
+ # - +ResultCode+: string representing the transaction result
14
+ def self.refunds(pay_in_id, filters = {})
15
+ url = url(pay_in_id) + '/refunds'
16
+ MangoPay.request(:get, url, {}, filters)
17
+ end
18
+
8
19
  module Card
9
20
 
10
21
  # See http://docs.mangopay.com/api-references/payins/payins-card-web/
@@ -95,5 +106,25 @@ module MangoPay
95
106
 
96
107
  end
97
108
 
109
+ module ApplePay
110
+ class Direct < Resource
111
+ include HTTPCalls::Create
112
+
113
+ def self.url(*)
114
+ "#{MangoPay.api_path}/payins/applepay/#{CGI.escape(class_name.downcase)}"
115
+ end
116
+ end
117
+ end
118
+
119
+ module GooglePay
120
+ class Direct < Resource
121
+ include HTTPCalls::Create
122
+
123
+ def self.url(*)
124
+ "#{MangoPay.api_path}/payins/googlepay/#{CGI.escape(class_name.downcase)}"
125
+ end
126
+ end
127
+ end
128
+
98
129
  end
99
130
  end
@@ -2,6 +2,17 @@ module MangoPay
2
2
  class PayOut < Resource
3
3
  include HTTPCalls::Fetch
4
4
 
5
+ # Fetches list of refunds belonging to the given +pay_out_id+.
6
+ #
7
+ # Optional +filters+ is a hash accepting following keys:
8
+ # - +page+, +per_page+, +sort+: pagination and sorting params (see MangoPay::HTTPCalls::Fetch::ClassMethods#fetch)
9
+ # - +Status+: TransactionStatus {CREATED, SUCCEEDED, FAILED}
10
+ # - +ResultCode+: string representing the transaction result
11
+ def self.refunds(pay_out_id, filters = {})
12
+ url = url(pay_out_id) + '/refunds'
13
+ MangoPay.request(:get, url, {}, filters)
14
+ end
15
+
5
16
  # See http://docs.mangopay.com/api-references/pay-out-bank-wire/
6
17
  class BankWire < Resource
7
18
  include HTTPCalls::Create
@@ -3,5 +3,16 @@ module MangoPay
3
3
  # See http://docs.mangopay.com/api-references/refund/
4
4
  class Refund < Resource
5
5
  include HTTPCalls::Fetch
6
+
7
+ # Fetches list of refunds belonging to given +repudiation_id+
8
+ #
9
+ # Optional +filters+ is a hash accepting following keys:
10
+ # - +page+, +per_page+, +sort+: pagination and sorting params (see MangoPay::HTTPCalls::Fetch::ClassMethods#fetch)
11
+ # - +Status+: TransactionStatus {CREATED, SUCCEEDED, FAILED}
12
+ # - +ResultCode+: string representing the transaction result
13
+ def self.of_repudiation(repudiation_id, filters = {})
14
+ url = "#{MangoPay.api_path}/repudiations/#{repudiation_id}/refunds"
15
+ MangoPay.request(:get, url, {}, filters)
16
+ end
6
17
  end
7
18
  end
@@ -5,5 +5,16 @@ module MangoPay
5
5
  include HTTPCalls::Create
6
6
  include HTTPCalls::Fetch
7
7
  include HTTPCalls::Refund
8
+
9
+ # Fetches list of refunds belonging to given +transfer_id+.
10
+ #
11
+ # Optional +filters+ is a hash accepting following keys:
12
+ # - +page+, +per_page+, +sort+: pagination and sorting params (see MangoPay::HTTPCalls::Fetch::ClassMethods#fetch)
13
+ # - +Status+: TransactionStatus {CREATED, SUCCEEDED, FAILED}
14
+ # - +ResultCode+: string representing the transaction result
15
+ def self.refunds(transfer_id, filters = {})
16
+ url = url(transfer_id) + '/refunds'
17
+ MangoPay.request(:get, url, {}, filters)
18
+ end
8
19
  end
9
20
  end
@@ -0,0 +1,26 @@
1
+ module MangoPay
2
+ # Provides API methods for the UBO entity.
3
+ class Ubo < Resource
4
+ class << self
5
+ def url(user_id, ubo_declaration_id, id = nil)
6
+ if id
7
+ "#{MangoPay.api_path}/users/#{user_id}/kyc/ubodeclarations/#{ubo_declaration_id}/ubos/#{id}"
8
+ else
9
+ "#{MangoPay.api_path}/users/#{user_id}/kyc/ubodeclarations/#{ubo_declaration_id}/ubos"
10
+ end
11
+ end
12
+
13
+ def create(user_id, ubo_declaration_id, params)
14
+ MangoPay.request(:post, url(user_id, ubo_declaration_id), params)
15
+ end
16
+
17
+ def fetch(user_id, ubo_declaration_id, ubo_id)
18
+ MangoPay.request(:get, url(user_id, ubo_declaration_id, ubo_id))
19
+ end
20
+
21
+ def update(user_id, ubo_declaration_id, ubo_id, params)
22
+ MangoPay.request(:put, url(user_id, ubo_declaration_id, ubo_id), params)
23
+ end
24
+ end
25
+ end
26
+ end