clover_sandbox_simulator 1.0.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/Gemfile +10 -0
- data/README.md +316 -0
- data/bin/simulate +209 -0
- data/lib/clover_sandbox_simulator/configuration.rb +51 -0
- data/lib/clover_sandbox_simulator/data/restaurant/categories.json +39 -0
- data/lib/clover_sandbox_simulator/data/restaurant/discounts.json +39 -0
- data/lib/clover_sandbox_simulator/data/restaurant/items.json +238 -0
- data/lib/clover_sandbox_simulator/data/restaurant/modifiers.json +62 -0
- data/lib/clover_sandbox_simulator/data/restaurant/tenders.json +41 -0
- data/lib/clover_sandbox_simulator/generators/data_loader.rb +54 -0
- data/lib/clover_sandbox_simulator/generators/entity_generator.rb +164 -0
- data/lib/clover_sandbox_simulator/generators/order_generator.rb +540 -0
- data/lib/clover_sandbox_simulator/services/base_service.rb +111 -0
- data/lib/clover_sandbox_simulator/services/clover/customer_service.rb +82 -0
- data/lib/clover_sandbox_simulator/services/clover/discount_service.rb +58 -0
- data/lib/clover_sandbox_simulator/services/clover/employee_service.rb +82 -0
- data/lib/clover_sandbox_simulator/services/clover/inventory_service.rb +120 -0
- data/lib/clover_sandbox_simulator/services/clover/order_service.rb +170 -0
- data/lib/clover_sandbox_simulator/services/clover/payment_service.rb +123 -0
- data/lib/clover_sandbox_simulator/services/clover/services_manager.rb +49 -0
- data/lib/clover_sandbox_simulator/services/clover/tax_service.rb +53 -0
- data/lib/clover_sandbox_simulator/services/clover/tender_service.rb +117 -0
- data/lib/clover_sandbox_simulator.rb +43 -0
- metadata +195 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faker"
|
|
4
|
+
|
|
5
|
+
module CloverSandboxSimulator
|
|
6
|
+
module Services
|
|
7
|
+
module Clover
|
|
8
|
+
# Manages Clover customers
|
|
9
|
+
class CustomerService < BaseService
|
|
10
|
+
# Fetch all customers
|
|
11
|
+
def get_customers
|
|
12
|
+
logger.info "Fetching customers..."
|
|
13
|
+
response = request(:get, endpoint("customers"))
|
|
14
|
+
elements = response&.dig("elements") || []
|
|
15
|
+
logger.info "Found #{elements.size} customers"
|
|
16
|
+
elements
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Get a specific customer
|
|
20
|
+
def get_customer(customer_id)
|
|
21
|
+
request(:get, endpoint("customers/#{customer_id}"))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Create a customer
|
|
25
|
+
def create_customer(first_name:, last_name:, email: nil, phone: nil)
|
|
26
|
+
logger.info "Creating customer: #{first_name} #{last_name}"
|
|
27
|
+
|
|
28
|
+
payload = {
|
|
29
|
+
"firstName" => first_name,
|
|
30
|
+
"lastName" => last_name
|
|
31
|
+
}
|
|
32
|
+
payload["emailAddresses"] = [{ "emailAddress" => email }] if email
|
|
33
|
+
payload["phoneNumbers"] = [{ "phoneNumber" => phone }] if phone
|
|
34
|
+
|
|
35
|
+
request(:post, endpoint("customers"), payload: payload)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Create sample customers if needed
|
|
39
|
+
def ensure_customers(count: 10)
|
|
40
|
+
existing = get_customers
|
|
41
|
+
return existing if existing.size >= count
|
|
42
|
+
|
|
43
|
+
needed = count - existing.size
|
|
44
|
+
logger.info "Creating #{needed} sample customers..."
|
|
45
|
+
|
|
46
|
+
new_customers = []
|
|
47
|
+
needed.times do
|
|
48
|
+
first = Faker::Name.first_name
|
|
49
|
+
last = Faker::Name.last_name
|
|
50
|
+
# Use example.com domain - Clover rejects .test domains
|
|
51
|
+
# Remove special chars, collapse dots, strip leading/trailing dots
|
|
52
|
+
safe_first = first.downcase.gsub(/[^a-z0-9]/, "")
|
|
53
|
+
safe_last = last.downcase.gsub(/[^a-z0-9]/, "")
|
|
54
|
+
customer = create_customer(
|
|
55
|
+
first_name: first,
|
|
56
|
+
last_name: last,
|
|
57
|
+
email: "#{safe_first}.#{safe_last}@example.com",
|
|
58
|
+
phone: Faker::PhoneNumber.cell_phone
|
|
59
|
+
)
|
|
60
|
+
new_customers << customer if customer
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
existing + new_customers
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Get a random customer (70% chance of returning a customer, 30% anonymous)
|
|
67
|
+
def random_customer
|
|
68
|
+
return nil if rand < 0.3 # 30% anonymous orders
|
|
69
|
+
|
|
70
|
+
customers = get_customers
|
|
71
|
+
customers.sample
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Delete a customer
|
|
75
|
+
def delete_customer(customer_id)
|
|
76
|
+
logger.info "Deleting customer: #{customer_id}"
|
|
77
|
+
request(:delete, endpoint("customers/#{customer_id}"))
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CloverSandboxSimulator
|
|
4
|
+
module Services
|
|
5
|
+
module Clover
|
|
6
|
+
# Manages Clover discounts
|
|
7
|
+
class DiscountService < BaseService
|
|
8
|
+
# Fetch all discounts
|
|
9
|
+
def get_discounts
|
|
10
|
+
logger.info "Fetching discounts..."
|
|
11
|
+
response = request(:get, endpoint("discounts"))
|
|
12
|
+
elements = response&.dig("elements") || []
|
|
13
|
+
logger.info "Found #{elements.size} discounts"
|
|
14
|
+
elements
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Get a specific discount
|
|
18
|
+
def get_discount(discount_id)
|
|
19
|
+
request(:get, endpoint("discounts/#{discount_id}"))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Create a fixed amount discount
|
|
23
|
+
def create_fixed_discount(name:, amount:)
|
|
24
|
+
logger.info "Creating fixed discount: #{name} ($#{amount / 100.0})"
|
|
25
|
+
|
|
26
|
+
request(:post, endpoint("discounts"), payload: {
|
|
27
|
+
"name" => name,
|
|
28
|
+
"amount" => -amount.abs # Clover expects negative for discounts
|
|
29
|
+
})
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Create a percentage discount
|
|
33
|
+
def create_percentage_discount(name:, percentage:)
|
|
34
|
+
logger.info "Creating percentage discount: #{name} (#{percentage}%)"
|
|
35
|
+
|
|
36
|
+
request(:post, endpoint("discounts"), payload: {
|
|
37
|
+
"name" => name,
|
|
38
|
+
"percentage" => percentage
|
|
39
|
+
})
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Delete a discount
|
|
43
|
+
def delete_discount(discount_id)
|
|
44
|
+
logger.info "Deleting discount: #{discount_id}"
|
|
45
|
+
request(:delete, endpoint("discounts/#{discount_id}"))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Select a random discount (30% chance of returning nil)
|
|
49
|
+
def random_discount
|
|
50
|
+
return nil if rand < 0.7 # 70% chance of no discount
|
|
51
|
+
|
|
52
|
+
discounts = get_discounts
|
|
53
|
+
discounts.sample
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faker"
|
|
4
|
+
|
|
5
|
+
module CloverSandboxSimulator
|
|
6
|
+
module Services
|
|
7
|
+
module Clover
|
|
8
|
+
# Manages Clover employees
|
|
9
|
+
class EmployeeService < BaseService
|
|
10
|
+
# Note: OWNER and ADMIN roles may not be available in sandbox
|
|
11
|
+
ROLES = %w[MANAGER EMPLOYEE].freeze
|
|
12
|
+
|
|
13
|
+
# Fetch all employees
|
|
14
|
+
def get_employees
|
|
15
|
+
logger.info "Fetching employees..."
|
|
16
|
+
response = request(:get, endpoint("employees"))
|
|
17
|
+
elements = response&.dig("elements") || []
|
|
18
|
+
# Filter active employees
|
|
19
|
+
active = elements.select { |e| e["deleted"] != true }
|
|
20
|
+
logger.info "Found #{active.size} active employees"
|
|
21
|
+
active
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Get a specific employee
|
|
25
|
+
def get_employee(employee_id)
|
|
26
|
+
request(:get, endpoint("employees/#{employee_id}"))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Create an employee
|
|
30
|
+
def create_employee(name:, email: nil, role: "EMPLOYEE", pin: nil)
|
|
31
|
+
logger.info "Creating employee: #{name}"
|
|
32
|
+
|
|
33
|
+
payload = {
|
|
34
|
+
"name" => name,
|
|
35
|
+
"role" => role
|
|
36
|
+
}
|
|
37
|
+
payload["email"] = email if email
|
|
38
|
+
payload["pin"] = pin if pin
|
|
39
|
+
|
|
40
|
+
request(:post, endpoint("employees"), payload: payload)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Create sample employees if needed
|
|
44
|
+
def ensure_employees(count: 3)
|
|
45
|
+
existing = get_employees
|
|
46
|
+
return existing if existing.size >= count
|
|
47
|
+
|
|
48
|
+
needed = count - existing.size
|
|
49
|
+
logger.info "Creating #{needed} sample employees..."
|
|
50
|
+
|
|
51
|
+
new_employees = []
|
|
52
|
+
needed.times do
|
|
53
|
+
name = Faker::Name.name
|
|
54
|
+
# Use example.com domain - Clover rejects .test domains
|
|
55
|
+
# Remove special chars, collapse dots, strip leading/trailing dots
|
|
56
|
+
safe_name = name.downcase.gsub(/[^a-z0-9]/, ".").gsub(/\.+/, ".").gsub(/^\.|\.$/, "")
|
|
57
|
+
employee = create_employee(
|
|
58
|
+
name: name,
|
|
59
|
+
email: "#{safe_name}@example.com",
|
|
60
|
+
role: ROLES.sample
|
|
61
|
+
)
|
|
62
|
+
new_employees << employee if employee
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
existing + new_employees
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Get a random employee
|
|
69
|
+
def random_employee
|
|
70
|
+
employees = get_employees
|
|
71
|
+
employees.sample
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Delete an employee
|
|
75
|
+
def delete_employee(employee_id)
|
|
76
|
+
logger.info "Deleting employee: #{employee_id}"
|
|
77
|
+
request(:delete, endpoint("employees/#{employee_id}"))
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CloverSandboxSimulator
|
|
4
|
+
module Services
|
|
5
|
+
module Clover
|
|
6
|
+
# Manages Clover inventory: categories, items, modifiers
|
|
7
|
+
class InventoryService < BaseService
|
|
8
|
+
# Fetch all categories
|
|
9
|
+
def get_categories
|
|
10
|
+
logger.info "Fetching categories..."
|
|
11
|
+
response = request(:get, endpoint("categories"))
|
|
12
|
+
elements = response&.dig("elements") || []
|
|
13
|
+
logger.info "Found #{elements.size} categories"
|
|
14
|
+
elements
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Create a category
|
|
18
|
+
def create_category(name:, sort_order: nil)
|
|
19
|
+
logger.info "Creating category: #{name}"
|
|
20
|
+
payload = { "name" => name }
|
|
21
|
+
payload["sortOrder"] = sort_order if sort_order
|
|
22
|
+
request(:post, endpoint("categories"), payload: payload)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Delete a category
|
|
26
|
+
def delete_category(category_id)
|
|
27
|
+
logger.info "Deleting category: #{category_id}"
|
|
28
|
+
request(:delete, endpoint("categories/#{category_id}"))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Fetch all items
|
|
32
|
+
def get_items
|
|
33
|
+
logger.info "Fetching items..."
|
|
34
|
+
response = request(:get, endpoint("items"), params: { expand: "categories,modifierGroups" })
|
|
35
|
+
elements = response&.dig("elements") || []
|
|
36
|
+
# Filter out deleted items
|
|
37
|
+
active_items = elements.reject { |item| item["deleted"] == true }
|
|
38
|
+
logger.info "Found #{active_items.size} active items"
|
|
39
|
+
active_items
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Create an item
|
|
43
|
+
def create_item(name:, price:, category_id: nil, sku: nil, hidden: false)
|
|
44
|
+
logger.info "Creating item: #{name} ($#{price / 100.0})"
|
|
45
|
+
|
|
46
|
+
payload = {
|
|
47
|
+
"name" => name,
|
|
48
|
+
"price" => price,
|
|
49
|
+
"priceType" => "FIXED",
|
|
50
|
+
"hidden" => hidden,
|
|
51
|
+
"defaultTaxRates" => true
|
|
52
|
+
}
|
|
53
|
+
payload["sku"] = sku if sku
|
|
54
|
+
|
|
55
|
+
item = request(:post, endpoint("items"), payload: payload)
|
|
56
|
+
|
|
57
|
+
# Associate with category if provided
|
|
58
|
+
if item && category_id
|
|
59
|
+
associate_item_with_category(item["id"], category_id)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
item
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Associate item with category
|
|
66
|
+
def associate_item_with_category(item_id, category_id)
|
|
67
|
+
logger.debug "Associating item #{item_id} with category #{category_id}"
|
|
68
|
+
request(:post, endpoint("category_items"), payload: {
|
|
69
|
+
"elements" => [{ "item" => { "id" => item_id }, "category" => { "id" => category_id } }]
|
|
70
|
+
})
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Delete an item
|
|
74
|
+
def delete_item(item_id)
|
|
75
|
+
logger.info "Deleting item: #{item_id}"
|
|
76
|
+
request(:delete, endpoint("items/#{item_id}"))
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Fetch all modifier groups
|
|
80
|
+
def get_modifier_groups
|
|
81
|
+
logger.info "Fetching modifier groups..."
|
|
82
|
+
response = request(:get, endpoint("modifier_groups"), params: { expand: "modifiers" })
|
|
83
|
+
elements = response&.dig("elements") || []
|
|
84
|
+
logger.info "Found #{elements.size} modifier groups"
|
|
85
|
+
elements
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Create a modifier group
|
|
89
|
+
def create_modifier_group(name:, min_required: 0, max_allowed: nil)
|
|
90
|
+
logger.info "Creating modifier group: #{name}"
|
|
91
|
+
payload = {
|
|
92
|
+
"name" => name,
|
|
93
|
+
"minRequired" => min_required
|
|
94
|
+
}
|
|
95
|
+
payload["maxAllowed"] = max_allowed if max_allowed
|
|
96
|
+
request(:post, endpoint("modifier_groups"), payload: payload)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Create a modifier within a group
|
|
100
|
+
def create_modifier(modifier_group_id:, name:, price: 0)
|
|
101
|
+
logger.info "Creating modifier: #{name} in group #{modifier_group_id}"
|
|
102
|
+
request(:post, endpoint("modifier_groups/#{modifier_group_id}/modifiers"), payload: {
|
|
103
|
+
"name" => name,
|
|
104
|
+
"price" => price
|
|
105
|
+
})
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Delete all categories and items
|
|
109
|
+
def delete_all
|
|
110
|
+
logger.warn "Deleting all inventory..."
|
|
111
|
+
|
|
112
|
+
get_items.each { |item| delete_item(item["id"]) }
|
|
113
|
+
get_categories.each { |cat| delete_category(cat["id"]) }
|
|
114
|
+
|
|
115
|
+
logger.info "All inventory deleted"
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CloverSandboxSimulator
|
|
4
|
+
module Services
|
|
5
|
+
module Clover
|
|
6
|
+
# Manages Clover orders and line items
|
|
7
|
+
class OrderService < BaseService
|
|
8
|
+
DINING_OPTIONS = %w[HERE TO_GO DELIVERY].freeze
|
|
9
|
+
|
|
10
|
+
# Fetch all orders
|
|
11
|
+
def get_orders(limit: 50, offset: 0, filter: nil)
|
|
12
|
+
logger.info "Fetching orders..."
|
|
13
|
+
params = { limit: limit, offset: offset }
|
|
14
|
+
params[:filter] = filter if filter
|
|
15
|
+
|
|
16
|
+
response = request(:get, endpoint("orders"), params: params)
|
|
17
|
+
response&.dig("elements") || []
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Get a single order with expanded data
|
|
21
|
+
def get_order(order_id)
|
|
22
|
+
request(:get, endpoint("orders/#{order_id}"), params: {
|
|
23
|
+
expand: "lineItems,discounts,payments,customers"
|
|
24
|
+
})
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Create an order shell
|
|
28
|
+
def create_order(employee_id: nil, customer_id: nil)
|
|
29
|
+
logger.info "Creating order..."
|
|
30
|
+
|
|
31
|
+
payload = {}
|
|
32
|
+
payload["employee"] = { "id" => employee_id } if employee_id
|
|
33
|
+
|
|
34
|
+
order = request(:post, endpoint("orders"), payload: payload)
|
|
35
|
+
|
|
36
|
+
if order && customer_id
|
|
37
|
+
add_customer_to_order(order["id"], customer_id)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
order
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Add line item to order
|
|
44
|
+
def add_line_item(order_id, item_id:, quantity: 1, note: nil)
|
|
45
|
+
logger.debug "Adding item #{item_id} to order #{order_id}"
|
|
46
|
+
|
|
47
|
+
payload = {
|
|
48
|
+
"item" => { "id" => item_id },
|
|
49
|
+
"quantity" => quantity
|
|
50
|
+
}
|
|
51
|
+
payload["note"] = note if note
|
|
52
|
+
|
|
53
|
+
request(:post, endpoint("orders/#{order_id}/line_items"), payload: payload)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Batch add line items (adds one by one as Clover doesn't support bulk)
|
|
57
|
+
def add_line_items(order_id, items)
|
|
58
|
+
logger.info "Adding #{items.size} items to order #{order_id}"
|
|
59
|
+
|
|
60
|
+
line_items = []
|
|
61
|
+
items.each do |item|
|
|
62
|
+
line_item = add_line_item(
|
|
63
|
+
order_id,
|
|
64
|
+
item_id: item[:item_id],
|
|
65
|
+
quantity: item[:quantity] || 1,
|
|
66
|
+
note: item[:note]
|
|
67
|
+
)
|
|
68
|
+
line_items << line_item if line_item
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
line_items
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Apply discount to order
|
|
75
|
+
def apply_discount(order_id, discount_id:, calculated_amount: nil)
|
|
76
|
+
logger.info "Applying discount #{discount_id} to order #{order_id}"
|
|
77
|
+
|
|
78
|
+
# Fetch discount details
|
|
79
|
+
discount_service = DiscountService.new(config: config)
|
|
80
|
+
discount = discount_service.get_discount(discount_id)
|
|
81
|
+
|
|
82
|
+
return nil unless discount
|
|
83
|
+
|
|
84
|
+
payload = { "name" => discount["name"] }
|
|
85
|
+
|
|
86
|
+
if calculated_amount
|
|
87
|
+
payload["amount"] = -calculated_amount.abs
|
|
88
|
+
elsif discount["amount"]
|
|
89
|
+
payload["amount"] = discount["amount"]
|
|
90
|
+
elsif discount["percentage"]
|
|
91
|
+
payload["percentage"] = discount["percentage"].to_s
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
request(:post, endpoint("orders/#{order_id}/discounts"), payload: payload)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Set dining option
|
|
98
|
+
def set_dining_option(order_id, option)
|
|
99
|
+
unless DINING_OPTIONS.include?(option)
|
|
100
|
+
raise ArgumentError, "Invalid dining option: #{option}. Must be one of #{DINING_OPTIONS.join(', ')}"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
request(:post, endpoint("orders/#{order_id}"), payload: { "diningOption" => option })
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Add customer to order
|
|
107
|
+
def add_customer_to_order(order_id, customer_id)
|
|
108
|
+
logger.debug "Adding customer #{customer_id} to order #{order_id}"
|
|
109
|
+
request(:post, endpoint("orders/#{order_id}"), payload: {
|
|
110
|
+
"customers" => { "elements" => [{ "id" => customer_id }] }
|
|
111
|
+
})
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Update order total
|
|
115
|
+
def update_total(order_id, total)
|
|
116
|
+
logger.debug "Updating order #{order_id} total to #{total}"
|
|
117
|
+
request(:post, endpoint("orders/#{order_id}"), payload: { "total" => total })
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Update order state
|
|
121
|
+
def update_state(order_id, state)
|
|
122
|
+
logger.info "Updating order #{order_id} state to #{state}"
|
|
123
|
+
request(:post, endpoint("orders/#{order_id}"), payload: { "state" => state })
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Calculate order total from line items
|
|
127
|
+
def calculate_total(order_id)
|
|
128
|
+
order = get_order(order_id)
|
|
129
|
+
return 0 unless order && order["lineItems"]&.dig("elements")
|
|
130
|
+
|
|
131
|
+
total = 0
|
|
132
|
+
|
|
133
|
+
order["lineItems"]["elements"].each do |line_item|
|
|
134
|
+
price = line_item["price"] || 0
|
|
135
|
+
quantity = line_item["quantity"] || 1
|
|
136
|
+
item_total = price * quantity
|
|
137
|
+
|
|
138
|
+
# Add modification prices
|
|
139
|
+
if line_item["modifications"]&.dig("elements")
|
|
140
|
+
line_item["modifications"]["elements"].each do |mod|
|
|
141
|
+
item_total += (mod["price"] || 0)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
total += item_total
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Subtract discounts
|
|
149
|
+
if order["discounts"]&.dig("elements")
|
|
150
|
+
order["discounts"]["elements"].each do |discount|
|
|
151
|
+
if discount["percentage"]
|
|
152
|
+
total -= (total * discount["percentage"] / 100.0).round
|
|
153
|
+
else
|
|
154
|
+
total -= (discount["amount"] || 0).abs
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
total
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Delete an order
|
|
163
|
+
def delete_order(order_id)
|
|
164
|
+
logger.info "Deleting order: #{order_id}"
|
|
165
|
+
request(:delete, endpoint("orders/#{order_id}"))
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CloverSandboxSimulator
|
|
4
|
+
module Services
|
|
5
|
+
module Clover
|
|
6
|
+
# Manages Clover payments
|
|
7
|
+
# NOTE: Credit/Debit card payments are BROKEN in Clover sandbox
|
|
8
|
+
# Use Cash, Check, Gift Card, External Payment, Store Credit instead
|
|
9
|
+
class PaymentService < BaseService
|
|
10
|
+
# Fetch all payments
|
|
11
|
+
def get_payments
|
|
12
|
+
logger.info "Fetching payments..."
|
|
13
|
+
response = request(:get, endpoint("payments"))
|
|
14
|
+
response&.dig("elements") || []
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Process a payment for an order
|
|
18
|
+
#
|
|
19
|
+
# @param order_id [String] The order ID
|
|
20
|
+
# @param amount [Integer] Subtotal amount in cents (before tip/tax)
|
|
21
|
+
# @param tender_id [String] The tender ID to use
|
|
22
|
+
# @param employee_id [String] The employee processing payment
|
|
23
|
+
# @param tip_amount [Integer] Tip amount in cents
|
|
24
|
+
# @param tax_amount [Integer] Tax amount in cents
|
|
25
|
+
# @return [Hash, nil] Payment response or nil on failure
|
|
26
|
+
def process_payment(order_id:, amount:, tender_id:, employee_id:, tip_amount: 0, tax_amount: 0)
|
|
27
|
+
logger.info "Processing payment for order #{order_id}: $#{amount / 100.0} + tip $#{tip_amount / 100.0} + tax $#{tax_amount / 100.0}"
|
|
28
|
+
|
|
29
|
+
payload = {
|
|
30
|
+
"order" => { "id" => order_id },
|
|
31
|
+
"tender" => { "id" => tender_id },
|
|
32
|
+
"employee" => { "id" => employee_id },
|
|
33
|
+
"offline" => false,
|
|
34
|
+
"amount" => amount,
|
|
35
|
+
"tipAmount" => tip_amount,
|
|
36
|
+
"taxAmount" => tax_amount,
|
|
37
|
+
"transactionSettings" => {
|
|
38
|
+
"disableCashBack" => false,
|
|
39
|
+
"cloverShouldHandleReceipts" => true,
|
|
40
|
+
"forcePinEntryOnSwipe" => false,
|
|
41
|
+
"disableRestartTransactionOnFailure" => false,
|
|
42
|
+
"allowOfflinePayment" => false,
|
|
43
|
+
"approveOfflinePaymentWithoutPrompt" => false,
|
|
44
|
+
"forceOfflinePayment" => false,
|
|
45
|
+
"disableReceiptSelection" => false,
|
|
46
|
+
"disableDuplicateCheck" => false,
|
|
47
|
+
"autoAcceptPaymentConfirmations" => false,
|
|
48
|
+
"autoAcceptSignature" => false,
|
|
49
|
+
"returnResultOnTransactionComplete" => false,
|
|
50
|
+
"disableCreditSurcharge" => false
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
response = request(:post, endpoint("orders/#{order_id}/payments"), payload: payload)
|
|
55
|
+
|
|
56
|
+
if response && response["id"]
|
|
57
|
+
logger.info "Payment successful: #{response["id"]}"
|
|
58
|
+
else
|
|
59
|
+
logger.error "Payment failed: #{response.inspect}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
response
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Process split payment across multiple tenders
|
|
66
|
+
#
|
|
67
|
+
# @param order_id [String] The order ID
|
|
68
|
+
# @param total_amount [Integer] Total amount including tax (before tip)
|
|
69
|
+
# @param tip_amount [Integer] Total tip amount
|
|
70
|
+
# @param tax_amount [Integer] Tax amount
|
|
71
|
+
# @param employee_id [String] Employee ID
|
|
72
|
+
# @param splits [Array<Hash>] Array of { tender:, percentage: } hashes
|
|
73
|
+
# @return [Array<Hash>] Array of payment responses
|
|
74
|
+
def process_split_payment(order_id:, total_amount:, tip_amount:, tax_amount:, employee_id:, splits:)
|
|
75
|
+
logger.info "Processing split payment for order #{order_id} across #{splits.size} tenders"
|
|
76
|
+
|
|
77
|
+
payments = []
|
|
78
|
+
remaining_amount = total_amount
|
|
79
|
+
remaining_tip = tip_amount
|
|
80
|
+
|
|
81
|
+
splits.each_with_index do |split, index|
|
|
82
|
+
tender = split[:tender]
|
|
83
|
+
percentage = split[:percentage]
|
|
84
|
+
is_last = (index == splits.size - 1)
|
|
85
|
+
|
|
86
|
+
# Calculate this payment's portion
|
|
87
|
+
if is_last
|
|
88
|
+
payment_amount = remaining_amount
|
|
89
|
+
payment_tip = remaining_tip
|
|
90
|
+
else
|
|
91
|
+
payment_amount = (total_amount * percentage / 100.0).round
|
|
92
|
+
payment_tip = (tip_amount * percentage / 100.0).round
|
|
93
|
+
remaining_amount -= payment_amount
|
|
94
|
+
remaining_tip -= payment_tip
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Tax only on first payment
|
|
98
|
+
payment_tax = index.zero? ? tax_amount : 0
|
|
99
|
+
|
|
100
|
+
payment = process_payment(
|
|
101
|
+
order_id: order_id,
|
|
102
|
+
amount: payment_amount,
|
|
103
|
+
tender_id: tender["id"],
|
|
104
|
+
employee_id: employee_id,
|
|
105
|
+
tip_amount: payment_tip,
|
|
106
|
+
tax_amount: payment_tax
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
payments << payment if payment
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
payments
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Generate a random tip amount (15-25% of subtotal)
|
|
116
|
+
def generate_tip(subtotal)
|
|
117
|
+
tip_percentage = rand(15..25)
|
|
118
|
+
(subtotal * tip_percentage / 100.0).round
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CloverSandboxSimulator
|
|
4
|
+
module Services
|
|
5
|
+
module Clover
|
|
6
|
+
# Central manager for all Clover services
|
|
7
|
+
# Provides lazy-loaded access to all service classes
|
|
8
|
+
class ServicesManager
|
|
9
|
+
attr_reader :config
|
|
10
|
+
|
|
11
|
+
def initialize(config: nil)
|
|
12
|
+
@config = config || CloverSandboxSimulator.configuration
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def inventory
|
|
16
|
+
@inventory ||= InventoryService.new(config: config)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def tender
|
|
20
|
+
@tender ||= TenderService.new(config: config)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def tax
|
|
24
|
+
@tax ||= TaxService.new(config: config)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def discount
|
|
28
|
+
@discount ||= DiscountService.new(config: config)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def order
|
|
32
|
+
@order ||= OrderService.new(config: config)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def payment
|
|
36
|
+
@payment ||= PaymentService.new(config: config)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def employee
|
|
40
|
+
@employee ||= EmployeeService.new(config: config)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def customer
|
|
44
|
+
@customer ||= CustomerService.new(config: config)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|