loyverse_api 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,115 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'loyverse_api'
5
+
6
+ # This example demonstrates basic usage of the Loyverse API gem
7
+
8
+ # Configure the gem with your access token
9
+ LoyverseApi.configure do |config|
10
+ config.access_token = ENV['LOYVERSE_ACCESS_TOKEN'] || 'your_access_token_here'
11
+ end
12
+
13
+ # Create a client instance
14
+ client = LoyverseApi.client
15
+
16
+ puts "=== Loyverse API Examples ==="
17
+ puts
18
+
19
+ # Example 1: List Categories
20
+ puts "1. Listing categories:"
21
+ begin
22
+ categories = client.categories.list(limit: 5)
23
+ if categories.empty?
24
+ puts " No categories found"
25
+ else
26
+ categories.each do |category|
27
+ puts " - #{category['name']} (#{category['color']})"
28
+ end
29
+ end
30
+ rescue LoyverseApi::Error => e
31
+ puts " Error: #{e.message}"
32
+ end
33
+ puts
34
+
35
+ # Example 2: List Items
36
+ puts "2. Listing items:"
37
+ begin
38
+ items = client.items.list(limit: 5)
39
+ if items.empty?
40
+ puts " No items found"
41
+ else
42
+ items.each do |item|
43
+ puts " - #{item['item_name']}"
44
+ if item['variants']
45
+ item['variants'].each do |variant|
46
+ puts " * SKU: #{variant['sku']}, Price: $#{variant['price']}"
47
+ end
48
+ end
49
+ end
50
+ end
51
+ rescue LoyverseApi::Error => e
52
+ puts " Error: #{e.message}"
53
+ end
54
+ puts
55
+
56
+ # Example 3: Get Inventory Levels
57
+ puts "3. Checking inventory:"
58
+ begin
59
+ inventory = client.inventory.list(limit: 5)
60
+ if inventory.empty?
61
+ puts " No inventory data found"
62
+ else
63
+ inventory.each do |level|
64
+ puts " - Variant: #{level['variant_id']}"
65
+ puts " Store: #{level['store_id']}"
66
+ puts " In Stock: #{level['in_stock']}"
67
+ puts " Updated: #{level['updated_at']}"
68
+ puts
69
+ end
70
+ end
71
+ rescue LoyverseApi::Error => e
72
+ puts " Error: #{e.message}"
73
+ end
74
+ puts
75
+
76
+ # Example 4: List Recent Receipts
77
+ puts "4. Listing recent receipts:"
78
+ begin
79
+ receipts = client.receipts.list(limit: 5, order: 'DESC')
80
+ if receipts.empty?
81
+ puts " No receipts found"
82
+ else
83
+ receipts.each do |receipt|
84
+ puts " - Receipt ##{receipt['receipt_number']}"
85
+ puts " Date: #{receipt['receipt_date']}"
86
+ puts " Total: $#{receipt['total_money']}"
87
+ puts " Items: #{receipt['line_items']&.count || 0}"
88
+ puts
89
+ end
90
+ end
91
+ rescue LoyverseApi::Error => e
92
+ puts " Error: #{e.message}"
93
+ end
94
+ puts
95
+
96
+ # Example 5: List Webhooks
97
+ puts "5. Listing webhooks:"
98
+ begin
99
+ webhooks = client.webhooks.list
100
+ if webhooks.empty?
101
+ puts " No webhooks configured"
102
+ else
103
+ webhooks.each do |webhook|
104
+ puts " - #{webhook['url']}"
105
+ puts " Events: #{webhook['event_types']&.join(', ')}"
106
+ puts " Description: #{webhook['description']}"
107
+ puts
108
+ end
109
+ end
110
+ rescue LoyverseApi::Error => e
111
+ puts " Error: #{e.message}"
112
+ end
113
+ puts
114
+
115
+ puts "=== Examples Complete ==="
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'loyverse_api'
5
+
6
+ # This example demonstrates how to create a new item with variants
7
+
8
+ LoyverseApi.configure do |config|
9
+ config.access_token = ENV['LOYVERSE_ACCESS_TOKEN'] || 'your_access_token_here'
10
+ end
11
+
12
+ client = LoyverseApi.client
13
+
14
+ puts "Creating a new item with variants..."
15
+ puts
16
+
17
+ begin
18
+ # First, create a category for the item
19
+ category = client.categories.create(
20
+ name: 'Beverages',
21
+ color: 'BLUE'
22
+ )
23
+ puts "Created category: #{category['name']} (ID: #{category['id']})"
24
+ puts
25
+
26
+ # Create an item with multiple variants
27
+ item = client.items.create(
28
+ item_name: 'Premium Coffee',
29
+ category_id: category['id'],
30
+ track_stock: true,
31
+ variants: [
32
+ {
33
+ sku: 'COFFEE-S-001',
34
+ barcode: '1234567890123',
35
+ price: 3.50,
36
+ cost: 1.50,
37
+ option1_value: 'Small'
38
+ },
39
+ {
40
+ sku: 'COFFEE-M-001',
41
+ barcode: '1234567890124',
42
+ price: 4.50,
43
+ cost: 2.00,
44
+ option1_value: 'Medium'
45
+ },
46
+ {
47
+ sku: 'COFFEE-L-001',
48
+ barcode: '1234567890125',
49
+ price: 5.50,
50
+ cost: 2.50,
51
+ option1_value: 'Large'
52
+ }
53
+ ]
54
+ )
55
+
56
+ puts "Created item: #{item['item_name']}"
57
+ puts "Item ID: #{item['id']}"
58
+ puts "Category: #{item['category_id']}"
59
+ puts
60
+ puts "Variants:"
61
+ item['variants'].each do |variant|
62
+ puts " - #{variant['option1_value']}: $#{variant['price']} (SKU: #{variant['sku']})"
63
+ end
64
+ puts
65
+ puts "Item created successfully!"
66
+
67
+ rescue LoyverseApi::Error => e
68
+ puts "Error creating item: #{e.message}"
69
+ puts "Error code: #{e.code}" if e.code
70
+ puts "Error details: #{e.details}" if e.details
71
+ end
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'loyverse_api'
5
+ require 'webrick'
6
+ require 'json'
7
+
8
+ # This example demonstrates a simple webhook receiver server
9
+
10
+ # Configure the Loyverse API client
11
+ LoyverseApi.configure do |config|
12
+ config.access_token = ENV['LOYVERSE_ACCESS_TOKEN'] || 'your_access_token_here'
13
+ end
14
+
15
+ client = LoyverseApi.client
16
+
17
+ # Webhook secret (if using OAuth 2.0 created webhooks)
18
+ WEBHOOK_SECRET = ENV['LOYVERSE_WEBHOOK_SECRET']
19
+
20
+ # Simple webhook handler
21
+ class WebhookHandler < WEBrick::HTTPServlet::AbstractServlet
22
+ def initialize(server, client)
23
+ super(server)
24
+ @client = client
25
+ end
26
+
27
+ def do_POST(request, response)
28
+ begin
29
+ # Get the raw body
30
+ body = request.body
31
+
32
+ # Verify signature if using OAuth 2.0
33
+ if WEBHOOK_SECRET
34
+ signature = request.header['x-loyverse-signature']&.first
35
+ is_valid = @client.webhooks.verify_signature(body, signature, WEBHOOK_SECRET)
36
+
37
+ unless is_valid
38
+ response.status = 401
39
+ response.body = JSON.generate({ error: 'Invalid signature' })
40
+ return
41
+ end
42
+ end
43
+
44
+ # Parse the webhook payload
45
+ payload = JSON.parse(body)
46
+
47
+ # Handle the webhook event
48
+ handle_event(payload)
49
+
50
+ # Respond with 200 OK
51
+ response.status = 200
52
+ response['Content-Type'] = 'application/json'
53
+ response.body = JSON.generate({ received: true })
54
+
55
+ rescue JSON::ParserError => e
56
+ response.status = 400
57
+ response.body = JSON.generate({ error: 'Invalid JSON' })
58
+ rescue => e
59
+ puts "Error processing webhook: #{e.message}"
60
+ response.status = 500
61
+ response.body = JSON.generate({ error: 'Internal server error' })
62
+ end
63
+ end
64
+
65
+ private
66
+
67
+ def handle_event(payload)
68
+ event_type = payload['event_type']
69
+ data = payload['data']
70
+ timestamp = payload['timestamp']
71
+
72
+ puts "\n=== Webhook Received ==="
73
+ puts "Event Type: #{event_type}"
74
+ puts "Timestamp: #{timestamp}"
75
+ puts "Data: #{JSON.pretty_generate(data)}"
76
+ puts "========================\n"
77
+
78
+ case event_type
79
+ when 'ORDER_CREATED'
80
+ handle_order_created(data)
81
+ when 'ITEM_UPDATED'
82
+ handle_item_updated(data)
83
+ when 'INVENTORY_UPDATED'
84
+ handle_inventory_updated(data)
85
+ else
86
+ puts "Unknown event type: #{event_type}"
87
+ end
88
+ end
89
+
90
+ def handle_order_created(data)
91
+ puts "New order created: Receipt ##{data['receipt_number']}"
92
+ # Add your custom logic here
93
+ end
94
+
95
+ def handle_item_updated(data)
96
+ puts "Item updated: #{data['item_name']}"
97
+ # Add your custom logic here
98
+ end
99
+
100
+ def handle_inventory_updated(data)
101
+ puts "Inventory updated for variant: #{data['variant_id']}"
102
+ puts "New stock level: #{data['in_stock']}"
103
+ # Add your custom logic here
104
+ end
105
+ end
106
+
107
+ # Create and start the server
108
+ port = ENV['PORT'] || 3000
109
+ server = WEBrick::HTTPServer.new(Port: port)
110
+
111
+ server.mount '/webhooks/loyverse', WebhookHandler, client
112
+
113
+ trap('INT') { server.shutdown }
114
+
115
+ puts "Webhook server listening on port #{port}"
116
+ puts "Webhook URL: http://localhost:#{port}/webhooks/loyverse"
117
+ puts "Press Ctrl+C to stop"
118
+ puts
119
+
120
+ server.start
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LoyverseApi
4
+ class Client
5
+ include Endpoints::Items
6
+ include Endpoints::Categories
7
+ include Endpoints::Inventory
8
+ include Endpoints::Receipts
9
+ include Endpoints::Webhooks
10
+ include Endpoints::Customers
11
+ include Endpoints::Discounts
12
+ include Endpoints::Employees
13
+ include Endpoints::Modifiers
14
+
15
+ attr_reader :configuration
16
+
17
+ def initialize(configuration = nil)
18
+ @configuration = configuration || LoyverseApi.configuration || Configuration.new
19
+
20
+ raise AuthenticationError, "Access token is required" if @configuration.access_token.nil?
21
+ end
22
+
23
+ def connection
24
+ @connection ||= Faraday.new(url: configuration.base_url) do |conn|
25
+ conn.request :json
26
+ conn.request :retry, {
27
+ max: 3,
28
+ interval: 0.5,
29
+ interval_randomness: 0.5,
30
+ backoff_factor: 2,
31
+ retry_statuses: [429, 500, 502, 503, 504],
32
+ methods: [:get, :post, :put, :delete]
33
+ }
34
+ conn.response :json, content_type: /\bjson$/
35
+ conn.headers['Authorization'] = "Bearer #{configuration.access_token}"
36
+ conn.headers['Content-Type'] = 'application/json'
37
+ conn.headers['Accept'] = 'application/json'
38
+ conn.adapter Faraday.default_adapter
39
+ conn.options.timeout = configuration.timeout
40
+ conn.options.open_timeout = configuration.open_timeout
41
+ end
42
+ end
43
+
44
+ def get(path, params: {})
45
+ handle_response do
46
+ response = connection.get(path, params)
47
+ response
48
+ end
49
+ end
50
+
51
+ def post(path, body: {})
52
+ handle_response do
53
+ response = connection.post(path, body)
54
+ response
55
+ end
56
+ end
57
+
58
+ def put(path, body: {})
59
+ handle_response do
60
+ response = connection.put(path, body)
61
+ response
62
+ end
63
+ end
64
+
65
+ def delete(path)
66
+ handle_response do
67
+ response = connection.delete(path)
68
+ response
69
+ end
70
+ end
71
+
72
+ private
73
+
74
+ def handle_response
75
+ response = yield
76
+
77
+ case response.status
78
+ when 200, 201, 204
79
+ response.body
80
+ when 400
81
+ raise BadRequestError.new(
82
+ error_message(response) || "Bad Request: #{response.body.inspect}",
83
+ code: error_code(response),
84
+ details: error_details(response)
85
+ )
86
+ when 401
87
+ raise AuthenticationError.new(
88
+ error_message(response),
89
+ code: error_code(response),
90
+ details: error_details(response)
91
+ )
92
+ when 403
93
+ raise AuthorizationError.new(
94
+ error_message(response),
95
+ code: error_code(response),
96
+ details: error_details(response)
97
+ )
98
+ when 404
99
+ raise NotFoundError.new(
100
+ error_message(response),
101
+ code: error_code(response),
102
+ details: error_details(response)
103
+ )
104
+ when 429
105
+ raise RateLimitError.new(
106
+ error_message(response) || "Rate limit exceeded",
107
+ code: error_code(response),
108
+ details: error_details(response)
109
+ )
110
+ when 500, 502, 503, 504
111
+ raise ServerError.new(
112
+ error_message(response) || "Server error occurred",
113
+ code: error_code(response),
114
+ details: error_details(response)
115
+ )
116
+ else
117
+ raise ApiError.new(
118
+ error_message(response) || "Unknown error occurred",
119
+ code: error_code(response),
120
+ details: error_details(response)
121
+ )
122
+ end
123
+ rescue Faraday::TimeoutError
124
+ raise Error, "Request timeout"
125
+ rescue Faraday::ConnectionFailed
126
+ raise Error, "Connection failed"
127
+ end
128
+
129
+ def error_message(response)
130
+ return nil unless response.body.is_a?(Hash)
131
+ response.body.dig("error", "message") || response.body["message"]
132
+ end
133
+
134
+ def error_code(response)
135
+ return nil unless response.body.is_a?(Hash)
136
+ response.body.dig("error", "code")
137
+ end
138
+
139
+ def error_details(response)
140
+ return nil unless response.body.is_a?(Hash)
141
+ response.body.dig("error", "details")
142
+ end
143
+
144
+ def format_time(time)
145
+ return nil if time.nil?
146
+ return time if time.is_a?(String)
147
+
148
+ if time.respond_to?(:strftime) && !time.respond_to?(:hour)
149
+ return "#{time.strftime('%Y-%m-%d')}T00:00:00.000Z"
150
+ end
151
+
152
+ return time.utc.strftime('%Y-%m-%dT%H:%M:%S.%LZ') if time.respond_to?(:utc)
153
+
154
+ time
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,13 @@
1
+ module LoyverseApi
2
+ class Configuration
3
+ attr_accessor :access_token, :api_base_url, :timeout, :open_timeout
4
+ alias_method :base_url, :api_base_url
5
+
6
+ def initialize
7
+ @api_base_url = "https://api.loyverse.com/v1.0/"
8
+ @timeout = 30
9
+ @open_timeout = 10
10
+ @access_token = nil
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,43 @@
1
+ module LoyverseApi
2
+ module Endpoints
3
+ module Categories
4
+ # Get a specific category by ID
5
+ # @param category_id [String] UUID of the category
6
+ # @return [Hash] Category details
7
+ def get_category(category_id)
8
+ get("categories/#{category_id}")
9
+ end
10
+
11
+ # List all categories
12
+ # @param limit [Integer] Maximum number of results per page (default: 250)
13
+ # @param cursor [String] Pagination cursor for next page
14
+ # @return [Hash] Response with categories array
15
+ def list_categories(limit: 250, cursor: nil)
16
+ params = {
17
+ limit: limit,
18
+ cursor: cursor
19
+ }.compact
20
+
21
+ get("categories", params: params)
22
+ end
23
+
24
+ # Create a new category
25
+ # @param name [String] Category name
26
+ # @param color [String] Color code (e.g., "ORANGE", "RED", "BLUE")
27
+ # @return [Hash] Created category details
28
+ def create_category(name:, color: nil)
29
+ body = { name: name }
30
+ body[:color] = color if color
31
+
32
+ post("categories", body: body)
33
+ end
34
+
35
+ # Delete a category
36
+ # @param category_id [String] UUID of the category
37
+ # @return [Hash] Response
38
+ def delete_category(category_id)
39
+ delete("categories/#{category_id}")
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,131 @@
1
+ module LoyverseApi
2
+ module Endpoints
3
+ module Customers
4
+ # Get a specific customer by ID
5
+ # @param customer_id [String] UUID of the customer
6
+ # @return [Hash] Customer details
7
+ def get_customer(customer_id)
8
+ get("customers/#{customer_id}")
9
+ end
10
+
11
+ # List customers
12
+ # @param limit [Integer] Maximum number of results per page (default: 250)
13
+ # @param cursor [String] Pagination cursor for next page
14
+ # @param email [String] Filter by email address (optional)
15
+ # @param phone_number [String] Filter by phone number (optional)
16
+ # @param updated_at_min [String, Time] Filter by minimum update time (ISO 8601)
17
+ # @param updated_at_max [String, Time] Filter by maximum update time (ISO 8601)
18
+ # @return [Hash] Response with customers array
19
+ def list_customers(limit: 250, cursor: nil, email: nil, phone_number: nil, updated_at_min: nil, updated_at_max: nil)
20
+ params = {
21
+ limit: limit,
22
+ cursor: cursor,
23
+ email: email,
24
+ phone_number: phone_number,
25
+ updated_at_min: format_time(updated_at_min),
26
+ updated_at_max: format_time(updated_at_max)
27
+ }.compact
28
+
29
+ get("customers", params: params)
30
+ end
31
+
32
+ # Create a new customer
33
+ # @param name [String] Customer name
34
+ # @param email [String] Customer email (optional)
35
+ # @param phone_number [String] Customer phone number (optional)
36
+ # @param address [String] Customer address (optional)
37
+ # @param city [String] Customer city (optional)
38
+ # @param region [String] Customer region/state (optional)
39
+ # @param postal_code [String] Customer postal code (optional)
40
+ # @param country [String] Customer country (optional)
41
+ # @param customer_code [String] Custom customer code (optional)
42
+ # @param note [String] Customer note (optional)
43
+ # @param first_visit [String, Time] Date of first visit (optional)
44
+ # @param total_visits [Integer] Total number of visits (optional)
45
+ # @param total_spent [Float] Total amount spent (optional)
46
+ # @return [Hash] Created customer details
47
+ def create_customer(
48
+ name:,
49
+ email: nil,
50
+ phone_number: nil,
51
+ address: nil,
52
+ city: nil,
53
+ region: nil,
54
+ postal_code: nil,
55
+ country: nil,
56
+ customer_code: nil,
57
+ note: nil,
58
+ first_visit: nil,
59
+ total_visits: nil,
60
+ total_spent: nil
61
+ )
62
+ body = {
63
+ name: name,
64
+ email: email,
65
+ phone_number: phone_number,
66
+ address: address,
67
+ city: city,
68
+ region: region,
69
+ postal_code: postal_code,
70
+ country: country,
71
+ customer_code: customer_code,
72
+ note: note,
73
+ first_visit: format_time(first_visit),
74
+ total_visits: total_visits,
75
+ total_spent: total_spent
76
+ }.compact
77
+
78
+ post("customers", body: body)
79
+ end
80
+
81
+ # Update an existing customer
82
+ # @param customer_id [String] UUID of the customer
83
+ # @param name [String] Customer name (optional)
84
+ # @param email [String] Customer email (optional)
85
+ # @param phone_number [String] Customer phone number (optional)
86
+ # @param address [String] Customer address (optional)
87
+ # @param city [String] Customer city (optional)
88
+ # @param region [String] Customer region/state (optional)
89
+ # @param postal_code [String] Customer postal code (optional)
90
+ # @param country [String] Customer country (optional)
91
+ # @param customer_code [String] Custom customer code (optional)
92
+ # @param note [String] Customer note (optional)
93
+ # @return [Hash] Updated customer details
94
+ def update_customer(
95
+ customer_id,
96
+ name: nil,
97
+ email: nil,
98
+ phone_number: nil,
99
+ address: nil,
100
+ city: nil,
101
+ region: nil,
102
+ postal_code: nil,
103
+ country: nil,
104
+ customer_code: nil,
105
+ note: nil
106
+ )
107
+ body = {
108
+ name: name,
109
+ email: email,
110
+ phone_number: phone_number,
111
+ address: address,
112
+ city: city,
113
+ region: region,
114
+ postal_code: postal_code,
115
+ country: country,
116
+ customer_code: customer_code,
117
+ note: note
118
+ }.compact
119
+
120
+ put("customers/#{customer_id}", body: body)
121
+ end
122
+
123
+ # Delete a customer
124
+ # @param customer_id [String] UUID of the customer
125
+ # @return [Hash] Response
126
+ def delete_customer(customer_id)
127
+ delete("customers/#{customer_id}")
128
+ end
129
+ end
130
+ end
131
+ end