jamm 2.3.0 → 2.4.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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +2 -12
  3. data/Gemfile.lock +4 -2
  4. data/compatibility/.gitignore +11 -0
  5. data/compatibility/1.3.0/Gemfile +7 -0
  6. data/compatibility/1.3.0/compat_test.rb +16 -0
  7. data/compatibility/1.4.0/Gemfile +7 -0
  8. data/compatibility/1.4.0/compat_test.rb +16 -0
  9. data/compatibility/1.4.1/Gemfile +7 -0
  10. data/compatibility/1.4.1/compat_test.rb +16 -0
  11. data/compatibility/1.5.0/Gemfile +7 -0
  12. data/compatibility/1.5.0/compat_test.rb +16 -0
  13. data/compatibility/1.6.0/Gemfile +7 -0
  14. data/compatibility/1.6.0/compat_test.rb +16 -0
  15. data/compatibility/1.7.0/Gemfile +7 -0
  16. data/compatibility/1.7.0/compat_test.rb +16 -0
  17. data/compatibility/2.0.0/Gemfile +7 -0
  18. data/compatibility/2.0.0/compat_test.rb +16 -0
  19. data/compatibility/2.1.0/Gemfile +7 -0
  20. data/compatibility/2.1.0/compat_test.rb +16 -0
  21. data/compatibility/2.2.0/Gemfile +7 -0
  22. data/compatibility/2.2.0/compat_test.rb +16 -0
  23. data/compatibility/2.3.0/Gemfile +7 -0
  24. data/compatibility/2.3.0/compat_test.rb +16 -0
  25. data/compatibility/Makefile +63 -0
  26. data/compatibility/README.md +119 -0
  27. data/compatibility/shared/suite.rb +146 -0
  28. data/compatibility/templates/Gemfile.tmpl +7 -0
  29. data/compatibility/templates/compat_test.rb.tmpl +16 -0
  30. data/lib/jamm/api/models/v1_charge_message.rb +4 -15
  31. data/lib/jamm/api/models/v1_error_type.rb +2 -1
  32. data/lib/jamm/api/models/v1_refund_info.rb +7 -7
  33. data/lib/jamm/api.rb +0 -1
  34. data/lib/jamm/client.rb +1 -0
  35. data/lib/jamm/version.rb +1 -1
  36. data/lib/jamm/webhook.rb +20 -138
  37. metadata +28 -3
  38. data/lib/jamm/api/models/charge_message_api_source.rb +0 -42
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cb73f3a736756e42ca49cedb1fb255e011fb39e0c34dabc2f85bfd537836afe1
4
- data.tar.gz: d5ce67a32f315473bca8800024d19b939b9dafefb358a077e7f30fa551dd01ed
3
+ metadata.gz: 17f04171e0380f881d022d968eafaecfab7de2ca6e0a91ed56b217a4434afcab
4
+ data.tar.gz: 2f98a4766f79146afc5a839a26b2f69674ac87330065f663d9c90441a52b5f74
5
5
  SHA512:
6
- metadata.gz: ff218dca883cf3515ce3d37a89147c6ce1a615fc53568f0916edb48211f6de1a5b4af7c5613066a74a3df017cd81ec76d734c9ecec259b3a78457af4eb3d4900
7
- data.tar.gz: 69cd60e93b565297f43fd08f7b6a768a6ae5955dddd47d4553b18756e5a3123cdb6548e596e1c9317928f613064c96c66630de9783fab57229d146e78ecfcbe2
6
+ metadata.gz: 1182b1fc82f6d8c009e9f98542baf35e2b4542ff81f8b49ba4de35319662a8a2bcec5e47f35b2f45041a491f4d9c7697997259263426960d11493cefa0eef547
7
+ data.tar.gz: a17d3a2f3a352b39a8c2021593dc24e4d960afd460bc2ecdea038186b078e9df92d8b6e6b0e9965995cf50408c1327eef01dbd3ffd39909aa222636e8155b337
data/CHANGELOG.md CHANGED
@@ -5,21 +5,11 @@ 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.3.0] - 2026-06-24
8
+ ## [2.4.0] - 2026-07-01
9
9
 
10
10
  ### Added
11
11
 
12
- - Resolve numeric enum wire values (`status`, `api_source`, …) onto their string enum constants on every webhook model, matching REST API responses (the backend serializes webhooks with `json.Marshal`, so all enums arrive numeric)
13
- - Surface the refund `rfd-` id on the flat `refund_id` attribute in addition to the nested `refund.id`
14
- - Parse `EVENT_TYPE_USER_ACCOUNT_DELETED` webhooks (previously raised `Unknown event type`)
15
-
16
- ### Fixed
17
-
18
- - Webhook parsing no longer fails on new fields added by the backend (forward-compatible)
19
- - Refund webhooks now expose the nested transaction fields and the refund's `rfd-` id (`refund.id`)
20
- - `status` on refund/charge webhooks is no longer left as a raw integer
21
- - Nested webhook fields (e.g. `refund.error`) are now typed model instances instead of raw Hashes, so `error.code` / `error.message` work instead of raising `NoMethodError`
22
- - `Webhook.parse` now accepts payloads with either string or symbol keys
12
+ - Send the `X-SDK-Version` request header (`ruby:<version>`) so the backend can track SDK version usage per merchant
23
13
 
24
14
  ## [2.2.0] - 2026-05-20
25
15
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- jamm (2.3.0)
4
+ jamm (2.4.0)
5
5
  base64 (~> 0.2)
6
6
  rest-client (~> 2.0)
7
7
  typhoeus (~> 1.0, >= 1.0.1)
@@ -24,7 +24,9 @@ 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)
27
+ ffi (1.17.0-aarch64-linux-gnu)
28
+ ffi (1.17.0-arm64-darwin)
29
+ ffi (1.17.0-x86_64-linux-gnu)
28
30
  gimei (1.5.0)
29
31
  hashdiff (1.1.0)
30
32
  http-accept (1.7.0)
