jamm 2.5.0 → 2.6.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: fe888992acfb9739e5206ab7aaa3428f3e246998e633ff60e09999c2d96f3992
4
- data.tar.gz: 6ee9932e4775b93a8b18e9dba2173871d6c36b0e727563111c8462801db481e3
3
+ metadata.gz: f15af21a2f10a3f3ea2c5636349ab7449d90997795bb8e9a729d063a02c70040
4
+ data.tar.gz: 48f9b4782863261fb11c8ce3e37f5f0819c8b87d24136c595df7396a0cbfd3b5
5
5
  SHA512:
6
- metadata.gz: c33699a5edbb8cd8f52c5848bc5534b735f9ebca0990b45ceb2105408a2e5fbad7aca6205a0f788c5b6213e954d9a9c56e7d7f71f8f2cb47f491f7786afe658d
7
- data.tar.gz: 73722cb39c3acbbd828c8011e4303f0ff1a8936b1dd390f34edbef5fe9dc26cf3375d6f025a8b20c8abd3e2a37fe80e3fbdc97b195dbbd899fcde93a099c3260
6
+ metadata.gz: 18be3aac7a0654593d068e7e8f7c889e02d6df8bf66bd6c00d024fd15aeff33f4611055a902f223f4fdfcb7e6dd518c0a3c27659bf79a9f030bcb39846ed9c54
7
+ data.tar.gz: 214589958f50dda8837dbc9d27017597fb9d88f10ee02923eee15ff278b6b1d5e7f809412dd0c5a6b475a936de575fff6f69c19949a4dec8fa06efcc41baccd2
data/CHANGELOG.md CHANGED
@@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [2.6.0] - 2026-07-22
9
+
10
+ ### Fixed
11
+
12
+ - Restored the forward-compatible `Jamm::Webhook.parse` first shipped in 2.3.0 (2.4.0 and 2.5.0 regressed to strict parsing):
13
+ - Webhook parsing no longer fails on new fields added by the backend (unknown fields are ignored instead of raising `ArgumentError`)
14
+ - Numeric enum wire values (`status`, …) resolve onto their string enum constants, matching REST API responses
15
+ - Refund webhooks expose the nested transaction fields, the refund's `rfd-` id (`refund_id` and `refund.id`), and typed nested models (`refund.error.code` works instead of raising `NoMethodError`)
16
+ - `EVENT_TYPE_USER_ACCOUNT_DELETED` webhooks parse (previously raised `Unknown event type`)
17
+ - `Webhook.parse` accepts payloads with either string or symbol keys
18
+ - `Webhook.parse` routes on the coerced event type, so event routing also survives a numeric `event_type` wire value
19
+
8
20
  ## [2.5.0] - 2026-07-03
9
21
 
10
22
  ### Added
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- jamm (2.5.0)
4
+ jamm (2.6.0)
5
5
  base64 (~> 0.2)
6
6
  rest-client (~> 2.0)
7
7
  typhoeus (~> 1.0, >= 1.0.1)
@@ -24,9 +24,7 @@ GEM
24
24
  ffi (>= 1.15.0)
25
25
  faker (3.5.1)
26
26
  i18n (>= 1.8.11, < 2)
27
- ffi (1.17.0-aarch64-linux-gnu)
28
- ffi (1.17.0-arm64-darwin)
29
- ffi (1.17.0-x86_64-linux-gnu)
27
+ ffi (1.17.0)
30
28
  gimei (1.5.0)
31
29
  hashdiff (1.1.0)
32
30
  http-accept (1.7.0)
data/lib/jamm/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Jamm
4
- VERSION = '2.5.0'
4
+ VERSION = '2.6.0'
5
5
  end
data/lib/jamm/webhook.rb CHANGED
@@ -11,39 +11,160 @@ module Jamm
11
11
  # Parse command is for parsing the received webhook message.
12
12
  # It does not call anything remotely, instead returns the suitable object.
13
13
  def self.parse(json)
