openpayu 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (102) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +18 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +45 -0
  5. data/.ruby-gemset +1 -0
  6. data/.ruby-version +1 -0
  7. data/.travis.yml +10 -0
  8. data/Gemfile +4 -0
  9. data/LICENSE.txt +22 -0
  10. data/README.md +193 -0
  11. data/Rakefile +1 -0
  12. data/doc/EmptyResponseError.html +123 -0
  13. data/doc/HttpStatusException.html +123 -0
  14. data/doc/NotImplementedException.html +123 -0
  15. data/doc/OpenPayU.html +436 -0
  16. data/doc/OpenPayU/Configuration.html +1530 -0
  17. data/doc/OpenPayU/Connection.html +381 -0
  18. data/doc/OpenPayU/Document.html +705 -0
  19. data/doc/OpenPayU/Documents.html +117 -0
  20. data/doc/OpenPayU/Documents/Request.html +503 -0
  21. data/doc/OpenPayU/Documents/Response.html +783 -0
  22. data/doc/OpenPayU/Models.html +117 -0
  23. data/doc/OpenPayU/Models/Address.html +861 -0
  24. data/doc/OpenPayU/Models/Buyer.html +587 -0
  25. data/doc/OpenPayU/Models/Buyer/Delivery.html +312 -0
  26. data/doc/OpenPayU/Models/Card.html +507 -0
  27. data/doc/OpenPayU/Models/Fee.html +367 -0
  28. data/doc/OpenPayU/Models/Model.html +1208 -0
  29. data/doc/OpenPayU/Models/NotifyResponse.html +297 -0
  30. data/doc/OpenPayU/Models/Order.html +1155 -0
  31. data/doc/OpenPayU/Models/PayMethod.html +507 -0
  32. data/doc/OpenPayU/Models/Product.html +787 -0
  33. data/doc/OpenPayU/Models/Refund.html +1020 -0
  34. data/doc/OpenPayU/Models/ShippingMethod.html +367 -0
  35. data/doc/OpenPayU/Models/StatusUpdate.html +647 -0
  36. data/doc/OpenPayU/Models/Token.html +507 -0
  37. data/doc/OpenPayU/Order.html +924 -0
  38. data/doc/OpenPayU/Refund.html +269 -0
  39. data/doc/OpenPayU/Token.html +288 -0
  40. data/doc/OpenPayU/XmlBuilder.html +277 -0
  41. data/doc/WrongConfigurationError.html +123 -0
  42. data/doc/WrongNotifyRequest.html +123 -0
  43. data/doc/WrongOrderParameters.html +267 -0
  44. data/doc/WrongSignatureException.html +123 -0
  45. data/doc/_index.html +446 -0
  46. data/doc/class_list.html +54 -0
  47. data/doc/css/common.css +1 -0
  48. data/doc/css/full_list.css +57 -0
  49. data/doc/css/style.css +338 -0
  50. data/doc/file.README.html +281 -0
  51. data/doc/file_list.html +56 -0
  52. data/doc/frames.html +26 -0
  53. data/doc/index.html +281 -0
  54. data/doc/js/app.js +214 -0
  55. data/doc/js/full_list.js +178 -0
  56. data/doc/js/jquery.js +4 -0
  57. data/doc/method_list.html +1007 -0
  58. data/doc/top-level-namespace.html +114 -0
  59. data/lib/openpayu.rb +66 -0
  60. data/lib/openpayu/configuration.rb +62 -0
  61. data/lib/openpayu/connection.rb +65 -0
  62. data/lib/openpayu/document.rb +105 -0
  63. data/lib/openpayu/documents/request.rb +34 -0
  64. data/lib/openpayu/documents/response.rb +48 -0
  65. data/lib/openpayu/exceptions.rb +19 -0
  66. data/lib/openpayu/models/address.rb +12 -0
  67. data/lib/openpayu/models/buyer.rb +11 -0
  68. data/lib/openpayu/models/buyer/delivery.rb +7 -0
  69. data/lib/openpayu/models/buyer/invoice.rb +8 -0
  70. data/lib/openpayu/models/card.rb +10 -0
  71. data/lib/openpayu/models/fee.rb +9 -0
  72. data/lib/openpayu/models/model.rb +151 -0
  73. data/lib/openpayu/models/notify_response.rb +9 -0
  74. data/lib/openpayu/models/order.rb +28 -0
  75. data/lib/openpayu/models/pay_method.rb +10 -0
  76. data/lib/openpayu/models/product.rb +10 -0
  77. data/lib/openpayu/models/refund.rb +41 -0
  78. data/lib/openpayu/models/shipping_method.rb +9 -0
  79. data/lib/openpayu/models/status_update.rb +14 -0
  80. data/lib/openpayu/models/token.rb +10 -0
  81. data/lib/openpayu/order.rb +119 -0
  82. data/lib/openpayu/refund.rb +26 -0
  83. data/lib/openpayu/token.rb +27 -0
  84. data/lib/openpayu/version.rb +4 -0
  85. data/lib/openpayu/xml_builder.rb +23 -0
  86. data/lib/openpayu_ruby.rb +2 -0
  87. data/openpayu_ruby.gemspec +40 -0
  88. data/spec/cassettes/cancel_order.yml +50 -0
  89. data/spec/cassettes/create_order.yml +51 -0
  90. data/spec/cassettes/refund_order.yml +100 -0
  91. data/spec/cassettes/retrieve_order.yml +51 -0
  92. data/spec/cassettes/update_order.yml +50 -0
  93. data/spec/integration/order_spec.rb +103 -0
  94. data/spec/openpayu.yml +11 -0
  95. data/spec/spec_helper.rb +27 -0
  96. data/spec/test_objects/order.rb +97 -0
  97. data/spec/unit/configuration_spec.rb +71 -0
  98. data/spec/unit/document_spec.rb +18 -0
  99. data/spec/unit/models/order_spec.rb +82 -0
  100. data/spec/unit/models/refund_spec.rb +24 -0
  101. data/spec/unit/openpayu_spec.rb +27 -0
  102. metadata +327 -0