@@ -0,0 +1,11 @@
1
+ # Per-version status emitted by `make report` for the PR comment workflow.
2
+ compat-report.tsv
3
+
4
+ # Installed published gems and per-directory bundler state -- pulled fresh from
5
+ # the registry per run.
6
+ */.bundle
7
+ */vendor
8
+
9
+ # Lockfiles are intentionally untracked: these install real published versions
10
+ # from the registry and are out of scope for our vulnerability checks.
11
+ */Gemfile.lock
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+ # Generated from templates/Gemfile.tmpl -- do not edit by hand.
3
+ # Regenerate with `make add VER=1.3.0` from packages/sdk/ruby/compatibility.
4
+ source 'https://rubygems.org'
5
+
6
+ gem 'jamm', '1.3.0'
7
+ gem 'test-unit', '~> 3.0'
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+ # Generated from templates/compat_test.rb.tmpl -- do not edit by hand.
3
+ # Regenerate with `make add VER=1.3.0` from packages/sdk/ruby/compatibility.
4
+ #
5
+ # Installs the published jamm@1.3.0 (pinned in the sibling Gemfile) and runs
6
+ # the shared backward-compat suite against it. Requiring 'jamm' here -- under this
7
+ # directory's bundle -- is what makes bundler resolve it to *this* pinned version.
8
+ require 'jamm'
9
+
10
+ require_relative '../shared/suite'
11
+
12
+ class CompatTest < Test::Unit::TestCase
13
+ include JammCompat::Tests
14
+
15
+ SDK_VERSION = '1.3.0'
16
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+ # Generated from templates/Gemfile.tmpl -- do not edit by hand.
3
+ # Regenerate with `make add VER=1.4.0` from packages/sdk/ruby/compatibility.
4
+ source 'https://rubygems.org'
5
+
6
+ gem 'jamm', '1.4.0'
7
+ gem 'test-unit', '~> 3.0'
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+ # Generated from templates/compat_test.rb.tmpl -- do not edit by hand.
3
+ # Regenerate with `make add VER=1.4.0` from packages/sdk/ruby/compatibility.
4
+ #
5
+ # Installs the published jamm@1.4.0 (pinned in the sibling Gemfile) and runs
6
+ # the shared backward-compat suite against it. Requiring 'jamm' here -- under this
7
+ # directory's bundle -- is what makes bundler resolve it to *this* pinned version.
8
+ require 'jamm'
9
+
10
+ require_relative '../shared/suite'
11
+
12
+ class CompatTest < Test::Unit::TestCase
13
+ include JammCompat::Tests
14
+
15
+ SDK_VERSION = '1.4.0'
16
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+ # Generated from templates/Gemfile.tmpl -- do not edit by hand.
3
+ # Regenerate with `make add VER=1.4.1` from packages/sdk/ruby/compatibility.
4
+ source 'https://rubygems.org'
5
+
6
+ gem 'jamm', '1.4.1'
7
+ gem 'test-unit', '~> 3.0'
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+ # Generated from templates/compat_test.rb.tmpl -- do not edit by hand.
3
+ # Regenerate with `make add VER=1.4.1` from packages/sdk/ruby/compatibility.
4
+ #
5
+ # Installs the published jamm@1.4.1 (pinned in the sibling Gemfile) and runs
6
+ # the shared backward-compat suite against it. Requiring 'jamm' here -- under this
7
+ # directory's bundle -- is what makes bundler resolve it to *this* pinned version.
8
+ require 'jamm'
9
+
10
+ require_relative '../shared/suite'
11
+
12
+ class CompatTest < Test::Unit::TestCase
13
+ include JammCompat::Tests
14
+
15
+ SDK_VERSION = '1.4.1'
16
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+ # Generated from templates/Gemfile.tmpl -- do not edit by hand.
3
+ # Regenerate with `make add VER=1.5.0` from packages/sdk/ruby/compatibility.
4
+ source 'https://rubygems.org'
5
+
6
+ gem 'jamm', '1.5.0'
7
+ gem 'test-unit', '~> 3.0'
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+ # Generated from templates/compat_test.rb.tmpl -- do not edit by hand.
3
+ # Regenerate with `make add VER=1.5.0` from packages/sdk/ruby/compatibility.
4
+ #
5
+ # Installs the published jamm@1.5.0 (pinned in the sibling Gemfile) and runs
6
+ # the shared backward-compat suite against it. Requiring 'jamm' here -- under this
7
+ # directory's bundle -- is what makes bundler resolve it to *this* pinned version.
8
+ require 'jamm'
9
+
10
+ require_relative '../shared/suite'
11
+
12
+ class CompatTest < Test::Unit::TestCase
13
+ include JammCompat::Tests
14
+
15
+ SDK_VERSION = '1.5.0'
16
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+ # Generated from templates/Gemfile.tmpl -- do not edit by hand.
3
+ # Regenerate with `make add VER=1.6.0` from packages/sdk/ruby/compatibility.
4
+ source 'https://rubygems.org'
5
+
6
+ gem 'jamm', '1.6.0'
7
+ gem 'test-unit', '~> 3.0'
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+ # Generated from templates/compat_test.rb.tmpl -- do not edit by hand.
3
+ # Regenerate with `make add VER=1.6.0` from packages/sdk/ruby/compatibility.
4
+ #
5
+ # Installs the published jamm@1.6.0 (pinned in the sibling Gemfile) and runs
6
+ # the shared backward-compat suite against it. Requiring 'jamm' here -- under this
7
+ # directory's bundle -- is what makes bundler resolve it to *this* pinned version.
8
+ require 'jamm'
9
+
10
+ require_relative '../shared/suite'
11
+
12
+ class CompatTest < Test::Unit::TestCase
13
+ include JammCompat::Tests
14
+
15
+ SDK_VERSION = '1.6.0'
16
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+ # Generated from templates/Gemfile.tmpl -- do not edit by hand.
3
+ # Regenerate with `make add VER=1.7.0` from packages/sdk/ruby/compatibility.
4
+ source 'https://rubygems.org'
5
+
6
+ gem 'jamm', '1.7.0'
7
+ gem 'test-unit', '~> 3.0'
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+ # Generated from templates/compat_test.rb.tmpl -- do not edit by hand.
3
+ # Regenerate with `make add VER=1.7.0` from packages/sdk/ruby/compatibility.
4
+ #
5
+ # Installs the published jamm@1.7.0 (pinned in the sibling Gemfile) and runs
6
+ # the shared backward-compat suite against it. Requiring 'jamm' here -- under this
7
+ # directory's bundle -- is what makes bundler resolve it to *this* pinned version.
8
+ require 'jamm'
9
+
10
+ require_relative '../shared/suite'
11
+
12
+ class CompatTest < Test::Unit::TestCase
13
+ include JammCompat::Tests
14
+
15
+ SDK_VERSION = '1.7.0'
16
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+ # Generated from templates/Gemfile.tmpl -- do not edit by hand.
3
+ # Regenerate with `make add VER=2.0.0` from packages/sdk/ruby/compatibility.
4
+ source 'https://rubygems.org'
5
+
6
+ gem 'jamm', '2.0.0'
7
+ gem 'test-unit', '~> 3.0'
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+ # Generated from templates/compat_test.rb.tmpl -- do not edit by hand.
3
+ # Regenerate with `make add VER=2.0.0` from packages/sdk/ruby/compatibility.
4
+ #
5
+ # Installs the published jamm@2.0.0 (pinned in the sibling Gemfile) and runs
6
+ # the shared backward-compat suite against it. Requiring 'jamm' here -- under this
7
+ # directory's bundle -- is what makes bundler resolve it to *this* pinned version.
8
+ require 'jamm'
9
+
10
+ require_relative '../shared/suite'
11
+
12
+ class CompatTest < Test::Unit::TestCase
13
+ include JammCompat::Tests
14
+
15
+ SDK_VERSION = '2.0.0'
16
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+ # Generated from templates/Gemfile.tmpl -- do not edit by hand.
3
+ # Regenerate with `make add VER=2.1.0` from packages/sdk/ruby/compatibility.
4
+ source 'https://rubygems.org'
5
+
6
+ gem 'jamm', '2.1.0'
7
+ gem 'test-unit', '~> 3.0'
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+ # Generated from templates/compat_test.rb.tmpl -- do not edit by hand.
3
+ # Regenerate with `make add VER=2.1.0` from packages/sdk/ruby/compatibility.
4
+ #
5
+ # Installs the published jamm@2.1.0 (pinned in the sibling Gemfile) and runs
6
+ # the shared backward-compat suite against it. Requiring 'jamm' here -- under this
7
+ # directory's bundle -- is what makes bundler resolve it to *this* pinned version.
8
+ require 'jamm'
9
+
10
+ require_relative '../shared/suite'
11
+
12
+ class CompatTest < Test::Unit::TestCase
13
+ include JammCompat::Tests
14
+
15
+ SDK_VERSION = '2.1.0'
16
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+ # Generated from templates/Gemfile.tmpl -- do not edit by hand.
3
+ # Regenerate with `make add VER=2.2.0` from packages/sdk/ruby/compatibility.
4
+ source 'https://rubygems.org'
5
+
6
+ gem 'jamm', '2.2.0'
7
+ gem 'test-unit', '~> 3.0'
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+ # Generated from templates/compat_test.rb.tmpl -- do not edit by hand.
3
+ # Regenerate with `make add VER=2.2.0` from packages/sdk/ruby/compatibility.
4
+ #
5
+ # Installs the published jamm@2.2.0 (pinned in the sibling Gemfile) and runs
6
+ # the shared backward-compat suite against it. Requiring 'jamm' here -- under this
7
+ # directory's bundle -- is what makes bundler resolve it to *this* pinned version.
8
+ require 'jamm'
9
+
10
+ require_relative '../shared/suite'
11
+
12
+ class CompatTest < Test::Unit::TestCase
13
+ include JammCompat::Tests
14
+
15
+ SDK_VERSION = '2.2.0'
16
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+ # Generated from templates/Gemfile.tmpl -- do not edit by hand.
3
+ # Regenerate with `make add VER=2.3.0` from packages/sdk/ruby/compatibility.
4
+ source 'https://rubygems.org'
5
+
6
+ gem 'jamm', '2.3.0'
7
+ gem 'test-unit', '~> 3.0'
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+ # Generated from templates/compat_test.rb.tmpl -- do not edit by hand.
3
+ # Regenerate with `make add VER=2.3.0` from packages/sdk/ruby/compatibility.
4
+ #
5
+ # Installs the published jamm@2.3.0 (pinned in the sibling Gemfile) and runs
6
+ # the shared backward-compat suite against it. Requiring 'jamm' here -- under this
7
+ # directory's bundle -- is what makes bundler resolve it to *this* pinned version.
8
+ require 'jamm'
9
+
10
+ require_relative '../shared/suite'
11
+
12
+ class CompatTest < Test::Unit::TestCase
13
+ include JammCompat::Tests
14
+
15
+ SDK_VERSION = '2.3.0'
16
+ end
@@ -0,0 +1,63 @@
1
+ .PHONY: ls install test report clean add
2
+
3
+ WORKDIR := /services/packages/sdk/ruby/compatibility
4
+
5
+ # Run inside the api container (same pattern as the parent SDK Makefile) so the
6
+ # SDK's env: 'local' host (api.jamm.test) resolves to the local backend.
7
+ ifneq ($(WORKDIR), $(PWD))
8
+ EXEC := docker compose exec -w $(WORKDIR) api
9
+ endif
10
+
11
+ # Pinned version directories -- everything except shared/ and templates/.
12
+ VERSIONS := $(filter-out shared/ templates/,$(wildcard */))
13
+
14
+ # List the version directories under test.
15
+ ls:
16
+ @echo "Compat versions:" $(VERSIONS:/=)
17
+
18
+ # Install each pinned gem version into its own bundle (lockfiles are gitignored
19
+ # on purpose). BUNDLE_GEMFILE scopes bundler to that directory's Gemfile.
20
+ install:
21
+ @$(EXEC) bash -c 'set -e; for d in $(VERSIONS); do \
22
+ echo "== install $$d =="; \
23
+ ( cd $$d && BUNDLE_GEMFILE=Gemfile bundle install ); \
24
+ done'
25
+
26
+ # Run the shared suite against every pinned version. Runs all versions even if
27
+ # one fails, then exits non-zero if any did -- so a single broken version is
28
+ # visible without masking the rest.
29
+ test:
30
+ @$(EXEC) bash -c 'rc=0; for d in $(VERSIONS); do \
31
+ echo "== test $$d =="; \
32
+ ( cd $$d && \
33
+ MERCHANT_CLIENT_ID=$(MERCHANT_CLIENT_ID) \
34
+ MERCHANT_CLIENT_SECRET=$(MERCHANT_CLIENT_SECRET) \
35
+ BUNDLE_GEMFILE=Gemfile bundle exec ruby compat_test.rb ) || rc=1; \
36
+ done; exit $$rc'
37
+
38
+ # CI variant of `test`: runs every version, writes per-version status to
39
+ # compat-report.tsv ("<version>\t<PASS|FAIL>"), and always exits 0. The
40
+ # workflow turns the file into a PR comment, so the merge is not blocked when
41
+ # an older published version is out of sync with the current API.
42
+ report:
43
+ @$(EXEC) bash -c 'rm -f compat-report.tsv; for d in $(VERSIONS); do \
44
+ v=$${d%/}; \
45
+ echo "== test $$v =="; \
46
+ if ( cd $$d && \
47
+ MERCHANT_CLIENT_ID=$(MERCHANT_CLIENT_ID) \
48
+ MERCHANT_CLIENT_SECRET=$(MERCHANT_CLIENT_SECRET) \
49
+ BUNDLE_GEMFILE=Gemfile bundle exec ruby compat_test.rb ); then status=PASS; else status=FAIL; fi; \
50
+ printf "%s\t%s\n" "$$v" "$$status" >> compat-report.tsv; \
51
+ done; echo "== summary =="; cat compat-report.tsv'
52
+
53
+ # Remove installed bundles and lockfiles for every version.
54
+ clean:
55
+ @for d in $(VERSIONS); do rm -rf $$d/.bundle $$d/vendor $$d/Gemfile.lock; done
56
+
57
+ # Scaffold a new version directory from the templates: make add VER=2.4.0
58
+ add:
59
+ @test -n "$(VER)" || { echo "usage: make add VER=x.y.z"; exit 1; }
60
+ @mkdir -p "$(VER)"
61
+ @sed 's/__VERSION__/$(VER)/g' templates/Gemfile.tmpl > "$(VER)/Gemfile"
62
+ @sed 's/__VERSION__/$(VER)/g' templates/compat_test.rb.tmpl > "$(VER)/compat_test.rb"
63
+ @echo "Created $(VER)/ -- run 'make install' then 'make test'."
@@ -0,0 +1,119 @@
1
+ # Ruby SDK backward-compatibility tests
2
+
3
+ This directory verifies that previously **published** versions of the `jamm` gem
4
+ still work against the current API — i.e. that the SDKs and the backend stay in
5
+ sync. The pre-release/latest SDK lives in `../lib` and is covered by
6
+ `../test.e2e`; this directory covers the **released** versions pulled from the
7
+ RubyGems registry.
8
+
9
+ ## Layout
10
+
11
+ ```
12
+ packages/sdk/
13
+ compatibility/
14
+ testdata/ # language-neutral backend webhook records; shared by every SDK harness
15
+ ruby/compatibility/
16
+ shared/
17
+ suite.rb # the shared, capability-gated smoke suite (single source of truth)
18
+ templates/ # source templates for each version directory
19
+ <version>/
20
+ Gemfile # pins jamm@<version> + test-unit
21
+ compat_test.rb # thin shim: requires the pinned gem, runs the shared suite
22
+ Makefile
23
+ ```
24
+
25
+ Each `<version>/` directory installs its own pinned gem into its own bundle, so
26
+ its `compat_test.rb` requires `jamm` and bundler resolves it to that directory's
27
+ version. The shim then mixes `JammCompat::Tests` into a `Test::Unit::TestCase`,
28
+ so the **same** assertions run against every version.
29
+
30
+ Lockfiles (`Gemfile.lock`) and installed bundles are git-ignored on purpose:
31
+ these install real published versions from the registry and are out of scope for
32
+ our vulnerability checks.
33
+
34
+ ## What is tested
35
+
36
+ The suite is capability-gated, because the public surface grew over time. Where a
37
+ service is missing in an older gem, its check is omitted (skipped) rather than
38
+ failed:
39
+
40
+ | Check | Touches API | Notes |
41
+ | ------------------------------ | ----------- | ------------------------------------------------- |
42
+ | `Jamm.configure` + environment | no | environment round-trips to `local` |
43
+ | `Jamm::Healthcheck.ping` | yes | skipped unless `MERCHANT_CLIENT_*` are set |
44
+ | `Jamm::Webhook.verify` | no | recomputes the HMAC signature; asserts the contract |
45
+ | `Jamm::Webhook.parse` | no | forward-compat: parses backend records carrying newer fields (see below) |
46
+
47
+ ### Forward-compatibility: `Jamm::Webhook.parse` against current-day records
48
+
49
+ `../../compatibility/testdata/*.json` are webhook payloads shaped as the backend
50
+ sends them, covering both shapes the harness cares about:
51
+
52
+ - `charge_success_api_source.json` — a flat charge carrying `api_source`
53
+ (`ChargeMessage` field 23). The backend marshals webhook content with Go's
54
+ `json.Marshal` (not `protojson`), so the enum goes out as its numeric value
55
+ (`"api_source": 3`); there is no enum-string form on the wire.
56
+ - `refund_succeeded_nested_api_source.json` — the nested refund wrapper
57
+ (`content.transaction` + `content.refund`) **with** `api_source`.
58
+ - `refund_succeeded_nested_no_api_source.json` — the same nested wrapper
59
+ **without** `api_source` (older backends, or charges created outside the charge
60
+ API), which must also parse, resolving to the model's default.
61
+
62
+ The suite asserts every parse-capable version **must** decode the core
63
+ `ChargeMessage` (`id`, `customer`) and ignore fields it predates. A version that
64
+ *has* `parse` but throws on these records **fails** the test — that failure is the
65
+ signal it is out of sync with the API, not an accepted outcome (mirrors the Node
66
+ harness).
67
+
68
+ > [!IMPORTANT]
69
+ > The `api_source` / nested-refund webhook feature was **rolled back on `main`**
70
+ > for release (PR #2316, reverting #2304/#2281/#2282/…). As of that revert the
71
+ > backend does **not** emit `api_source` in charge webhooks and the latest SDK
72
+ > source (`../lib`, currently `2.2.0`) has no forward-compat parse. These fixtures
73
+ > are retained deliberately — covering both the with- and without-`api_source`
74
+ > shapes — so this harness is the ready-made signal for which published versions
75
+ > tolerate the contract once it re-lands.
76
+
77
+ Expected behavior across the pinned versions, from each gem's CHANGELOG (the
78
+ authoritative matrix is whatever `make test` reports against the installed gems):
79
+
80
+ | version | nested refund wrapper | `api_source` resolution | expected parse result |
81
+ | -------------- | --------------------- | ----------------------- | ------------------------------------------------------ |
82
+ | 1.3.0 – 1.7.0 | no | no | refund events unhandled → `parse` raises (out of sync) |
83
+ | 2.0.0 – 2.2.0 | partial | no | strict decode throws on `api_source` (out of sync) |
84
+ | 2.3.0 | yes | yes | ✅ decodes core fields, ignores/resolves newer fields |
85
+
86
+ Verified locally: the suite passes 100% against a forward-compat-capable SDK
87
+ source and fails the three `parse` fixtures against the reverted `2.2.0` source —
88
+ i.e. the harness flags out-of-sync versions exactly as intended.
89
+
90
+ ## Running
91
+
92
+ Pinned versions are installed from the registry; the live `healthcheck` check
93
+ talks to the local backend (`api.jamm.test`), so the suite runs inside the `api`
94
+ container like the parent SDK's e2e tests.
95
+
96
+ ```sh
97
+ # from packages/sdk/ruby/compatibility
98
+ make install # install all pinned versions
99
+ make test \
100
+ MERCHANT_CLIENT_ID=... \
101
+ MERCHANT_CLIENT_SECRET=... # run the suite against every version
102
+ ```
103
+
104
+ Without credentials the offline checks still run and the live `healthcheck`
105
+ check is skipped.
106
+
107
+ ## Pinned versions
108
+
109
+ The latest 10 versions published to the RubyGems registry (the installable
110
+ source of truth): `1.3.0, 1.4.0, 1.4.1, 1.5.0, 1.6.0, 1.7.0, 2.0.0, 2.1.0,
111
+ 2.2.0, 2.3.0`. The registry and the GitHub tags can disagree — pin to what
112
+ RubyGems actually serves, since that is what merchants install.
113
+
114
+ To add a newly released version:
115
+
116
+ ```sh
117
+ make add VER=2.4.0 # scaffolds 2.4.0/ from templates/
118
+ make install && make test
119
+ ```
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'openssl'
5
+ require 'test/unit'
6
+
7
+ # Shared backward-compatibility smoke suite for the published `jamm` gem.
8
+ #
9
+ # Each compatibility/<version>/ directory pins and installs its own published gem
10
+ # version (via its sibling Gemfile), then its compat_test.rb shim builds a
11
+ # Test::Unit::TestCase that `include`s JammCompat::Tests. Pinning happens through
12
+ # bundler, so `require 'jamm'` in that process resolves to *this* directory's
13
+ # version and the same assertions run against every version.
14
+ #
15
+ # The suite is capability-gated: the public surface grew across versions, so a
16
+ # check for a service that did not exist yet is omitted (skipped) rather than
17
+ # failed. The one deliberate exception is webhook.parse -- see PARSE_FIXTURES.
18
+ module JammCompat
19
+ # Shared decoded-core assertions: every parse-capable version must decode these
20
+ # from each fixture regardless of the newer fields it carries.
21
+ EXPECTED = { id: 'trx-00000000000000000000', customer: 'cus-00000000000000000000' }.freeze
22
+
23
+ # Backend webhook records shaped exactly as the current API emits them (see
24
+ # packages/sdk/compatibility/testdata/), carrying fields newer than most published gems: api_source
25
+ # (ChargeMessage field 23) and the nested { transaction, refund } refund
26
+ # wrapper. The backend marshals webhook content with Go's json.Marshal (not
27
+ # protojson), so enums go out numeric ("api_source": 3) -- there is no
28
+ # enum-string form on the wire, so one fixture per record shape is enough.
29
+ # Each charge/refund shape is covered both with and without api_source so the
30
+ # post-revert backend (no api_source emitted) is exercised alongside the
31
+ # re-landed-feature shape.
32
+ PARSE_FIXTURES = [
33
+ { file: 'charge_success_api_source.json', event: 'CHARGE_SUCCESS + api_source' },
34
+ { file: 'charge_success_without_api_source.json', event: 'CHARGE_SUCCESS, no api_source' },
35
+ { file: 'refund_succeeded_nested_api_source.json', event: 'REFUND_SUCCEEDED, nested wrapper + api_source' },
36
+ { file: 'refund_succeeded_nested_no_api_source.json', event: 'REFUND_SUCCEEDED, nested wrapper, no api_source' }
37
+ ].freeze
38
+
39
+ # Payload for the webhook.verify contract check. The signature is NOT hardcoded:
40
+ # verify() HMAC-signs JSON.dump(data) with the configured client_secret, so the
41
+ # suite recomputes the expected signature for whatever secret it configured.
42
+ # Fixed pseudo IDs, not real merchant data.
43
+ WEBHOOK_DATA = {
44
+ customer: 'cus-000000000000000000',
45
+ created_at: '2024-11-29T02:16:12.168127Z',
46
+ activated_at: '2024-11-29T02:16:18.040142301Z',
47
+ merchant_name: 'TestMerchant1'
48
+ }.freeze
49
+
50
+ # Mirrors the e2e credential env vars. When unset, the live-API healthcheck is
51
+ # skipped so the offline checks still run (e.g. in CI without a reachable backend).
52
+ CLIENT_ID = ENV['MERCHANT_CLIENT_ID'] || 'compat-client-id'
53
+ CLIENT_SECRET = ENV['MERCHANT_CLIENT_SECRET'] || 'compat-client-secret'
54
+ HAS_API_CREDS = !(ENV['MERCHANT_CLIENT_ID'].to_s.empty? || ENV['MERCHANT_CLIENT_SECRET'].to_s.empty?)
55
+
56
+ # Fixtures live in the language-neutral packages/sdk/compatibility/testdata/
57
+ # directory so every SDK harness consumes the same backend records.
58
+ def self.testdata_path(file)
59
+ File.join(__dir__, '..', '..', '..', 'compatibility', 'testdata', file)
60
+ end
61
+
62
+ # The actual checks. A version directory's shim mixes this into a
63
+ # Test::Unit::TestCase, so test/unit auto-discovers every `test_*` method.
64
+ module Tests
65
+ def load_fixture(file)
66
+ JSON.parse(File.read(JammCompat.testdata_path(file)), symbolize_names: true)
67
+ end
68
+
69
+ def configure!
70
+ Jamm.configure(
71
+ client_id: JammCompat::CLIENT_ID,
72
+ client_secret: JammCompat::CLIENT_SECRET,
73
+ env: 'local'
74
+ )
75
+ end
76
+
77
+ # config -- expected in every published version.
78
+ def test_config_round_trips_local_environment
79
+ omit('Jamm.configure absent in this version') unless Jamm.respond_to?(:configure)
80
+ configure!
81
+ omit('Jamm.environment reader absent in this version') unless Jamm.respond_to?(:environment)
82
+ assert_equal('local', Jamm.environment)
83
+ end
84
+
85
+ # healthcheck -- hits the live local API (api.jamm.test), so it needs creds and
86
+ # a reachable backend; skipped otherwise.
87
+ def test_healthcheck_pings_api
88
+ unless defined?(Jamm::Healthcheck) && Jamm::Healthcheck.respond_to?(:ping)
89
+ omit('healthcheck service absent in this version')
90
+ end
91
+ omit('MERCHANT_CLIENT_* not set; live API check skipped') unless JammCompat::HAS_API_CREDS
92
+
93
+ configure!
94
+ res = Jamm::Healthcheck.ping
95
+ assert_not_nil(res)
96
+ assert_true(res.ok)
97
+ end
98
+
99
+ # webhook.verify -- offline signing-contract check: recompute the signature the
100
+ # SDK expects, confirm verify() accepts it, then confirm a tampered (but
101
+ # well-formed) signature is rejected.
102
+ def test_webhook_verify_accepts_valid_and_rejects_tampered
103
+ unless defined?(Jamm::Webhook) && Jamm::Webhook.respond_to?(:verify)
104
+ omit('webhook.verify absent in this version')
105
+ end
106
+
107
+ configure!
108
+ digest = OpenSSL::HMAC.hexdigest(
109
+ OpenSSL::Digest.new('sha256'),
110
+ JammCompat::CLIENT_SECRET,
111
+ JSON.dump(JammCompat::WEBHOOK_DATA)
112
+ )
113
+
114
+ assert_nothing_raised do
115
+ Jamm::Webhook.verify(data: JammCompat::WEBHOOK_DATA, signature: "sha256=#{digest}")
116
+ end
117
+
118
+ assert_raise do
119
+ Jamm::Webhook.verify(data: JammCompat::WEBHOOK_DATA, signature: "sha256=#{'0' * 64}")
120
+ end
121
+ end
122
+
123
+ # webhook.parse forward-compat -- pretends the backend is sending current-day
124
+ # records that carry api_source and the nested refund wrapper. Every
125
+ # parse-capable version MUST decode the core ChargeMessage and ignore fields it
126
+ # predates.
127
+ #
128
+ # NOTE: there is intentionally no rescue here. A version that *has* parse but
129
+ # throws on these records is out of sync with the API -- that failure is the
130
+ # signal, not an accepted outcome (mirrors the Node harness, where only the
131
+ # latest version passes). Only a version with no parse capability at all is
132
+ # omitted.
133
+ JammCompat::PARSE_FIXTURES.each do |fx|
134
+ slug = fx[:file].sub(/\.json\z/, '')
135
+ define_method("test_webhook_parse_tolerates_#{slug}") do
136
+ unless defined?(Jamm::Webhook) && Jamm::Webhook.respond_to?(:parse)
137
+ omit('webhook.parse absent in this version')
138
+ end
139
+
140
+ msg = Jamm::Webhook.parse(load_fixture(fx[:file]))
141
+ assert_equal(JammCompat::EXPECTED[:id], msg.content.id, fx[:event])
142
+ assert_equal(JammCompat::EXPECTED[:customer], msg.content.customer, fx[:event])
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+ # Generated from templates/Gemfile.tmpl -- do not edit by hand.
3
+ # Regenerate with `make add VER=__VERSION__` from packages/sdk/ruby/compatibility.
4
+ source 'https://rubygems.org'
5
+
6
+ gem 'jamm', '__VERSION__'
7
+ gem 'test-unit', '~> 3.0'
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+ # Generated from templates/compat_test.rb.tmpl -- do not edit by hand.
3
+ # Regenerate with `make add VER=__VERSION__` from packages/sdk/ruby/compatibility.
4
+ #
5
+ # Installs the published jamm@__VERSION__ (pinned in the sibling Gemfile) and runs
6
+ # the shared backward-compat suite against it. Requiring 'jamm' here -- under this
7
+ # directory's bundle -- is what makes bundler resolve it to *this* pinned version.
8
+ require 'jamm'
9
+
10
+ require_relative '../shared/suite'
11
+
12
+ class CompatTest < Test::Unit::TestCase
13
+ include JammCompat::Tests
14
+
15
+ SDK_VERSION = '__VERSION__'
16
+ end
@@ -62,8 +62,6 @@ module Api
62
62
 
