payu_pl 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b782044448418c46fdc1576fc1e2a8b1c3855930e4758dfb692ca5707accc4e2
4
- data.tar.gz: 18f16cc358d142bd54bd2ed9c696e4ee4300686e47cee7502bf104ca4f0558d5
3
+ metadata.gz: a3153797e232dc9495c8f175f37558a8f360688b9f53e311cf925652a55db620
4
+ data.tar.gz: ca4dcc6531dec57bbc908c39cd5dd801d8fb9e5e33de22eca1df876e40abdd8e
5
5
  SHA512:
6
- metadata.gz: 93a1fc0a92eb1031ad67cb93aef28f2997a48f500fb4d8d3353bbcb55c2b389b01c67e88cd0612c227ce337e421984921469149f212d1e3b03ea8a2b123750b9
7
- data.tar.gz: 3e40a0fe16dbbf8e83e5ce7bde53697f1273f34f3c5b1705174d40d9232a365f4466b2feb4667545d4a89ae6e11e37ee417fcdf3f22518d357f5fd1704137d24
6
+ metadata.gz: 144220b42c84c7745c1f41755afe6c118bf9b0d2b06567abdea30ab21403341c7991525f914e70da3f971781ae4944d66b22cb6826603c4fcf96f18a2c88f1c8
7
+ data.tar.gz: c7a8325b86ca1102977cbfbe776b6013c31c4a78ddf7f16d7401933aeebf881d8aa47ac427a47a3f9ab28ed7ff539e5f5f4cc56c0dce074a7a3af2cdbc712d64
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.0] - 2025-12-31
4
+
5
+ - Add Shop Data endpoint: `GET /api/v2_1/shops/{shopId}` (`Client#retrieve_shop_data`)
6
+ - Add Payouts endpoints: `POST /api/v2_1/payouts`, `GET /api/v2_1/payouts/{payoutId}` (`Client#create_payout`, `Client#retrieve_payout`)
7
+ - Add Statements endpoint: `GET /api/v2_1/reports/{reportId}` (`Client#retrieve_statement`) returning binary `data` + extracted `filename`
8
+ - Extend `Transport#request` with `return_headers:` to expose response headers for binary downloads
9
+ - Update README and RBS, and add client specs for the new endpoints
10
+
3
11
  ## [0.1.0] - 2025-12-30
4
12
 
5
13
  - Add i18n-backed validation messages (English + Polish) and `PayuPl.configure` locale support
data/README.md CHANGED
@@ -14,6 +14,12 @@ This gem focuses on the standard payment flow endpoints:
14
14
  - Refunds
15
15
  - Transaction retrieve
16
16
 
17
+ Additional supported areas:
18
+
19
+ - Retrieve Shop Data
20
+ - Payouts
21
+ - Statements
22
+
17
23
  ## Installation
18
24
 
19
25
  Add to your Gemfile:
@@ -106,6 +112,78 @@ client.retrieve_refund(order_id, "5000000142")
106
112
  client.retrieve_transactions(order_id)