14
- out = Jamm::OpenAPI::MerchantWebhookMessage.new(json)
15
-
16
- case json[:event_type]
17
- when Jamm::OpenAPI::EventType::CHARGE_CREATED
18
- out.content = Jamm::OpenAPI::ChargeMessage.new(json[:content])
14
+ # Webhook payloads may arrive with string or symbol keys depending on how
15
+ # the caller decoded the JSON (e.g. JSON.parse with or without
16
+ # symbolize_names: true). Normalize to symbols so event-type routing,
17
+ # wrapper flattening, and field lookups are reliable either way.
18
+ json = deep_symbolize_keys(json)
19
+
20
+ out = build(Jamm::OpenAPI::MerchantWebhookMessage, json)
21
+
22
+ # Route on the coerced event type: the wire value is an enum string today,
23
+ # but `build` also resolves a numeric wire value onto its string constant,
24
+ # so routing survives either serialization.
25
+ case out.event_type
26
+ when Jamm::OpenAPI::EventType::CHARGE_CREATED,
27
+ Jamm::OpenAPI::EventType::CHARGE_UPDATED,
28
+ Jamm::OpenAPI::EventType::REFUND_SUCCEEDED,
29
+ Jamm::OpenAPI::EventType::REFUND_FAILED,
30
+ Jamm::OpenAPI::EventType::CHARGE_SUCCESS,
31
+ Jamm::OpenAPI::EventType::CHARGE_FAIL
32
+ out.content = build(Jamm::OpenAPI::ChargeMessage, flatten_charge_content(json[:content]))
19
33
  return out
20
34
 
21
- when Jamm::OpenAPI::EventType::CHARGE_UPDATED
22
- out.content = Jamm::OpenAPI::ChargeMessage.new(json[:content])
35
+ when Jamm::OpenAPI::EventType::CONTRACT_ACTIVATED
36
+ out.content = build(Jamm::OpenAPI::ContractMessage, json[:content])
23
37
  return out
24
38
 
25
- when Jamm::OpenAPI::EventType::REFUND_SUCCEEDED
26
- out.content = Jamm::OpenAPI::ChargeMessage.new(json[:content])
39
+ when Jamm::OpenAPI::EventType::USER_ACCOUNT_DELETED
40
+ out.content = build(Jamm::OpenAPI::UserAccountMessage, json[:content])
27
41
  return out
42
+ end
28
43
 
29
- when Jamm::OpenAPI::EventType::REFUND_FAILED
30
- out.content = Jamm::OpenAPI::ChargeMessage.new(json[:content])
31
- return out
44
+ raise 'Unknown event type'
45
+ end
32
46
 
33
- when Jamm::OpenAPI::EventType::CHARGE_SUCCESS
34
- out.content = Jamm::OpenAPI::ChargeMessage.new(json[:content])
35
- return out
47
+ # Build a generated model from a webhook payload while normalizing the
48
+ # quirks of the webhook wire format. Applied to every model, so charges,
49
+ # contracts, user-account and refund messages all benefit:
50
+ #
51
+ # 1. Forward-compat: the Jamm backend can add new fields to webhook
52
+ # payloads at any time. The generated model `initialize` raises
53
+ # ArgumentError on any key outside `attribute_map`, so unknown keys are
54
+ # dropped first. Known keys are snake_case, matching `attribute_map`.
55
+ # 2. Numeric enums: the backend serializes webhook payloads with Go's
56
+ # `json.Marshal` (not protojson), so every enum field (status,
57
+ # api_source, ...) arrives as its integer value, while the generated
58
+ # enums are string-based. Each integer is mapped back to the enum string
59
+ # so it matches the values returned by the REST API.
60
+ # 3. Nested models: the generated `initialize` assigns nested objects
61
+ # verbatim (the `_deserialize` coercion only runs from `build_from_hash`,
62
+ # which expects camelCase keys the webhook does not use). So a nested
63
+ # field like `refund.error` would stay a raw Hash and `error.code` would
64
+ # raise NoMethodError. We coerce nested model fields (and arrays of them)
65
+ # recursively so the typed accessors work.
66
+ def self.build(klass, attributes)
67
+ return nil if attributes.nil?
68
+
69
+ known = klass.attribute_map
70
+ types = klass.openapi_types
71
+ filtered = attributes.each_with_object({}) do |(key, value), acc|
72
+ sym = key.to_sym
73
+ next unless known.key?(sym)
74
+
75
+ acc[sym] = coerce(types[sym], value)
76
+ end
36
77
 
37
- when Jamm::OpenAPI::EventType::CHARGE_FAIL
38
- out.content = Jamm::OpenAPI::ChargeMessage.new(json[:content])
39
- return out
78
+ klass.new(filtered)
79
+ end
40
80
 