63
63
  attr_accessor :refund
64
64
 
65
- attr_accessor :api_source
66
-
67
65
  class EnumAttributeValidator
68
66
  attr_reader :datatype
69
67
  attr_reader :allowable_values
@@ -107,8 +105,7 @@ module Api
107
105
  :'consumption_tax' => :'consumptionTax',
108
106
  :'error' => :'error',
109
107
  :'refund_id' => :'refundId',
110
- :'refund' => :'refund',
111
- :'api_source' => :'apiSource'
108
+ :'refund' => :'refund'
112
109
  }
113
110
  end
114
111
 
@@ -138,8 +135,7 @@ module Api
138
135
  :'consumption_tax' => :'Integer',
139
136
  :'error' => :'Apiv1Error',
140
137
  :'refund_id' => :'String',
141
- :'refund' => :'RefundInfo',
142
- :'api_source' => :'ChargeMessageApiSource'
138
+ :'refund' => :'RefundInfo'
143
139
  }
144
140
  end
145
141
 
@@ -241,12 +237,6 @@ module Api
241
237
  if attributes.key?(:'refund')
242
238
  self.refund = attributes[:'refund']
243
239
  end
244
-
245
- if attributes.key?(:'api_source')
246
- self.api_source = attributes[:'api_source']
247
- else
248
- self.api_source = 'API_SOURCE_UNSPECIFIED'
249
- end
250
240
  end
