paymos 1.3.0 → 1.5.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: c747bf5230f29f3a2dbed376f50af76f02c05b65b9a89558190dace7f76971dc
4
- data.tar.gz: 6f26d4b9c429dd3d09d9f9c4edda6772bd8fd9b4fa77eb184188de10d831c61e
3
+ metadata.gz: 49ac8a8d2ef52cb2e7b6bcfe98000a10f9dd01d5a3f2d5aee34dad4cbf9ff96f
4
+ data.tar.gz: 3d4e5c92f6a16ff27a3c44194f47366972d4a107de0f1547a75cabb1cca55a60
5
5
  SHA512:
6
- metadata.gz: 865bc2412c06f86d0f152bf2767308a2fcb06cf7386f253bd2467d509a1fb8609c1a16aa3130c85600e1c57302d1c7d598bc05c8f8d7907b289b93a0dd540153
7
- data.tar.gz: 9ecf3a0cce176412d201d3ec956731b1924f9b4c0ec188d5d0717e9a39367a24beac7031c74e0678cd887513443791d73aaae9c918929094b044196e723ce552
6
+ metadata.gz: 1db9fc7682ffbc870ea487f7012f4088e56b5c11eab2884254ae05fd17abc378cc54c507e2ac9e427c051b3ff8867a63327b5dbb41ff93214fe9751226605689
7
+ data.tar.gz: 371e65ded8041b3ed73b0688da5058fa09f08d42d3e91c51b5554cd166b6bc4738e27238e0c397d14b7fe0c0a063ecfd6e2c5bfd6092c1be2b15a6edfca0f593
data/CHANGELOG.md CHANGED
@@ -2,6 +2,26 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [1.5.0] - 2026-10-30
6
+
7
+ - fix(plugins): reject invalid Woo refund controls and unblock SDK checks
8
+ - fix(sdk): preserve current refund contracts in TON conformance
9
+ - fix(ton): return friendly addresses with existing webhook schema
10
+ - feat(sdk-ruby): add refund create, list and get
11
+
12
+ - **403 is no longer reported as an authentication failure.** `Paymos::PermissionError` (subclasses `Paymos::AuthenticationError`, so existing `rescue` blocks keep working) now carries every 403.
13
+ - **Base URL must use HTTPS.** Plain HTTP is accepted only against a loopback host, which is what a local integration test points at. The API secret signs every request, so a plaintext base URL put that credential on the wire.
14
+
15
+ ## [1.4.0] - 2026-10-30
16
+
17
+ - fix(plugins): reject invalid Woo refund controls and unblock SDK checks
18
+ - fix(sdk): preserve current refund contracts in TON conformance
19
+ - fix(ton): return friendly addresses with existing webhook schema
20
+ - feat(sdk-ruby): add refund create, list and get
21
+
22
+ - **403 is no longer reported as an authentication failure.** `Paymos::PermissionError` (subclasses `Paymos::AuthenticationError`, so existing `rescue` blocks keep working) now carries every 403.
23
+ - **Base URL must use HTTPS.** Plain HTTP is accepted only against a loopback host, which is what a local integration test points at. The API secret signs every request, so a plaintext base URL put that credential on the wire.
24
+
5
25
  ## [1.3.0] - 2026-10-14
6
26
 
7
27
  - Make the SDK CI tier green
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Paymos Ruby SDK
2
2
 
3
- Official Ruby client for the Paymos Merchant API — invoices, withdrawals, balances,
3
+ Official Ruby client for the Paymos Merchant API — invoices, withdrawals, refunds, balances,
4
4
  and static per-customer wallets with their confirmed-deposit feed. Its only runtime dependency is
5
5
  Ruby's official `base64` gem (required separately by modern Ruby versions).
6
6
 
@@ -32,6 +32,44 @@ Responses are immutable Ruby objects with documented RBS signatures; request
32
32
  keywords remain idiomatic `snake_case` and are converted directly to the Paymos
33
33
  wire contract.
34
34
 
