Class: CloverSandboxSimulator::Services::Clover::TaxService

Inherits:
BaseService
  • Object
show all
Defined in:
lib/clover_sandbox_simulator/services/clover/tax_service.rb

Overview

Manages Clover tax rates and item-tax associations

Constant Summary collapse

DEFAULT_TAX_RATES =

Default tax rates for restaurants

[
  { name: "Sales Tax", rate: 8.25, is_default: true, description: "Standard sales tax" },
  { name: "Alcohol Tax", rate: 10.0, is_default: false, description: "Additional tax on alcoholic beverages" },
  { name: "Prepared Food Tax", rate: 8.25, is_default: false, description: "Tax on prepared food items" }
].freeze
CACHE_TTL_SECONDS =

Cache configuration

300
CACHE_MAX_SIZE =

5 minutes

1000

Instance Attribute Summary

Attributes inherited from BaseService

#config, #logger

Instance Method Summary collapse

Methods inherited from BaseService

#initialize

Constructor Details

This class inherits a constructor from CloverSandboxSimulator::Services::BaseService

Instance Method Details

#assign_taxes_by_category(items, tax_rates, category_tax_mapping = nil) ⇒ Object

Assign tax rates to items based on their category

Parameters:

  • items (Array<Hash>)

    Items with category information

  • tax_rates (Array<Hash>)

    Available tax rates

  • category_tax_mapping (Hash) (defaults to: nil)

    Mapping of category names to tax rate names



228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/clover_sandbox_simulator/services/clover/tax_service.rb', line 228

def assign_taxes_by_category(items, tax_rates, category_tax_mapping = nil)
  category_tax_mapping ||= default_category_tax_mapping

  # Build a lookup for tax rates by name
  tax_rate_lookup = tax_rates.each_with_object({}) do |rate, hash|
    hash[rate["name"]&.downcase] = rate["id"]
  end

  items.each do |item|
    category = item.dig("categories", "elements", 0, "name") || item["category"]
    next unless category

    # Find applicable tax rates for this category
    applicable_taxes = category_tax_mapping[category] || category_tax_mapping["default"] || ["Sales Tax"]

    applicable_taxes.each do |tax_name|
      tax_rate_id = tax_rate_lookup[tax_name.downcase]
      next unless tax_rate_id

      begin
        associate_item_with_tax_rate(item["id"], tax_rate_id)
      rescue ApiError => e
        logger.debug "Could not assign tax to item #{item["id"]}: #{e.message}"
      end
    end
  end
end

#associate_item_with_tax_rate(item_id, tax_rate_id) ⇒ Object

Associate an item with a tax rate



87
88
89
90
91
92
93
94
# File 'lib/clover_sandbox_simulator/services/clover/tax_service.rb', line 87

def associate_item_with_tax_rate(item_id, tax_rate_id)
  logger.info "Associating item #{item_id} with tax rate #{tax_rate_id}"
  request(:post, endpoint("tax_rate_items"), payload: {
    "elements" => [
      { "item" => { "id" => item_id }, "taxRate" => { "id" => tax_rate_id } }
    ]
  })
end

#associate_items_with_tax_rate(item_ids, tax_rate_id) ⇒ Object

Associate multiple items with a tax rate



103
104
105
106
107
108
109
# File 'lib/clover_sandbox_simulator/services/clover/tax_service.rb', line 103

def associate_items_with_tax_rate(item_ids, tax_rate_id)
  logger.info "Associating #{item_ids.size} items with tax rate #{tax_rate_id}"
  elements = item_ids.map do |item_id|
    { "item" => { "id" => item_id }, "taxRate" => { "id" => tax_rate_id } }
  end
  request(:post, endpoint("tax_rate_items"), payload: { "elements" => elements })
end

#calculate_item_tax(item_id, amount) ⇒ Integer

Calculate tax for a specific item based on its assigned tax rates

Parameters:

  • item_id (String)

    The item ID

  • amount (Integer)

    The amount in cents

Returns:

  • (Integer)

    The total tax in cents