251
241
 
252
242
  # Show invalid properties with the reasons. Usually used together with valid?
@@ -287,8 +277,7 @@ module Api
287
277
  consumption_tax == o.consumption_tax &&
288
278
  error == o.error &&
289
279
  refund_id == o.refund_id &&
290
- refund == o.refund &&
291
- api_source == o.api_source
280
+ refund == o.refund
292
281
  end
293
282
 
294
283
  # @see the `==` method
@@ -300,7 +289,7 @@ module Api
300
289
  # Calculates hash code according to all attributes.
301
290
  # @return [Integer] Hash code
302
291
  def hash
303
- [id, customer, status, description, merchant_name, initial_amount, discount, final_amount, amount_refunded, currency, processed_at, jamm_fee, created_at, updated_at, original_transaction_jamm_fee, consumption_tax, error, refund_id, refund, api_source].hash
292
+ [id, customer, status, description, merchant_name, initial_amount, discount, final_amount, amount_refunded, currency, processed_at, jamm_fee, created_at, updated_at, original_transaction_jamm_fee, consumption_tax, error, refund_id, refund].hash
304
293
  end
305
294
 
306
295
  # Builds the object from hash
@@ -38,6 +38,7 @@ module Api
38
38
  PAYMENT_CUSTOMER_NOT_FOUND = "ERROR_TYPE_PAYMENT_CUSTOMER_NOT_FOUND".freeze