35
+ Create invoice refunds with a Payment key. The server calculates the amount and
36
+ returns it as a decimal string; there are no amount or destination arguments:
37
+
38
+ ```ruby
39
+ refund = paymos.refunds.create(
40
+ invoice_id: invoice.invoice_id,
41
+ mode: Paymos::RefundMode::OVERPAYMENT,
42
+ external_order_id: "refund_order_123_overpayment"
43
+ )
44
+ puts "#{refund.withdrawal_id}: #{refund.amount} #{refund.currency}"
45
+
46
+ filters = {
47
+ invoice_id: invoice.invoice_id,
48
+ status: [Paymos::WithdrawalStatus::COMPLETED],
49
+ created_from: "2026-01-01T00:00:00Z",
50
+ created_to: "2027-01-01T00:00:00Z",
51
+ limit: 100
52
+ }
53
+ page = paymos.refunds.list(**filters)
54
+ next_page = paymos.refunds.list(**filters, cursor: page.next_cursor) if page.next_cursor
55
+ refund = paymos.refunds.get(refund.withdrawal_id)
56
+ ```
57
+
58
+ `full` returns all remaining payment; `overpayment` returns all remaining excess.
59
+ For a payment of 120 against an invoice of 100, these are initially 120 and 20.
60
+ After the overpayment refund completes, a new full refund returns 100. Refunds
61
+ and their pages are immutable, including their decimal strings and first sender.
62
+
63
+ Keep the same invoice, mode and `external_order_id` when retrying an uncertain
64
+ outcome. Creation returns 201 and an identical replay returns 200; both yield a
65
+ `Paymos::Refund`. Changing the mode under the same key raises
66
+ `Paymos::ConflictError` (409). Missing or foreign refund IDs raise
67
+ `Paymos::NotFoundError` (404). List periods use RFC 3339 timestamps, and
68
+ `paymos.refunds.each` provides the same bounded cursor iteration as invoices.
69
+ In sandbox, use `paymos.withdrawals.simulate_completion(refund.withdrawal_id)`,
70
+ then read the refund again. Refunds have their own list and do not appear in the
71
+ ordinary withdrawals list.
72
+
35
73
  A payment channel is one payer's reusable set of deposit addresses. Create it,
36
74
  read its rails, then poll the confirmed-deposit feed and persist the cursor:
37
75
 
data/lib/paymos/client.rb CHANGED
@@ -19,7 +19,7 @@ module Paymos
19
19
  class Client
20
20
  SAFE_METHODS = %w[GET HEAD OPTIONS].freeze
21
21
 
22
- attr_reader :invoices, :withdrawals, :balances, :system, :payment_channels, :payment_channel_deposits
22
+ attr_reader :invoices, :withdrawals, :refunds, :balances, :system, :payment_channels, :payment_channel_deposits
23
23
 
24
24
  def initialize(api_key:, api_secret:, base_url: 'https://api.paymos.io', timeout: 30,
25
25
  max_retries: 2, base_delay: 0.150, transport: nil,
@@ -43,6 +43,7 @@ module Paymos
43
43
  @random = random
44
44
  @invoices = Invoices.new(self)
45
45
  @withdrawals = Withdrawals.new(self)
46
+ @refunds = Refunds.new(self)
46
47
  @payment_channels = PaymentChannels.new(self)
47
48
  @payment_channel_deposits = PaymentChannelDeposits.new(self)
48
49
  @balances = Balances.new(self)
@@ -296,6 +297,53 @@ module Paymos
296
297
  end
297
298
  end
298
299
 
300
+ class Refunds < Resource
301
+ def create(invoice_id:, mode:, external_order_id:)
302
+ values = { invoice_id:, mode:, external_order_id: }
303
+ missing = values.select { |_name, value| !value.is_a?(String) || value.strip.empty? }.keys
304
+ raise ArgumentError, "Required values are missing: #{missing.join(', ')}" unless missing.empty?
305
+ raise ArgumentError, 'Invalid refund mode' unless [RefundMode::FULL, RefundMode::OVERPAYMENT].include?(mode)
306
+
307
+ Refund.from(@client.request('POST', '/v1/refunds', payload: values))
308
+ end
309
+
310
+ def simulate(refund_id, status)
311
+ Refund.from(@client.request('POST', "/v1/sandbox/refunds/#{segment(refund_id)}/simulate",
312
+ payload: { status: }))
313
+ end
314
+
315
+ def get(refund_id) = Refund.from(@client.request('GET', "/v1/refunds/#{segment(refund_id)}"))
316
+
317
+ def list(limit: nil, cursor: nil, invoice_id: nil, status: nil, created_from: nil, created_to: nil)
318
+ filters = validate_list_filters({ limit:, cursor:, invoice_id:, status: }.compact)
319
+ validate_period(created_from, created_to)
320
+ filters.merge!({ created_from:, created_to: }.compact)
321
+ Page.from(@client.request('GET', '/v1/refunds', query: Signing.query(filters)), item_type: Refund)
322
+ end
323
+
324
+ def each(max_pages: 100, **filters, &block)
325
+ return enum_for(__method__, max_pages: max_pages, **filters) unless block_given?
326
+
327
+ Pagination.each(method(:list), filters, max_pages:, &block)
328
+ end
329
+
330
+ private
331
+
332
+ def validate_period(created_from, created_to)
333
+ from, to = [created_from, created_to].map do |value|
334
+ next if value.nil?
335
+
336
+ unless value.is_a?(String) &&
337
+ value.match?(/\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})\z/i)
338
+ raise ArgumentError, 'created_from and created_to must be RFC 3339 timestamps'
339
+ end
340
+
341
+ Time.iso8601(value)
342
+ end
343
+ raise ArgumentError, 'created_to must be later than created_from' if from && to && from >= to
344
+ end
345
+ end
346
+
299
347
  class PaymentChannels < Resource
