mintsoft 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/.rspec +3 -0
- data/.serena/memories/code_style_conventions.md +35 -0
- data/.serena/memories/codebase_structure.md +35 -0
- data/.serena/memories/development_environment.md +37 -0
- data/.serena/memories/project_overview.md +28 -0
- data/.serena/memories/suggested_commands.md +63 -0
- data/.serena/memories/task_completion_checklist.md +46 -0
- data/.serena/project.yml +68 -0
- data/.standard.yml +3 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +165 -0
- data/Rakefile +10 -0
- data/claudedocs/AUTH_CLIENT_DESIGN.md +294 -0
- data/claudedocs/FINAL_SIMPLIFIED_DESIGN.md +553 -0
- data/claudedocs/IMPLEMENTATION_SUMMARY.md +140 -0
- data/claudedocs/INDEX.md +83 -0
- data/claudedocs/MINIMAL_IMPLEMENTATION_PLAN.md +316 -0
- data/claudedocs/TOKEN_ONLY_CLIENT_DESIGN.md +408 -0
- data/claudedocs/USAGE_EXAMPLES.md +482 -0
- data/examples/complete_workflow.rb +146 -0
- data/lib/mintsoft/auth_client.rb +104 -0
- data/lib/mintsoft/base.rb +44 -0
- data/lib/mintsoft/client.rb +37 -0
- data/lib/mintsoft/errors.rb +9 -0
- data/lib/mintsoft/objects/order.rb +16 -0
- data/lib/mintsoft/objects/return.rb +26 -0
- data/lib/mintsoft/objects/return_reason.rb +11 -0
- data/lib/mintsoft/resources/base_resource.rb +53 -0
- data/lib/mintsoft/resources/orders.rb +36 -0
- data/lib/mintsoft/resources/returns.rb +90 -0
- data/lib/mintsoft/version.rb +5 -0
- data/lib/mintsoft.rb +17 -0
- data/sig/mintsoft.rbs +4 -0
- metadata +161 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
# Token-Only Client Design
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The Mintsoft client is extremely simplified - it only accepts a pre-obtained API token on initialization. All token management (obtaining, storing, renewal) is the user's responsibility outside the gem.
|
|
6
|
+
|
|
7
|
+
## Client Interface
|
|
8
|
+
|
|
9
|
+
### Initialization
|
|
10
|
+
```ruby
|
|
11
|
+
# Only way to initialize client
|
|
12
|
+
client = Mintsoft::Client.new(token: "your_api_token_here")
|
|
13
|
+
|
|
14
|
+
# Optional base URL override
|
|
15
|
+
client = Mintsoft::Client.new(
|
|
16
|
+
token: "your_api_token_here",
|
|
17
|
+
base_url: "https://custom.mintsoft.com/api"
|
|
18
|
+
)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### No Token Management Methods
|
|
22
|
+
```ruby
|
|
23
|
+
# Client does NOT provide these methods:
|
|
24
|
+
# client.authenticate(username, password)
|
|
25
|
+
# client.refresh_token
|
|
26
|
+
# client.token_valid?
|
|
27
|
+
# client.get_token
|
|
28
|
+
# client.set_credentials(username, password)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Implementation
|
|
32
|
+
|
|
33
|
+
### 1. Simplified Client Class (Faraday-based)
|
|
34
|
+
```ruby
|
|
35
|
+
# lib/mintsoft/client.rb
|
|
36
|
+
require "faraday"
|
|
37
|
+
require "faraday/net_http"
|
|
38
|
+
|
|
39
|
+
module Mintsoft
|
|
40
|
+
class Client
|
|
41
|
+
BASE_URL = "https://api.mintsoft.com".freeze
|
|
42
|
+
|
|
43
|
+
attr_reader :token
|
|
44
|
+
|
|
45
|
+
def initialize(token:, base_url: BASE_URL, conn_opts: {})
|
|
46
|
+
raise ArgumentError, "Token is required" if token.nil? || token.empty?
|
|
47
|
+
|
|
48
|
+
@token = token
|
|
49
|
+
@base_url = base_url
|
|
50
|
+
@conn_opts = conn_opts
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def connection
|
|
54
|
+
@connection ||= Faraday.new do |conn|
|
|
55
|
+
conn.url_prefix = @base_url
|
|
56
|
+
conn.options.merge!(@conn_opts)
|
|
57
|
+
conn.request :authorization, :Bearer, @token
|
|
58
|
+
conn.request :json
|
|
59
|
+
conn.response :json, content_type: "application/json"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def orders
|
|
64
|
+
@orders ||= Resources::Orders.new(self)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def returns
|
|
68
|
+
@returns ||= Resources::Returns.new(self)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 2. Resources Use BaseResource Pattern
|
|
75
|
+
```ruby
|
|
76
|
+
# lib/mintsoft/resources/base_resource.rb
|
|
77
|
+
module Mintsoft
|
|
78
|
+
class BaseResource
|
|
79
|
+
attr_reader :client
|
|
80
|
+
|
|
81
|
+
def initialize(client)
|
|
82
|
+
@client = client
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
protected
|
|
86
|
+
|
|
87
|
+
def get_request(url, params: {}, headers: {})
|
|
88
|
+
handle_response client.connection.get(url, params, headers)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def post_request(url, body: {}, headers: {})
|
|
92
|
+
handle_response client.connection.post(url, body, headers)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def handle_response(response)
|
|
98
|
+
case response.status
|
|
99
|
+
when 400
|
|
100
|
+
raise ValidationError, "Invalid request: #{response.body}"
|
|
101
|
+
when 401
|
|
102
|
+
raise AuthenticationError, "Invalid or expired token"
|
|
103
|
+
when 403
|
|
104
|
+
raise AuthenticationError, "Access denied"
|
|
105
|
+
when 404
|
|
106
|
+
raise NotFoundError, "Resource not found"
|
|
107
|
+
when 500
|
|
108
|
+
raise APIError, "Internal server error"
|
|
109
|
+
else
|
|
110
|
+
response
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### 3. Orders Resource Example
|
|
118
|
+
```ruby
|
|
119
|
+
# lib/mintsoft/resources/orders.rb
|
|
120
|
+
module Mintsoft
|
|
121
|
+
class OrderResource < BaseResource
|
|
122
|
+
def search(order_number)
|
|
123
|
+
validate_order_number!(order_number)
|
|
124
|
+
|
|
125
|
+
response = get_request('/api/Order/Search', params: { OrderNumber: order_number })
|
|
126
|
+
|
|
127
|
+
if response.success?
|
|
128
|
+
parse_orders(response.body)
|
|
129
|
+
else
|
|
130
|
+
[]
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
private
|
|
135
|
+
|
|
136
|
+
def validate_order_number!(order_number)
|
|
137
|
+
raise ValidationError, "Order number required" if order_number.nil? || order_number.empty?
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def parse_orders(data)
|
|
141
|
+
return [] unless data.is_a?(Array)
|
|
142
|
+
data.map { |order_data| Order.new(order_data) }
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## User Token Management Examples
|
|
149
|
+
|
|
150
|
+
### Example 1: Using AuthClient (Recommended)
|
|
151
|
+
```ruby
|
|
152
|
+
# Initialize auth client
|
|
153
|
+
auth_client = Mintsoft::AuthClient.new
|
|
154
|
+
|
|
155
|
+
# Get token directly as string
|
|
156
|
+
token = auth_client.auth.authenticate(
|
|
157
|
+
ENV['MINTSOFT_USERNAME'],
|
|
158
|
+
ENV['MINTSOFT_PASSWORD']
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Use token with main client
|
|
162
|
+
client = Mintsoft::Client.new(token: token)
|
|
163
|
+
|
|
164
|
+
puts "Token obtained: #{token[0..7]}...#{token[-4..-1]}" # Show first 8 + last 4 chars for security
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Example 2: Token with Caching
|
|
168
|
+
```ruby
|
|
169
|
+
class TokenManager
|
|
170
|
+
def initialize(username, password)
|
|
171
|
+
@username = username
|
|
172
|
+
@password = password
|
|
173
|
+
@auth_client = Mintsoft::AuthClient.new
|
|
174
|
+
@token = nil
|
|
175
|
+
@token_expires_at = nil
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def current_token
|
|
179
|
+
if token_expired?
|
|
180
|
+
refresh_token!
|
|
181
|
+
end
|
|
182
|
+
@token
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def client
|
|
186
|
+
Mintsoft::Client.new(token: current_token)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def token_info
|
|
190
|
+
{
|
|
191
|
+
token: @token ? "#{@token[0..7]}...#{@token[-4..-1]}" : nil,
|
|
192
|
+
expires_at: @token_expires_at,
|
|
193
|
+
expired: token_expired?
|
|
194
|
+
}
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
private
|
|
198
|
+
|
|
199
|
+
def token_expired?
|
|
200
|
+
@token.nil? || @token_expires_at.nil? || Time.now >= @token_expires_at
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def refresh_token!
|
|
204
|
+
@token = @auth_client.auth.authenticate(@username, @password)
|
|
205
|
+
@token_expires_at = Time.now + 23.hours # 23 hours to be safe (Mintsoft tokens typically last 24h)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Usage
|
|
210
|
+
token_manager = TokenManager.new('username', 'password')
|
|
211
|
+
client = token_manager.client
|
|
212
|
+
puts "Token info: #{token_manager.token_info}"
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Example 3: Token with Redis Storage
|
|
216
|
+
```ruby
|
|
217
|
+
class RedisTokenManager
|
|
218
|
+
def initialize(username, password, redis_client)
|
|
219
|
+
@username = username
|
|
220
|
+
@password = password
|
|
221
|
+
@redis = redis_client
|
|
222
|
+
@token_key = "mintsoft:token:#{username}"
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def current_token
|
|
226
|
+
token = @redis.get(@token_key)
|
|
227
|
+
|
|
228
|
+
if token.nil?
|
|
229
|
+
token = fetch_and_store_token
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
token
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def client
|
|
236
|
+
Mintsoft::Client.new(token: current_token)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
private
|
|
240
|
+
|
|
241
|
+
def fetch_and_store_token
|
|
242
|
+
token = get_mintsoft_token(@username, @password)
|
|
243
|
+
|
|
244
|
+
# Store with 23 hour expiry (1 hour buffer)
|
|
245
|
+
@redis.setex(@token_key, 23.hours.to_i, token)
|
|
246
|
+
|
|
247
|
+
token
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Usage
|
|
252
|
+
redis = Redis.new(url: ENV['REDIS_URL'])
|
|
253
|
+
token_manager = RedisTokenManager.new('username', 'password', redis)
|
|
254
|
+
client = token_manager.client
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Example 4: Error Handling with Retry
|
|
258
|
+
```ruby
|
|
259
|
+
class RobustTokenManager
|
|
260
|
+
def initialize(username, password)
|
|
261
|
+
@username = username
|
|
262
|
+
@password = password
|
|
263
|
+
@token = nil
|
|
264
|
+
@token_expires_at = nil
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def execute_with_client(&block)
|
|
268
|
+
client = Mintsoft::Client.new(token: current_token)
|
|
269
|
+
|
|
270
|
+
begin
|
|
271
|
+
block.call(client)
|
|
272
|
+
rescue Mintsoft::AuthenticationError => e
|
|
273
|
+
if e.status_code == 401
|
|
274
|
+
# Token expired, refresh and retry once
|
|
275
|
+
invalidate_token!
|
|
276
|
+
client = Mintsoft::Client.new(token: current_token)
|
|
277
|
+
block.call(client)
|
|
278
|
+
else
|
|
279
|
+
raise e
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
private
|
|
285
|
+
|
|
286
|
+
def current_token
|
|
287
|
+
if token_expired?
|
|
288
|
+
refresh_token!
|
|
289
|
+
end
|
|
290
|
+
@token
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def invalidate_token!
|
|
294
|
+
@token = nil
|
|
295
|
+
@token_expires_at = nil
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def token_expired?
|
|
299
|
+
@token.nil? || @token_expires_at.nil? || Time.now >= @token_expires_at
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def refresh_token!
|
|
303
|
+
@token = get_mintsoft_token(@username, @password)
|
|
304
|
+
@token_expires_at = Time.now + 23.hours
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Usage
|
|
309
|
+
token_manager = RobustTokenManager.new('username', 'password')
|
|
310
|
+
|
|
311
|
+
result = token_manager.execute_with_client do |client|
|
|
312
|
+
orders = client.orders.search("ORD-2024-001")
|
|
313
|
+
# ... rest of workflow
|
|
314
|
+
orders
|
|
315
|
+
end
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
## Complete Workflow Example
|
|
319
|
+
|
|
320
|
+
```ruby
|
|
321
|
+
# Step 1: Get token using AuthClient
|
|
322
|
+
auth_client = Mintsoft::AuthClient.new
|
|
323
|
+
auth_response = auth_client.auth.authenticate(
|
|
324
|
+
ENV['MINTSOFT_USERNAME'],
|
|
325
|
+
ENV['MINTSOFT_PASSWORD']
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# Step 2: Use token with main client
|
|
329
|
+
client = Mintsoft::Client.new(token: auth_response.token)
|
|
330
|
+
|
|
331
|
+
begin
|
|
332
|
+
# 1. Search order
|
|
333
|
+
orders = client.orders.search("ORD-2024-001")
|
|
334
|
+
order = orders.first
|
|
335
|
+
raise "Order not found" unless order
|
|
336
|
+
|
|
337
|
+
# 2. Get return reasons
|
|
338
|
+
reasons = client.returns.reasons
|
|
339
|
+
damage_reason = reasons.first
|
|
340
|
+
|
|
341
|
+
# 3. Create return
|
|
342
|
+
return_obj = client.returns.create(order.id)
|
|
343
|
+
|
|
344
|
+
# 4. Add item
|
|
345
|
+
client.returns.add_item(return_obj.id, {
|
|
346
|
+
product_id: 123,
|
|
347
|
+
quantity: 2,
|
|
348
|
+
reason_id: damage_reason.id,
|
|
349
|
+
unit_value: 25.00
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
puts "Return created successfully!"
|
|
353
|
+
|
|
354
|
+
rescue Mintsoft::AuthenticationError => e
|
|
355
|
+
if e.status_code == 401
|
|
356
|
+
puts "Token expired or invalid - please obtain new token"
|
|
357
|
+
# User must handle token renewal and retry
|
|
358
|
+
end
|
|
359
|
+
rescue Mintsoft::APIError => e
|
|
360
|
+
puts "API Error: #{e.message}"
|
|
361
|
+
end
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
## File Structure (Final)
|
|
365
|
+
|
|
366
|
+
```
|
|
367
|
+
lib/
|
|
368
|
+
├── mintsoft.rb # Main entry point
|
|
369
|
+
├── mintsoft/
|
|
370
|
+
│ ├── version.rb # Version constant
|
|
371
|
+
│ ├── client.rb # Main API client (Faraday-based, token-only)
|
|
372
|
+
│ ├── auth_client.rb # Authentication client (Faraday-based)
|
|
373
|
+
│ ├── base.rb # Base OpenStruct object
|
|
374
|
+
│ ├── errors.rb # Error classes
|
|
375
|
+
│ ├── resources/
|
|
376
|
+
│ │ ├── base_resource.rb # Base resource with common Faraday patterns
|
|
377
|
+
│ │ ├── auth.rb # Auth resource (POST /api/auth)
|
|
378
|
+
│ │ ├── orders.rb # Orders.search
|
|
379
|
+
│ │ └── returns.rb # Returns.reasons, create, add_item
|
|
380
|
+
│ └── objects/
|
|
381
|
+
│ ├── order.rb # Order object
|
|
382
|
+
│ ├── return.rb # Return with nested items
|
|
383
|
+
│ └── return_reason.rb # ReturnReason object
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
## Key Benefits
|
|
387
|
+
|
|
388
|
+
### 1. **Extreme Simplicity**
|
|
389
|
+
- Client only needs token parameter
|
|
390
|
+
- No authentication logic in gem
|
|
391
|
+
- Minimal surface area for bugs
|
|
392
|
+
|
|
393
|
+
### 2. **User Control**
|
|
394
|
+
- User decides token storage strategy
|
|
395
|
+
- User handles token expiration as needed
|
|
396
|
+
- User controls when to refresh tokens
|
|
397
|
+
|
|
398
|
+
### 3. **Security**
|
|
399
|
+
- No credential handling in gem
|
|
400
|
+
- User controls sensitive data
|
|
401
|
+
- Clear separation of concerns
|
|
402
|
+
|
|
403
|
+
### 4. **Flexibility**
|
|
404
|
+
- Works with any token management strategy
|
|
405
|
+
- Easy to integrate with existing auth systems
|
|
406
|
+
- Supports different storage backends (Redis, database, etc.)
|
|
407
|
+
|
|
408
|
+
This design makes the gem as simple as possible while giving users complete control over authentication and token management.
|