39
39
  PAYMENT_CUSTOMER_INACTIVE = "ERROR_TYPE_PAYMENT_CUSTOMER_INACTIVE".freeze
40
40
  PAYMENT_SERVICE_DISABLED = "ERROR_TYPE_PAYMENT_SERVICE_DISABLED".freeze
41
+ PAYMENT_BANK_UNAVAILABLE = "ERROR_TYPE_PAYMENT_BANK_UNAVAILABLE".freeze
41
42
  CSV_VALIDATION_FAILED = "ERROR_TYPE_CSV_VALIDATION_FAILED".freeze
42
43
  CSV_TOTP_REQUIRED = "ERROR_TYPE_CSV_TOTP_REQUIRED".freeze
43
44
  CSV_TOTP_INVALID = "ERROR_TYPE_CSV_TOTP_INVALID".freeze
@@ -55,7 +56,7 @@ module Api
55
56
  TOTP_DISABLE_FAILED = "ERROR_TYPE_TOTP_DISABLE_FAILED".freeze
56
57
 
57
58
  def self.all_vars
58
- @all_vars ||= [UNSPECIFIED, AUTH_FAILED, AUTH_REJECTED, ACCOUNT_CREATION_FAILED, ACCOUNT_MODIFICATION_FAILED, ACCOUNT_DELETION_FAILED, ACCOUNT_BANK_REGISTRATION_FAILED, KYC_REJECTED, NOTIFICATION_WEBHOOK_FAILED, NOTIFICATION_EMAIL_FAILED, NOTIFICATION_SMS_FAILED, PAYMENT_GATEWAY_UNAVAILABLE, PAYMENT_GATEWAY_FAILED, PAYMENT_VALIDATION_FAILED, PAYMENT_CHARGE_FAILED, PAYMENT_CHARGE_REJECTED, PAYMENT_CHARGE_OVER_LIMIT, PAYMENT_CHARGE_SUBSCRIPTION_EXPIRED, PAYMENT_LINK_EXPIRED, PAYMENT_CHARGE_INSUFFICIENT_FUNDS, PAYMENT_CUSTOMER_NOT_FOUND, PAYMENT_CUSTOMER_INACTIVE, PAYMENT_SERVICE_DISABLED, CSV_VALIDATION_FAILED, CSV_TOTP_REQUIRED, CSV_TOTP_INVALID, CSV_TOTP_EXPIRED, CSV_TOTP_LOCKED, CSV_BATCH_TOO_LARGE, CSV_CUSTOMER_NOT_FOUND, CSV_PROCESSING_FAILED, CSV_CHALLENGE_NOT_FOUND, CSV_DUPLICATE_USER, TOTP_SETUP_FAILED, TOTP_ALREADY_ENABLED, TOTP_NOT_ENABLED, TOTP_SETUP_INVALID, TOTP_DISABLE_FAILED].freeze
59
+ @all_vars ||= [UNSPECIFIED, AUTH_FAILED, AUTH_REJECTED, ACCOUNT_CREATION_FAILED, ACCOUNT_MODIFICATION_FAILED, ACCOUNT_DELETION_FAILED, ACCOUNT_BANK_REGISTRATION_FAILED, KYC_REJECTED, NOTIFICATION_WEBHOOK_FAILED, NOTIFICATION_EMAIL_FAILED, NOTIFICATION_SMS_FAILED, PAYMENT_GATEWAY_UNAVAILABLE, PAYMENT_GATEWAY_FAILED, PAYMENT_VALIDATION_FAILED, PAYMENT_CHARGE_FAILED, PAYMENT_CHARGE_REJECTED, PAYMENT_CHARGE_OVER_LIMIT, PAYMENT_CHARGE_SUBSCRIPTION_EXPIRED, PAYMENT_LINK_EXPIRED, PAYMENT_CHARGE_INSUFFICIENT_FUNDS, PAYMENT_CUSTOMER_NOT_FOUND, PAYMENT_CUSTOMER_INACTIVE, PAYMENT_SERVICE_DISABLED, PAYMENT_BANK_UNAVAILABLE, CSV_VALIDATION_FAILED, CSV_TOTP_REQUIRED, CSV_TOTP_INVALID, CSV_TOTP_EXPIRED, CSV_TOTP_LOCKED, CSV_BATCH_TOO_LARGE, CSV_CUSTOMER_NOT_FOUND, CSV_PROCESSING_FAILED, CSV_CHALLENGE_NOT_FOUND, CSV_DUPLICATE_USER, TOTP_SETUP_FAILED, TOTP_ALREADY_ENABLED, TOTP_NOT_ENABLED, TOTP_SETUP_INVALID, TOTP_DISABLE_FAILED].freeze
59
60
  end