300
348
  # Create — or fetch. The same project plus external id answers 200 with the channel that already
301
349
  # exists instead of 201 with a duplicate; both are a channel, so a caller never reads the status.
data/lib/paymos/models.rb CHANGED
@@ -23,6 +23,11 @@ module Paymos
23
23
  CANCELLED = 'cancelled'
24
24
  end
25
25
 
26
+ module RefundMode
27
+ FULL = 'full'
28
+ OVERPAYMENT = 'overpayment'
29
+ end
30
+
26
31
  class Model
27
32
  class << self
28
33
  attr_reader :field_names, :required_names
@@ -127,6 +132,14 @@ module Paymos
127
132
  network destination_address created_at]
128
133
  end
129
134
 
135
+ class Refund < Model
136
+ fields :withdrawal_id, :invoice_id, :mode, :external_order_id, :status, :is_final, :is_test,
137
+ :amount, :fee, :currency, :network, :destination_address, :first_sender, :tx_hash,
138
+ :explorer_url, :created_at, :completed_at, :failed_at, :cancelled_at, :type, :project_id,
139
+ required: %i[withdrawal_id invoice_id mode external_order_id status is_final is_test amount
140
+ currency network destination_address first_sender created_at]
141
+ end
142
+
130
143
  # One accepted token on one rail. A missing minimum_deposit means the rail cannot be quoted right
131
144
  # now — it never means the token has no minimum.
132
145
  class PaymentChannelToken < Model
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Paymos
4
- VERSION = '1.3.0'
4
+ VERSION = '1.5.0'
5
5
  end
data/sig/paymos.rbs CHANGED
@@ -133,6 +133,35 @@ module Paymos
133
133
  attr_reader cancelled_at: Integer?
134
134
  end
135
135
 
136
+ module RefundMode
137
+ FULL: "full"
138
+ OVERPAYMENT: "overpayment"
139
+ end
140
+
141
+ class Refund < Model
142
+ attr_reader withdrawal_id: String
143
+ attr_reader invoice_id: String
144
+ attr_reader mode: "full" | "overpayment"
145
+ attr_reader external_order_id: String
146
+ attr_reader status: String
147
+ attr_reader is_final: bool
148
+ attr_reader is_test: bool
149
+ attr_reader amount: String
150
+ attr_reader fee: String?
151
+ attr_reader currency: String
152
+ attr_reader network: String
153
+ attr_reader destination_address: String
154
+ attr_reader first_sender: String
155
+ attr_reader type: String?
156
+ attr_reader project_id: String?
157
+ attr_reader tx_hash: String?
158
+ attr_reader explorer_url: String?
159
+ attr_reader created_at: Integer
160
+ attr_reader completed_at: Integer?
161
+ attr_reader failed_at: Integer?
162
+ attr_reader cancelled_at: Integer?
163
+ end
164
+
136
165
  class PaymentChannelToken < Model
137
166
  attr_reader symbol: String
138
167
  attr_reader minimum_deposit: String?
@@ -245,6 +274,15 @@ module Paymos
245
274
  | (?max_pages: Integer, **untyped filters) -> Enumerator[Withdrawal, void]
246
275
  end
247
276
 
277
+ class Refunds
278
+ def simulate: (String refund_id, String status) -> Refund
279
+ def create: (invoice_id: String, mode: "full" | "overpayment", external_order_id: String) -> Refund
280
+ def get: (String refund_id) -> Refund
281
+ def list: (?limit: Integer?, ?cursor: String?, ?invoice_id: String?, ?status: String | Array[String]?, ?created_from: String?, ?created_to: String?) -> Page[Refund]
282
+ def each: (?max_pages: Integer, **untyped filters) { (Refund) -> void } -> void
283
+ | (?max_pages: Integer, **untyped filters) -> Enumerator[Refund, void]
284
+ end
285
+
248
286
  class PaymentChannels
249
287
  def create: (project_id: String, external_id: String) -> PaymentChannel
250
288
  def get: (String payment_channel_id) -> PaymentChannel
@@ -272,6 +310,7 @@ module Paymos
272
310
  class Client
273
311
  attr_reader invoices: Invoices
274
312
  attr_reader withdrawals: Withdrawals
313
+ attr_reader refunds: Refunds
275
314
  attr_reader payment_channels: PaymentChannels
276
315
  attr_reader payment_channel_deposits: PaymentChannelDeposits
277
316
  attr_reader balances: Balances
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: paymos
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paymos Labs