truetrial 1.0.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: 83e4d15852c9adc42118fa0641acc217e2c36991cf933bcb51545488a7328ef7
4
+ data.tar.gz: 9581b060426ad8f2e39a581bba1869015b6b38f27574d27376447e437f85f3e9
5
+ SHA512:
6
+ metadata.gz: 366fb7f35dd26fb7269d621e90b119407219bb4eeaf5f91f4d69d30e56bed72a25146ac707afa6ffb157feb157c92d520eacfd3654c787cc9d4b06899c532c59
7
+ data.tar.gz: 76f50dcc1ea4195da33a4f21da58845b6ef389a741a00b0ec420b7a3e688d84db9bf264a54c0ad36a4eea01a847b22f0e5c39c321e14d468d0195134f0419edc
data/README.md ADDED
@@ -0,0 +1,233 @@
1
+ # TrueTrial Ruby SDK
2
+
3
+ Official Ruby client for the [TrueTrial](https://truetrial.com) API -- compliance-first trial, warranty, and subscription management.
4
+
5
+ ## Requirements
6
+
7
+ - Ruby >= 3.1
8
+ - Faraday ~> 2.0
9
+
10
+ ## Installation
11
+
12
+ Add to your Gemfile:
13
+
14
+ ```ruby
15
+ gem "truetrial", "~> 1.0"
16
+ ```
17
+
18
+ Then run:
19
+
20
+ ```sh
21
+ bundle install
22
+ ```
23
+
24
+ Or install directly:
25
+
26
+ ```sh
27
+ gem install truetrial
28
+ ```
29
+
30
+ ## Quick Start
31
+
32
+ ```ruby
33
+ require "truetrial"
34
+
35
+ client = TrueTrial::Client.new(api_key: "tt_live_your_key_here")
36
+
37
+ # List orders
38
+ orders = client.orders.list(status: "trial_active", page: 1)
39
+
40
+ # Create an order
41
+ order = client.orders.create(
42
+ external_order_id: "ORD-12345",
43
+ product_type: "physical",
44
+ total_cents: 4999,
45
+ currency: "USD"
46
+ )
47
+
48
+ # Get order details
49
+ order = client.orders.get("01HXYZ...")
50
+
51
+ # Check order status
52
+ status = client.orders.status("01HXYZ...")
53
+ ```
54
+
55
+ ## Configuration
56
+
57
+ ```ruby
58
+ client = TrueTrial::Client.new(
59
+ api_key: "tt_live_your_key_here",
60
+ base_url: "https://truetrial.test/api/v1" # default
61
+ )
62
+ ```
63
+
64
+ ## API Reference
65
+
66
+ ### Orders
67
+
68
+ ```ruby
69
+ client.orders.list(status: "delivered", page: 2)
70
+ client.orders.create(external_order_id: "ORD-123", product_type: "physical", total_cents: 2999)
71
+ client.orders.get("01HXYZ")
72
+ client.orders.status("01HXYZ")
73
+ ```
74
+
75
+ ### Shipments
76
+
77
+ ```ruby
78
+ client.shipments.create("01ORDER_ID", carrier: "ups", tracking_number: "1Z999AA10123456784")
79
+ client.shipments.list("01ORDER_ID")
80
+ ```
81
+
82
+ ### Digital Delivery
83
+
84
+ ```ruby
85
+ client.digital_delivery.confirm("01ORDER_ID", delivered_at: "2026-01-15T10:00:00Z")
86
+ ```
87
+
88
+ ### Temporal Elements (Trials, Warranties, etc.)
89
+
90
+ ```ruby
91
+ client.temporal.get("01ORDER_ID")
92
+ client.temporal.extend("01ORDER_ID", duration: 7, unit: "days", reason: "Customer request")
93
+ client.temporal.adjust("01ORDER_ID", begins_at: "2026-01-20T00:00:00Z")
94
+ client.temporal.claim("01ORDER_ID", reason: "Product defect", description: "Screen cracked on arrival")
95
+ client.temporal.resolve_claim("01ORDER_ID", status: "claim_approved", notes: "Replacement shipped")
96
+ ```
97
+
98
+ ### Cancellations
99
+
100
+ ```ruby
101
+ client.cancellations.create("01ORDER_ID", reason: "Changed mind")
102
+ client.cancellations.get("01ORDER_ID")
103
+ ```
104
+
105
+ ### Webhooks
106
+
107
+ ```ruby
108
+ client.webhooks.list
109
+ client.webhooks.create(url: "https://example.com/webhooks", events: ["order.delivered", "trial.started"])
110
+ client.webhooks.delete("01WEBHOOK_ID")
111
+ ```
112
+
113
+ ### System
114
+
115
+ ```ruby
116
+ client.system.carrier_health
117
+ ```
118
+
119
+ ## Error Handling
120
+
121
+ All API errors inherit from `TrueTrial::Error`:
122
+
123
+ ```ruby
124
+ begin
125
+ client.orders.get("nonexistent")
126
+ rescue TrueTrial::AuthenticationError => e
127
+ # 401 - Invalid or missing API key
128
+ puts e.message
129
+ rescue TrueTrial::NotFoundError => e
130
+ # 404 - Resource not found
131
+ puts e.message
132
+ rescue TrueTrial::ValidationError => e
133
+ # 422 - Validation failed
134
+ puts e.errors # => { "email" => ["is required"] }
135
+ rescue TrueTrial::RateLimitError => e
136
+ # 429 - Too many requests
137
+ puts "Retry after #{e.retry_after} seconds"
138
+ rescue TrueTrial::ServerError => e
139
+ # 500+ - Server error
140
+ puts e.status_code
141
+ rescue TrueTrial::Error => e
142
+ # Catch-all for unexpected errors
143
+ puts e.message
144
+ end
145
+ ```
146
+
147
+ ## Webhook Verification
148
+
149
+ TrueTrial signs webhook payloads with HMAC SHA-256. Verify incoming webhooks to ensure authenticity:
150
+
151
+ ```ruby
152
+ # In your webhook controller / endpoint
153
+ payload = request.body.read
154
+ signature = request.headers["X-TrueTrial-Signature"]
155
+ timestamp = request.headers["X-TrueTrial-Timestamp"]
156
+ event_name = request.headers["X-TrueTrial-Event"]
157
+ secret = "whsec_your_webhook_secret"
158
+
159
+ # Simple verification (returns boolean)
160
+ if TrueTrial::Webhook.verify?(payload, signature, secret)
161
+ event = JSON.parse(payload)
162
+ # process event...
163
+ end
164
+
165
+ # Verify with timestamp tolerance (recommended for production)
166
+ if TrueTrial::Webhook.verify?(payload, signature, secret, tolerance: 300, timestamp: timestamp)
167
+ event = JSON.parse(payload)
168
+ # process event...
169
+ end
170
+
171
+ # Or use construct_event to verify and parse in one step (raises on failure)
172
+ begin
173
+ event = TrueTrial::Webhook.construct_event(payload, signature, secret, tolerance: 300, timestamp: timestamp)
174
+
175
+ case event_name
176
+ when "order.delivered"
177
+ handle_delivery(event["data"])
178
+ when "trial.started"
179
+ handle_trial_start(event["data"])
180
+ when "trial.expiring"
181
+ handle_trial_expiring(event["data"])
182
+ end
183
+ rescue TrueTrial::Error => e
184
+ # Invalid signature
185
+ render json: { error: e.message }, status: 400
186
+ end
187
+ ```
188
+
189
+ ## Enums
190
+
191
+ The following string values are used across the API:
192
+
193
+ **OrderStatus:** `received`, `shipped`, `in_transit`, `delivered`, `trial_active`, `converted`, `returned`, `expired`, `cancelled`
194
+
195
+ **TemporalType:** `trial`, `evaluation`, `subscription`, `warranty`, `guarantee`
196
+
197
+ **TemporalStatus:** `pending`, `active`, `expiring`, `expired`, `converted`, `cancelled`, `suspended`, `renewed`, `claimed`, `claim_approved`, `claim_denied`
198
+
199
+ **ShipmentStatus:** `pending`, `in_transit`, `out_for_delivery`, `delivered`, `failed`, `returned_to_sender`
200
+
201
+ **ProductType:** `physical`, `digital`
202
+
203
+ **Carrier:** `ups`, `fedex`, `usps`, `dhl`, `shippo`, `aftership`
204
+
205
+ **DurationUnit:** `days`, `weeks`, `months`, `years`
206
+
207
+ **DeliverySource:** `webhook`, `poll`, `manual`, `fallback_carrier`
208
+
209
+ **WebhookEvent:** `order.created`, `order.delivered`, `trial.started`, `trial.expiring`, `trial.expired`, `trial.converted`, `cancellation.initiated`, `risk_score.changed`, `subscription.renewed`, `warranty.claimed`, `temporal.extended`, `temporal.adjusted`, `warranty.claim_resolved`, `payment.succeeded`, `payment.failed`, `dispute.created`, `dispute.won`, `dispute.lost`
210
+
211
+ ## Type Objects
212
+
213
+ The SDK provides type classes for deserializing API responses:
214
+
215
+ ```ruby
216
+ data = client.orders.get("01HXYZ")
217
+ order = TrueTrial::Types::Order.from_hash(data)
218
+ puts order.id
219
+ puts order.status
220
+ ```
221
+
222
+ Available types: `Order`, `Shipment`, `TemporalElement`, `Cancellation`, `WebhookSubscription`
223
+
224
+ ## Development
225
+
226
+ ```sh
227
+ bundle install
228
+ bundle exec rspec
229
+ ```
230
+
231
+ ## License
232
+
233
+ MIT
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TrueTrial
4
+ # Main entry point for interacting with the TrueTrial API.
5
+ #
6
+ # @example
7
+ # client = TrueTrial::Client.new(api_key: "tt_live_abc123")
8
+ # client.orders.list
9
+ class Client
10
+ DEFAULT_BASE_URL = "https://truetrial.test/api/v1"
11
+
12
+ # @param api_key [String] your TrueTrial API key
13
+ # @param base_url [String] API base URL (defaults to production)
14
+ def initialize(api_key:, base_url: DEFAULT_BASE_URL)
15
+ @http_client = HttpClient.new(api_key: api_key, base_url: base_url)
16
+ end
17
+
18
+ # @return [TrueTrial::Resources::Orders]
19
+ def orders
20
+ @orders ||= Resources::Orders.new(@http_client)
21
+ end
22
+
23
+ # @return [TrueTrial::Resources::Shipments]
24
+ def shipments
25
+ @shipments ||= Resources::Shipments.new(@http_client)
26
+ end
27
+
28
+ # @return [TrueTrial::Resources::DigitalDelivery]
29
+ def digital_delivery
30
+ @digital_delivery ||= Resources::DigitalDelivery.new(@http_client)
31
+ end
32
+
33
+ # @return [TrueTrial::Resources::Temporal]
34
+ def temporal
35
+ @temporal ||= Resources::Temporal.new(@http_client)
36
+ end
37
+
38
+ # @return [TrueTrial::Resources::Cancellations]
39
+ def cancellations
40
+ @cancellations ||= Resources::Cancellations.new(@http_client)
41
+ end
42
+
43
+ # @return [TrueTrial::Resources::Webhooks]
44
+ def webhooks
45
+ @webhooks ||= Resources::Webhooks.new(@http_client)
46
+ end
47
+
48
+ # @return [TrueTrial::Resources::System]
49
+ def system
50
+ @system ||= Resources::System.new(@http_client)
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TrueTrial
4
+ # Base error class for all TrueTrial API errors.
5
+ class Error < StandardError
6
+ attr_reader :status_code, :response_body
7
+
8
+ def initialize(message = nil, status_code: nil, response_body: nil)
9
+ @status_code = status_code
10
+ @response_body = response_body
11
+ super(message)
12
+ end
13
+ end
14
+
15
+ # Raised when the API key is missing or invalid (HTTP 401).
16
+ class AuthenticationError < Error
17
+ def initialize(message = "Invalid or missing API key", response_body: nil)
18
+ super(message, status_code: 401, response_body: response_body)
19
+ end
20
+ end
21
+
22
+ # Raised when request validation fails (HTTP 422).
23
+ class ValidationError < Error
24
+ attr_reader :errors
25
+
26
+ def initialize(message = "Validation failed", errors: {}, response_body: nil)
27
+ @errors = errors
28
+ super(message, status_code: 422, response_body: response_body)
29
+ end
30
+ end
31
+
32
+ # Raised when the requested resource is not found (HTTP 404).
33
+ class NotFoundError < Error
34
+ def initialize(message = "Resource not found", response_body: nil)
35
+ super(message, status_code: 404, response_body: response_body)
36
+ end
37
+ end
38
+
39
+ # Raised when the rate limit is exceeded (HTTP 429).
40
+ class RateLimitError < Error
41
+ attr_reader :retry_after
42
+
43
+ def initialize(message = "Rate limit exceeded", retry_after: nil, response_body: nil)
44
+ @retry_after = retry_after
45
+ super(message, status_code: 429, response_body: response_body)
46
+ end
47
+ end
48
+
49
+ # Raised when the server returns a 5xx error.
50
+ class ServerError < Error
51
+ def initialize(message = "Internal server error", status_code: 500, response_body: nil)
52
+ super(message, status_code: status_code, response_body: response_body)
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "json"
5
+
6
+ module TrueTrial
7
+ # Low-level HTTP client wrapping Faraday for TrueTrial API communication.
8
+ class HttpClient
9
+ def initialize(api_key:, base_url:)
10
+ @connection = Faraday.new(url: base_url) do |conn|
11
+ conn.headers["X-Api-Key"] = api_key
12
+ conn.headers["Content-Type"] = "application/json"
13
+ conn.headers["Accept"] = "application/json"
14
+ conn.headers["User-Agent"] = "truetrial-ruby/#{TrueTrial::VERSION}"
15
+ conn.adapter Faraday.default_adapter
16
+ end
17
+ end
18
+
19
+ # Performs a GET request.
20
+ #
21
+ # @param path [String] the API endpoint path
22
+ # @param params [Hash] optional query parameters
23
+ # @return [Hash] parsed JSON response
24
+ def get(path, params: {})
25
+ response = @connection.get(path) do |req|
26
+ req.params = params unless params.empty?
27
+ end
28
+ handle_response(response)
29
+ end
30
+
31
+ # Performs a POST request.
32
+ #
33
+ # @param path [String] the API endpoint path
34
+ # @param body [Hash] the request body
35
+ # @return [Hash] parsed JSON response
36
+ def post(path, body: {})
37
+ response = @connection.post(path) do |req|
38
+ req.body = JSON.generate(body)
39
+ end
40
+ handle_response(response)
41
+ end
42
+
43
+ # Performs a DELETE request.
44
+ #
45
+ # @param path [String] the API endpoint path
46
+ # @return [Hash] parsed JSON response
47
+ def delete(path)
48
+ response = @connection.delete(path)
49
+ handle_response(response)
50
+ end
51
+
52
+ private
53
+
54
+ # Maps HTTP status codes to appropriate error classes.
55
+ #
56
+ # @param response [Faraday::Response]
57
+ # @return [Hash] parsed response body on success
58
+ # @raise [TrueTrial::Error] on error responses
59
+ def handle_response(response)
60
+ body = parse_body(response.body)
61
+
62
+ case response.status
63
+ when 200..299
64
+ body
65
+ when 401
66
+ raise AuthenticationError.new(
67
+ error_message(body, "Invalid or missing API key"),
68
+ response_body: body
69
+ )
70
+ when 404
71
+ raise NotFoundError.new(
72
+ error_message(body, "Resource not found"),
73
+ response_body: body
74
+ )
75
+ when 422
76
+ raise ValidationError.new(
77
+ error_message(body, "Validation failed"),
78
+ errors: body.is_a?(Hash) ? body.fetch("errors", {}) : {},
79
+ response_body: body
80
+ )
81
+ when 429
82
+ retry_after = response.headers["Retry-After"]&.to_i
83
+ raise RateLimitError.new(
84
+ error_message(body, "Rate limit exceeded"),
85
+ retry_after: retry_after,
86
+ response_body: body
87
+ )
88
+ when 500..599
89
+ raise ServerError.new(
90
+ error_message(body, "Internal server error"),
91
+ status_code: response.status,
92
+ response_body: body
93
+ )
94
+ else
95
+ raise Error.new(
96
+ error_message(body, "Unexpected response"),
97
+ status_code: response.status,
98
+ response_body: body
99
+ )
100
+ end
101
+ end
102
+
103
+ # Safely parses JSON, returning the raw string on failure.
104
+ def parse_body(raw)
105
+ return nil if raw.nil? || raw.empty?
106
+
107
+ JSON.parse(raw)
108
+ rescue JSON::ParserError
109
+ raw
110
+ end
111
+
112
+ # Extracts an error message from the response body hash.
113
+ def error_message(body, default)
114
+ return default unless body.is_a?(Hash)
115
+
116
+ body["message"] || body["error"] || default
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TrueTrial
4
+ module Resources
5
+ # Provides access to cancellation-related API endpoints.
6
+ class Cancellations
7
+ def initialize(http_client)
8
+ @http = http_client
9
+ end
10
+
11
+ # Creates a cancellation for an order.
12
+ #
13
+ # @param order_id [String] the order ULID
14
+ # @param data [Hash] cancellation attributes (reason, etc.)
15
+ # @return [Hash] the created cancellation
16
+ def create(order_id, data)
17
+ @http.post("/orders/#{order_id}/cancellations", body: data)
18
+ end
19
+
20
+ # Retrieves the cancellation for an order.
21
+ #
22
+ # @param order_id [String] the order ULID
23
+ # @return [Hash] the cancellation
24
+ def get(order_id)
25
+ @http.get("/orders/#{order_id}/cancellations")
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TrueTrial
4
+ module Resources
5
+ # Provides access to digital delivery confirmation endpoints.
6
+ class DigitalDelivery
7
+ def initialize(http_client)
8
+ @http = http_client
9
+ end
10
+
11
+ # Confirms digital delivery for an order.
12
+ #
13
+ # @param order_id [String] the order ULID
14
+ # @param data [Hash] digital delivery confirmation attributes
15
+ # @return [Hash] the delivery confirmation result
16
+ def confirm(order_id, data)
17
+ @http.post("/orders/#{order_id}/digital-delivery", body: data)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TrueTrial
4
+ module Resources
5
+ # Provides access to order-related API endpoints.
6
+ class Orders
7
+ def initialize(http_client)
8
+ @http = http_client
9
+ end
10
+
11
+ # Lists orders with optional filters.
12
+ #
13
+ # @param filters [Hash] optional query filters (status, page, per_page, etc.)
14
+ # @return [Hash] paginated list of orders
15
+ def list(filters = {})
16
+ @http.get("/orders", params: filters)
17
+ end
18
+
19
+ # Creates a new order.
20
+ #
21
+ # @param data [Hash] order attributes
22
+ # @return [Hash] the created order
23
+ def create(data)
24
+ @http.post("/orders", body: data)
25
+ end
26
+
27
+ # Retrieves a single order by ID.
28
+ #
29
+ # @param id [String] the order ULID
30
+ # @return [Hash] the order
31
+ def get(id)
32
+ @http.get("/orders/#{id}")
33
+ end
34
+
35
+ # Retrieves the current status of an order.
36
+ #
37
+ # @param id [String] the order ULID
38
+ # @return [Hash] the order status
39
+ def status(id)
40
+ @http.get("/orders/#{id}/status")
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TrueTrial
4
+ module Resources
5
+ # Provides access to shipment-related API endpoints.
6
+ class Shipments
7
+ def initialize(http_client)
8
+ @http = http_client
9
+ end
10
+
11
+ # Creates a shipment for an order.
12
+ #
13
+ # @param order_id [String] the order ULID
14
+ # @param data [Hash] shipment attributes (carrier, tracking_number, etc.)
15
+ # @return [Hash] the created shipment
16
+ def create(order_id, data)
17
+ @http.post("/orders/#{order_id}/shipments", body: data)
18
+ end
19
+
20
+ # Lists all shipments for an order.
21
+ #
22
+ # @param order_id [String] the order ULID
23
+ # @return [Hash] list of shipments
24
+ def list(order_id)
25
+ @http.get("/orders/#{order_id}/shipments")
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TrueTrial
4
+ module Resources
5
+ # Provides access to system-level API endpoints.
6
+ class System
7
+ def initialize(http_client)
8
+ @http = http_client
9
+ end
10
+
11
+ # Retrieves carrier health status.
12
+ #
13
+ # @return [Hash] carrier health information
14
+ def carrier_health
15
+ @http.get("/carrier-health")
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TrueTrial
4
+ module Resources
5
+ # Provides access to temporal element API endpoints (trials, warranties, etc.).
6
+ class Temporal
7
+ def initialize(http_client)
8
+ @http = http_client
9
+ end
10
+
11
+ # Retrieves the temporal element for an order.
12
+ #
13
+ # @param order_id [String] the order ULID
14
+ # @return [Hash] the temporal element
15
+ def get(order_id)
16
+ @http.get("/orders/#{order_id}/temporal")
17
+ end
18
+
19
+ # Extends a temporal element duration.
20
+ #
21
+ # @param order_id [String] the order ULID
22
+ # @param data [Hash] extension attributes (duration, unit, reason)
23
+ # @return [Hash] the updated temporal element
24
+ def extend(order_id, data)
25
+ @http.post("/orders/#{order_id}/temporal/extend", body: data)
26
+ end
27
+
28
+ # Adjusts a temporal element (modify start/end dates).
29
+ #
30
+ # @param order_id [String] the order ULID
31
+ # @param data [Hash] adjustment attributes
32
+ # @return [Hash] the updated temporal element
33
+ def adjust(order_id, data)
34
+ @http.post("/orders/#{order_id}/temporal/adjust", body: data)
35
+ end
36
+
37
+ # Initiates a warranty or guarantee claim.
38
+ #
39
+ # @param order_id [String] the order ULID
40
+ # @param data [Hash] claim attributes (reason, description)
41
+ # @return [Hash] the created claim
42
+ def claim(order_id, data)
43
+ @http.post("/orders/#{order_id}/temporal/claim", body: data)
44
+ end
45
+
46
+ # Resolves a warranty or guarantee claim.
47
+ #
48
+ # @param order_id [String] the order ULID
49
+ # @param data [Hash] resolution attributes (status, notes)
50
+ # @return [Hash] the resolved claim
51
+ def resolve_claim(order_id, data)
52
+ @http.post("/orders/#{order_id}/temporal/resolve-claim", body: data)
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TrueTrial
4
+ module Resources
5
+ # Provides access to webhook subscription management endpoints.
6
+ class Webhooks
7
+ def initialize(http_client)
8
+ @http = http_client
9
+ end
10
+
11
+ # Lists all webhook subscriptions.
12
+ #
13
+ # @return [Hash] list of webhook subscriptions
14
+ def list
15
+ @http.get("/webhooks")
16
+ end
17
+
18
+ # Creates a new webhook subscription.
19
+ #
20
+ # @param data [Hash] webhook attributes (url, events, secret)
21
+ # @return [Hash] the created webhook subscription
22
+ def create(data)
23
+ @http.post("/webhooks", body: data)
24
+ end
25
+
26
+ # Deletes a webhook subscription.
27
+ #
28
+ # @param id [String] the webhook subscription ULID
29
+ # @return [Hash] deletion confirmation
30
+ def delete(id)
31
+ @http.delete("/webhooks/#{id}")
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TrueTrial
4
+ module Types
5
+ # Represents a cancellation returned from the TrueTrial API.
6
+ class Cancellation
7
+ attr_reader :id, :order_id, :reason, :cancelled_by, :notes,
8
+ :created_at, :updated_at
9
+
10
+ def initialize(attributes = {})
11
+ @id = attributes["id"]
12
+ @order_id = attributes["order_id"]
13
+ @reason = attributes["reason"]
14
+ @cancelled_by = attributes["cancelled_by"]
15
+ @notes = attributes["notes"]
16
+ @created_at = attributes["created_at"]
17
+ @updated_at = attributes["updated_at"]
18
+ end
19
+
20
+ # Builds a Cancellation from an API response hash.
21
+ #
22
+ # @param hash [Hash] raw API response data
23
+ # @return [Cancellation]
24
+ def self.from_hash(hash)
25
+ new(hash)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TrueTrial
4
+ module Types
5
+ # Represents an order returned from the TrueTrial API.
6
+ class Order
7
+ attr_reader :id, :tenant_id, :consumer_id, :external_order_id, :status,
8
+ :product_type, :total_cents, :currency, :metadata,
9
+ :created_at, :updated_at
10
+
11
+ def initialize(attributes = {})
12
+ @id = attributes["id"]
13
+ @tenant_id = attributes["tenant_id"]
14
+ @consumer_id = attributes["consumer_id"]
15
+ @external_order_id = attributes["external_order_id"]
16
+ @status = attributes["status"]
17
+ @product_type = attributes["product_type"]
18
+ @total_cents = attributes["total_cents"]
19
+ @currency = attributes["currency"]
20
+ @metadata = attributes["metadata"]
21
+ @created_at = attributes["created_at"]
22
+ @updated_at = attributes["updated_at"]
23
+ end
24
+
25
+ # Builds an Order from an API response hash.
26
+ #
27
+ # @param hash [Hash] raw API response data
28
+ # @return [Order]
29
+ def self.from_hash(hash)
30
+ new(hash)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TrueTrial
4
+ module Types
5
+ # Represents a shipment returned from the TrueTrial API.
6
+ class Shipment
7
+ attr_reader :id, :order_id, :carrier, :tracking_number, :status,
8
+ :shipped_at, :delivered_at, :delivery_source,
9
+ :created_at, :updated_at
10
+
11
+ def initialize(attributes = {})
12
+ @id = attributes["id"]
13
+ @order_id = attributes["order_id"]
14
+ @carrier = attributes["carrier"]
15
+ @tracking_number = attributes["tracking_number"]
16
+ @status = attributes["status"]
17
+ @shipped_at = attributes["shipped_at"]
18
+ @delivered_at = attributes["delivered_at"]
19
+ @delivery_source = attributes["delivery_source"]
20
+ @created_at = attributes["created_at"]
21
+ @updated_at = attributes["updated_at"]
22
+ end
23
+
24
+ # Builds a Shipment from an API response hash.
25
+ #
26
+ # @param hash [Hash] raw API response data
27
+ # @return [Shipment]
28
+ def self.from_hash(hash)
29
+ new(hash)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TrueTrial
4
+ module Types
5
+ # Represents a temporal element (trial, warranty, subscription, etc.)
6
+ # returned from the TrueTrial API.
7
+ class TemporalElement
8
+ attr_reader :id, :order_id, :type, :status, :duration, :duration_unit,
9
+ :begins_at, :expires_at, :converted_at, :cancelled_at,
10
+ :created_at, :updated_at
11
+
12
+ def initialize(attributes = {})
13
+ @id = attributes["id"]
14
+ @order_id = attributes["order_id"]
15
+ @type = attributes["type"]
16
+ @status = attributes["status"]
17
+ @duration = attributes["duration"]
18
+ @duration_unit = attributes["duration_unit"]
19
+ @begins_at = attributes["begins_at"]
20
+ @expires_at = attributes["expires_at"]
21
+ @converted_at = attributes["converted_at"]
22
+ @cancelled_at = attributes["cancelled_at"]
23
+ @created_at = attributes["created_at"]
24
+ @updated_at = attributes["updated_at"]
25
+ end
26
+
27
+ # Builds a TemporalElement from an API response hash.
28
+ #
29
+ # @param hash [Hash] raw API response data
30
+ # @return [TemporalElement]
31
+ def self.from_hash(hash)
32
+ new(hash)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TrueTrial
4
+ module Types
5
+ # Represents a webhook subscription returned from the TrueTrial API.
6
+ class WebhookSubscription
7
+ attr_reader :id, :url, :events, :secret, :active,
8
+ :created_at, :updated_at
9
+
10
+ def initialize(attributes = {})
11
+ @id = attributes["id"]
12
+ @url = attributes["url"]
13
+ @events = attributes["events"]
14
+ @secret = attributes["secret"]
15
+ @active = attributes["active"]
16
+ @created_at = attributes["created_at"]
17
+ @updated_at = attributes["updated_at"]
18
+ end
19
+
20
+ # Builds a WebhookSubscription from an API response hash.
21
+ #
22
+ # @param hash [Hash] raw API response data
23
+ # @return [WebhookSubscription]
24
+ def self.from_hash(hash)
25
+ new(hash)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TrueTrial
4
+ VERSION = "1.0.0"
5
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+ require "json"
5
+ require "time"
6
+
7
+ module TrueTrial
8
+ # Utilities for verifying and parsing incoming TrueTrial webhooks.
9
+ #
10
+ # Webhook payloads are signed with HMAC SHA-256. The signature is delivered
11
+ # in the X-TrueTrial-Signature header and is computed from the raw JSON body
12
+ # using the webhook secret.
13
+ module Webhook
14
+ module_function
15
+
16
+ # Verifies that a webhook payload matches its signature.
17
+ #
18
+ # @param payload [String] the raw JSON request body
19
+ # @param signature [String] the value of the X-TrueTrial-Signature header
20
+ # @param secret [String] the webhook signing secret
21
+ # @param tolerance [Integer, nil] maximum age of the event in seconds (optional)
22
+ # @param timestamp [String, nil] the value of the X-TrueTrial-Timestamp header (required when tolerance is set)
23
+ # @return [Boolean] true if the signature is valid
24
+ def verify?(payload, signature, secret, tolerance: nil, timestamp: nil)
25
+ expected = compute_signature(payload, secret)
26
+ valid = secure_compare(expected, signature)
27
+
28
+ if valid && tolerance && timestamp
29
+ event_time = Time.parse(timestamp)
30
+ valid = (Time.now - event_time).abs <= tolerance
31
+ end
32
+
33
+ valid
34
+ rescue ArgumentError, TypeError
35
+ false
36
+ end
37
+
38
+ # Verifies and parses a webhook payload into a hash.
39
+ #
40
+ # @param payload [String] the raw JSON request body
41
+ # @param signature [String] the value of the X-TrueTrial-Signature header
42
+ # @param secret [String] the webhook signing secret
43
+ # @param tolerance [Integer, nil] maximum age of the event in seconds (optional)
44
+ # @param timestamp [String, nil] the value of the X-TrueTrial-Timestamp header (required when tolerance is set)
45
+ # @return [Hash] the parsed event data
46
+ # @raise [TrueTrial::Error] if the signature is invalid
47
+ def construct_event(payload, signature, secret, tolerance: nil, timestamp: nil)
48
+ unless verify?(payload, signature, secret, tolerance: tolerance, timestamp: timestamp)
49
+ raise Error.new("Invalid webhook signature")
50
+ end
51
+
52
+ JSON.parse(payload)
53
+ end
54
+
55
+ # Computes the HMAC SHA-256 hex digest for a payload.
56
+ #
57
+ # @param payload [String] the raw payload
58
+ # @param secret [String] the signing secret
59
+ # @return [String] hex-encoded HMAC signature
60
+ def compute_signature(payload, secret)
61
+ OpenSSL::HMAC.hexdigest("SHA256", secret, payload)
62
+ end
63
+
64
+ # Performs a constant-time string comparison to prevent timing attacks.
65
+ #
66
+ # @param a [String]
67
+ # @param b [String]
68
+ # @return [Boolean]
69
+ def secure_compare(a, b)
70
+ return false unless a.bytesize == b.bytesize
71
+
72
+ OpenSSL.fixed_length_secure_compare(a, b)
73
+ rescue NoMethodError
74
+ # Fallback for older OpenSSL versions
75
+ l = a.unpack("C*")
76
+ r = b.unpack("C*")
77
+ result = 0
78
+ l.zip(r) { |x, y| result |= x ^ y }
79
+ result.zero?
80
+ end
81
+ end
82
+ end
data/lib/truetrial.rb ADDED
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "truetrial/version"
4
+ require_relative "truetrial/errors"
5
+ require_relative "truetrial/http_client"
6
+ require_relative "truetrial/client"
7
+ require_relative "truetrial/webhook"
8
+
9
+ require_relative "truetrial/types/order"
10
+ require_relative "truetrial/types/shipment"
11
+ require_relative "truetrial/types/temporal_element"
12
+ require_relative "truetrial/types/cancellation"
13
+ require_relative "truetrial/types/webhook_subscription"
14
+
15
+ require_relative "truetrial/resources/orders"
16
+ require_relative "truetrial/resources/shipments"
17
+ require_relative "truetrial/resources/digital_delivery"
18
+ require_relative "truetrial/resources/temporal"
19
+ require_relative "truetrial/resources/cancellations"
20
+ require_relative "truetrial/resources/webhooks"
21
+ require_relative "truetrial/resources/system"
22
+
23
+ module TrueTrial
24
+ end
metadata ADDED
@@ -0,0 +1,108 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: truetrial
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - TrueTrial
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-05-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: webmock
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ description: Official Ruby client for the TrueTrial compliance-first trial and warranty
56
+ management platform.
57
+ email:
58
+ - support@truetrial.com
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - README.md
64
+ - lib/truetrial.rb
65
+ - lib/truetrial/client.rb
66
+ - lib/truetrial/errors.rb
67
+ - lib/truetrial/http_client.rb
68
+ - lib/truetrial/resources/cancellations.rb
69
+ - lib/truetrial/resources/digital_delivery.rb
70
+ - lib/truetrial/resources/orders.rb
71
+ - lib/truetrial/resources/shipments.rb
72
+ - lib/truetrial/resources/system.rb
73
+ - lib/truetrial/resources/temporal.rb
74
+ - lib/truetrial/resources/webhooks.rb
75
+ - lib/truetrial/types/cancellation.rb
76
+ - lib/truetrial/types/order.rb
77
+ - lib/truetrial/types/shipment.rb
78
+ - lib/truetrial/types/temporal_element.rb
79
+ - lib/truetrial/types/webhook_subscription.rb
80
+ - lib/truetrial/version.rb
81
+ - lib/truetrial/webhook.rb
82
+ homepage: https://github.com/truetrial/truetrial-ruby-sdk
83
+ licenses:
84
+ - MIT
85
+ metadata:
86
+ homepage_uri: https://github.com/truetrial/truetrial-ruby-sdk
87
+ source_code_uri: https://github.com/truetrial/truetrial-ruby-sdk
88
+ changelog_uri: https://github.com/truetrial/truetrial-ruby-sdk/blob/main/CHANGELOG.md
89
+ post_install_message:
90
+ rdoc_options: []
91
+ require_paths:
92
+ - lib
93
+ required_ruby_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '3.1'
98
+ required_rubygems_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ requirements: []
104
+ rubygems_version: 3.4.19
105
+ signing_key:
106
+ specification_version: 4
107
+ summary: Ruby SDK for the TrueTrial API
108
+ test_files: []