epos_now_client 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 486c114791db7ffda314246e447b5c03a56879dc08a8551d9e695338ad02bdbe
4
+ data.tar.gz: 6c43a07100a3f83a2fbce1feea144aaa4eda78b0d11cf1e636a7309d413bf9ea
5
+ SHA512:
6
+ metadata.gz: a64dbe7f3883d91a09ef3dca33a721571dbe2c574f88462fdfe6ac50207e0a69919b2ac9976207f63521e1f68bda6950b57cf0d976d8decefddb3bac81ec494e
7
+ data.tar.gz: df5c985d5b28e3728ca1a92792b4e0635f5f7dac069e855c8c272e0a860a3e46a406bfcc0484e4f673a4ac074179206823f3b4006136e871ecc0151b4431d1e9
data/CHANGELOG.md ADDED
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2026-03-13
9
+
10
+ ### Added
11
+
12
+ - Initial release
13
+ - Basic Auth configuration with API Key and Secret
14
+ - HTTP connection layer with retry logic and exponential backoff
15
+ - Automatic pagination (200 items/page)
16
+ - 10 resource classes: Products, Categories, Transactions, TenderTypes, TaxGroups, Customers, Staff, Devices, Brands, Locations
17
+ - Search and filtering support via Epos Now query syntax
18
+ - Structured error hierarchy: AuthenticationError, RateLimitError, NotFoundError, ValidationError, ServerError, ConnectionError, TimeoutError
19
+ - Per-client and global configuration
20
+ - Optional logging support
21
+ - 131 specs with 100% line and branch coverage
22
+ - RuboCop with 0 offenses
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TheOwnerStack
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,326 @@
1
+ # EposNowClient
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/epos_now_client.svg)](https://rubygems.org/gems/epos_now_client)
4
+ [![CI](https://github.com/dan1d/epos_now_client/actions/workflows/ci.yml/badge.svg)](https://github.com/dan1d/epos_now_client/actions)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE.txt)
6
+
7
+ A lightweight, fully-tested Ruby client for the [Epos Now REST API (V4)](https://developer.eposnowhq.com).
8
+
9
+ **131 specs | 100% line coverage | 100% branch coverage | 0 RuboCop offenses**
10
+
11
+ ## Features
12
+
13
+ - **Basic Auth** with API Key + Secret (Base64-encoded)
14
+ - **10 resource classes**: Products, Categories, Transactions, TenderTypes, TaxGroups, Customers, Staff, Devices, Brands, Locations
15
+ - **Automatic pagination** (200 items/page, fetches all pages)
16
+ - **Retry with exponential backoff** on transient network errors
17
+ - **Structured error hierarchy** for clean exception handling
18
+ - **Search & filtering** using Epos Now query syntax
19
+ - **Zero runtime dependencies** beyond HTTParty
20
+
21
+ ## Installation
22
+
23
+ Add to your Gemfile:
24
+
25
+ ```ruby
26
+ gem 'epos_now_client'
27
+ ```
28
+
29
+ Then run:
30
+
31
+ ```bash
32
+ bundle install
33
+ ```
34
+
35
+ Or install directly:
36
+
37
+ ```bash
38
+ gem install epos_now_client
39
+ ```
40
+
41
+ ## Quick Start
42
+
43
+ ```ruby
44
+ require 'epos_now_client'
45
+
46
+ client = EposNowClient::Client.new(
47
+ api_key: 'your_api_key',
48
+ api_secret: 'your_api_secret'
49
+ )
50
+
51
+ # Fetch all products
52
+ products = client.products.all
53
+
54
+ # Create a category
55
+ client.categories.create_category({ 'Name' => 'Drinks' })
56
+
57
+ # Search products
58
+ client.products.search('(Name|contains|Burger)')
59
+ ```
60
+
61
+ ## Authentication
62
+
63
+ Epos Now uses **Basic Auth** with an API Key and Secret. Get your credentials from the Epos Now Backoffice:
64
+
65
+ 1. Navigate to **Web Integrations > REST API**
66
+ 2. Click **Add Device**
67
+ 3. Copy the **API Key** and **API Secret**
68
+
69
+ > **Note:** Epos Now uses a single API endpoint (`https://api.eposnowhq.com`) for all environments. There is no separate sandbox URL. Developer accounts use the same API with test credentials.
70
+
71
+ ## Configuration
72
+
73
+ ### Global (recommended for single-account apps)
74
+
75
+ ```ruby
76
+ EposNowClient.configure do |config|
77
+ config.api_key = ENV['EPOS_NOW_API_KEY']
78
+ config.api_secret = ENV['EPOS_NOW_API_SECRET']
79
+ config.base_url = 'https://api.eposnowhq.com' # default
80
+ config.timeout = 30 # default (seconds)
81
+ config.open_timeout = 10 # default (seconds)
82
+ config.max_retries = 3 # default
83
+ config.retry_delay = 1.0 # default (seconds, doubles each retry)
84
+ config.per_page = 200 # default
85
+ config.logger = Logger.new($stdout) # optional
86
+ end
87
+
88
+ client = EposNowClient::Client.new
89
+ ```
90
+
91
+ ### Per-client (for multi-account apps)
92
+
93
+ ```ruby
94
+ client = EposNowClient::Client.new(
95
+ api_key: 'merchant_api_key',
96
+ api_secret: 'merchant_api_secret',
97
+ timeout: 60
98
+ )
99
+ ```
100
+
101
+ ### Rails initializer
102
+
103
+ ```ruby
104
+ # config/initializers/epos_now.rb
105
+ EposNowClient.configure do |config|
106
+ config.api_key = Rails.application.credentials.dig(:epos_now, :api_key)
107
+ config.api_secret = Rails.application.credentials.dig(:epos_now, :api_secret)
108
+ config.logger = Rails.logger
109
+ end
110
+ ```
111
+
112
+ ## Usage
113
+
114
+ ### Categories
115
+
116
+ ```ruby
117
+ client.categories.all # List all (paginated)
118
+ client.categories.find(42) # Get by ID
119
+ client.categories.create_category({ 'Name' => 'Drinks' }) # Create
120
+ client.categories.update_category(42, { 'Name' => 'Bev' }) # Update
121
+ client.categories.delete_category(42) # Delete
122
+ ```
123
+
124
+ ### Products
125
+
126
+ ```ruby
127
+ client.products.all # List all
128
+ client.products.find(10) # Get by ID
129
+ client.products.create_product({ # Create
130
+ 'Name' => 'Burger',
131
+ 'SalePrice' => 9.99,
132
+ 'CategoryId' => 1
133
+ })
134
+ client.products.update_product(10, { 'SalePrice' => 11.99 }) # Update
135
+ client.products.delete_product(10) # Delete
136
+ client.products.search('(Name|contains|Burg)') # Search
137
+ ```
138
+
139
+ ### Transactions
140
+
141
+ ```ruby
142
+ client.transactions.all # List all
143
+ client.transactions.find(100) # Get by ID
144
+ client.transactions.by_date( # By date range
145
+ start_date: '2026-03-01',
146
+ end_date: '2026-03-13'
147
+ )
148
+ client.transactions.latest # Latest
149
+ client.transactions.create_transaction({ # Create
150
+ 'TransactionItems' => [{ 'ProductId' => 1, 'Quantity' => 2 }],
151
+ 'Tenders' => [{ 'TenderTypeId' => 1, 'Amount' => 25.50 }],
152
+ 'ServiceType' => 0 # 0=EatIn, 1=Takeaway, 2=Delivery
153
+ })
154
+ client.transactions.delete_transaction(200) # Delete
155
+ client.transactions.validate(payload) # Validate
156
+ ```
157
+
158
+ ### Tender Types
159
+
160
+ ```ruby
161
+ client.tender_types.all
162
+ client.tender_types.find(1)
163
+ client.tender_types.create_tender_type({ 'Name' => 'Gift Card' })
164
+ ```
165
+
166
+ ### Tax Groups
167
+
168
+ ```ruby
169
+ client.tax_groups.all
170
+ client.tax_groups.find(1)
171
+ ```
172
+
173
+ ### Customers
174
+
175
+ ```ruby
176
+ client.customers.all
177
+ client.customers.find(1)
178
+ client.customers.create_customer({ 'FirstName' => 'Jane', 'LastName' => 'Doe' })
179
+ client.customers.update_customer(1, { 'FirstName' => 'Janet' })
180
+ client.customers.delete_customer(1)
181
+ client.customers.search('(FirstName|contains|Jane)')
182
+ ```
183
+
184
+ ### Staff
185
+
186
+ ```ruby
187
+ client.staff.all
188
+ client.staff.find(1)
189
+ client.staff.create_staff({ 'FirstName' => 'Bob', 'Pin' => '1234' })
190
+ client.staff.update_staff(1, { 'FirstName' => 'Robert' })
191
+ client.staff.delete_staff(1)
192
+ ```
193
+
194
+ ### Devices
195
+
196
+ ```ruby
197
+ client.devices.all
198
+ client.devices.find(1)
199
+ ```
200
+
201
+ ### Brands
202
+
203
+ ```ruby
204
+ client.brands.all
205
+ client.brands.find(1)
206
+ client.brands.create_brand({ 'Name' => 'Coca-Cola' })
207
+ client.brands.update_brand(1, { 'Name' => 'PepsiCo' })
208
+ client.brands.delete_brand(1)
209
+ ```
210
+
211
+ ### Locations
212
+
213
+ ```ruby
214
+ client.locations.all
215
+ client.locations.find(1)
216
+ ```
217
+
218
+ ## Pagination
219
+
220
+ All `.all` methods automatically paginate through every page (200 items per page):
221
+
222
+ ```ruby
223
+ # Fetches ALL products, regardless of how many pages exist
224
+ all_products = client.products.all
225
+ ```
226
+
227
+ ## Search & Filtering
228
+
229
+ Epos Now supports search filters using the syntax `(PropertyName|Operator|Value)`:
230
+
231
+ ```ruby
232
+ # Available operators: contains, StartsWith, EndsWith, >, >=, <, <=, like
233
+ client.products.search('(Name|contains|Burger)')
234
+ client.customers.search('(FirstName|StartsWith|J)')
235
+ client.products.search('(SalePrice|>|10.00)')
236
+ ```
237
+
238
+ ## Error Handling
239
+
240
+ All errors inherit from `EposNowClient::Error` and include `status` and `body` attributes:
241
+
242
+ ```ruby
243
+ begin
244
+ client.products.find(999)
245
+ rescue EposNowClient::AuthenticationError => e
246
+ # 401 - Invalid API key or secret
247
+ rescue EposNowClient::NotFoundError => e
248
+ # 404 - Resource not found
249
+ rescue EposNowClient::ValidationError => e
250
+ # 422 - Invalid request payload
251
+ rescue EposNowClient::RateLimitError => e
252
+ # 429 - API rate limit exceeded
253
+ rescue EposNowClient::ServerError => e
254
+ # 500-599 - Epos Now server error
255
+ rescue EposNowClient::ConnectionError => e
256
+ # Network errors (after all retries exhausted)
257
+ rescue EposNowClient::Error => e
258
+ # Catch-all
259
+ e.status # HTTP status code (Integer or nil)
260
+ e.body # Parsed response body (Hash, String, or nil)
261
+ e.message # Human-readable error message
262
+ end
263
+ ```
264
+
265
+ ## Retry Logic
266
+
267
+ The client automatically retries on transient network errors with exponential backoff:
268
+
269
+ - `Errno::ECONNRESET`, `Errno::ECONNREFUSED`, `Errno::ETIMEDOUT`
270
+ - `Net::OpenTimeout`, `Net::ReadTimeout`
271
+ - `SocketError`
272
+
273
+ ```ruby
274
+ EposNowClient.configure do |config|
275
+ config.max_retries = 3 # Retry up to 3 times (default)
276
+ config.retry_delay = 1.0 # 1s, 2s, 4s backoff (default)
277
+ end
278
+ ```
279
+
280
+ ## API Reference
281
+
282
+ | Resource | Methods |
283
+ |----------|---------|
284
+ | `client.categories` | `all`, `find`, `create_category`, `update_category`, `delete_category` |
285
+ | `client.products` | `all`, `find`, `create_product`, `update_product`, `delete_product`, `search` |
286
+ | `client.transactions` | `all`, `find`, `create_transaction`, `delete_transaction`, `by_date`, `latest`, `validate` |
287
+ | `client.tender_types` | `all`, `find`, `create_tender_type` |
288
+ | `client.tax_groups` | `all`, `find` |
289
+ | `client.customers` | `all`, `find`, `create_customer`, `update_customer`, `delete_customer`, `search` |
290
+ | `client.staff` | `all`, `find`, `create_staff`, `update_staff`, `delete_staff` |
291
+ | `client.devices` | `all`, `find` |
292
+ | `client.brands` | `all`, `find`, `create_brand`, `update_brand`, `delete_brand` |
293
+ | `client.locations` | `all`, `find` |
294
+
295
+ ## Development
296
+
297
+ ```bash
298
+ git clone https://github.com/dan1d/epos_now_client.git
299
+ cd epos_now_client
300
+ bundle install
301
+
302
+ # Run the full suite
303
+ bundle exec rake # RuboCop + RSpec
304
+
305
+ # Or individually
306
+ bundle exec rspec # 131 specs, 100% line + branch coverage
307
+ bundle exec rubocop # 0 offenses
308
+
309
+ # View coverage report
310
+ open coverage/index.html
311
+ ```
312
+
313
+ ## Contributing
314
+
315
+ 1. Fork it
316
+ 2. Create your feature branch (`git checkout -b feature/my-feature`)
317
+ 3. Write tests first (TDD)
318
+ 4. Ensure 100% coverage (`bundle exec rspec`)
319
+ 5. Ensure 0 RuboCop offenses (`bundle exec rubocop`)
320
+ 6. Commit your changes (`git commit -m 'Add my feature'`)
321
+ 7. Push to the branch (`git push origin feature/my-feature`)
322
+ 8. Create a Pull Request
323
+
324
+ ## License
325
+
326
+ MIT License. See [LICENSE.txt](LICENSE.txt) for details.
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EposNowClient
4
+ class Client
5
+ attr_reader :configuration, :connection
6
+
7
+ def initialize(api_key: nil, api_secret: nil, base_url: nil, **)
8
+ @configuration = build_configuration(api_key, api_secret, base_url, **)
9
+ @connection = Connection.new(@configuration)
10
+ end
11
+
12
+ def categories
13
+ @categories ||= Resources::Categories.new(connection)
14
+ end
15
+
16
+ def products
17
+ @products ||= Resources::Products.new(connection)
18
+ end
19
+
20
+ def transactions
21
+ @transactions ||= Resources::Transactions.new(connection)
22
+ end
23
+
24
+ def tender_types
25
+ @tender_types ||= Resources::TenderTypes.new(connection)
26
+ end
27
+
28
+ def tax_groups
29
+ @tax_groups ||= Resources::TaxGroups.new(connection)
30
+ end
31
+
32
+ def customers
33
+ @customers ||= Resources::Customers.new(connection)
34
+ end
35
+
36
+ def staff
37
+ @staff ||= Resources::Staff.new(connection)
38
+ end
39
+
40
+ def devices
41
+ @devices ||= Resources::Devices.new(connection)
42
+ end
43
+
44
+ def brands
45
+ @brands ||= Resources::Brands.new(connection)
46
+ end
47
+
48
+ def locations
49
+ @locations ||= Resources::Locations.new(connection)
50
+ end
51
+
52
+ private
53
+
54
+ def build_configuration(api_key, api_secret, base_url, **options)
55
+ config = EposNowClient.configuration.dup
56
+ config.api_key = api_key if api_key
57
+ config.api_secret = api_secret if api_secret
58
+ config.base_url = base_url if base_url
59
+ options.each { |key, value| config.send(:"#{key}=", value) }
60
+ config
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+
5
+ module EposNowClient
6
+ class Configuration
7
+ attr_accessor :api_key, :api_secret, :timeout, :open_timeout,
8
+ :max_retries, :retry_delay, :logger, :per_page
9
+ attr_reader :base_url
10
+
11
+ DEFAULT_BASE_URL = 'https://api.eposnowhq.com'
12
+ DEFAULT_TIMEOUT = 30
13
+ DEFAULT_OPEN_TIMEOUT = 10
14
+ DEFAULT_MAX_RETRIES = 3
15
+ DEFAULT_RETRY_DELAY = 1.0
16
+ DEFAULT_PER_PAGE = 200
17
+
18
+ def initialize
19
+ @api_key = nil
20
+ @api_secret = nil
21
+ @base_url = DEFAULT_BASE_URL
22
+ @timeout = DEFAULT_TIMEOUT
23
+ @open_timeout = DEFAULT_OPEN_TIMEOUT
24
+ @max_retries = DEFAULT_MAX_RETRIES
25
+ @retry_delay = DEFAULT_RETRY_DELAY
26
+ @per_page = DEFAULT_PER_PAGE
27
+ @logger = nil
28
+ end
29
+
30
+ def auth_token
31
+ validate!
32
+ Base64.strict_encode64("#{api_key}:#{api_secret}")
33
+ end
34
+
35
+ def validate!
36
+ raise AuthenticationError, 'API key is required' if api_key.nil? || api_key.empty?
37
+ raise AuthenticationError, 'API secret is required' if api_secret.nil? || api_secret.empty?
38
+ end
39
+
40
+ def base_url=(url)
41
+ @base_url = url&.chomp('/')
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'httparty'
4
+ require 'json'
5
+
6
+ module EposNowClient
7
+ class Connection
8
+ RETRYABLE_ERRORS = [
9
+ Errno::ECONNRESET,
10
+ Errno::ECONNREFUSED,
11
+ Errno::ETIMEDOUT,
12
+ Net::OpenTimeout,
13
+ Net::ReadTimeout,
14
+ SocketError
15
+ ].freeze
16
+
17
+ RETRYABLE_STATUS_CODES = [502, 503, 504].freeze
18
+
19
+ def initialize(configuration)
20
+ @configuration = configuration
21
+ end
22
+
23
+ def get(path, params: {})
24
+ request(:get, path, query: params)
25
+ end
26
+
27
+ def post(path, body: {})
28
+ request(:post, path, body: body.to_json)
29
+ end
30
+
31
+ def put(path, body: {})
32
+ request(:put, path, body: body.to_json)
33
+ end
34
+
35
+ def delete(path, body: nil)
36
+ options = {}
37
+ options[:body] = body.to_json if body
38
+ request(:delete, path, **options)
39
+ end
40
+
41
+ private
42
+
43
+ def request(method, path, **)
44
+ url = build_url(path)
45
+ attempts = 0
46
+
47
+ begin
48
+ attempts += 1
49
+ response = execute_request(method, url, **)
50
+ handle_response(response)
51
+ rescue *RETRYABLE_ERRORS => e
52
+ raise ConnectionError, "Connection failed: #{e.message}" unless retryable?(attempts)
53
+
54
+ sleep_before_retry(attempts)
55
+ retry
56
+ end
57
+ end
58
+
59
+ def execute_request(method, url, **)
60
+ HTTParty.send(
61
+ method,
62
+ url,
63
+ **default_options, **
64
+ )
65
+ end
66
+
67
+ def default_options
68
+ {
69
+ headers: {
70
+ 'Authorization' => "Basic #{@configuration.auth_token}",
71
+ 'Content-Type' => 'application/json',
72
+ 'Accept' => 'application/json'
73
+ },
74
+ timeout: @configuration.timeout,
75
+ open_timeout: @configuration.open_timeout
76
+ }
77
+ end
78
+
79
+ def build_url(path)
80
+ "#{@configuration.base_url}/api/v4/#{path}"
81
+ end
82
+
83
+ def handle_response(response)
84
+ log_response(response)
85
+
86
+ return parse_body(response) if response.code.between?(200, 204)
87
+
88
+ raise_error_for(response)
89
+ end
90
+
91
+ def raise_error_for(response)
92
+ error_class, message = error_mapping(response)
93
+ raise error_class.new(message, status: response.code, body: parse_body(response))
94
+ end
95
+
96
+ def error_mapping(response)
97
+ case response.code
98
+ when 401 then [AuthenticationError, 'Authentication failed (401)']
99
+ when 404 then [NotFoundError, 'Resource not found (404)']
100
+ when 422 then [ValidationError, "Validation failed (422): #{response.body}"]
101
+ when 429 then [RateLimitError, 'Rate limit exceeded (429)']
102
+ when 500..599 then [ServerError, "Server error (#{response.code})"]
103
+ else [Error, "Unexpected response (#{response.code}): #{response.body}"]
104
+ end
105
+ end
106
+
107
+ def parse_body(response)
108
+ return nil if response.body.nil? || response.body.empty?
109
+
110
+ JSON.parse(response.body)
111
+ rescue JSON::ParserError
112
+ response.body
113
+ end
114
+
115
+ def retryable?(attempts)
116
+ attempts < @configuration.max_retries
117
+ end
118
+
119
+ def sleep_before_retry(attempts)
120
+ delay = @configuration.retry_delay * (2**(attempts - 1))
121
+ log_retry(attempts, delay)
122
+ sleep(delay)
123
+ end
124
+
125
+ def log_response(response)
126
+ return unless @configuration.logger
127
+
128
+ @configuration.logger.debug(
129
+ "EposNowClient [#{response.code}] #{response.request.http_method::METHOD} #{response.request.uri}"
130
+ )
131
+ end
132
+
133
+ def log_retry(attempts, delay)
134
+ return unless @configuration.logger
135
+
136
+ @configuration.logger.warn(
137
+ "EposNowClient retry #{attempts}/#{@configuration.max_retries} in #{delay}s"
138
+ )
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EposNowClient
4
+ class Error < StandardError
5
+ attr_reader :status, :body
6
+
7
+ def initialize(message = nil, status: nil, body: nil)
8
+ @status = status
9
+ @body = body
10
+ super(message)
11
+ end
12
+ end
13
+
14
+ class AuthenticationError < Error; end
15
+ class RateLimitError < Error; end
16
+ class NotFoundError < Error; end
17
+ class ValidationError < Error; end
18
+ class ServerError < Error; end
19
+ class ConnectionError < Error; end
20
+ class TimeoutError < Error; end
21
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EposNowClient
4
+ module Resources
5
+ class BaseResource
6
+ attr_reader :connection
7
+
8
+ def initialize(connection)
9
+ @connection = connection
10
+ end
11
+
12
+ private
13
+
14
+ def resource_name
15
+ raise NotImplementedError, "#{self.class} must implement #resource_name"
16
+ end
17
+
18
+ def list(params = {})
19
+ connection.get(resource_name, params: params)
20
+ end
21
+
22
+ def fetch(id)
23
+ connection.get("#{resource_name}/#{id}")
24
+ end
25
+
26
+ def create(attributes)
27
+ connection.post(resource_name, body: attributes)
28
+ end
29
+
30
+ def update(id, attributes)
31
+ connection.put("#{resource_name}/#{id}", body: attributes)
32
+ end
33
+
34
+ def destroy(id)
35
+ connection.delete("#{resource_name}/#{id}")
36
+ end
37
+
38
+ def fetch_all_pages(params = {})
39
+ page = 1
40
+ all_records = []
41
+
42
+ loop do
43
+ results = list(params.merge(page: page))
44
+ break if results.nil? || !results.is_a?(Array) || results.empty?
45
+
46
+ all_records.concat(results)
47
+ break if results.length < 200
48
+
49
+ page += 1
50
+ end
51
+
52
+ all_records
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EposNowClient
4
+ module Resources
5
+ class Brands < BaseResource
6
+ def all(params = {})
7
+ fetch_all_pages(params)
8
+ end
9
+
10
+ def find(id)
11
+ fetch(id)
12
+ end
13
+
14
+ def create_brand(attributes)
15
+ create(attributes)
16
+ end
17
+
18
+ def update_brand(id, attributes)
19
+ update(id, attributes)
20
+ end
21
+
22
+ def delete_brand(id)
23
+ destroy(id)
24
+ end
25
+
26
+ private
27
+
28
+ def resource_name
29
+ 'Brand'
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EposNowClient
4
+ module Resources
5
+ class Categories < BaseResource
6
+ def all(params = {})
7
+ fetch_all_pages(params)
8
+ end
9
+
10
+ def find(id)
11
+ fetch(id)
12
+ end
13
+
14
+ def create_category(attributes)
15
+ create(attributes)
16
+ end
17
+
18
+ def update_category(id, attributes)
19
+ update(id, attributes)
20
+ end
21
+
22
+ def delete_category(id)
23
+ destroy(id)
24
+ end
25
+
26
+ private
27
+
28
+ def resource_name
29
+ 'Category'
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EposNowClient
4
+ module Resources
5
+ class Customers < BaseResource
6
+ def all(params = {})
7
+ fetch_all_pages(params)
8
+ end
9
+
10
+ def find(id)
11
+ fetch(id)
12
+ end
13
+
14
+ def create_customer(attributes)
15
+ create(attributes)
16
+ end
17
+
18
+ def update_customer(id, attributes)
19
+ update(id, attributes)
20
+ end
21
+
22
+ def delete_customer(id)
23
+ destroy(id)
24
+ end
25
+
26
+ def search(query, params = {})
27
+ list(params.merge(search: query))
28
+ end
29
+
30
+ private
31
+
32
+ def resource_name
33
+ 'Customer'
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EposNowClient
4
+ module Resources
5
+ class Devices < BaseResource
6
+ def all(params = {})
7
+ fetch_all_pages(params)
8
+ end
9
+
10
+ def find(id)
11
+ fetch(id)
12
+ end
13
+
14
+ private
15
+
16
+ def resource_name
17
+ 'Device'
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EposNowClient
4
+ module Resources
5
+ class Locations < BaseResource
6
+ def all(params = {})
7
+ fetch_all_pages(params)
8
+ end
9
+
10
+ def find(id)
11
+ fetch(id)
12
+ end
13
+
14
+ private
15
+
16
+ def resource_name
17
+ 'Location'
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EposNowClient
4
+ module Resources
5
+ class Products < BaseResource
6
+ def all(params = {})
7
+ fetch_all_pages(params)
8
+ end
9
+
10
+ def find(id)
11
+ fetch(id)
12
+ end
13
+
14
+ def create_product(attributes)
15
+ create(attributes)
16
+ end
17
+
18
+ def update_product(id, attributes)
19
+ update(id, attributes)
20
+ end
21
+
22
+ def delete_product(id)
23
+ destroy(id)
24
+ end
25
+
26
+ def search(query, params = {})
27
+ list(params.merge(search: query))
28
+ end
29
+
30
+ private
31
+
32
+ def resource_name
33
+ 'Product'
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EposNowClient
4
+ module Resources
5
+ class Staff < BaseResource
6
+ def all(params = {})
7
+ fetch_all_pages(params)
8
+ end
9
+
10
+ def find(id)
11
+ fetch(id)
12
+ end
13
+
14
+ def create_staff(attributes)
15
+ create(attributes)
16
+ end
17
+
18
+ def update_staff(id, attributes)
19
+ update(id, attributes)
20
+ end
21
+
22
+ def delete_staff(id)
23
+ destroy(id)
24
+ end
25
+
26
+ private
27
+
28
+ def resource_name
29
+ 'Staff'
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EposNowClient
4
+ module Resources
5
+ class TaxGroups < BaseResource
6
+ def all(params = {})
7
+ fetch_all_pages(params)
8
+ end
9
+
10
+ def find(id)
11
+ fetch(id)
12
+ end
13
+
14
+ private
15
+
16
+ def resource_name
17
+ 'TaxGroup'
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EposNowClient
4
+ module Resources
5
+ class TenderTypes < BaseResource
6
+ def all(params = {})
7
+ fetch_all_pages(params)
8
+ end
9
+
10
+ def find(id)
11
+ fetch(id)
12
+ end
13
+
14
+ def create_tender_type(attributes)
15
+ create(attributes)
16
+ end
17
+
18
+ private
19
+
20
+ def resource_name
21
+ 'TenderType'
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EposNowClient
4
+ module Resources
5
+ class Transactions < BaseResource
6
+ def all(params = {})
7
+ fetch_all_pages(params)
8
+ end
9
+
10
+ def find(id)
11
+ fetch(id)
12
+ end
13
+
14
+ def create_transaction(attributes)
15
+ create(attributes)
16
+ end
17
+
18
+ def delete_transaction(id)
19
+ destroy(id)
20
+ end
21
+
22
+ def by_date(start_date:, end_date:, params: {})
23
+ connection.get(
24
+ "#{resource_name}/GetByDate",
25
+ params: params.merge(startDate: start_date, endDate: end_date)
26
+ )
27
+ end
28
+
29
+ def latest(params = {})
30
+ connection.get("#{resource_name}/GetLatest", params: params)
31
+ end
32
+
33
+ def validate(payload)
34
+ connection.post("#{resource_name}/Validate", body: payload)
35
+ end
36
+
37
+ private
38
+
39
+ def resource_name
40
+ 'Transaction'
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EposNowClient
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'epos_now_client/version'
4
+ require_relative 'epos_now_client/error'
5
+ require_relative 'epos_now_client/configuration'
6
+ require_relative 'epos_now_client/connection'
7
+ require_relative 'epos_now_client/resources/base_resource'
8
+ require_relative 'epos_now_client/resources/categories'
9
+ require_relative 'epos_now_client/resources/products'
10
+ require_relative 'epos_now_client/resources/transactions'
11
+ require_relative 'epos_now_client/resources/tender_types'
12
+ require_relative 'epos_now_client/resources/tax_groups'
13
+ require_relative 'epos_now_client/resources/customers'
14
+ require_relative 'epos_now_client/resources/staff'
15
+ require_relative 'epos_now_client/resources/devices'
16
+ require_relative 'epos_now_client/resources/brands'
17
+ require_relative 'epos_now_client/resources/locations'
18
+ require_relative 'epos_now_client/client'
19
+
20
+ module EposNowClient
21
+ class << self
22
+ def configuration
23
+ @configuration ||= Configuration.new
24
+ end
25
+
26
+ def configure
27
+ yield(configuration)
28
+ end
29
+
30
+ def reset_configuration!
31
+ @configuration = Configuration.new
32
+ end
33
+ end
34
+ end
metadata ADDED
@@ -0,0 +1,205 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: epos_now_client
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Daniel Dominguez
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: base64
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.2'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.2'
26
+ - !ruby/object:Gem::Dependency
27
+ name: httparty
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '0.22'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.22'
40
+ - !ruby/object:Gem::Dependency
41
+ name: bundler
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '2.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '2.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: logger
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.6'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.6'
68
+ - !ruby/object:Gem::Dependency
69
+ name: rake
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '13.0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '13.0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: rspec
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '3.13'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '3.13'
96
+ - !ruby/object:Gem::Dependency
97
+ name: rubocop
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '1.75'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '1.75'
110
+ - !ruby/object:Gem::Dependency
111
+ name: rubocop-rspec
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '3.6'
117
+ type: :development
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '3.6'
124
+ - !ruby/object:Gem::Dependency
125
+ name: simplecov
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: '0.22'
131
+ type: :development
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - "~>"
136
+ - !ruby/object:Gem::Version
137
+ version: '0.22'
138
+ - !ruby/object:Gem::Dependency
139
+ name: webmock
140
+ requirement: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - "~>"
143
+ - !ruby/object:Gem::Version
144
+ version: '3.24'
145
+ type: :development
146
+ prerelease: false
147
+ version_requirements: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - "~>"
150
+ - !ruby/object:Gem::Version
151
+ version: '3.24'
152
+ description: A lightweight, fully-tested Ruby client for the Epos Now REST API. Supports
153
+ Basic Auth, pagination, and all V4 resources including products, categories, transactions,
154
+ customers, staff, and more.
155
+ email:
156
+ - danielfromarg@gmail.com
157
+ executables: []
158
+ extensions: []
159
+ extra_rdoc_files: []
160
+ files:
161
+ - CHANGELOG.md
162
+ - LICENSE.txt
163
+ - README.md
164
+ - lib/epos_now_client.rb
165
+ - lib/epos_now_client/client.rb
166
+ - lib/epos_now_client/configuration.rb
167
+ - lib/epos_now_client/connection.rb
168
+ - lib/epos_now_client/error.rb
169
+ - lib/epos_now_client/resources/base_resource.rb
170
+ - lib/epos_now_client/resources/brands.rb
171
+ - lib/epos_now_client/resources/categories.rb
172
+ - lib/epos_now_client/resources/customers.rb
173
+ - lib/epos_now_client/resources/devices.rb
174
+ - lib/epos_now_client/resources/locations.rb
175
+ - lib/epos_now_client/resources/products.rb
176
+ - lib/epos_now_client/resources/staff.rb
177
+ - lib/epos_now_client/resources/tax_groups.rb
178
+ - lib/epos_now_client/resources/tender_types.rb
179
+ - lib/epos_now_client/resources/transactions.rb
180
+ - lib/epos_now_client/version.rb
181
+ homepage: https://github.com/dan1d/epos_now_client
182
+ licenses:
183
+ - MIT
184
+ metadata:
185
+ source_code_uri: https://github.com/dan1d/epos_now_client
186
+ changelog_uri: https://github.com/dan1d/epos_now_client/blob/main/CHANGELOG.md
187
+ rubygems_mfa_required: 'true'
188
+ rdoc_options: []
189
+ require_paths:
190
+ - lib
191
+ required_ruby_version: !ruby/object:Gem::Requirement
192
+ requirements:
193
+ - - ">="
194
+ - !ruby/object:Gem::Version
195
+ version: 3.2.0
196
+ required_rubygems_version: !ruby/object:Gem::Requirement
197
+ requirements:
198
+ - - ">="
199
+ - !ruby/object:Gem::Version
200
+ version: '0'
201
+ requirements: []
202
+ rubygems_version: 3.6.9
203
+ specification_version: 4
204
+ summary: Ruby client for the Epos Now POS API (V4)
205
+ test_files: []