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 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`.
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Chargezen
4
+ VERSION = "0.1.0"
5
+ end
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: []