117
118
119
120
121
122
123
124
125
# File 'lib/clover_sandbox_simulator/services/clover/tax_service.rb', line 117

def calculate_item_tax(item_id, amount)
  rates = get_tax_rates_for_item(item_id)
  return 0 if rates.empty?

  # Sum up all applicable tax rates (convert from basis points to percentage)
  total_rate_percentage = rates.sum { |r| (r["rate"] || 0) / 100_000.0 }

  (amount * total_rate_percentage / 100.0).round
end

#calculate_items_tax(items) ⇒ Integer

Calculate tax for multiple items with bounded caching

Parameters:

  • items (Array<Hash>)

    Array of { item_id:, amount: }

Returns:

  • (Integer)

    The total tax in cents



134
135
136
137
138
139
140
141
142
143
# File 'lib/clover_sandbox_simulator/services/clover/tax_service.rb', line 134

def calculate_items_tax(items)
  items.sum do |item|
    item_id = item[:item_id]
    amount = item[:amount]

    rates = get_cached_tax_rates(item_id)
    total_rate = rates.sum { |r| (r["rate"] || 0) / 100_000.0 }
    (amount * total_rate / 100.0).round
  end
end

#calculate_tax(subtotal, tax_rate = nil) ⇒ Object

Calculate tax for an amount using a flat rate



53
54
55
56
# File 'lib/clover_sandbox_simulator/services/clover/tax_service.rb', line 53

def calculate_tax(subtotal, tax_rate = nil)
  rate = tax_rate || config.tax_rate
  (subtotal * rate / 100.0).round
end

#clear_cacheObject

Clear the tax rates cache



146
147
148
149
# File 'lib/clover_sandbox_simulator/services/clover/tax_service.rb', line 146

def clear_cache
  @item_tax_rates_cache = {}
  @cache_timestamps = {}
end

#create_tax_rate(name:, rate:, is_default: false) ⇒ Object

Create a tax rate



32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/clover_sandbox_simulator/services/clover/tax_service.rb', line 32

def create_tax_rate(name:, rate:, is_default: false)
  logger.info "Creating tax rate: #{name} (#{rate}%)"

  # Rate is stored as basis points (8.25% = 825000)
  rate_basis_points = (rate * 100_000).to_i

  request(:post, endpoint("tax_rates"), payload: {
    "name" => name,
    "rate" => rate_basis_points,
    "isDefault" => is_default,
    "taxType" => "VAT_EXEMPT" # For US sales tax
  })
end

#default_tax_rateObject

Get default tax rate



25
26
27
28
29
# File 'lib/clover_sandbox_simulator/services/clover/tax_service.rb', line 25

def default_tax_rate
  rates = get_tax_rates
  # Find default rate or return first active one
  rates.find { |r| r["isDefault"] == true } || rates.first
end

#delete_tax_rate(tax_rate_id) ⇒ Object

Delete a tax rate



47
48
49
50
# File 'lib/clover_sandbox_simulator/services/clover/tax_service.rb', line 47

def delete_tax_rate(tax_rate_id)
  logger.info "Deleting tax rate: #{tax_rate_id}"
  request(:delete, endpoint("tax_rates/#{tax_rate_id}"))
end

#get_cached_tax_rates(item_id) ⇒ Object

Get tax rates from cache or API with TTL and size limits



152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/clover_sandbox_simulator/services/clover/tax_service.rb', line 152

def get_cached_tax_rates(item_id)
  @item_tax_rates_cache ||= {}
  @cache_timestamps ||= {}

  now = Time.now.to_i

  # Check if cached and not expired
  if @item_tax_rates_cache.key?(item_id)
    if now - @cache_timestamps[item_id] < CACHE_TTL_SECONDS
      return @item_tax_rates_cache[item_id]
    else
      # Expired - remove from cache
      @item_tax_rates_cache.delete(item_id)
      @cache_timestamps.delete(item_id)
    end
  end

  # Prune cache if too large (LRU-style: remove oldest entries)
  if @item_tax_rates_cache.size >= CACHE_MAX_SIZE
    prune_cache(CACHE_MAX_SIZE / 2)
  end

  # Fetch and cache
  rates = get_tax_rates_for_item(item_id)
  @item_tax_rates_cache[item_id] = rates
  @cache_timestamps[item_id] = now
  rates
