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.
- checksums.yaml +7 -0
- data/.env.sample +1 -0
- data/.gitignore +54 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +12 -0
- data/Gemfile +7 -0
- data/LICENSE +21 -0
- data/LICENSE.txt +21 -0
- data/README.md +516 -0
- data/Rakefile +6 -0
- data/examples/basic_usage.rb +115 -0
- data/examples/create_item.rb +71 -0
- data/examples/webhook_server.rb +120 -0
- data/lib/loyverse_api/client.rb +157 -0
- data/lib/loyverse_api/configuration.rb +13 -0
- data/lib/loyverse_api/endpoints/categories.rb +43 -0
- data/lib/loyverse_api/endpoints/customers.rb +131 -0
- data/lib/loyverse_api/endpoints/discounts.rb +91 -0
- data/lib/loyverse_api/endpoints/employees.rb +31 -0
- data/lib/loyverse_api/endpoints/inventory.rb +41 -0
- data/lib/loyverse_api/endpoints/items.rb +75 -0
- data/lib/loyverse_api/endpoints/modifiers.rb +51 -0
- data/lib/loyverse_api/endpoints/receipts.rb +115 -0
- data/lib/loyverse_api/endpoints/webhooks.rb +85 -0
- data/lib/loyverse_api/errors.rb +19 -0
- data/lib/loyverse_api/version.rb +3 -0
- data/lib/loyverse_api.rb +33 -0
- data/loyverse_api.gemspec +34 -0
- metadata +170 -0
|
@@ -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
|
data/lib/loyverse_api.rb
ADDED
|
@@ -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
|