chargezen 0.1.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 +7 -0
- data/README.md +32 -0
- data/lib/chargezen/version.rb +5 -0
- data/lib/chargezen.rb +114 -0
- metadata +46 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 9042991df178156dda472065dcd4f384ea83f15b2bdd0ef7b0de7b9fa9db9821
|
|
4
|
+
data.tar.gz: 61dea04fd191f9162095747695ae4890407176f112b2034a37b2914bc5e76590
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 5a681d05535c3a540ee64f889f704131d2613ae4e5a3070c69d380faa36d7672777e6934ba6caebaf0c6d3a04be67602024f0efef2d99a97139684be9df9ef00
|
|
7
|
+
data.tar.gz: 8615a7288dd9fa1f909abd0b836d3e9a8494c8357fb89fd276bda6917c11900ca5f25136a3fa023d8d5a6f38ee383a25a2530e2aee15a8cc9b613308562bd9d5
|
data/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Chargezen Ruby SDK
|
|
2
|
+
|
|
3
|
+
The Chargezen API (`https://api.chargezen.com/v1`) from your backend: customers, subscriptions, products,
|
|
4
|
+
plans and email. Plain Ruby, no dependencies. Keys are secret; never use them in a browser.
|
|
5
|
+
|
|
6
|
+
```ruby
|
|
7
|
+
require "chargezen"
|
|
8
|
+
require "securerandom"
|
|
9
|
+
|
|
10
|
+
chargezen = Chargezen::Client.new(key_id: ENV.fetch("CHARGEZEN_KEY_ID"), secret: ENV.fetch("CHARGEZEN_SECRET"))
|
|
11
|
+
|
|
12
|
+
customers = chargezen.get("/customers", query: { query: "ada@example.com" })
|
|
13
|
+
customer_id = customers["customers"].first.dig("customer_ref", "id")
|
|
14
|
+
|
|
15
|
+
# Every change takes an Idempotency-Key. Keep it: sending the same key again retries safely.
|
|
16
|
+
chargezen.post("/customers/#{customer_id}/subscriptions/123/pause", { resume_on: "2027-01-12" },
|
|
17
|
+
idempotency_key: SecureRandom.uuid)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Answers are the parsed JSON bodies. A refusal raises `Chargezen::Error` with `status`, `code`, `message`
|
|
21
|
+
and `failure`; `failure == "temporary"` means a retry with the same Idempotency-Key can succeed.
|
|
22
|
+
|
|
23
|
+
`order_now` repeats Order now while the charge is still processing, and `wait_for_subscription_creation`
|
|
24
|
+
follows a new subscription until it is ready.
|
|
25
|
+
|
|
26
|
+
Every path, parameter and body is in the API contract, `Chargezen/public_api/openapi.yaml`.
|
|
27
|
+
|
|
28
|
+
## Development
|
|
29
|
+
|
|
30
|
+
The journey test runs this SDK against a real Chargezen API on Factory:
|
|
31
|
+
`../scripts/cz-test playwright --config playwright.journey.config.js`. The gem build:
|
|
32
|
+
`../scripts/cz-test playwright --config playwright.build-check.config.js`.
|
data/lib/chargezen.rb
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "uri"
|
|
6
|
+
require_relative "chargezen/version"
|
|
7
|
+
|
|
8
|
+
# The Chargezen API (https://api.chargezen.com/v1) from a merchant's backend. Paths, parameters and bodies
|
|
9
|
+
# are the ones in the API contract, Chargezen/public_api/openapi.yaml.
|
|
10
|
+
module Chargezen
|
|
11
|
+
# A refusal from the API. `failure` is "temporary" when a retry (with the same Idempotency-Key) can succeed.
|
|
12
|
+
class Error < StandardError
|
|
13
|
+
attr_reader :status, :code, :failure
|
|
14
|
+
|
|
15
|
+
def initialize(status:, message:, code: nil, failure: nil)
|
|
16
|
+
super(message)
|
|
17
|
+
@status = status
|
|
18
|
+
@code = code
|
|
19
|
+
@failure = failure
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# One API answer: the HTTP status and the parsed JSON body.
|
|
24
|
+
Response = Struct.new(:status, :body)
|
|
25
|
+
|
|
26
|
+
class Client
|
|
27
|
+
def initialize(key_id:, secret:, base_url: "https://api.chargezen.com/v1")
|
|
28
|
+
@key_id = key_id
|
|
29
|
+
@secret = secret
|
|
30
|
+
@base_url = base_url.chomp("/")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# chargezen.get("/customers", query: { query: "ada@example.com" }) returns the parsed JSON body.
|
|
34
|
+
def get(path, query: {})
|
|
35
|
+
request(Net::HTTP::Get, path, query: query).body
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Every change takes an Idempotency-Key: make one per change (SecureRandom.uuid) and send the same key
|
|
39
|
+
# again to retry it safely. Returns the parsed JSON body.
|
|
40
|
+
def post(path, body = {}, idempotency_key:)
|
|
41
|
+
request(Net::HTTP::Post, path, body: body, headers: { "Idempotency-Key" => idempotency_key }).body
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Places the subscription's next coming order now. While the charge is still settling the API answers
|
|
45
|
+
# 202 "processing"; this repeats the request with the same Idempotency-Key, which never charges twice,
|
|
46
|
+
# until the order is placed or needs the customer's payment approval, or `timeout` seconds pass (then
|
|
47
|
+
# it returns the last "processing" answer: call again with the same key to keep waiting).
|
|
48
|
+
def order_now(customer_id:, subscription_id:, billing_occurrence:, idempotency_key:, interval:, timeout:)
|
|
49
|
+
path = "/customers/#{customer_id}/subscriptions/#{subscription_id}/order_now"
|
|
50
|
+
poll(interval: interval, timeout: timeout) do
|
|
51
|
+
response = request(Net::HTTP::Post, path, body: { billing_occurrence: billing_occurrence },
|
|
52
|
+
headers: { "Idempotency-Key" => idempotency_key })
|
|
53
|
+
[response.body, response.status == 202]
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Follows a subscription creation (the id POST /customers/{customer_id}/subscriptions returned) until it
|
|
58
|
+
# is "ready", or `timeout` seconds pass (then it returns the last "pending" answer).
|
|
59
|
+
def wait_for_subscription_creation(creation_id, interval:, timeout:)
|
|
60
|
+
poll(interval: interval, timeout: timeout) do
|
|
61
|
+
body = get("/subscription_creations/#{creation_id}")
|
|
62
|
+
[body, body["status"] == "pending"]
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
# Repeats the block, which returns [answer, still_waiting], until it stops waiting or time runs out.
|
|
69
|
+
def poll(interval:, timeout:)
|
|
70
|
+
deadline = monotonic_now + timeout
|
|
71
|
+
loop do
|
|
72
|
+
answer, still_waiting = yield
|
|
73
|
+
return answer unless still_waiting && monotonic_now + interval <= deadline
|
|
74
|
+
|
|
75
|
+
sleep interval
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def request(method, path, query: {}, body: nil, headers: {})
|
|
80
|
+
uri = URI("#{@base_url}#{path}")
|
|
81
|
+
uri.query = encode_query(query) unless query.empty?
|
|
82
|
+
http_request = method.new(uri, headers.merge("X-Chargezen-Key-Id" => @key_id, "Authorization" => "Bearer #{@secret}",
|
|
83
|
+
"Accept" => "application/json"))
|
|
84
|
+
if body
|
|
85
|
+
http_request["Content-Type"] = "application/json"
|
|
86
|
+
http_request.body = JSON.generate(body)
|
|
87
|
+
end
|
|
88
|
+
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(http_request) }
|
|
89
|
+
answer(response)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def answer(response)
|
|
93
|
+
status = response.code.to_i
|
|
94
|
+
body = response.body.to_s.empty? ? {} : JSON.parse(response.body)
|
|
95
|
+
return Response.new(status, body) if status < 400
|
|
96
|
+
|
|
97
|
+
# The email API refuses with { "error": "message" }; every other path with { "error": { code, message, failure } }.
|
|
98
|
+
error = body["error"].is_a?(Hash) ? body["error"] : { "message" => body["error"].to_s }
|
|
99
|
+
raise Error.new(status: status, code: error["code"], message: error["message"], failure: error["failure"])
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Nested hashes become key[child]=value, the way the API reads product_selection and plan_ref.
|
|
103
|
+
def encode_query(params, prefix = nil)
|
|
104
|
+
params.flat_map do |key, value|
|
|
105
|
+
name = prefix ? "#{prefix}[#{key}]" : key.to_s
|
|
106
|
+
value.is_a?(Hash) ? encode_query(value, name) : URI.encode_www_form([[name, value]])
|
|
107
|
+
end.join("&")
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def monotonic_now
|
|
111
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: chargezen
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Chargezen
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-11-09 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description:
|
|
14
|
+
email:
|
|
15
|
+
executables: []
|
|
16
|
+
extensions: []
|
|
17
|
+
extra_rdoc_files: []
|
|
18
|
+
files:
|
|
19
|
+
- README.md
|
|
20
|
+
- lib/chargezen.rb
|
|
21
|
+
- lib/chargezen/version.rb
|
|
22
|
+
homepage: https://github.com/Chargezen/chargezen-ruby
|
|
23
|
+
licenses:
|
|
24
|
+
- MIT
|
|
25
|
+
metadata:
|
|
26
|
+
source_code_uri: https://github.com/Chargezen/chargezen-ruby
|
|
27
|
+
post_install_message:
|
|
28
|
+
rdoc_options: []
|
|
29
|
+
require_paths:
|
|
30
|
+
- lib
|
|
31
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
32
|
+
requirements:
|
|
33
|
+
- - ">="
|
|
34
|
+
- !ruby/object:Gem::Version
|
|
35
|
+
version: '3.0'
|
|
36
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0'
|
|
41
|
+
requirements: []
|
|
42
|
+
rubygems_version: 3.4.10
|
|
43
|
+
signing_key:
|
|
44
|
+
specification_version: 4
|
|
45
|
+
summary: Official Ruby SDK for the Chargezen API
|
|
46
|
+
test_files: []
|