creem 0.0.1

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.
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+ require "json"
6
+
7
+ module Creem
8
+ class Client
9
+ attr_reader :config
10
+
11
+ def initialize(api_key: nil, test_mode: nil, timeout: nil, open_timeout: nil)
12
+ @config = Configuration.new
13
+ @config.api_key = api_key || Creem.configuration.api_key
14
+ @config.test_mode = test_mode.nil? ? Creem.configuration.test_mode : test_mode
15
+ @config.timeout = timeout || Creem.configuration.timeout
16
+ @config.open_timeout = open_timeout || Creem.configuration.open_timeout
17
+
18
+ raise ConfigurationError, "API key is required" if @config.api_key.nil? || @config.api_key.empty?
19
+ end
20
+
21
+ def products
22
+ @products ||= Resources::Products.new(self)
23
+ end
24
+
25
+ def checkouts
26
+ @checkouts ||= Resources::Checkouts.new(self)
27
+ end
28
+
29
+ def subscriptions
30
+ @subscriptions ||= Resources::Subscriptions.new(self)
31
+ end
32
+
33
+ def customers
34
+ @customers ||= Resources::Customers.new(self)
35
+ end
36
+
37
+ def transactions
38
+ @transactions ||= Resources::Transactions.new(self)
39
+ end
40
+
41
+ def licenses
42
+ @licenses ||= Resources::Licenses.new(self)
43
+ end
44
+
45
+ def discounts
46
+ @discounts ||= Resources::Discounts.new(self)
47
+ end
48
+
49
+ def get(path, params = {})
50
+ request(:get, path, params: params)
51
+ end
52
+
53
+ def post(path, body = {})
54
+ request(:post, path, body: body)
55
+ end
56
+
57
+ private
58
+
59
+ def request(method, path, params: {}, body: nil)
60
+ uri = build_uri(path, params)
61
+ http = build_http(uri)
62
+ req = build_request(method, uri, body)
63
+ response = http.request(req)
64
+ handle_response(response)
65
+ end
66
+
67
+ def build_uri(path, params)
68
+ uri = URI.parse("#{@config.base_url}#{path}")
69
+ if params && !params.empty?
70
+ query = params.reject { |_k, v| v.nil? }.map { |k, v| "#{k}=#{URI.encode_www_form_component(v)}" }.join("&")
71
+ uri.query = query unless query.empty?
72
+ end
73
+ uri
74
+ end
75
+
76
+ def build_http(uri)
77
+ http = Net::HTTP.new(uri.host, uri.port)
78
+ http.use_ssl = true
79
+ http.read_timeout = @config.timeout
80
+ http.open_timeout = @config.open_timeout
81
+ http
82
+ end
83
+
84
+ def build_request(method, uri, body)
85
+ req = case method
86
+ when :get then Net::HTTP::Get.new(uri)
87
+ when :post then Net::HTTP::Post.new(uri)
88
+ else raise ArgumentError, "Unsupported HTTP method: #{method}"
89
+ end
90
+ req["x-api-key"] = @config.api_key
91
+ req["Content-Type"] = "application/json"
92
+ req["Accept"] = "application/json"
93
+ req.body = JSON.generate(body) if body && !body.empty?
94
+ req
95
+ end
96
+
97
+ def handle_response(response)
98
+ body = response.body && !response.body.empty? ? JSON.parse(response.body) : nil
99
+ status = response.code.to_i
100
+
101
+ case status
102
+ when 200..299
103
+ body
104
+ when 400
105
+ raise BadRequestError.new("Bad Request", status: status, body: body)
106
+ when 401
107
+ raise AuthenticationError.new("Unauthorized - Invalid or missing API key", status: status, body: body)
108
+ when 404
109
+ raise NotFoundError.new("Not Found", status: status, body: body)
110
+ when 429
111
+ raise RateLimitError.new("Rate limit exceeded", status: status, body: body)
112
+ when 500..599
113
+ raise ServerError.new("Server error", status: status, body: body)
114
+ else
115
+ raise ApiError.new("Unexpected response", status: status, body: body)
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Creem
4
+ class Configuration
5
+ attr_accessor :api_key, :test_mode, :timeout, :open_timeout
6
+
7
+ def initialize
8
+ @api_key = nil
9
+ @test_mode = false
10
+ @timeout = 30
11
+ @open_timeout = 10
12
+ end
13
+
14
+ def base_url
15
+ test_mode ? TEST_BASE_URL : PRODUCTION_BASE_URL
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Creem
4
+ class Error < StandardError; end
5
+
6
+ class ConfigurationError < Error; end
7
+
8
+ class ApiError < Error
9
+ attr_reader :status, :body
10
+
11
+ def initialize(message = nil, status: nil, body: nil)
12
+ @status = status
13
+ @body = body
14
+ super(message || "API request failed with status #{status}")
15
+ end
16
+ end
17
+
18
+ class AuthenticationError < ApiError; end
19
+ class BadRequestError < ApiError; end
20
+ class NotFoundError < ApiError; end
21
+ class RateLimitError < ApiError; end
22
+ class ServerError < ApiError; end
23
+ class WebhookSignatureError < Error; end
24
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Creem
4
+ module Resources
5
+ class Base
6
+ attr_reader :client
7
+
8
+ def initialize(client)
9
+ @client = client
10
+ end
11
+
12
+ private
13
+
14
+ def get(path, params = {})
15
+ client.get(path, params)
16
+ end
17
+
18
+ def post(path, body = {})
19
+ client.post(path, body)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Creem
6
+ module Resources
7
+ class Checkouts < Base
8
+ def create(product_id:, success_url: nil, request_id: nil, units: nil, discount_code: nil,
9
+ customer: nil, custom_fields: nil, metadata: nil)
10
+ body = { product_id: product_id }
11
+ body[:success_url] = success_url if success_url
12
+ body[:request_id] = request_id if request_id
13
+ body[:units] = units if units
14
+ body[:discount_code] = discount_code if discount_code
15
+ body[:customer] = customer if customer
16
+ body[:custom_fields] = custom_fields if custom_fields
17
+ body[:metadata] = metadata if metadata
18
+ post("/checkouts", body)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Creem
6
+ module Resources
7
+ class Customers < Base
8
+ def list(page_number: 1, page_size: 50)
9
+ get("/customers/list", page_number: page_number, page_size: page_size)
10
+ end
11
+
12
+ def retrieve(customer_id: nil, email: nil)
13
+ params = {}
14
+ params[:customer_id] = customer_id if customer_id
15
+ params[:email] = email if email
16
+ get("/customers", params)
17
+ end
18
+
19
+ def billing_portal(customer_id)
20
+ post("/customers/billing-portal", { customer_id: customer_id })
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Creem
6
+ module Resources
7
+ class Discounts < Base
8
+ def list(page_number: 1, page_size: 10)
9
+ get("/discounts/search", page_number: page_number, page_size: page_size)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Creem
6
+ module Resources
7
+ class Licenses < Base
8
+ def list(page_number: 1, page_size: 10)
9
+ get("/licenses/search", page_number: page_number, page_size: page_size)
10
+ end
11
+
12
+ def validate(key:, instance_id:)
13
+ post("/licenses/validate", { key: key, instance_id: instance_id })
14
+ end
15
+
16
+ def activate(key:, instance_name:)
17
+ post("/licenses/activate", { key: key, instance_name: instance_name })
18
+ end
19
+
20
+ def deactivate(key:, instance_id:)
21
+ post("/licenses/deactivate", { key: key, instance_id: instance_id })
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Creem
6
+ module Resources
7
+ class Products < Base
8
+ def list(page_number: 1, page_size: 10)
9
+ get("/products/search", page_number: page_number, page_size: page_size)
10
+ end
11
+
12
+ def retrieve(product_id)
13
+ get("/products", product_id: product_id)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Creem
6
+ module Resources
7
+ class Subscriptions < Base
8
+ def list(page_number: 1, page_size: 10)
9
+ get("/subscriptions/search", page_number: page_number, page_size: page_size)
10
+ end
11
+
12
+ def retrieve(subscription_id)
13
+ get("/subscriptions", subscription_id: subscription_id)
14
+ end
15
+
16
+ def update(id, items: nil, update_behavior: nil)
17
+ body = {}
18
+ body[:items] = items if items
19
+ body[:update_behavior] = update_behavior if update_behavior
20
+ post("/subscriptions/#{id}", body)
21
+ end
22
+
23
+ def cancel(id, mode: nil, on_execute: nil)
24
+ body = {}
25
+ body[:mode] = mode if mode
26
+ body[:onExecute] = on_execute if on_execute
27
+ post("/subscriptions/#{id}/cancel", body)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Creem
6
+ module Resources
7
+ class Transactions < Base
8
+ def list(page_number: 1, page_size: 10)
9
+ get("/transactions/search", page_number: page_number, page_size: page_size)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Creem
4
+ VERSION = '0.0.1'
5
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+
5
+ module Creem
6
+ class Webhook
7
+ def self.verify_signature(payload:, secret:, signature:)
8
+ computed = OpenSSL::HMAC.hexdigest("SHA256", secret, payload)
9
+ secure_compare(computed, signature)
10
+ end
11
+
12
+ def self.verify_signature!(payload:, secret:, signature:)
13
+ unless verify_signature(payload: payload, secret: secret, signature: signature)
14
+ raise WebhookSignatureError, "Invalid webhook signature"
15
+ end
16
+
17
+ true
18
+ end
19
+
20
+ def self.construct_event(payload:, secret:, signature:)
21
+ verify_signature!(payload: payload, secret: secret, signature: signature)
22
+ JSON.parse(payload)
23
+ end
24
+
25
+ def self.secure_compare(a, b)
26
+ return false unless a.bytesize == b.bytesize
27
+
28
+ OpenSSL.fixed_length_secure_compare(a, b)
29
+ end
30
+
31
+ private_class_method :secure_compare
32
+ end
33
+ end
data/lib/creem.rb ADDED
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "creem/version"
4
+ require_relative "creem/configuration"
5
+ require_relative "creem/errors"
6
+ require_relative "creem/client"
7
+ require_relative "creem/webhook"
8
+ require_relative "creem/resources/products"
9
+ require_relative "creem/resources/checkouts"
10
+ require_relative "creem/resources/subscriptions"
11
+ require_relative "creem/resources/customers"
12
+ require_relative "creem/resources/transactions"
13
+ require_relative "creem/resources/licenses"
14
+ require_relative "creem/resources/discounts"
15
+
16
+ module Creem
17
+ PRODUCTION_BASE_URL = "https://api.creem.io/v1"
18
+ TEST_BASE_URL = "https://test-api.creem.io/v1"
19
+
20
+ class << self
21
+ def configure
22
+ yield(configuration)
23
+ end
24
+
25
+ def configuration
26
+ @configuration ||= Configuration.new
27
+ end
28
+
29
+ def reset_configuration!
30
+ @configuration = Configuration.new
31
+ end
32
+ end
33
+ end
metadata ADDED
@@ -0,0 +1,157 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: creem
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Creem Contributors
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: net-http
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rake
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '13.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '13.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rspec
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.12'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.12'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rubocop
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.50'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.50'
68
+ - !ruby/object:Gem::Dependency
69
+ name: webmock
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '3.18'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '3.18'
82
+ description: Official Ruby SDK for Creem - a Merchant of Record platform for SaaS
83
+ and digital businesses. Manage products, checkouts, subscriptions, customers, transactions,
84
+ licenses, and discounts.
85
+ email:
86
+ - richard.sun@ai-firstly.com
87
+ executables: []
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - ".github/workflows/ci.yml"
92
+ - ".github/workflows/release.yml"
93
+ - ".gitignore"
94
+ - ".rspec"
95
+ - ".rubocop.yml"
96
+ - ".ruby-version"
97
+ - CHANGELOG.md
98
+ - CLAUDE.md
99
+ - Gemfile
100
+ - Gemfile.lock
101
+ - LICENSE
102
+ - Makefile
103
+ - README.md
104
+ - Rakefile
105
+ - creem.gemspec
106
+ - docs/api_reference.md
107
+ - docs/architecture.md
108
+ - docs/development.md
109
+ - docs/error_handling.md
110
+ - docs/getting_started.md
111
+ - docs/webhooks.md
112
+ - lib/creem.rb
113
+ - lib/creem/client.rb
114
+ - lib/creem/configuration.rb
115
+ - lib/creem/errors.rb
116
+ - lib/creem/resources/base.rb
117
+ - lib/creem/resources/checkouts.rb
118
+ - lib/creem/resources/customers.rb
119
+ - lib/creem/resources/discounts.rb
120
+ - lib/creem/resources/licenses.rb
121
+ - lib/creem/resources/products.rb
122
+ - lib/creem/resources/subscriptions.rb
123
+ - lib/creem/resources/transactions.rb
124
+ - lib/creem/version.rb
125
+ - lib/creem/webhook.rb
126
+ homepage: https://github.com/ai-firstly/creem-ruby
127
+ licenses:
128
+ - MIT
129
+ metadata:
130
+ allowed_push_host: https://rubygems.org
131
+ homepage_uri: https://github.com/ai-firstly/creem-ruby
132
+ source_code_uri: https://github.com/ai-firstly/creem-ruby
133
+ changelog_uri: https://github.com/ai-firstly/creem-ruby/blob/main/CHANGELOG.md
134
+ documentation_uri: https://rubydoc.info/gems/creem
135
+ bug_tracker_uri: https://github.com/ai-firstly/creem-ruby/issues
136
+ rubygems_mfa_required: 'true'
137
+ rdoc_options: []
138
+ require_paths:
139
+ - lib
140
+ required_ruby_version: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: 3.3.0
145
+ - - "<"
146
+ - !ruby/object:Gem::Version
147
+ version: '5.0'
148
+ required_rubygems_version: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ requirements: []
154
+ rubygems_version: 3.6.9
155
+ specification_version: 4
156
+ summary: Ruby SDK for the Creem payment platform API
157
+ test_files: []