60
61
 
61
62
  # Builds the enum from string
@@ -17,7 +17,7 @@ module Api
17
17
  # RefundInfo contains refund-specific details for refund and refund_failed webhook events.
18
18
  class RefundInfo
19
19
  # External refund identifier (rfd-*).
20
- attr_accessor :id
20
+ attr_accessor :refund_id
21
21
 
22
22
  # Amount refunded for this event.
23
23
  attr_accessor :amount_refunded
@@ -39,7 +39,7 @@ module Api
39
39
  # Attribute mapping from ruby-style variable name to JSON key.
40
40
  def self.attribute_map
41
41
  {
42
- :'id' => :'id',
42
+ :'refund_id' => :'refundId',
43
43
  :'amount_refunded' => :'amountRefunded',
44
44
  :'jamm_fee' => :'jammFee',
45
45
  :'consumption_tax' => :'consumptionTax',
@@ -57,7 +57,7 @@ module Api
57
57
  # Attribute type mapping.
58
58
  def self.openapi_types
59
59
  {
60
- :'id' => :'String',
60
+ :'refund_id' => :'String',
61
61
  :'amount_refunded' => :'Integer',
62
62
  :'jamm_fee' => :'Integer',
63
63
  :'consumption_tax' => :'Integer',
@@ -88,8 +88,8 @@ module Api
88
88
  h[k.to_sym] = v
89
89
  }
90
90
 
91
- if attributes.key?(:'id')
92
- self.id = attributes[:'id']
91
+ if attributes.key?(:'refund_id')
92
+ self.refund_id = attributes[:'refund_id']
93
93
  end
94
94
 
95
95
  if attributes.key?(:'amount_refunded')
@@ -137,7 +137,7 @@ module Api
137
137
  def ==(o)
138
138
  return true if self.equal?(o)
139
139
  self.class == o.class &&
140
- id == o.id &&
140
+ refund_id == o.refund_id &&
141
141
  amount_refunded == o.amount_refunded &&
142
142
  jamm_fee == o.jamm_fee &&
143
143
  consumption_tax == o.consumption_tax &&
@@ -155,7 +155,7 @@ module Api
155
155
  # Calculates hash code according to all attributes.
156
156
  # @return [Integer] Hash code
157
157
  def hash
158
- [id, amount_refunded, jamm_fee, consumption_tax, original_transaction_fee_waived, error, processed_at].hash
158
+ [refund_id, amount_refunded, jamm_fee, consumption_tax, original_transaction_fee_waived, error, processed_at].hash
159
159
  end
160
160
 
161
161
  # Builds the object from hash
data/lib/jamm/api.rb CHANGED
@@ -19,7 +19,6 @@ require 'jamm/api/configuration'
19
19
  # Models
20
20
  require 'jamm/api/models/apiv1_error'
21
21
  require 'jamm/api/models/apiv1_status'
22
- require 'jamm/api/models/charge_message_api_source'
23
22
  require 'jamm/api/models/customer_service_update_customer_body'
24
23
  require 'jamm/api/models/googlerpc_status'
25
24
  require 'jamm/api/models/protobuf_any'
data/lib/jamm/client.rb CHANGED
@@ -9,6 +9,7 @@ module Jamm
9
9
  base.config.host = Jamm.openapi.config.host
10
10
  base.config.scheme = Jamm.openapi.config.scheme
11
11
  base.default_headers['Authorization'] = "Bearer #{Jamm::OAuth.token}"
12
+ base.default_headers['X-SDK-Version'] = "ruby:#{Jamm::VERSION}"
12
13
 
13
14
  # Platform feature, optionally set merchant id to call Jamm API
14
15
  # on behalf of the merchant.
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.3.0'
4
+ VERSION = '2.4.0'
5
5
  end
data/lib/jamm/webhook.rb CHANGED
@@ -11,157 +11,39 @@ 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
- # 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)
14
+ out = Jamm::OpenAPI::MerchantWebhookMessage.new(json)
21
15
 
22
16
  case json[:event_type]
23
- when Jamm::OpenAPI::EventType::CHARGE_CREATED,
24
- Jamm::OpenAPI::EventType::CHARGE_UPDATED,
25
- Jamm::OpenAPI::EventType::REFUND_SUCCEEDED,
26
- Jamm::OpenAPI::EventType::REFUND_FAILED,
27
- Jamm::OpenAPI::EventType::CHARGE_SUCCESS,
28
- Jamm::OpenAPI::EventType::CHARGE_FAIL
29
- out.content = build(Jamm::OpenAPI::ChargeMessage, flatten_charge_content(json[:content]))
17
+ when Jamm::OpenAPI::EventType::CHARGE_CREATED
18
+ out.content = Jamm::OpenAPI::ChargeMessage.new(json[:content])
30
19
  return out
31
20
 
32
- when Jamm::OpenAPI::EventType::CONTRACT_ACTIVATED
33
- out.content = build(Jamm::OpenAPI::ContractMessage, json[:content])
21
+ when Jamm::OpenAPI::EventType::CHARGE_UPDATED
22
+ out.content = Jamm::OpenAPI::ChargeMessage.new(json[:content])
34
23
  return out
35
24
 
36
- when Jamm::OpenAPI::EventType::USER_ACCOUNT_DELETED
37
- out.content = build(Jamm::OpenAPI::UserAccountMessage, json[:content])
25
+ when Jamm::OpenAPI::EventType::REFUND_SUCCEEDED
26
+ out.content = Jamm::OpenAPI::ChargeMessage.new(json[:content])
38
27
  return out
39
- end
40
-
41
- raise 'Unknown event type'
42
- end
43
28
 
44
- # Build a generated model from a webhook payload while normalizing the
45
- # quirks of the webhook wire format. Applied to every model, so charges,
46
- # contracts, user-account and refund messages all benefit:
47
- #
48
- # 1. Forward-compat: the Jamm backend can add new fields to webhook
49
- # payloads at any time. The generated model `initialize` raises
50
- # ArgumentError on any key outside `attribute_map`, so unknown keys are
51
- # dropped first. Known keys are snake_case, matching `attribute_map`.
52
- # 2. Numeric enums: the backend serializes webhook payloads with Go's
53
- # `json.Marshal` (not protojson), so every enum field (status,
54
- # api_source, ...) arrives as its integer value, while the generated
55
- # enums are string-based. Each integer is mapped back to the enum string
56
- # so it matches the values returned by the REST API.
57
- # 3. Nested models: the generated `initialize` assigns nested objects
58
- # verbatim (the `_deserialize` coercion only runs from `build_from_hash`,
59
- # which expects camelCase keys the webhook does not use). So a nested
60
- # field like `refund.error` would stay a raw Hash and `error.code` would
61
- # raise NoMethodError. We coerce nested model fields (and arrays of them)
62
- # recursively so the typed accessors work.
63
- def self.build(klass, attributes)
64
- return nil if attributes.nil?
65
-
66
- known = klass.attribute_map
67
- types = klass.openapi_types
68
- filtered = attributes.each_with_object({}) do |(key, value), acc|
69
- sym = key.to_sym
70
- next unless known.key?(sym)
71
-
72
- acc[sym] = coerce(types[sym], value)
73
- end
29
+ when Jamm::OpenAPI::EventType::REFUND_FAILED
30
+ out.content = Jamm::OpenAPI::ChargeMessage.new(json[:content])
31
+ return out
74
32
 
75
- klass.new(filtered)
76
- end
33
+ when Jamm::OpenAPI::EventType::CHARGE_SUCCESS
34
+ out.content = Jamm::OpenAPI::ChargeMessage.new(json[:content])
35
+ return out
77
36
 
78
- # Refund webhooks (REFUND_SUCCEEDED / REFUND_FAILED) deliver `content` as a
79
- # nested { transaction, refund } wrapper instead of a flat ChargeMessage.
80
- # Flatten it back into a ChargeMessage so callers always receive the same shape.
81
- def self.flatten_charge_content(content)
82
- return content unless content.is_a?(Hash) && content.key?(:transaction)
83
-
84
- refund = content[:refund]
85
- # Keep `refund` as the raw Hash: `build` coerces it into a typed RefundInfo
86
- # (and recursively types its nested `error`). Also surface the refund's
87
- # `rfd-` id on the flat `refund_id` attribute the model documents.
88
- charge = content[:transaction].merge(refund: refund)
89
- charge[:refund_id] = refund[:id] if refund.is_a?(Hash) && !refund[:id].nil?
90
- charge
91
- end
37
+ when Jamm::OpenAPI::EventType::CHARGE_FAIL
38
+ out.content = Jamm::OpenAPI::ChargeMessage.new(json[:content])
39
+ return out
92
40
 
93
- # Coerce a raw webhook value into the shape the generated model expects,
94
- # based on the field's openapi type: numeric enums become their string
95
- # constant, nested models become typed instances, and `Array<T>` elements are
96
- # coerced by `T`. Anything else passes through untouched.
97
- def self.coerce(type, value)
98
- return value if value.nil?
99
-
100
- inner = array_inner_type(type)
101
- return coerce_array(inner, value) unless inner.nil?
102
-
103
- klass = openapi_const(type)
104
- return value if klass.nil?
105
-
106
- if klass.respond_to?(:all_vars)
107
- resolve_enum(klass, value)
108
- elsif klass.respond_to?(:openapi_types) && value.is_a?(Hash)
109
- build(klass, value)
110
- else
111
- value
41
+ when Jamm::OpenAPI::EventType::CONTRACT_ACTIVATED
42
+ out.content = Jamm::OpenAPI::ContractMessage.new(json[:content])
43
+ return out
112
44
  end
113
- end
114
45
 
115
- # Coerce each element of an `Array<T>` field by its inner type `T`.
116
- def self.coerce_array(inner_type, value)
117
- return value unless value.is_a?(Array)
118
-
119
- value.map { |element| coerce(inner_type, element) }
120
- end
121
-
122
- # Extract `T` from an `Array<T>` openapi type, or nil when not an array type.
123
- def self.array_inner_type(type)
124
- match = type.to_s.match(/\AArray<(.+)>\z/)
125
- match && match[1]
126
- end
127
-
128
- # Map a numeric enum wire value onto its string enum constant. A value that
129
- # is already a string (REST-style) passes through untouched.
130
- def self.resolve_enum(enum, value)
131
- return value unless value.is_a?(Integer)
132
-
133
- vars = enum.all_vars
134
- # Guard the bounds explicitly: Ruby maps negative indices from the end of
135
- # the array, so any unexpected wire value must fall back to the *_UNSPECIFIED
136
- # member (index 0) rather than silently selecting the wrong constant.
137
- value.between?(0, vars.length - 1) ? vars[value] : vars[0]
138
- end
139
-
140
- # Resolve an openapi_types entry (e.g. :ChargeMessageApiSource, :RefundInfo)
141
- # to its generated class, or nil when the type is a primitive (String,
142
- # Integer, ...) or otherwise unresolvable.
143
- def self.openapi_const(type)
144
- return nil if type.nil?
145
-
146
- name = type.to_s
147
- return nil unless Jamm::OpenAPI.const_defined?(name)
148
-
149
- Jamm::OpenAPI.const_get(name)
150
- rescue NameError
151
- nil
152
- end
153
-
154
- # Recursively convert Hash keys to symbols so parsing is robust regardless of
155
- # how the caller decoded the webhook JSON.
156
- def self.deep_symbolize_keys(value)
157
- case value
158
- when Hash
159
- value.each_with_object({}) { |(k, v), acc| acc[k.to_sym] = deep_symbolize_keys(v) }
160
- when Array
161
- value.map { |v| deep_symbolize_keys(v) }
162
- else
163
- value
164
- end
46
+ raise 'Unknown event type'
165
47
  end
166
48
 
167
49
  # 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.3.0
4
+ version: 2.4.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-06-24 00:00:00.000000000 Z
11
+ date: 2026-07-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64
@@ -72,6 +72,32 @@ files:
72
72
  - LICENSE
73
73
  - README.md
74
74
  - Rakefile
75
+ - compatibility/.gitignore
76
+ - compatibility/1.3.0/Gemfile
77
+ - compatibility/1.3.0/compat_test.rb
78
+ - compatibility/1.4.0/Gemfile
79
+ - compatibility/1.4.0/compat_test.rb
80
+ - compatibility/1.4.1/Gemfile
81
+ - compatibility/1.4.1/compat_test.rb
82
+ - compatibility/1.5.0/Gemfile
83
+ - compatibility/1.5.0/compat_test.rb
84
+ - compatibility/1.6.0/Gemfile
85
+ - compatibility/1.6.0/compat_test.rb
86
+ - compatibility/1.7.0/Gemfile
87
+ - compatibility/1.7.0/compat_test.rb
88
+ - compatibility/2.0.0/Gemfile
89
+ - compatibility/2.0.0/compat_test.rb
90
+ - compatibility/2.1.0/Gemfile
91
+ - compatibility/2.1.0/compat_test.rb
92
+ - compatibility/2.2.0/Gemfile
93
+ - compatibility/2.2.0/compat_test.rb
94
+ - compatibility/2.3.0/Gemfile
95
+ - compatibility/2.3.0/compat_test.rb
96
+ - compatibility/Makefile
97
+ - compatibility/README.md
98
+ - compatibility/shared/suite.rb
99
+ - compatibility/templates/Gemfile.tmpl
100
+ - compatibility/templates/compat_test.rb.tmpl
75
101
  - jamm.gemspec
76
102
  - lib/jamm.rb
77
103
  - lib/jamm/api.rb
@@ -84,7 +110,6 @@ files:
84
110
  - lib/jamm/api/configuration.rb
85
111
  - lib/jamm/api/models/apiv1_error.rb
86
112
  - lib/jamm/api/models/apiv1_status.rb
87
- - lib/jamm/api/models/charge_message_api_source.rb
88
113
  - lib/jamm/api/models/customer_service_update_customer_body.rb
89
114
  - lib/jamm/api/models/googlerpc_status.rb
90
115
  - lib/jamm/api/models/protobuf_any.rb
@@ -1,42 +0,0 @@
1
- =begin
2
- #Jamm API
3
-
4
- #No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
5
-
6
- The version of the OpenAPI document: 1.0
7
-
8
- Generated by: https://openapi-generator.tech
9
- Generator version: 7.9.0
10
-
11
- =end
12
-
13
- require 'date'
14
- require 'time'
15
-
16
- module Api
17
- class ChargeMessageApiSource
18
- UNSPECIFIED = "API_SOURCE_UNSPECIFIED".freeze
19
- OFF_SESSION_SYNC = "API_SOURCE_OFF_SESSION_SYNC".freeze
20
- OFF_SESSION_ASYNC = "API_SOURCE_OFF_SESSION_ASYNC".freeze
21
- ON_SESSION = "API_SOURCE_ON_SESSION".freeze
22
-
23
- def self.all_vars
24
- @all_vars ||= [UNSPECIFIED, OFF_SESSION_SYNC, OFF_SESSION_ASYNC, ON_SESSION].freeze
25
- end
26
-
27
- # Builds the enum from string
28
- # @param [String] The enum value in the form of the string
29
- # @return [String] The enum value
30
- def self.build_from_hash(value)
31
- new.build_from_hash(value)
32
- end
33
-
34
- # Builds the enum from string
35
- # @param [String] The enum value in the form of the string
36
- # @return [String] The enum value
37
- def build_from_hash(value)
38
- return value if ChargeMessageApiSource.all_vars.include?(value)
39
- raise "Invalid ENUM value #{value} for class #ChargeMessageApiSource"
40
- end
41
- end
42
- end