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,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LoyverseApi
4
+ module Endpoints
5
+ module Discounts
6
+ # Discount type constants
7
+ TYPE_FIXED_PERCENT = "FIXED_PERCENT"
8
+ TYPE_FIXED_AMOUNT = "FIXED_AMOUNT"
9
+ TYPE_VARIABLE_PERCENT = "VARIABLE_PERCENT"
10
+ TYPE_VARIABLE_AMOUNT = "VARIABLE_AMOUNT"
11
+ TYPE_DISCOUNT_BY_POINTS = "DISCOUNT_BY_POINTS"
12
+
13
+ # Applies to constants
14
+ APPLIES_TO_RECEIPT = "RECEIPT"
15
+ APPLIES_TO_ITEM = "ITEM"
16
+
17
+ # Default applies_to value
18
+ DEFAULT_APPLIES_TO = APPLIES_TO_RECEIPT
19
+
20
+ # Get a specific discount by ID
21
+ # @param discount_id [String] UUID of the discount
22
+ # @return [Hash] Discount details
23
+ def get_discount(discount_id)
24
+ get("discounts/#{discount_id}")
25
+ end
26
+
27
+ # List discounts
28
+ # @param limit [Integer] Maximum number of results per page (default: 250)
29
+ # @param cursor [String] Pagination cursor for next page
30
+ # @param updated_at_min [String, Time] Filter by minimum update time (ISO 8601)
31
+ # @param updated_at_max [String, Time] Filter by maximum update time (ISO 8601)
32
+ # @return [Hash] Response with discounts array
33
+ def list_discounts(limit: 250, cursor: nil, updated_at_min: nil, updated_at_max: nil)
34
+ params = {
35
+ limit: limit,
36
+ cursor: cursor,
37
+ updated_at_min: format_time(updated_at_min),
38
+ updated_at_max: format_time(updated_at_max)
39
+ }.compact
40
+
41
+ get("discounts", params: params)
42
+ end
43
+
44
+ # Create a new discount
45
+ # @param name [String] Discount name
46
+ # @param type [String] Type of discount: "FIXED_PERCENT", "FIXED_AMOUNT", "VARIABLE_PERCENT", "VARIABLE_AMOUNT", or "DISCOUNT_BY_POINTS"
47
+ # @param discount_amount [Float] Discount amount (percentage or fixed value, optional for variable types)
48
+ # @param applies_to [String] What the discount applies to: "RECEIPT" or "ITEM" (optional, default: "RECEIPT")
49
+ # @param enabled [Boolean] Whether the discount is enabled (optional)
50
+ # @return [Hash] Created discount details
51
+ def create_discount(name:, type:, discount_amount: nil, applies_to: DEFAULT_APPLIES_TO, enabled: true)
52
+ body = {
53
+ name: name,
54
+ type: type.upcase,
55
+ discount_amount: discount_amount,
56
+ applies_to: applies_to.upcase,
57
+ enabled: enabled
58
+ }.compact
59
+
60
+ post("discounts", body: body)
61
+ end
62
+
63
+ # Update an existing discount
64
+ # @param discount_id [String] UUID of the discount
65
+ # @param name [String] Discount name (optional)
66
+ # @param type [String] Type of discount: "FIXED_PERCENT", "FIXED_AMOUNT", "VARIABLE_PERCENT", "VARIABLE_AMOUNT", or "DISCOUNT_BY_POINTS" (optional)
67
+ # @param discount_amount [Float] Discount amount (optional)
68
+ # @param applies_to [String] What the discount applies to (optional)
69
+ # @param enabled [Boolean] Whether the discount is enabled (optional)
70
+ # @return [Hash] Updated discount details
71
+ def update_discount(discount_id, name: nil, type: nil, discount_amount: nil, applies_to: nil, enabled: nil)
72
+ body = {
73
+ name: name,
74
+ type: type&.upcase,
75
+ discount_amount: discount_amount,
76
+ applies_to: applies_to&.upcase,
77
+ enabled: enabled
78
+ }.compact
79
+
80
+ put("discounts/#{discount_id}", body: body)
81
+ end
82
+
83
+ # Delete a discount
84
+ # @param discount_id [String] UUID of the discount
85
+ # @return [Hash] Response
86
+ def delete_discount(discount_id)
87
+ delete("discounts/#{discount_id}")
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LoyverseApi
4
+ module Endpoints
5
+ module Employees
6
+ # Get a specific employee by ID
7
+ # @param employee_id [String] UUID of the employee
8
+ # @return [Hash] Employee details
9
+ def get_employee(employee_id)
10
+ get("employees/#{employee_id}")
11
+ end
12
+
13
+ # List employees
14
+ # @param limit [Integer] Maximum number of results per page (default: 250)
15
+ # @param cursor [String] Pagination cursor for next page
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 employees array
19
+ def list_employees(limit: 250, cursor: nil, updated_at_min: nil, updated_at_max: nil)
20
+ params = {
21
+ limit: limit,
22
+ cursor: cursor,
23
+ updated_at_min: format_time(updated_at_min),
24
+ updated_at_max: format_time(updated_at_max)
25
+ }.compact
26
+
27
+ get("employees", params: params)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,41 @@
1
+ module LoyverseApi
2
+ module Endpoints
3
+ module Inventory
4
+ # List inventory levels
5
+ # @param variant_id [String] Filter by specific variant UUID (optional)
6
+ # @param store_id [String] Filter by specific store UUID (optional)
7
+ # @param updated_at_min [String, Time] Filter by minimum update time (optional)
8
+ # @param updated_at_max [String, Time] Filter by maximum update time (optional)
9
+ # @param limit [Integer] Maximum number of results per page (default: 250)
10
+ # @param cursor [String] Pagination cursor for next page
11
+ # @return [Hash] Response with inventory levels array
12
+ def list_inventory(variant_id: nil, store_id: nil, updated_at_min: nil, updated_at_max: nil, limit: 250, cursor: nil)
13
+ params = {
14
+ limit: limit,
15
+ variant_id: variant_id,
16
+ store_id: store_id,
17
+ cursor: cursor,
18
+ updated_at_min: format_time(updated_at_min),
19
+ updated_at_max: format_time(updated_at_max)
20
+ }.compact
21
+
22
+ get("inventory", params: params)
23
+ end
24
+
25
+ # Update inventory level for a variant at a specific store
26
+ # @param variant_id [String] UUID of the variant
27
+ # @param store_id [String] UUID of the store
28
+ # @param in_stock [Integer] New stock quantity
29
+ # @return [Hash] Updated inventory level
30
+ def update_inventory(variant_id:, store_id:, in_stock:)
31
+ body = {
32
+ variant_id: variant_id,
33
+ store_id: store_id,
34
+ in_stock: in_stock
35
+ }
36
+
37
+ put("inventory", body: body)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,75 @@
1
+ module LoyverseApi
2
+ module Endpoints
3
+ module Items
4
+ # Get a specific item by ID
5
+ # @param item_id [String] UUID of the item
6
+ # @return [Hash] Item details
7
+ def get_item(item_id)
8
+ get("items/#{item_id}")
9
+ end
10
+
11
+ # List items
12
+ # @param limit [Integer] Maximum number of results per page (default: 250)
13
+ # @param cursor [String] Pagination cursor for next page
14
+ # @param updated_at_min [String, Time] Filter by minimum update time (ISO 8601)
15
+ # @param updated_at_max [String, Time] Filter by maximum update time (ISO 8601)
16
+ # @return [Hash] Response with items array
17
+ def list_items(limit: 250, cursor: nil, updated_at_min: nil, updated_at_max: nil)
18
+ params = {
19
+ limit: limit,
20
+ cursor: cursor,
21
+ updated_at_min: format_time(updated_at_min),
22
+ updated_at_max: format_time(updated_at_max)
23
+ }.compact
24
+
25
+ get("items", params: params)
26
+ end
27
+
28
+ # Create a new item
29
+ # @param item_name [String] Name of the item
30
+ # @param category_id [String] UUID of the category (optional)
31
+ # @param variants [Array<Hash>] Array of variant hashes
32
+ # @param track_stock [Boolean] Whether to track inventory
33
+ # @param sold_by_weight [Boolean] Whether item is sold by weight
34
+ # @param is_composite [Boolean] Whether item is composite
35
+ # @return [Hash] Created item details
36
+ def create_item(item_name:, category_id: nil, variants: [], track_stock: false, sold_by_weight: false, is_composite: false)
37
+ body = {
38
+ item_name: item_name,
39
+ track_stock: track_stock,
40
+ sold_by_weight: sold_by_weight,
41
+ is_composite: is_composite
42
+ }
43
+ body[:category_id] = category_id if category_id
44
+ body[:variants] = variants unless variants.empty?
45
+
46
+ post("items", body: body)
47
+ end
48
+
49
+ # Update an existing item
50
+ # @param item_id [String] UUID of the item
51
+ # @param item_name [String] Name of the item (optional)
52
+ # @param category_id [String] UUID of the category (optional)
53
+ # @param variants [Array<Hash>] Array of variant hashes (optional)
54
+ # @param track_stock [Boolean] Whether to track inventory (optional)
55
+ # @return [Hash] Updated item details
56
+ def update_item(item_id, item_name: nil, category_id: nil, variants: nil, track_stock: nil)
57
+ body = {
58
+ item_name: item_name,
59
+ category_id: category_id,
60
+ variants: variants,
61
+ track_stock: track_stock
62
+ }.compact
63
+
64
+ put("items/#{item_id}", body: body)
65
+ end
66
+
67
+ # Delete an item
68
+ # @param item_id [String] UUID of the item
69
+ # @return [Hash] Response
70
+ def delete_item(item_id)
71
+ delete("items/#{item_id}")
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LoyverseApi
4
+ module Endpoints
5
+ module Modifiers
6
+ # Get a specific modifier by ID
7
+ # @param modifier_id [String] UUID of the modifier
8
+ # @return [Hash] Modifier details
9
+ def get_modifier(modifier_id)
10
+ get("modifiers/#{modifier_id}")
11
+ end
12
+
13
+ # List modifiers
14
+ # @param limit [Integer] Maximum number of results per page (default: 250)
15
+ # @param cursor [String] Pagination cursor for next page
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 modifiers array
19
+ def list_modifiers(limit: 250, cursor: nil, updated_at_min: nil, updated_at_max: nil)
20
+ params = {
21
+ limit: limit,
22
+ cursor: cursor,
23
+ updated_at_min: format_time(updated_at_min),
24
+ updated_at_max: format_time(updated_at_max)
25
+ }.compact
26
+
27
+ get("modifiers", params: params)
28
+ end
29
+
30
+ # Create a new modifier
31
+ # @param name [String] Modifier name
32
+ # @param options [Array<Hash>] Array of modifier options with name and price
33
+ # @return [Hash] Created modifier details
34
+ def create_modifier(name:, options:)
35
+ body = {
36
+ name: name,
37
+ options: options
38
+ }
39
+
40
+ post("modifiers", body: body)
41
+ end
42
+
43
+ # Delete a modifier
44
+ # @param modifier_id [String] UUID of the modifier
45
+ # @return [Hash] Response
46
+ def delete_modifier(modifier_id)
47
+ delete("modifiers/#{modifier_id}")
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,115 @@
1
+ module LoyverseApi
2
+ module Endpoints
3
+ module Receipts
4
+ # Get a specific receipt by receipt number
5
+ # @param receipt_number [String, Integer] Receipt number (not UUID)
6
+ # @return [Hash] Receipt details
7
+ def get_receipt(receipt_number)
8
+ get("receipts/#{receipt_number}")
9
+ end
10
+
11
+ # List receipts
12
+ # @param receipt_numbers [Array<String, Integer>] Array of specific receipt numbers (optional)
13
+ # @param since_receipt_number [String, Integer] Return receipts after this number (optional)
14
+ # @param before_receipt_number [String, Integer] Return receipts before this number (optional)
15
+ # @param store_id [String] Filter by store UUID (optional)
16
+ # @param order [String] Sort order: "ASC" or "DESC" (default: "DESC")
17
+ # @param source [String] Filter by source (e.g., "POS", "API") (optional)
18
+ # @param updated_at_min [String, Time] Filter by minimum update time (optional)
19
+ # @param updated_at_max [String, Time] Filter by maximum update time (optional)
20
+ # @param created_at_min [String, Time] Filter by minimum creation time (optional)
21
+ # @param created_at_max [String, Time] Filter by maximum creation time (optional)
22
+ # @param limit [Integer] Maximum number of results per page (default: 250)
23
+ # @param cursor [String] Pagination cursor for next page
24
+ # @return [Hash] Response with receipts array
25
+ def list_receipts(
26
+ receipt_numbers: nil,
27
+ since_receipt_number: nil,
28
+ before_receipt_number: nil,
29
+ store_id: nil,
30
+ order: "DESC",
31
+ source: nil,
32
+ updated_at_min: nil,
33
+ updated_at_max: nil,
34
+ created_at_min: nil,
35
+ created_at_max: nil,
36
+ limit: 250,
37
+ cursor: nil
38
+ )
39
+ params = {
40
+ limit: limit,
41
+ order: order,
42
+ receipt_numbers: receipt_numbers ? Array(receipt_numbers).join(",") : nil,
43
+ since_receipt_number: since_receipt_number,
44
+ before_receipt_number: before_receipt_number,
45
+ store_id: store_id,
46
+ source: source,
47
+ cursor: cursor,
48
+ updated_at_min: format_time(updated_at_min),
49
+ updated_at_max: format_time(updated_at_max),
50
+ created_at_min: format_time(created_at_min),
51
+ created_at_max: format_time(created_at_max)
52
+ }.compact
53
+
54
+ get("receipts", params: params)
55
+ end
56
+
57
+ # Create a new receipt
58
+ # @param receipt_date [String, Time] Receipt date in ISO 8601 format
59
+ # @param store_id [String] UUID of the store
60
+ # @param line_items [Array<Hash>] Array of line item hashes
61
+ # @param payments [Array<Hash>] Array of payment hashes
62
+ # @param receipt_type [String] Type of receipt (default: "SALE")
63
+ # @param employee_id [String] UUID of the employee (optional)
64
+ # @param customer_id [String] UUID of the customer (optional)
65
+ # @param note [String] Receipt note (optional)
66
+ # @param source [String] Source of receipt (default: "API")
67
+ # @return [Hash] Created receipt details
68
+ def create_receipt(
69
+ receipt_date:,
70
+ store_id:,
71
+ line_items:,
72
+ payments:,
73
+ receipt_type: "SALE",
74
+ employee_id: nil,
75
+ customer_id: nil,
76
+ note: nil,
77
+ source: "API"
78
+ )
79
+ body = {
80
+ receipt_date: format_time(receipt_date),
81
+ receipt_type: receipt_type,
82
+ store_id: store_id,
83
+ line_items: line_items,
84
+ payments: payments,
85
+ source: source
86
+ }
87
+ body[:employee_id] = employee_id if employee_id
88
+ body[:customer_id] = customer_id if customer_id
89
+ body[:note] = note if note
90
+
91
+ post("receipts", body: body)
92
+ end
93
+
94
+ # Create a refund for a receipt
95
+ # @param receipt_number [String, Integer] Receipt number to refund
96
+ # @param refund_date [String, Time] Refund date in ISO 8601 format
97
+ # @param line_items [Array<Hash>] Array of line items to refund
98
+ # @param payments [Array<Hash>] Array of refund payments
99
+ # @param employee_id [String] UUID of the employee (optional)
100
+ # @param note [String] Refund note (optional)
101
+ # @return [Hash] Created refund receipt details
102
+ def create_refund(receipt_number, refund_date:, line_items:, payments:, employee_id: nil, note: nil)
103
+ body = {
104
+ refund_date: format_time(refund_date),
105
+ line_items: line_items,
106
+ payments: payments
107
+ }
108
+ body[:employee_id] = employee_id if employee_id
109
+ body[:note] = note if note
110
+
111
+ post("receipts/#{receipt_number}/refund", body: body)
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,85 @@
1
+ module LoyverseApi
2
+ module Endpoints
3
+ module Webhooks
4
+ # Get a specific webhook by ID
5
+ # @param webhook_id [String] UUID of the webhook
6
+ # @return [Hash] Webhook details
7
+ def get_webhook(webhook_id)
8
+ get("webhooks/#{webhook_id}")
9
+ end
10
+
11
+ # List all webhooks
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 webhooks array
15
+ def list_webhooks(limit: 250, cursor: nil)
16
+ params = {
17
+ limit: limit,
18
+ cursor: cursor
19
+ }.compact
20
+
21
+ get("webhooks", params: params)
22
+ end
23
+
24
+ # Create a new webhook
25
+ # @param url [String] Webhook endpoint URL
26
+ # @param event_types [Array<String>] Array of event types to subscribe to
27
+ # @param description [String] Webhook description (optional)
28
+ # @return [Hash] Created webhook details
29
+ #
30
+ # Available event types:
31
+ # - ORDER_CREATED
32
+ # - ITEM_UPDATED
33
+ # - INVENTORY_UPDATED
34
+ def create_webhook(url:, event_types:, description: nil)
35
+ body = {
36
+ url: url,
37
+ event_types: Array(event_types)
38
+ }
39
+ body[:description] = description if description
40
+
41
+ post("webhooks", body: body)
42
+ end
43
+
44
+ # Delete a webhook
45
+ # @param webhook_id [String] UUID of the webhook
46
+ # @return [Hash] Response
47
+ def delete_webhook(webhook_id)
48
+ delete("webhooks/#{webhook_id}")
49
+ end
50
+
51
+ # Verify webhook signature (for OAuth 2.0 created webhooks)
52
+ # @param payload [String] Raw request body
53
+ # @param signature [String] X-Loyverse-Signature header value
54
+ # @param secret [String] Your webhook secret
55
+ # @return [Boolean] True if signature is valid
56
+ def verify_webhook_signature(payload, signature, secret)
57
+ return false if payload.nil? || signature.nil? || secret.nil?
58
+
59
+ require "openssl"
60
+
61
+ computed_signature = OpenSSL::HMAC.hexdigest(
62
+ OpenSSL::Digest.new("sha256"),
63
+ secret,
64
+ payload
65
+ )
66
+
67
+ secure_compare(computed_signature, signature)
68
+ end
69
+
70
+ private
71
+
72
+ # Constant-time string comparison to prevent timing attacks
73
+ def secure_compare(a, b)
74
+ return false if a.nil? || b.nil? || a.bytesize != b.bytesize
75
+
76
+ l = a.unpack("C*")
77
+ r = 0
78
+ i = -1
79
+
80
+ b.each_byte { |byte| r |= byte ^ l[i += 1] }
81
+ r == 0
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,19 @@
1
+ module LoyverseApi
2
+ class Error < StandardError
3
+ attr_reader :code, :details
4
+
5
+ def initialize(message = nil, code: nil, details: nil)
6
+ @code = code
7
+ @details = details
8
+ super(message)
9
+ end
10
+ end
11
+
12
+ class AuthenticationError < Error; end
13
+ class AuthorizationError < Error; end
14
+ class NotFoundError < Error; end
15
+ class BadRequestError < Error; end
16
+ class RateLimitError < Error; end
17
+ class ServerError < Error; end
18
+ class ApiError < Error; end
19
+ end
@@ -0,0 +1,3 @@
1
+ module LoyverseApi
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,33 @@
1
+ require "faraday"
2
+ require "faraday/retry"
3
+ require "json"
4
+ require "time"
5
+
6
+ require_relative "loyverse_api/version"
7
+ require_relative "loyverse_api/configuration"
8
+ require_relative "loyverse_api/errors"
9
+ require_relative "loyverse_api/endpoints/items"
10
+ require_relative "loyverse_api/endpoints/categories"
11
+ require_relative "loyverse_api/endpoints/inventory"
12
+ require_relative "loyverse_api/endpoints/receipts"
13
+ require_relative "loyverse_api/endpoints/webhooks"
14
+ require_relative "loyverse_api/endpoints/customers"
15
+ require_relative "loyverse_api/endpoints/discounts"
16
+ require_relative "loyverse_api/endpoints/employees"
17
+ require_relative "loyverse_api/endpoints/modifiers"
18
+ require_relative "loyverse_api/client"
19
+
20
+ module LoyverseApi
21
+ class << self
22
+ attr_accessor :configuration
23
+ end
24
+
25
+ def self.configure
26
+ self.configuration ||= Configuration.new
27
+ yield(configuration)
28
+ end
29
+
30
+ def self.client
31
+ Client.new(configuration)
32
+ end
33
+ end
@@ -0,0 +1,34 @@
1
+ require_relative 'lib/loyverse_api/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "loyverse_api"
5
+ spec.version = LoyverseApi::VERSION
6
+ spec.authors = ["Loyverse API Wrapper"]
7
+ spec.email = ["hola@alvarodelgado.dev"]
8
+
9
+ spec.summary = %q{Ruby wrapper for the Loyverse API}
10
+ spec.description = %q{A comprehensive Ruby gem for interacting with the Loyverse API, supporting authentication, webhooks, and all major resources including items, inventory, receipts, and categories.}
11
+ spec.homepage = "https://github.com/yourusername/loyverse_api"
12
+ spec.license = "MIT"
13
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.6.0")
14
+
15
+ spec.metadata["homepage_uri"] = spec.homepage
16
+ spec.metadata["source_code_uri"] = spec.homepage
17
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
18
+
19
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
20
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
21
+ end
22
+ spec.bindir = "exe"
23
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
+ spec.require_paths = ["lib"]
25
+
26
+ spec.add_dependency "faraday", "~> 2.0"
27
+ spec.add_dependency "faraday-retry", "~> 2.0"
28
+
29
+ spec.add_development_dependency "bundler", "~> 2.0"
30
+ spec.add_development_dependency "rake", "~> 13.0"
31
+ spec.add_development_dependency "rspec", "~> 3.0"
32
+ spec.add_development_dependency "webmock", "~> 3.0"
33
+ spec.add_development_dependency "vcr", "~> 6.0"
34
+ end