41
- when Jamm::OpenAPI::EventType::CONTRACT_ACTIVATED
42
- out.content = Jamm::OpenAPI::ContractMessage.new(json[:content])
43
- return out
81
+ # Refund webhooks (REFUND_SUCCEEDED / REFUND_FAILED) deliver `content` as a
82
+ # nested { transaction, refund } wrapper instead of a flat ChargeMessage.
83
+ # Flatten it back into a ChargeMessage so callers always receive the same shape.
84
+ def self.flatten_charge_content(content)
85
+ return content unless content.is_a?(Hash) && content.key?(:transaction)
86
+
87
+ refund = content[:refund]
88
+ # Keep `refund` as the raw Hash: `build` coerces it into a typed RefundInfo
89
+ # (and recursively types its nested `error`). Also surface the refund's
90
+ # `rfd-` id on the flat `refund_id` attribute the model documents.
91
+ charge = content[:transaction].merge(refund: refund)
92
+ charge[:refund_id] = refund[:id] if refund.is_a?(Hash) && !refund[:id].nil?
93
+ charge
94
+ end
95
+
96
+ # Coerce a raw webhook value into the shape the generated model expects,
97
+ # based on the field's openapi type: numeric enums become their string
98
+ # constant, nested models become typed instances, and `Array<T>` elements are
99
+ # coerced by `T`. Anything else passes through untouched.
100
+ def self.coerce(type, value)
101
+ return value if value.nil?
102
+
103
+ inner = array_inner_type(type)
104
+ return coerce_array(inner, value) unless inner.nil?
105
+
106
+ klass = openapi_const(type)
107
+ return value if klass.nil?
108
+
109
+ if klass.respond_to?(:all_vars)
110
+ resolve_enum(klass, value)
111
+ elsif klass.respond_to?(:openapi_types) && value.is_a?(Hash)
112
+ build(klass, value)
113
+ else
114
+ value
44
115
  end
116
+ end
45
117
 
46
- raise 'Unknown event type'
118
+ # Coerce each element of an `Array<T>` field by its inner type `T`.
119
+ def self.coerce_array(inner_type, value)
120
+ return value unless value.is_a?(Array)
121
+
122
+ value.map { |element| coerce(inner_type, element) }
123
+ end
124
+
125
+ # Extract `T` from an `Array<T>` openapi type, or nil when not an array type.
126
+ def self.array_inner_type(type)
127
+ match = type.to_s.match(/\AArray<(.+)>\z/)
128
+ match && match[1]
129
+ end
130
+
131
+ # Map a numeric enum wire value onto its string enum constant. A value that
132
+ # is already a string (REST-style) passes through untouched.
133
+ def self.resolve_enum(enum, value)
134
+ return value unless value.is_a?(Integer)
135
+
136
+ vars = enum.all_vars
137
+ # Guard the bounds explicitly: Ruby maps negative indices from the end of
138
+ # the array, so any unexpected wire value must fall back to the *_UNSPECIFIED
139
+ # member (index 0) rather than silently selecting the wrong constant.
140
+ value.between?(0, vars.length - 1) ? vars[value] : vars[0]
141
+ end
142
+
143
+ # Resolve an openapi_types entry (e.g. :ChargeMessageApiSource, :RefundInfo)
144
+ # to its generated class, or nil when the type is a primitive (String,
145
+ # Integer, ...) or otherwise unresolvable.
146
+ def self.openapi_const(type)
147
+ return nil if type.nil?
148
+
149
+ name = type.to_s
150
+ return nil unless Jamm::OpenAPI.const_defined?(name)
151
+
152
+ Jamm::OpenAPI.const_get(name)
153
+ rescue NameError
154
+ nil
155
+ end
156
+
157
+ # Recursively convert Hash keys to symbols so parsing is robust regardless of
158
+ # how the caller decoded the webhook JSON.
159
+ def self.deep_symbolize_keys(value)
160
+ case value
161
+ when Hash
162
+ value.each_with_object({}) { |(k, v), acc| acc[k.to_sym] = deep_symbolize_keys(v) }
163
+ when Array
164
+ value.map { |v| deep_symbolize_keys(v) }
165
+ else
166
+ value
167
+ end
47
168
  end
48
169
 
49
170
  # Verify message.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jamm
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.5.0
4
+ version: 2.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jamm
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-07-15 00:00:00.000000000 Z
11
+ date: 2026-07-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64