end

#get_items_for_tax_rate(tax_rate_id) ⇒ Object

Get all items associated with a specific tax rate



61
62
63
64
65
66
67
# File 'lib/clover_sandbox_simulator/services/clover/tax_service.rb', line 61

def get_items_for_tax_rate(tax_rate_id)
  logger.info "Fetching items for tax rate: #{tax_rate_id}"
  response = request(:get, endpoint("tax_rates/#{tax_rate_id}/items"))
  elements = response&.dig("elements") || []
  logger.info "Found #{elements.size} items for tax rate"
  elements
end

#get_tax_ratesObject

Fetch all tax rates



16
17
18
19
20
21
22
# File 'lib/clover_sandbox_simulator/services/clover/tax_service.rb', line 16

def get_tax_rates
  logger.info "Fetching tax rates..."
  response = request(:get, endpoint("tax_rates"))
  elements = response&.dig("elements") || []
  logger.info "Found #{elements.size} tax rates"
  elements
end

#get_tax_rates_for_item(item_id) ⇒ Object

Note:

This endpoint may not be available in sandbox environments

Get all tax rates associated with a specific item



71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/clover_sandbox_simulator/services/clover/tax_service.rb', line 71

def get_tax_rates_for_item(item_id)
  logger.debug "Fetching tax rates for item: #{item_id}"
  begin
    response = request(:get, endpoint("items/#{item_id}/tax_rates"))
    response&.dig("elements") || []
  rescue ApiError => e
    if e.message.include?("405")
      logger.debug "Getting item tax rates not supported in this environment"
      []
    else
      raise
    end
  end
end

#prune_cache(keep_count) ⇒ Object

Remove oldest cache entries



182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/clover_sandbox_simulator/services/clover/tax_service.rb', line 182

def prune_cache(keep_count)
  return if @cache_timestamps.empty?

  # Sort by timestamp and keep only the newest entries
  sorted_keys = @cache_timestamps.sort_by { |_, ts| ts }.map(&:first)
  keys_to_remove = sorted_keys.take(sorted_keys.size - keep_count)

  keys_to_remove.each do |key|
    @item_tax_rates_cache.delete(key)
    @cache_timestamps.delete(key)
  end

  logger.debug "Pruned #{keys_to_remove.size} expired cache entries"
end

#remove_item_from_tax_rate(item_id, tax_rate_id) ⇒ Object

Remove an item from a tax rate



97
98
99
100
# File 'lib/clover_sandbox_simulator/services/clover/tax_service.rb', line 97

def remove_item_from_tax_rate(item_id, tax_rate_id)
  logger.info "Removing item #{item_id} from tax rate #{tax_rate_id}"
  request(:delete, endpoint("tax_rate_items"), params: { item: item_id, taxRate: tax_rate_id })
end

#setup_default_tax_ratesObject

Set up default tax rates for a restaurant if they don’t exist



200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'lib/clover_sandbox_simulator/services/clover/tax_service.rb', line 200

def setup_default_tax_rates
  logger.info "Setting up default tax rates..."
  existing = get_tax_rates
  existing_names = existing.map { |r| r["name"]&.downcase }

  created = []
  DEFAULT_TAX_RATES.each do |rate_data|
    if existing_names.include?(rate_data[:name].downcase)
      logger.debug "Tax rate '#{rate_data[:name]}' already exists, skipping"
      created << existing.find { |r| r["name"]&.downcase == rate_data[:name].downcase }
    else
      rate = create_tax_rate(
        name: rate_data[:name],
        rate: rate_data[:rate],
        is_default: rate_data[:is_default]
      )
      created << rate if rate
    end
  end

  logger.info "Tax rates ready: #{created.size}"
  created
end