@@ -0,0 +1,48 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module OpenPayU
3
+ module Documents
4
+ class Response < Document
5
+ attr_accessor :parsed_data, :response, :request, :message_name, :body
6
+
7
+ def initialize(data, message_name)
8
+ @response = data[:response]
9
+ @request = data[:request]
10
+ @message_name = message_name
11
+ @body = @response.body.is_a?(StringIO) ? @response.body.string :
12
+ @response.body
13
+ parse_data if verify_response
14
+ end
15
+
16
+ def method_missing(method_name)
17
+ @parsed_data[method_name.to_s]
18
+ end
19
+
20
+ def parse_data
21
+ if OpenPayU::Configuration.data_format == 'xml'
22
+ @parsed_data = Hash.from_xml(@body)
23
+ else
24
+ @parsed_data = JSON.parse(@body)
25
+ end
26
+ if @parsed_data['OpenPayU'] && @parsed_data['OpenPayU'][@message_name]
27
+ @parsed_data = underscore_keys @parsed_data['OpenPayU'][@message_name]
28
+ elsif @parsed_data['OpenPayU']
29
+ @parsed_data = underscore_keys @parsed_data['OpenPayU']
30
+ end
31
+ @parsed_data
32
+ end
33
+
34
+ def order_status
35
+ if @message_name == 'OrderRetrieveResponse'
36
+ @parsed_data['orders']['order']['status']
37
+ else
38
+ @parsed_data['order'] ? @parsed_data['order']['status'] : ''
39
+ end
40
+ end
41
+
42
+ Models::Order::STATUSES.each do |method|
43
+ define_method((method.downcase + '?').to_sym) { order_status == method }
44
+ end
45
+
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,19 @@
1
+ # -*- encoding : utf-8 -*-
2
+ class WrongConfigurationError < StandardError; end
3
+ class HttpStatusException < StandardError; end
4
+ class EmptyResponseError < StandardError; end
5
+ class WrongSignatureException < StandardError; end
6
+ class WrongNotifyRequest < StandardError; end
7
+ class NotImplementedException < StandardError; end
8
+ class WrongOrderParameters < StandardError
9
+
10
+ def initialize(order)
11
+ @order = order
12
+ end
13
+
14
+ def message
15
+ @order.all_errors.map do |k, v|
16
+ "Class #{k} missing attributes: #{v.messages.inspect}"
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,12 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module OpenPayU
3
+ module Models
4
+ class Address < Model
5
+ attr_accessor :street, :postal_box, :postal_code, :city, :state,
6
+ :country_code, :name, :recipient_name, :recipient_email,
7
+ :recipient_phone
8
+ validates :street, :postal_code, :city, :country_code, :recipient_name,
9
+ presence: true
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,11 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module OpenPayU
3
+ module Models
4
+ class Buyer < Model
5
+ attr_accessor :email, :phone, :first_name, :last_name, :language, :NIN
6
+ validates :email, :phone, :first_name, :last_name, presence: true
7
+ has_one_object :delivery # not required
8
+ has_one_object :invoice # not required
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,7 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module OpenPayU
3
+ module Models
4
+ class Buyer::Delivery < Address
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,8 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module OpenPayU
3
+ module Models
4
+ class Buyer::Delivery < Address
5
+ attr_accessor :TIN, :e_invoice_requested
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,10 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module OpenPayU
3
+ module Models
4
+ class Card < Model
5
+ attr_accessor :number, :expiration_month, :expiration_year, :CVV,
6
+ :cardholder
7
+ validates :number, :expiration_month, :expiration_year, presence: true
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module OpenPayU
3
+ module Models
4
+ class Fee < Model
5
+ attr_accessor :amount, :description, :type
6
+ validates :amount, presence: true
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,151 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'active_model'
3
+
4
+ module OpenPayU
5
+ module Models
6
+ class Model
7
+
8
+ include ActiveModel::Validations
9
+ include ActiveModel::Serializers::JSON
10
+ include ActiveModel::Serializers::Xml
11
+
12
+ attr_accessor :all_errors
13
+
14
+ def initialize(values)
15
+ values.each_pair do |k, v|
16
+ send("#{k}=", v)
17
+ end
18
+ after_initialize
19
+ end
20
+
21
+ def after_initialize
22
+ end
23
+
24
+ def attributes
25
+ instance_values
26
+ end
27
+
28
+ def to_json
29
+ prepare_keys.to_json
30
+ end
31
+
32
+ def prepare_data(request_type)
33
+ if OpenPayU::Configuration.data_format == 'xml'
34
+ generate_xml(request_type)
35
+ else
36
+ {
37
+ 'OpenPayU' => {
38
+ request_type => prepare_keys(instance_values)
39
+ }
40
+ }.to_json
41
+ end
42
+ end
43
+
44
+ def generate_xml(request_type)
45
+ '<?xml version="1.0" encoding="UTF-8"?>
46
+ <OpenPayU xmlns="http://www.openpayu.com/20/openpayu.xsd">' +
47
+ prepare_keys(instance_values).to_xml(
48
+ builder: OpenPayU::XmlBuilder.new(request_type, indent: 2),
49
+ root: request_type,
50
+ skip_types: true,
51
+ skip_instruct: true) + '</OpenPayU>'
52
+ end
53
+
54
+ def get_instance_values
55
+ instance_values.delete_if do |k, v|
56
+ %w(all_errors errors validation_context).include?(k)
57
+ end
58
+ end
59
+
60
+ def prepare_keys(attrs = {}, hash = get_instance_values)
61
+ hash.each_pair do |k, v|
62
+ attrs[k.camelize] =
63
+ if v.is_a? Array
64
+ v.map do |element|
65
+ { element.class.name.demodulize => element.prepare_keys }
66
+ end
67
+ elsif v.class.name =~ /OpenPayU::Models/
68
+ v.prepare_keys
69
+ else
70
+ v
71
+ end
72
+ end
73
+ attrs
74
+ end
75
+
76
+ def validate_all_objects
77
+ @all_errors = {}
78
+ instance_values.each_pair do |k, v|
79
+ if v.is_a? Array
80
+ v.each do |element|
81
+ if element.validate_all_objects.any?
82
+ @all_errors[element.class.name] = element.errors
83
+ end
84
+ end
85
+ elsif v.class.name =~ /OpenPayU::Models/
86
+ @all_errors[v.class.name] = v.errors unless v.valid?
87
+ end
88
+ end
89
+ @all_errors[self.class.name] = errors unless valid?
90
+
91
+ @all_errors
92
+ end
93
+
94
+ def all_objects_valid?
95
+ !validate_all_objects.any?
96
+ end
97
+
98
+ def to_flatten_hash(source = nil, target = {}, namespace = nil, index = nil)
99
+ source ||= prepare_keys(instance_values)
100
+ prefix = "#{namespace}." if namespace
101
+ case source
102
+ when Hash
103
+ array_index = "[#{index}]" if index
104
+ source.each do |key, value|
105
+ to_flatten_hash(value, target, "#{prefix}#{key}#{array_index}")
106
+ end
107
+ when Array
108
+ source.each_with_index do |value, i|
109
+ to_flatten_hash(value, target, namespace, i)
110
+ end
111
+ else
112
+ target[namespace] = source
113
+ end
114
+ target
115
+ end
116
+
117
+ class << self
118
+
119
+ def has_many_objects(association, class_name)
120
+ define_writer(association, class_name)
121
+ define_reader(association)
122
+ end
123
+
124
+ def has_one_object(association)
125
+ define_writer(association, association)
126
+ define_reader(association)
127
+ end
128
+
129
+ def define_writer(association, class_name)
130
+ class_eval <<-CODE
131
+ def #{association}=(value)
132
+ @#{association} =
133
+ if value.class.name == "Array"
134
+ value.collect do |val|
135
+ #{class_name.to_s.camelize}.new(val)
136
+ end
137
+ else
138
+ #{class_name.to_s.camelize}.new(value)
139
+ end
140
+ end
141
+ CODE
142
+ end
143
+
144
+ def define_reader(association)
145
+ attr_reader association
146
+ end
147
+
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,9 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module OpenPayU
3
+ module Models
4
+ class NotifyResponse < Model
5
+ attr_accessor :res_id, :status
6
+ validates :res_id, :status, presence: true
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,28 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'securerandom'
3
+
4
+ module OpenPayU
5
+ module Models
6
+ class Order < Model
7
+ STATUSES = %w(NEW PENDING CANCELLED REJECTED
8
+ COMPLETED WAITING_FOR_CONFIRMATION)
9
+ attr_accessor :customer_ip, :ext_order_id, :merchant_pos_id, :description,
10
+ :currency_code, :total_amount, :validity_time, :notify_url, :order_url,
11
+ :continue_url, :req_id, :ref_req_id, :properties
12
+ validates :customer_ip, :ext_order_id, :merchant_pos_id, :description,
13
+ :currency_code, :total_amount, presence: true
14
+ has_one_object :buyer # not required
15
+ has_one_object :fee # not required
16
+ has_many_objects :pay_methods, :pay_method # not required
17
+ has_many_objects :products, :product # not required
18
+ has_many_objects :shipping_methods, :shipping_method # not required
19
+
20
+ def after_initialize
21
+ @req_id ||= "{#{SecureRandom.uuid}}"
22
+ @notify_url ||= OpenPayU::Configuration.notify_url
23
+ @order_url ||= OpenPayU::Configuration.order_url
24
+ @continue_url ||= OpenPayU::Configuration.continue_url
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,10 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module OpenPayU
3
+ module Models
4
+ class PayMethod < Model
5
+ attr_accessor :type, :value, :ext_pay_method_id, :amount, :currency_code
6
+ validates :type, presence: true
7
+ has_one_object :card
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module OpenPayU
3
+ module Models
4
+ class Product < Model
5
+ attr_accessor :name, :unit_price, :quantity, :discount, :extra_info,
6
+ :code, :version, :weight, :size
7
+ validates :name, :unit_price, :quantity, presence: true
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,41 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module OpenPayU
3
+ module Models
4
+ class Refund < Model
5
+ attr_accessor :order_id, :ext_refund_id, :type, :amount,
6
+ :commission_amount, :proxy_commision_amount, :currency_code,
7
+ :description, :bank_description, :source_account_number
8
+ validates :description, :order_id, presence: true
9
+
10
+ def prepare_data(request_type)
11
+ if OpenPayU::Configuration.data_format == 'xml'
12
+ generate_xml(request_type)
13
+ else
14
+ {
15
+ 'OpenPayU' => {
16
+ request_type => {
17
+ 'OrderId' => @order_id,
18
+ 'Refund' => prepare_keys(instance_values)
19
+ }
20
+ }
21
+ }.to_json
22
+ end
23
+ end
24
+
25
+ def generate_xml(request_type)
26
+ '<?xml version="1.0" encoding="UTF-8"?>
27
+ <OpenPayU xmlns="http://www.openpayu.com/20/openpayu.xsd">' +
28
+ prepare_keys({
29
+ 'OrderId' => @order_id,
30
+ 'Refund' => prepare_keys(instance_values)
31
+ }).to_xml(
32
+ builder: OpenPayU::XmlBuilder.new(request_type, indent: 2),
33
+ root: request_type,
34
+ skip_types: true,
35
+ skip_instruct: true
36
+ ) + '</OpenPayU>'
37
+ end
38
+
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,9 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module OpenPayU
3
+ module Models
4
+ class ShippingMethod < Model
5
+ attr_accessor :country, :price, :nazwa
6
+ validates :country, :price, :nazwa, presence: true
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,14 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module OpenPayU
3
+ module Models
4
+ class StatusUpdate < Model
5
+ attr_accessor :customer_id, :customer_email, :order_id,
6
+ :order_creation_date, :order_status, :custom_status, :reason
7
+ validates :order_id, :order_creation_date, :order_status, presence: true
8
+ validates :order_status,
9
+ inclusion: {
10
+ in: Order::STATUSES
11
+ }
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,10 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module OpenPayU
3
+ module Models
4
+ class Token < Model
5
+ attr_accessor :type, :customer_id, :email, :first_name, :last_name
6
+
7
+ has_one_object :card
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,119 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module OpenPayU
3
+
4
+ # A class responsible for dealing with Order
5
+
6
+ class Order
7
+
8
+ # Retrieves information about the order
9
+ # - Sends to PayU OrderRetrieveRequest
10
+ #
11
+ # @param [String] order_id PayU OrderId sent back in OrderCreateResponse
12
+ # @return [Documents::Response] Response class object order
13
+ # with OrderRetrieveResponse
14
+ def self.retrieve(order_id)
15
+ url = Configuration.get_base_url + "order/#{order_id}." +
16
+ Configuration.data_format
17
+ request = Documents::Request.new(url)
18
+ Documents::Response.new(
19
+ Connection.get(url, request.body, request.headers),
20
+ 'OrderRetrieveResponse'
21
+ )
22
+ end
23
+
24
+ # Creates new Order
25
+ # - Sends to PayU OrderCreateRequest
26
+ #
27
+ # @param [Hash] order A Hash object containing full {Models::Order} object
28
+ # @return [Documents::Response] Response class object order
29
+ # with OrderCreateResponse
30
+ # @raise [WrongOrderParameters] if provided Hash 'order' isn't a valid
31
+ # object (with all required fields)
32
+ def self.create(order)
33
+ @order = Models::Order.new(order)
34
+ if @order.all_objects_valid?
35
+ url = Configuration.get_base_url + 'order.' + Configuration.data_format
36
+ request =
37
+ Documents::Request.new(@order.prepare_data('OrderCreateRequest'))
38
+ Documents::Response.new(
39
+ Connection.post(url, request.body, request.headers),
40
+ 'OrderCreateResponse'
41
+ )
42
+ else
43
+ raise WrongOrderParameters.new(@order)
44
+ end
45
+ end
46
+
47
+ # Updates Order status
48
+ # - Sends to PayU OrderStatusUpdateRequest
49
+ #
50
+ # @param [Hash] status_update A Hash object containing full
51
+ # {Models::StatusUpdate} object
52
+ # @return [Documents::Response] Response class object order
53
+ # with OrderStatusUpdateResponse
54
+ # @raise [NotImplementedException] This feature is not yet implemented
55
+ def self.status_update(status_update)
56
+ @update = OpenPayU::Models::StatusUpdate.new status_update
57
+ url = Configuration.get_base_url + "order/#{@update.order_id}/status." +
58
+ Configuration.data_format
59
+ request =
60
+ Documents::Request.new(@update.prepare_data('OrderStatusUpdateRequest'))
61
+ Documents::Response.new(
62
+ Connection.put(url, request.body, request.headers),
63
+ 'OrderStatusUpdateResponse'
64
+ )
65
+ end
66
+
67
+ # Cancels Order
68
+ # - Sends to PayU OrderCancelRequest
69
+ #
70
+ # @param [String] order_id PayU OrderId sent back in OrderCreateResponse
71
+ # @return [Documents::Response] Response class object order
72
+ # with OrderCancelResponse
73
+ def self.cancel(order_id)
74
+ url = Configuration.get_base_url + "order/#{order_id}." +
75
+ Configuration.data_format
76
+ request = Documents::Request.new(url)
77
+ Documents::Response.new(
78
+ Connection.delete(url, request.body, request.headers),
79
+ 'OrderCancelResponse'
80
+ )
81
+ end
82
+
83
+ # Transforms OrderNotifyRequest to [Documents::Response]
84
+ #
85
+ # @param [Rack::Request||ActionDispatch::Request] request object od request
86
+ # received from PayU
87
+ # @return [Documents::Response] Response class object order
88
+ # with OrderNotifyRequest
89
+ # @raise [WrongNotifyRequest] when generated response is empty
90
+ def self.consume_notification(request)
91
+ response = Documents::Response.new(
92
+ { response: request, request: nil },
93
+ 'OrderNotifyRequest'
94
+ )
95
+ if !response.nil?
96
+ response
97
+ else
98
+ raise(
99
+ WrongNotifyRequest,
100
+ "Invalid OrderNotifyRequest: #{request.inspect}"
101
+ )
102
+ end
103
+ end
104
+
105
+ # Creates OrderNotifyResponse to send to PayU
106
+ #
107
+ # @param [String] request_id value of ReqId(UUID) from OrderNotifyRequest
108
+ # @return [String] Response in XML or JSON (depends on gem configuration)
109
+ def self.build_notify_response(request_id)
110
+ response = Models::NotifyResponse.new({
111
+ res_id: request_id,
112
+ status: { 'StatusCode' => 'SUCCESS' }
113
+ })
114
+ response.prepare_data('OrderNotifyResponse')
115
+ end
116
+
117
+ end
118
+
119
+ end