107
113
  ```
108
114
 
115
+ ### Retrieve shop data
116
+
117
+ ```ruby
118
+ client.retrieve_shop_data("SHOP_ID")
119
+ ```
120
+
121
+ ### Payouts
122
+
123
+ PayU supports multiple payout request schemas (Standard Payout, Bank Account Payout, Card Payout, Payout for Marketplace, FxPayout).
124
+ This client sends the JSON payload as-is, so your request must match one of the schemas from PayU docs.
125
+
126
+ Note: Payouts are a permissioned product in PayU. If your POS/shop is not enabled for payouts (common in sandbox or without the right agreement), PayU may respond with HTTP `403` (e.g. `ERROR_VALUE_INVALID` / "Permission denied for given action").
127
+
128
+ ```ruby
129
+ # Standard Payout
130
+ standard_payout = {
131
+ shopId: "1a2B3Cx",
132
+ payout: {
133
+ extPayoutId: "payout-123",
134
+ amount: 10_000,
135
+ description: "Payout"
136
+ }
137
+ }
138
+
139
+ # Bank Account Payout
140
+ bank_account_payout = {
141
+ shopId: "1a2B3Cx",
142
+ payout: { extPayoutId: "payout-124", amount: 10_000, description: "Payout" },
143
+ account: { accountNumber: "PL61109010140000071219812874" },
144
+ customerAddress: { name: "Jane Doe" }
145
+ }
146
+
147
+ # Card Payout (use either cardToken or card)
148
+ card_payout = {
149
+ shopId: "1a2B3Cx",
150
+ payout: { extPayoutId: "payout-125", amount: 10_000, description: "Payout" },
151
+ payee: { extCustomerId: "customer-id-1", accountCreationDate: "2025-03-27T00:00:00.000Z", email: "email@email.com" },
152
+ customerAddress: { name: "Jane Doe" },
153
+ cardToken: "TOKC_..."
154
+ }
155
+
156
+ # Payout for Marketplace
157
+ marketplace_payout = {
158
+ shopId: "1a2B3Cx",
159
+ account: { extCustomerId: "submerchant1" },
160
+ payout: { extPayoutId: "payout-126", amount: 10_000, currencyCode: "PLN", description: "Payout" }
161
+ }
162
+
163
+ # FxPayout
164
+ fx_payout = {
165
+ shopId: "1a2B3Cx",
166
+ account: { extCustomerId: "submerchant1" },
167
+ payout: { extPayoutId: "payout-127", amount: 10_000, currencyCode: "PLN", description: "Payout" },
168
+ fxData: { partnerId: "...", currencyCode: "EUR", amount: 2500, rate: 0.25, tableId: "2055" }
169
+ }
170
+
171
+ client.create_payout(standard_payout)
172
+ client.retrieve_payout("PAYOUT_ID")
173
+ ```
174
+
175
+ ### Statements
176
+
177
+ This endpoint returns binary data and includes filename metadata from `Content-Disposition`.
178
+
179
+ ```ruby
180
+ statement = client.retrieve_statement("REPORT_ID")
181
+ # => { data: "...", filename: "...", content_type: "...", http_status: 200 }
182
+
183
+ safe_filename = File.basename(statement.fetch(:filename)).gsub(/[^0-9A-Za-z.\-]/, "_")
184
+ File.binwrite(safe_filename, statement.fetch(:data))
185
+ ```
186
+
109
187
  ## Errors and validation
110
188
 
111
189
  HTTP errors are mapped to Ruby exceptions:
@@ -1,6 +1,7 @@
1
1
  en:
2
2
  payu_pl:
3
3
  validation:
4
+ hash: "must be a hash"
4
5
  max_length: "must be at most %{max} characters"
5
6
  ip_address: "must be a valid IPv4 or IPv6 address"
6
7
  iso_4217: "must be a 3-letter ISO 4217 code"
@@ -1,6 +1,7 @@
1
1
  pl:
2
2
  payu_pl:
3
3
  validation:
4
+ hash: "musi być hashem"
4
5
  max_length: "musi mieć maksymalnie %{max} znaków"
5
6
  ip_address: "musi być poprawnym adresem IPv4 lub IPv6"
6
7
  iso_4217: "musi być 3-literowym kodem ISO 4217"
@@ -73,6 +73,25 @@ module PayuPl
73
73
  Refunds::Retrieve.new(client: self).call(order_id, refund_id)
74
74
  end
75
75
 
76
+ # Shops
77
+ def retrieve_shop_data(shop_id)
78
+ Shops::Retrieve.new(client: self).call(shop_id)
79
+ end
80
+
81
+ # Payouts
82
+ def create_payout(payout_request)
83
+ Payouts::Create.new(client: self).call(payout_request)
84
+ end
85
+
86
+ def retrieve_payout(payout_id)
87
+ Payouts::Retrieve.new(client: self).call(payout_id)
88
+ end
89
+
90
+ # Statements
91
+ def retrieve_statement(report_id)
92
+ Statements::Retrieve.new(client: self).call(report_id)
93
+ end
94
+
76
95
  private
77
96
 
78
97
  def validate!
@@ -25,10 +25,13 @@ module PayuPl
25
25
  required(:currencyCode).filled(:string)
26
26
  required(:totalAmount).filled(:string)
27
27
 
28
+ optional(:validityTime).filled(:string)
29
+
28
30
  required(:products).array(:hash) do
29
31
  required(:name).filled(:string)
30
32
  required(:unitPrice).filled(:string)
31
33
  required(:quantity).filled(:string)
34
+ optional(:virtual).filled(:bool)
32
35
  end
33
36
  end
34
37
 
@@ -96,6 +99,13 @@ module PayuPl
96
99
  key.failure(PayuPl.t(:numeric_string)) unless value.match?(/\A\d+\z/)
97
100
  end
98
101
 
102
+ rule(:validityTime) do
103
+ next if value.nil?
104
+
105
+ # OpenAPI: string containing seconds
106
+ key.failure(PayuPl.t(:numeric_string)) unless value.match?(/\A\d+\z/)
107
+ end
108
+
99
109
  rule(:products) do
100
110
  key.failure(PayuPl.t(:min_items, min: 1)) if value.nil? || value.empty?
101
111
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/validation"
4
+
5
+ module PayuPl
6
+ module Contracts
7
+ class PayoutCreateContract < Dry::Validation::Contract
8
+ params do
9
+ required(:payload).filled
10
+ end
11
+
12
+ rule(:payload) do
13
+ val = value
14
+
15
+ unless val.is_a?(Hash)
16
+ key.failure(PayuPl.t(:hash))
17
+ next
18
+ end
19
+
20
+ key.failure(PayuPl.t(:min_items, min: 1)) if val.empty?
21
+ end
22
+ end
23
+ end
24
+ end
@@ -8,6 +8,10 @@ module PayuPl
8
8
 
9
9
  ORDERS = "/api/v2_1/orders"
10
10
 
11
+ SHOPS = "/api/v2_1/shops"
12
+ PAYOUTS = "/api/v2_1/payouts"
13
+ REPORTS = "/api/v2_1/reports"
14
+
11
15
  def self.order(order_id)
12
16
  "#{ORDERS}/#{URI.encode_www_form_component(order_id.to_s)}"
13
17
  end
@@ -27,5 +31,17 @@ module PayuPl
27
31
  def self.order_refund(order_id, refund_id)
28
32
  "#{order_refunds(order_id)}/#{URI.encode_www_form_component(refund_id.to_s)}"
29
33
  end
34
+
35
+ def self.shop(shop_id)
36
+ "#{SHOPS}/#{URI.encode_www_form_component(shop_id.to_s)}"
37
+ end
38
+
39
+ def self.payout(payout_id)
40
+ "#{PAYOUTS}/#{URI.encode_www_form_component(payout_id.to_s)}"
41
+ end
42
+
43
+ def self.report(report_id)
44
+ "#{REPORTS}/#{URI.encode_www_form_component(report_id.to_s)}"
45
+ end
30
46
  end
31
47
  end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PayuPl
4
+ module Payouts
5
+ class Create < Operations::Base
6
+ def call(payout_request)
7
+ validate_contract!(Contracts::PayoutCreateContract, { payload: payout_request }, input: payout_request)
8
+
9
+ # Do NOT use result.to_h here: dry-schema would drop unknown keys, and PayU supports multiple payout schemas.
10
+ transport.request(:post, Endpoints::PAYOUTS, json: payout_request)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PayuPl
4
+ module Payouts
5
+ class Retrieve < Operations::Base
6
+ def call(payout_id)
7
+ validate_id!(payout_id, input_key: :payout_id)
8
+ transport.request(:get, Endpoints.payout(payout_id))
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PayuPl
4
+ module Shops
5
+ class Retrieve < Operations::Base
6
+ def call(shop_id)
7
+ validate_id!(shop_id, input_key: :shop_id)
8
+ transport.request(:get, Endpoints.shop(shop_id))
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PayuPl
4
+ module Statements
5
+ class Retrieve < Operations::Base
6
+ FILENAME_REGEX = /filename="?(?<filename>[^";]+)"?/i.freeze
7
+
8
+ def call(report_id)
9
+ validate_id!(report_id, input_key: :report_id)
10
+
11
+ response = transport.request(
12
+ :get,
13
+ Endpoints.report(report_id),
14
+ headers: { "Accept" => "application/octet-stream" },
15
+ return_headers: true
16
+ )
17
+
18
+ headers = response.fetch(:headers)
19
+ content_type = headers["content-type"]
20
+ content_disposition = headers["content-disposition"].to_s
21
+
22
+ filename = content_disposition.match(FILENAME_REGEX)&.named_captures&.fetch("filename", nil)
23
+
24
+ {
25
+ data: response.fetch(:body),
26
+ filename: filename,
27
+ content_type: content_type,
28
+ http_status: response.fetch(:http_status)
29
+ }
30
+ end
31
+ end
32
+ end
33
+ end
@@ -15,7 +15,7 @@ module PayuPl
15
15
  validate!
16
16
  end
17
17
 
18
- def request(method, path, headers: {}, json: :__no_json_argument_given, form: nil, authorize: true)
18
+ def request(method, path, headers: {}, json: :__no_json_argument_given, form: nil, authorize: true, return_headers: false)
19
19
  uri = URI.join(@base_url, path)
20
20
  http = Net::HTTP.new(uri.host, uri.port)
21
21
  http.use_ssl = (uri.scheme == "https")
@@ -66,7 +66,7 @@ module PayuPl
66
66
  raise NetworkError.new("Network failure", original: e)
67
67
  end
68
68
 
69
- handle_response(res)
69
+ handle_response(res, return_headers: return_headers)
70
70
  end
71
71
 
72
72
  private
@@ -78,13 +78,21 @@ module PayuPl
78
78
  raise ArgumentError, "base_url is invalid"
79
79
  end
80
80
 
81
- def handle_response(res)
81
+ def handle_response(res, return_headers: false)
82
82
  http_status = res.code.to_i
83
83
  correlation_id = res["Correlation-Id"] || res["correlation-id"]
84
84
  raw_body = res.body
85
85
  parsed = parse_body(res)
86
86
 
87
- return parsed if http_status >= 200 && http_status < 400
87
+ if http_status >= 200 && http_status < 400
88
+ return parsed unless return_headers
89
+
90
+ return {
91
+ body: parsed,
92
+ headers: extract_headers(res),
93
+ http_status: http_status
94
+ }
95
+ end
88
96
 
89
97
  message = build_error_message(http_status, parsed, raw_body)
90
98
 
@@ -107,6 +115,14 @@ module PayuPl
107
115
  )
108
116
  end
109
117
 
118
+ def extract_headers(res)
119
+ return {} unless res.respond_to?(:each_header)
120
+
121
+ headers = {}
122
+ res.each_header { |k, v| headers[k.to_s.downcase] = v }
123
+ headers
124
+ end
125
+
110
126
  def parse_body(res)
111
127
  body = res.body
112
128
  return nil if body.nil? || body.empty?
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PayuPl
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/payu_pl.rb CHANGED
@@ -12,6 +12,7 @@ require_relative "payu_pl/contracts/order_create_contract"
12
12
  require_relative "payu_pl/contracts/id_contract"
13
13
  require_relative "payu_pl/contracts/capture_contract"
14
14
  require_relative "payu_pl/contracts/refund_create_contract"
15
+ require_relative "payu_pl/contracts/payout_create_contract"
15
16
 
16
17
  require_relative "payu_pl/authorize/oauth_token"
17
18
 
@@ -24,6 +25,13 @@ require_relative "payu_pl/orders/transactions"
24
25
  require_relative "payu_pl/refunds/create"
25
26
  require_relative "payu_pl/refunds/list"
26
27
  require_relative "payu_pl/refunds/retrieve"
28
+
29
+ require_relative "payu_pl/shops/retrieve"
30
+
31
+ require_relative "payu_pl/payouts/create"
32
+ require_relative "payu_pl/payouts/retrieve"
33
+
34
+ require_relative "payu_pl/statements/retrieve"
27
35
  require_relative "payu_pl/client"
28
36
 
29
37
  module PayuPl
data/sig/payu_pl.rbs CHANGED
@@ -56,7 +56,7 @@ module PayuPl
56
56
 
57
57
  class Transport
58
58
  def initialize: (base_url: String, access_token_provider: ^() -> String?, ?open_timeout: Integer, ?read_timeout: Integer) -> void
59
- def request: (untyped method, String path, ?headers: Hash[String, String], ?json: untyped, ?form: Hash[untyped, untyped], ?authorize: bool) -> untyped
59
+ def request: (untyped method, String path, ?headers: Hash[String, String], ?json: untyped, ?form: Hash[untyped, untyped]?, ?authorize: bool, ?return_headers: bool) -> untyped
60
60
  end
61
61
 
62
62
  class Client
@@ -90,5 +90,12 @@ module PayuPl
90
90
  def retrieve_refund: (untyped order_id, untyped refund_id) -> untyped
91
91
 
92
92
  def retrieve_transactions: (untyped order_id) -> untyped
93
+
94
+ def retrieve_shop_data: (untyped shop_id) -> untyped
95
+
96
+ def create_payout: (untyped payout_request) -> untyped
97
+ def retrieve_payout: (untyped payout_id) -> untyped
98
+
99
+ def retrieve_statement: (untyped report_id) -> untyped
93
100
  end
94
101
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: payu_pl
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmytro Koval
@@ -73,6 +73,7 @@ files:
73
73
  - lib/payu_pl/contracts/capture_contract.rb
74
74
  - lib/payu_pl/contracts/id_contract.rb
75
75
  - lib/payu_pl/contracts/order_create_contract.rb
76
+ - lib/payu_pl/contracts/payout_create_contract.rb
76
77
  - lib/payu_pl/contracts/refund_create_contract.rb
77
78
  - lib/payu_pl/endpoints.rb
78
79
  - lib/payu_pl/errors.rb
@@ -82,9 +83,13 @@ files:
82
83
  - lib/payu_pl/orders/create.rb
83
84
  - lib/payu_pl/orders/retrieve.rb
84
85
  - lib/payu_pl/orders/transactions.rb
86
+ - lib/payu_pl/payouts/create.rb
87
+ - lib/payu_pl/payouts/retrieve.rb
85
88
  - lib/payu_pl/refunds/create.rb
86
89
  - lib/payu_pl/refunds/list.rb
87
90
  - lib/payu_pl/refunds/retrieve.rb
91
+ - lib/payu_pl/shops/retrieve.rb
92
+ - lib/payu_pl/statements/retrieve.rb
88
93
  - lib/payu_pl/transport.rb
89
94
  - lib/payu_pl/version.rb
90
95
  - sig/payu_pl.rbs