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,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "faraday/net_http"
5
+
6
+ module Mintsoft
7
+ class AuthClient
8
+ BASE_URL = "https://api.mintsoft.com"
9
+
10
+ attr_reader :base_url, :conn_opts
11
+
12
+ def initialize(base_url: BASE_URL, conn_opts: {})
13
+ @base_url = base_url
14
+ @conn_opts = conn_opts
15
+ end
16
+
17
+ def connection
18
+ @connection ||= Faraday.new do |conn|
19
+ conn.url_prefix = @base_url
20
+ conn.options.merge!(@conn_opts)
21
+ conn.request :json
22
+ conn.response :json, content_type: "application/json"
23
+ conn.adapter Faraday.default_adapter
24
+ end
25
+ end
26
+
27
+ def auth
28
+ @auth ||= AuthResource.new(self)
29
+ end
30
+
31
+ private
32
+
33
+ class AuthResource
34
+ def initialize(client)
35
+ @client = client
36
+ end
37
+
38
+ def authenticate(username, password)
39
+ validate_credentials!(username, password)
40
+
41
+ response = @client.connection.post("/api/auth") do |req|
42
+ req.body = {
43
+ username: username,
44
+ password: password
45
+ }
46
+ end
47
+
48
+ if response.success?
49
+ handle_success_response(response.body)
50
+ else
51
+ handle_error_response(response)
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def validate_credentials!(username, password)
58
+ raise ValidationError, "Username required" if username.nil? || username.empty?
59
+ raise ValidationError, "Password required" if password.nil? || password.empty?
60
+ end
61
+
62
+ def handle_success_response(body)
63
+ token = body.dig("token") || body.dig("access_token") || body.dig("Token")
64
+ raise APIError, "No token found in response" unless token
65
+
66
+ AuthResponse.new(body.merge("token" => token))
67
+ end
68
+
69
+ def handle_error_response(response)
70
+ case response.status
71
+ when 401
72
+ raise AuthenticationError, "Invalid credentials"
73
+ when 400
74
+ raise ValidationError, "Invalid request: #{extract_error_message(response.body)}"
75
+ else
76
+ raise APIError, "Authentication failed: #{response.status} - #{extract_error_message(response.body)}"
77
+ end
78
+ end
79
+
80
+ def extract_error_message(body)
81
+ return body if body.is_a?(String)
82
+ return body["error"] || body["message"] || body.to_s if body.is_a?(Hash)
83
+
84
+ "Unknown error"
85
+ end
86
+ end
87
+
88
+ class AuthResponse < Base
89
+ def token
90
+ super || access_token || Token
91
+ end
92
+
93
+ def expires_at
94
+ return nil unless expires_in
95
+
96
+ Time.now + expires_in.to_i
97
+ end
98
+
99
+ def expired?
100
+ expires_at && Time.now >= expires_at
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support"
4
+ require "active_support/core_ext/string"
5
+ require "ostruct"
6
+
7
+ module Mintsoft
8
+ class Base < OpenStruct
9
+ def initialize(attributes)
10
+ super to_ostruct(attributes)
11
+ end
12
+
13
+ def to_ostruct(obj)
14
+ if obj.is_a?(Hash)
15
+ OpenStruct.new(obj.transform_keys { |key| key.to_s.underscore }.transform_values { |val| to_ostruct(val) })
16
+ elsif obj.is_a?(Array)
17
+ obj.map { |o| to_ostruct(o) }
18
+ else # Assumed to be a primitive value
19
+ obj
20
+ end
21
+ end
22
+
23
+ # Convert back to hash without table key, including nested structures
24
+ def to_hash
25
+ ostruct_to_hash(self)
26
+ end
27
+
28
+ private
29
+
30
+ def ostruct_to_hash(object)
31
+ case object
32
+ when OpenStruct
33
+ hash = object.to_h.reject { |k, _| k == :table }
34
+ hash.transform_keys(&:to_s).transform_values { |value| ostruct_to_hash(value) }
35
+ when Array
36
+ object.map { |item| ostruct_to_hash(item) }
37
+ when Hash
38
+ object.transform_keys(&:to_s).transform_values { |value| ostruct_to_hash(value) }
39
+ else
40
+ object
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "faraday/net_http"
5
+
6
+ module Mintsoft
7
+ class Client
8
+ BASE_URL = "https://api.mintsoft.com"
9
+
10
+ attr_reader :token, :base_url, :conn_opts
11
+
12
+ def initialize(token:, base_url: BASE_URL, conn_opts: {})
13
+ @token = token
14
+ @base_url = base_url
15
+ @conn_opts = conn_opts
16
+ end
17
+
18
+ def connection
19
+ @connection ||= Faraday.new do |conn|
20
+ conn.url_prefix = @base_url
21
+ conn.options.merge!(@conn_opts)
22
+ conn.request :authorization, :Bearer, @token
23
+ conn.request :json
24
+ conn.response :json, content_type: "application/json"
25
+ conn.adapter Faraday.default_adapter
26
+ end
27
+ end
28
+
29
+ def orders
30
+ @orders ||= Resources::Orders.new(self)
31
+ end
32
+
33
+ def returns
34
+ @returns ||= Resources::Returns.new(self)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mintsoft
4
+ class Error < StandardError; end
5
+ class APIError < Error; end
6
+ class AuthenticationError < APIError; end
7
+ class NotFoundError < APIError; end
8
+ class ValidationError < APIError; end
9
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mintsoft
4
+ module Objects
5
+ class Order < Base
6
+ # Convenience methods for common API response formats
7
+ def order_id
8
+ id || order_id
9
+ end
10
+
11
+ def order_ref
12
+ order_number || order_reference || ref
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mintsoft
4
+ module Objects
5
+ class Return < Base
6
+ # Access nested items as OpenStruct objects
7
+ def items
8
+ return_items || items_array || []
9
+ end
10
+
11
+ def items_count
12
+ items.length
13
+ end
14
+
15
+ # Access item properties through OpenStruct
16
+ def item_quantities
17
+ items.map(&:quantity).sum
18
+ end
19
+
20
+ # Convenience methods for common API response formats
21
+ def return_id
22
+ id || return_id
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mintsoft
4
+ module Objects
5
+ class ReturnReason < Base
6
+ def active?
7
+ active == true
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mintsoft
4
+ module Resources
5
+ class BaseResource
6
+ def initialize(client)
7
+ @client = client
8
+ end
9
+
10
+ protected
11
+
12
+ def get_request(url, params: {})
13
+ @client.connection.get(url) do |req|
14
+ req.params.merge!(params) unless params.empty?
15
+ end
16
+ end
17
+
18
+ def post_request(url, body: {})
19
+ @client.connection.post(url) do |req|
20
+ req.body = body unless body.empty?
21
+ end
22
+ end
23
+
24
+ def handle_response(response)
25
+ if response.success?
26
+ response.body
27
+ else
28
+ handle_error(response)
29
+ end
30
+ end
31
+
32
+ def handle_error(response)
33
+ case response.status
34
+ when 401
35
+ raise AuthenticationError, "Invalid or expired token"
36
+ when 400
37
+ raise ValidationError, "Invalid request data: #{extract_error_message(response.body)}"
38
+ when 404
39
+ raise NotFoundError, "Resource not found"
40
+ else
41
+ raise APIError, "API error: #{response.status} - #{extract_error_message(response.body)}"
42
+ end
43
+ end
44
+
45
+ def extract_error_message(body)
46
+ return body if body.is_a?(String)
47
+ return body["error"] || body["message"] || body.to_s if body.is_a?(Hash)
48
+
49
+ "Unknown error"
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mintsoft
4
+ module Resources
5
+ class Orders < BaseResource
6
+ def search(order_number)
7
+ validate_order_number!(order_number)
8
+
9
+ response = get_request("/api/Order/Search", params: {"OrderNumber" => order_number})
10
+
11
+ if response.success?
12
+ parse_orders(response.body)
13
+ else
14
+ case response.status
15
+ when 404
16
+ [] # Return empty array for not found
17
+ else
18
+ handle_error(response)
19
+ end
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def validate_order_number!(order_number)
26
+ raise ValidationError, "Order number required" if order_number.nil? || order_number.empty?
27
+ end
28
+
29
+ def parse_orders(data)
30
+ return [] unless data.is_a?(Array)
31
+
32
+ data.map { |order_data| Objects::Order.new(order_data) }
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mintsoft
4
+ module Resources
5
+ class Returns < BaseResource
6
+ def reasons
7
+ response = get_request("/api/Return/Reasons")
8
+
9
+ if response.success?
10
+ parse_reasons(response.body)
11
+ else
12
+ handle_error(response)
13
+ end
14
+ end
15
+
16
+ def create(order_id)
17
+ validate_order_id!(order_id)
18
+
19
+ response = post_request("/api/Return/CreateReturn/#{order_id}")
20
+
21
+ if response.success?
22
+ # Extract return ID from ToolkitResult and create Return object
23
+ return_id = extract_return_id(response.body)
24
+ Objects::Return.new({
25
+ "id" => return_id,
26
+ "order_id" => order_id,
27
+ "status" => "pending"
28
+ })
29
+ else
30
+ handle_error(response)
31
+ end
32
+ end
33
+
34
+ def add_item(return_id, item_attributes)
35
+ validate_return_id!(return_id)
36
+ validate_item_attributes!(item_attributes)
37
+
38
+ payload = format_item_payload(item_attributes)
39
+ response = post_request("/api/Return/#{return_id}/AddItem", body: payload)
40
+
41
+ if response.success?
42
+ true # Simple success indicator
43
+ else
44
+ handle_error(response)
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def validate_order_id!(order_id)
51
+ raise ValidationError, "Order ID required" unless order_id&.to_i&.positive?
52
+ end
53
+
54
+ def validate_return_id!(return_id)
55
+ raise ValidationError, "Return ID required" unless return_id&.to_i&.positive?
56
+ end
57
+
58
+ def validate_item_attributes!(attrs)
59
+ required = [:product_id, :quantity, :reason_id]
60
+ required.each do |field|
61
+ raise ValidationError, "#{field} required" unless attrs[field]
62
+ end
63
+ raise ValidationError, "Quantity must be positive" unless attrs[:quantity].to_i > 0
64
+ end
65
+
66
+ def parse_reasons(data)
67
+ return [] unless data.is_a?(Array)
68
+
69
+ data.map { |reason_data| Objects::ReturnReason.new(reason_data) }
70
+ end
71
+
72
+ def extract_return_id(toolkit_result)
73
+ # Parse ToolkitResult to extract return ID - handles various response formats
74
+ toolkit_result.dig("result", "return_id") ||
75
+ toolkit_result.dig("data", "id") ||
76
+ toolkit_result["id"]
77
+ end
78
+
79
+ def format_item_payload(attrs)
80
+ {
81
+ "ProductId" => attrs[:product_id],
82
+ "Quantity" => attrs[:quantity],
83
+ "ReasonId" => attrs[:reason_id],
84
+ "UnitValue" => attrs[:unit_value],
85
+ "Notes" => attrs[:notes]
86
+ }.compact
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mintsoft
4
+ VERSION = "0.1.0"
5
+ end
data/lib/mintsoft.rb ADDED
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "mintsoft/version"
4
+ require_relative "mintsoft/errors"
5
+ require_relative "mintsoft/base"
6
+ require_relative "mintsoft/auth_client"
7
+ require_relative "mintsoft/client"
8
+ require_relative "mintsoft/resources/base_resource"
9
+ require_relative "mintsoft/resources/orders"
10
+ require_relative "mintsoft/resources/returns"
11
+ require_relative "mintsoft/objects/order"
12
+ require_relative "mintsoft/objects/return"
13
+ require_relative "mintsoft/objects/return_reason"
14
+
15
+ module Mintsoft
16
+ # Main entry point for the Mintsoft API wrapper
17
+ end
data/sig/mintsoft.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Mintsoft
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,161 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mintsoft
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Andy Chong
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: faraday
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: faraday-net_http
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '3.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '3.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: activesupport
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '7.0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '7.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rspec
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '3.0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '3.0'
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.0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '3.0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: vcr
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '6.0'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '6.0'
96
+ description: A Ruby gem for interacting with the Mintsoft API
97
+ email:
98
+ - andygg1996personal@gmail.com
99
+ executables: []
100
+ extensions: []
101
+ extra_rdoc_files: []
102
+ files:
103
+ - ".rspec"
104
+ - ".serena/memories/code_style_conventions.md"
105
+ - ".serena/memories/codebase_structure.md"
106
+ - ".serena/memories/development_environment.md"
107
+ - ".serena/memories/project_overview.md"
108
+ - ".serena/memories/suggested_commands.md"
109
+ - ".serena/memories/task_completion_checklist.md"
110
+ - ".serena/project.yml"
111
+ - ".standard.yml"
112
+ - CHANGELOG.md
113
+ - CODE_OF_CONDUCT.md
114
+ - LICENSE.txt
115
+ - README.md
116
+ - Rakefile
117
+ - claudedocs/AUTH_CLIENT_DESIGN.md
118
+ - claudedocs/FINAL_SIMPLIFIED_DESIGN.md
119
+ - claudedocs/IMPLEMENTATION_SUMMARY.md
120
+ - claudedocs/INDEX.md
121
+ - claudedocs/MINIMAL_IMPLEMENTATION_PLAN.md
122
+ - claudedocs/TOKEN_ONLY_CLIENT_DESIGN.md
123
+ - claudedocs/USAGE_EXAMPLES.md
124
+ - examples/complete_workflow.rb
125
+ - lib/mintsoft.rb
126
+ - lib/mintsoft/auth_client.rb
127
+ - lib/mintsoft/base.rb
128
+ - lib/mintsoft/client.rb
129
+ - lib/mintsoft/errors.rb
130
+ - lib/mintsoft/objects/order.rb
131
+ - lib/mintsoft/objects/return.rb
132
+ - lib/mintsoft/objects/return_reason.rb
133
+ - lib/mintsoft/resources/base_resource.rb
134
+ - lib/mintsoft/resources/orders.rb
135
+ - lib/mintsoft/resources/returns.rb
136
+ - lib/mintsoft/version.rb
137
+ - sig/mintsoft.rbs
138
+ homepage: https://github.com/Postco/mintsoft
139
+ licenses:
140
+ - MIT
141
+ metadata:
142
+ homepage_uri: https://github.com/Postco/mintsoft
143
+ source_code_uri: https://github.com/Postco/mintsoft
144
+ rdoc_options: []
145
+ require_paths:
146
+ - lib
147
+ required_ruby_version: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ">="
150
+ - !ruby/object:Gem::Version
151
+ version: 3.0.0
152
+ required_rubygems_version: !ruby/object:Gem::Requirement
153
+ requirements:
154
+ - - ">="
155
+ - !ruby/object:Gem::Version
156
+ version: '0'
157
+ requirements: []
158
+ rubygems_version: 3.7.1
159
+ specification_version: 4
160
+ summary: Mintsoft API Wrapper
161
+ test_files: []