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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +2 -12
- data/Gemfile.lock +4 -2
- data/compatibility/.gitignore +11 -0
- data/compatibility/1.3.0/Gemfile +7 -0
- data/compatibility/1.3.0/compat_test.rb +16 -0
- data/compatibility/1.4.0/Gemfile +7 -0
- data/compatibility/1.4.0/compat_test.rb +16 -0
- data/compatibility/1.4.1/Gemfile +7 -0
- data/compatibility/1.4.1/compat_test.rb +16 -0
- data/compatibility/1.5.0/Gemfile +7 -0
- data/compatibility/1.5.0/compat_test.rb +16 -0
- data/compatibility/1.6.0/Gemfile +7 -0
- data/compatibility/1.6.0/compat_test.rb +16 -0
- data/compatibility/1.7.0/Gemfile +7 -0
- data/compatibility/1.7.0/compat_test.rb +16 -0
- data/compatibility/2.0.0/Gemfile +7 -0
- data/compatibility/2.0.0/compat_test.rb +16 -0
- data/compatibility/2.1.0/Gemfile +7 -0
- data/compatibility/2.1.0/compat_test.rb +16 -0
- data/compatibility/2.2.0/Gemfile +7 -0
- data/compatibility/2.2.0/compat_test.rb +16 -0
- data/compatibility/2.3.0/Gemfile +7 -0
- data/compatibility/2.3.0/compat_test.rb +16 -0
- data/compatibility/Makefile +63 -0
- data/compatibility/README.md +119 -0
- data/compatibility/shared/suite.rb +146 -0
- data/compatibility/templates/Gemfile.tmpl +7 -0
- data/compatibility/templates/compat_test.rb.tmpl +16 -0
- data/lib/jamm/api/models/v1_charge_message.rb +4 -15
- data/lib/jamm/api/models/v1_error_type.rb +2 -1
- data/lib/jamm/api/models/v1_refund_info.rb +7 -7
- data/lib/jamm/api.rb +0 -1
- data/lib/jamm/client.rb +1 -0
- data/lib/jamm/version.rb +1 -1
- data/lib/jamm/webhook.rb +20 -138
- metadata +28 -3
- 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 17f04171e0380f881d022d968eafaecfab7de2ca6e0a91ed56b217a4434afcab
|
|
4
|
+
data.tar.gz: 2f98a4766f79146afc5a839a26b2f69674ac87330065f663d9c90441a52b5f74
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
8
|
+
## [2.4.0] - 2026-07-01
|
|
9
9
|
|
|
10
10
|
### Added
|
|
11
11
|
|
|
12
|
-
-
|
|
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.
|
|
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,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,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,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,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,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,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,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,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,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,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
|
|
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 :
|
|
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
|
-
:'
|
|
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
|
-
:'
|
|
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?(:'
|
|
92
|
-
self.
|
|
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
|
-
|
|
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
|
-
[
|
|
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
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
|
-
|
|
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
|
-
|
|
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::
|
|
33
|
-
out.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::
|
|
37
|
-
out.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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
76
|
-
|
|
33
|
+
when Jamm::OpenAPI::EventType::CHARGE_SUCCESS
|
|
34
|
+
out.content = Jamm::OpenAPI::ChargeMessage.new(json[:content])
|
|
35
|
+
return out
|
|
77
36
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
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.
|
|
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-
|
|
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
|