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.
@@ -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.