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 +4 -4
- data/CHANGELOG.md +12 -0
- data/Gemfile.lock +2 -4
- data/lib/jamm/version.rb +1 -1
- data/lib/jamm/webhook.rb +143 -22
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f15af21a2f10a3f3ea2c5636349ab7449d90997795bb8e9a729d063a02c70040
|
|
4
|
+
data.tar.gz: 48f9b4782863261fb11c8ce3e37f5f0819c8b87d24136c595df7396a0cbfd3b5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
|
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
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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::
|
|
22
|
-
out.content = Jamm::OpenAPI::
|
|
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::
|
|
26
|
-
out.content = Jamm::OpenAPI::
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
return out
|
|
44
|
+
raise 'Unknown event type'
|
|
45
|
+
end
|
|
32
46
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
return out
|
|
78
|
+
klass.new(filtered)
|
|
79
|
+
end
|
|
40
80
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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.
|
|
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-
|
|
11
|
+
date: 2026-07-23 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: base64
|