poslavu 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .env
19
+ .rvmrc
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Will Glynn
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,106 @@
1
+ = POSLavu
2
+
3
+ POSLavu[http://www.poslavu.com/] is a hosted point-of-sale system. They
4
+ provide an API.
5
+
6
+ The POSLavu API is, franky, disgusting. It feels like something someone might
7
+ have come up with their first week using MySQL and PHP. There's POST parameters
8
+ and XML fragments and JSON scattered about. Error handling and input
9
+ sanitization are afterthoughts. There's no direction or cohesiveness. Tell me:
10
+ would *you* expose an "order" table with 91 different columns? POSLavu did.
11
+
12
+ This gem wraps that API into something that's more reasonable than using their
13
+ API directly. A gem can't fix the data model, but it can add some sanity to the
14
+ access methods.
15
+
16
+ Naturally, you'll need a POSLavu account to do anything useful.
17
+
18
+ == Installation
19
+
20
+ Add this line to your application's Gemfile:
21
+
22
+ gem 'poslavu'
23
+
24
+ And then execute:
25
+
26
+ $ bundle
27
+
28
+ Or install it yourself as:
29
+
30
+ $ gem install poslavu
31
+
32
+ == Usage
33
+
34
+ All usage starts by instantiating a client object.
35
+
36
+ client = POSLavu::Client.new('dataname', 'token', 'key')
37
+
38
+ From here, you can invoke API methods directly:
39
+
40
+ client.invoke('command', 'parameter' => 'value')
41
+
42
+ This is the low-level interface. Hopefully you'll never need it.
43
+
44
+ The POSLavu gem provides a higher-level query interface. Say you want to
45
+ iterate over all your orders:
46
+
47
+ client.table('orders').each { |order|
48
+ # ...
49
+ }
50
+
51
+ Done. This will issue multiple API calls as needed, traversing the list of
52
+ orders one page at a time. Naturally, the resulting object is +Enumerable+,
53
+ so you can call +.map+ or +.inject+ or whatever other normal things you
54
+ might want to do.
55
+
56
+ Now, say you want a list of orders that have produced 4 checks:
57
+
58
+ client.table('orders').where('no_of_checks' => 4).each { |order|
59
+ # ...
60
+ }
61
+
62
+ Or maybe you want to restrict by date:
63
+
64
+ client.table('orders').filter('opened', :between, '2012-10-01', '2012-10-02')
65
+
66
+ It also supports pagination, in case you'd like to handle that yourself:
67
+
68
+ client.table('orders').page(1, 50)
69
+ client.table('orders').page(2, 50)
70
+ client.table('orders').page(3, 50)
71
+
72
+ Client#table returns a POSLavu::QueryScope, which lets you chain various
73
+ conditions and lazily retrieve the results. Records are encapsulated by
74
+ POSLavu::Row, which is just a Hash that came from the POSLavu API.
75
+
76
+ == Development
77
+
78
+ POSLavu uses rspec and WebMock to validate functionality.
79
+
80
+ There is a component of the test suite that runs read-only queries against
81
+ the live POSLavu API. This is intended as a smoke test, principally exercising
82
+ the RPC mechanism, although it can also identify changes in the server-side
83
+ data model.
84
+
85
+ Running the live component of the test suite requires POSLavu API credentials.
86
+ This is safe to run against a live site; it does not modify any data. You can
87
+ pass in your credentials using environment variables or by creating a +.env+
88
+ file with the following:
89
+
90
+ POSLAVU_DATANAME=foobar
91
+ POSLAVU_KEY=q834SCx...
92
+ POSLAVU_TOKEN=EZcWR0n...
93
+
94
+ You can determine the proper values in the
95
+ API[http://admin.poslavu.com/cp/index.php?mode=api] tab of the POSLavu Control
96
+ Panel. Once you're ready, say:
97
+
98
+ $ bundle exec rake live
99
+
100
+ == Contributing
101
+
102
+ 1. Fork it
103
+ 2. Create your feature branch (<tt>git checkout -b my-new-feature</tt>)
104
+ 3. Commit your changes (<tt>git commit -am 'Added some feature'</tt>)
105
+ 4. Push to the branch (<tt>git push origin my-new-feature</tt>)
106
+ 5. Create new Pull Request
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+ require "rspec/core/rake_task"
4
+
5
+ RSpec::Core::RakeTask.new do |t|
6
+ t.rspec_opts = ['--color --format documentation --tag ~live']
7
+ end
8
+
9
+ task :default => :spec
10
+ task :test => :spec
11
+
12
+ desc "Run tests against the live POSLavu servers"
13
+ task :live do
14
+ sh "bundle exec rspec --tag live --color --format documentation"
15
+ end
@@ -0,0 +1,12 @@
1
+ require "poslavu/version"
2
+
3
+ require "poslavu/row"
4
+ require "poslavu/client"
5
+ require "poslavu/query_scope"
6
+
7
+ module POSLavu
8
+ end
9
+
10
+ # Add some aliases so as to not be picky about capitalization
11
+ Object.const_set(:POSlavu, POSLavu)
12
+ Object.const_set(:Poslavu, POSLavu)
@@ -0,0 +1,110 @@
1
+ require "faraday"
2
+ require "nokogiri"
3
+
4
+ # POSLavu::Client communicates with the POSLavu API over HTTPS.
5
+ #
6
+ # You must provide a +dataname+, +token+, and +key+ to create a Client.
7
+ # From here, you can call Client#invoke to run arbitrary API commands.
8
+ #
9
+ # Clients don't hold any state except for a Faraday::Connection, which
10
+ # may or may not use persistent connections depending on your default
11
+ # Faraday adapter.
12
+ class POSLavu::Client
13
+
14
+ # Encapsulates an error thrown by the POSLavu API. All failing API calls
15
+ # via Client#invoke raise a subclass of this exception.
16
+ #
17
+ # See also:
18
+ # * CommandFailedError
19
+ # * CommunicationError
20
+ class Error < RuntimeError; end
21
+
22
+ # Represents an error returned by the API. POSLavu couldn't even be bothered to
23
+ # use HTTP status codes, so there's nothing machine-readable here. If you need
24
+ # to distinguish various types of errors, you'll have to do string matching and
25
+ # hope they don't change anything.
26
+ class CommandFailedError < Error; end
27
+
28
+ # Represents an error outside the scope of normal API failures, including all IP-,
29
+ # TCP-, and HTTP-level errors.
30
+ class CommunicationError < Error; end
31
+
32
+ # The API endpoint as a string
33
+ URL = "https://admin.poslavu.com/cp/reqserv/"
34
+
35
+ # Create a new Client with the specified credentials. These values can be
36
+ # retrieved from this[http://admin.poslavu.com/cp/index.php?mode=api] page as
37
+ # required.
38
+ def initialize(dataname, token, key)
39
+ @parameters = {
40
+ 'dataname' => dataname,
41
+ 'token' => token,
42
+ 'key' => key
43
+ }
44
+ end
45
+
46
+ # Returns this Client's +dataname+.
47
+ def dataname
48
+ @parameters['dataname']
49
+ end
50
+
51
+ def inspect #:nodoc:
52
+ "#<POSLavu::Client dataname=#{@parameters['dataname'].inspect}>"
53
+ end
54
+
55
+ # Returns an object that allows you to access the specified table.
56
+ #
57
+ # # Find all orders for a given table
58
+ # client.table('orders').where('table_id' => 5).each { |row|
59
+ # # ...
60
+ # }
61
+ #
62
+ # See POSLavu::QueryScope for the query syntax.
63
+ def table(table)
64
+ POSLavu::QueryScope.new(self, table)
65
+ end
66
+
67
+ # Invokes a command, accepting a +parameters+ hash, and returning an array of
68
+ # POSLavu::Row objects.
69
+ #
70
+ # The POSLavu API flattens all these parameters into a single POST request.
71
+ # +command+ is broken out as a convenience because #table handles querying, and
72
+ # specifying <tt>'cmd' => 'foo'</tt> repeatedly doesn't feel necessary.
73
+ def invoke(command, parameters = {})
74
+ final_parameters = @parameters.merge(parameters).merge('cmd' => command)
75
+
76
+ response = connection.post URL, final_parameters
77
+
78
+ fragment = Nokogiri::XML.fragment(response.body)
79
+ elements = fragment.children.select(&:element?) # .element_children doesn't work
80
+
81
+ if elements.empty?
82
+ if fragment.to_s.strip.empty?
83
+ # did we actually get no data?
84
+ return []
85
+ else
86
+ # this is apparently how errors are signalled
87
+ raise CommandFailedError, fragment.to_s
88
+ end
89
+ else
90
+ # assume all the elements are <row>s, and let Row explode if we're wrong
91
+ elements.map { |element|
92
+ POSLavu::Row.from_nokogiri(element)
93
+ }
94
+ end
95
+
96
+ rescue Faraday::Error::ClientError
97
+ raise CommunicationError, $!.to_s
98
+ end
99
+
100
+ protected
101
+ def connection #:nodoc:
102
+ @connection ||= Faraday.new(:url => URL) { |faraday|
103
+ faraday.request :url_encoded
104
+ faraday.response :raise_error
105
+ faraday.adapter Faraday.default_adapter
106
+ }.tap { |connection|
107
+ connection.headers[:user_agent] = "POSLavu Ruby #{POSLavu::VERSION}"
108
+ }
109
+ end
110
+ end
@@ -0,0 +1,160 @@
1
+ require 'multi_json'
2
+
3
+ # QueryScope represents a retrievable set of records. You can obtain one by
4
+ # calling POSLavu::Client#table() for the table in question.
5
+ #
6
+ # Query scopes are chainable. Given an initial scope representing all records
7
+ # in a table, you may further restrict the records of interest by with #filter,
8
+ # #page, and #where.
9
+ #
10
+ # Query scopes are Enumerable. #each is the obvious access method, but #to_a,
11
+ # #map, #inject, and all your friends are also available.
12
+ #
13
+ # Query scopes are lazy loading. You can manipulate them as much as you want
14
+ # without performing any API calls. The request is actually performed once you
15
+ # call #each or any other Enumerable method. If you've called #page, the results
16
+ # are held in memory. If not, #each issues multiple requests (internally
17
+ # paginating) and does not hold the result in memory.
18
+ class POSLavu::QueryScope
19
+ include Enumerable
20
+
21
+ # The name of the table, as passed to POSLavu::Client#table
22
+ attr_reader :table
23
+
24
+ # The list of operators supported by the POSLavu API (and thus supported by #filter).
25
+ Operators = ['<=', '>=', '<', '>', '=', '<>', '!=', 'BETWEEN', 'LIKE', 'NOT LIKE']
26
+
27
+ # Returns a new QueryScope with the specified filter applied.
28
+ #
29
+ # The POSLavu API has a basic query language modeled after SQL, probably because
30
+ # they're shoveling the filter straight into SQL. It supports restricting +field+s
31
+ # using a set of Operators. All of them require a value for comparison, except
32
+ # +BETWEEN+, which requires two values.
33
+ #
34
+ # +LIKE+ and +NOT LIKE+ accept +'%'+ as a wildcard. There is no mechanism for
35
+ # pattern-matching strings containing a percent sign.
36
+ def filter(field, operator, value, value2=nil)
37
+ operator = operator.to_s.upcase
38
+ raise ArgumentError, "invalid operator" unless Operators.include?(operator)
39
+
40
+ chain { |x|
41
+ filter = { 'field' => field, 'operator' => operator, 'value1' => value.to_s }
42
+ filter['value2'] = value2.to_s if operator == 'BETWEEN'
43
+ x.filters << filter
44
+ }
45
+ end
46
+
47
+ # Returns a new QueryScope, restricting results to the specified page number.
48
+ #
49
+ # Pages are 1-indexed: the first page is page 1, not page 0.
50
+ def page(number, records_per_page=40)
51
+ raise ArgumentError, "the first page number is 1 (got #{number})" if number < 1
52
+
53
+ chain { |x|
54
+ x.start_record = (number - 1) * records_per_page
55
+ x.record_count = records_per_page
56
+ }
57
+ end
58
+
59
+ # Returns a new QueryScope, restricting results to rows matching the specified
60
+ # hash. It is a convenience method around #filter. The following two statements
61
+ # are exactly equivalent:
62
+ #
63
+ # client.table('orders').where('table_id' => 4, 'no_of_checks' => 2)
64
+ # client.table('orders').filter('table_id', '=', 4).filter('no_of_checks', '=', 2)
65
+ def where(hash)
66
+ scope = self
67
+ hash.each { |key,value|
68
+ scope = scope.filter(key, '=', value)
69
+ }
70
+ scope
71
+ end
72
+
73
+ # Iterate over the records represented by this query scope.
74
+ #
75
+ # If this scope has an explicit #page set, the results will be retrieved and
76
+ # memoized. Otherwise, this scope will internally paginate and make successive
77
+ # requests, yielding each row in turn, and the results will not be memoized.
78
+ def each(&block)
79
+ if @rows
80
+ # we've been memoized
81
+ @rows.each(&block)
82
+
83
+ elsif start_record
84
+ # we represent a single page
85
+ # do the fetching and iterate
86
+ @rows = fetch_rows
87
+
88
+ @rows.each(&block)
89
+ else
90
+ # we represent the whole set of possible records
91
+ # fetch repeatedly, in pages
92
+ page_number = 1
93
+ records_per_page = 100
94
+
95
+ loop {
96
+ # create a scope for this page
97
+ inner_scope = page(page_number, records_per_page)
98
+
99
+ # fetch the records as an array
100
+ records = inner_scope.to_a
101
+
102
+ # pass them to the caller
103
+ records.each(&block)
104
+
105
+ # is this the last page?
106
+ if records.size < records_per_page
107
+ # was this the first page?
108
+ if page_number == 1
109
+ # this is the only page
110
+ # memoize
111
+ @rows = records
112
+ end
113
+
114
+ # regardless, we're done
115
+ break
116
+ end
117
+
118
+ page_number += 1
119
+ }
120
+
121
+ end
122
+
123
+ self
124
+ end
125
+
126
+ #:nodoc:
127
+ protected
128
+ attr_accessor :filters, :start_record, :record_count
129
+ attr_accessor :rows
130
+
131
+ def initialize(client, table)
132
+ @client = client
133
+ @table = table
134
+
135
+ @filters = []
136
+ @start_record = nil
137
+ @record_count = nil
138
+ @rows = nil
139
+ end
140
+
141
+ def chain(&block)
142
+ dup.tap { |copy|
143
+ copy.rows = nil
144
+ yield(copy)
145
+ }
146
+ end
147
+
148
+ def fetch_rows
149
+ @client.invoke('list', to_params)
150
+ end
151
+
152
+ def to_params
153
+ {
154
+ 'table' => @table,
155
+ 'limit' => "#{start_record},#{record_count}"
156
+ }.tap do |params|
157
+ params['filters'] = MultiJson.dump(filters) unless filters.empty?
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,63 @@
1
+ require "nokogiri"
2
+
3
+ # The POSLavu API principally operates on database rows exchanged in
4
+ # XML fragments. These are encapsulated as POSLavu::Row objects, which
5
+ # is really just a Hash with some additional methods.
6
+ class POSLavu::Row < Hash
7
+ # Instantiate a Row, optionally copying an existing Hash.
8
+ def initialize(hash_to_copy = nil)
9
+ if hash_to_copy
10
+ hash_to_copy.each { |key,value|
11
+ self[key.to_sym] = value.to_s
12
+ }
13
+ end
14
+ end
15
+
16
+ # Instantiate a Row given a string containing a <tt><row/></tt> XML fragment.
17
+ # This XML fragment must contain exactly one <tt><row></tt> element at the root.
18
+ def self.from_xml(string)
19
+ fragment = Nokogiri::XML.fragment(string)
20
+ from_nokogiri(fragment)
21
+ end
22
+
23
+ # Instantiate a Row from a Nokogiri::XML::Node or similar. If you're using
24
+ # the public interface, you shouldn't ever need to call this.
25
+ def self.from_nokogiri(xml) # :nodoc:
26
+ raise ArgumentError, "argument is not a Nokogiri node" unless xml.kind_of?(Nokogiri::XML::Node)
27
+
28
+ if xml.element? && xml.name == 'row'
29
+ xml_row = xml
30
+ else
31
+ rows = xml.xpath('./row')
32
+ raise ArgumentError, "argument does not directly contain a <row> element" if rows.empty?
33
+ raise ArgumentError, "argument contains more than one <row> element" if rows.size > 1
34
+
35
+ xml_row = rows.first
36
+ end
37
+
38
+ new.tap { |row|
39
+ xml_row.element_children.each { |element|
40
+ row[element.name.to_sym] = element.text
41
+ }
42
+ }
43
+ end
44
+
45
+ # Adds this Row to a Nokogiri::XML::Node. If you're using the public
46
+ # interface, you shouldn't ever need to call this.
47
+ def to_nokogiri(doc) # :nodoc:
48
+ row = doc.create_element('row'); doc.add_child(row)
49
+ each { |key,value|
50
+ element = doc.create_element(key.to_s)
51
+ element.add_child(doc.create_text_node(value.to_s))
52
+ row.add_child(element)
53
+ }
54
+ row
55
+ end
56
+
57
+ # Transform this Row into a string containing a <tt><row/></tt> XML fragment
58
+ def to_xml
59
+ doc = Nokogiri::XML::Document.new
60
+ element = to_nokogiri(doc)
61
+ element.to_s
62
+ end
63
+ end
@@ -0,0 +1,4 @@
1
+ module POSLavu
2
+ # The version of the POSLavu gem.
3
+ VERSION = "0.0.1"
4
+ end
@@ -0,0 +1,38 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/poslavu/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Will Glynn"]
6
+ gem.email = ["will@willglynn.com"]
7
+ gem.description = %q{POSLavu is a hosted point-of-sale system. The `poslavu` gem provides access to the API.}
8
+ gem.summary = %q{POSLavu API client}
9
+ gem.homepage = "http://github.com/willglynn/poslavu"
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "poslavu"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = POSLavu::VERSION
17
+
18
+ gem.rdoc_options += [
19
+ '--title', 'POSLavu API Client',
20
+ '--main', 'README.rdoc',
21
+ '--exclude', 'spec',
22
+ '--exclude', 'Gemfile',
23
+ '--exclude', 'Rakefile'
24
+ ]
25
+
26
+ gem.add_dependency "nokogiri", "~> 1.5"
27
+ gem.add_dependency "faraday", "~> 0.8"
28
+ gem.add_dependency "multi_json", "~> 1.3"
29
+
30
+ gem.add_development_dependency "bundler", "~> 1.1"
31
+ gem.add_development_dependency "dotenv", "~> 0.2"
32
+ gem.add_development_dependency "guard", "~> 1.4"
33
+ gem.add_development_dependency "rspec", "~> 2.11"
34
+ gem.add_development_dependency "rake", "~> 0.9.2"
35
+ gem.add_development_dependency "webmock", "~> 1.8"
36
+
37
+ gem.add_development_dependency "pry"
38
+ end
@@ -0,0 +1,12 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Global aliases" do
4
+ describe Poslavu do
5
+ it { should eql(POSlavu) }
6
+ end
7
+
8
+ describe POSlavu do
9
+ it { should eql(POSLavu) }
10
+ end
11
+ end
12
+
@@ -0,0 +1,182 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Live tests", :live => true do
4
+ if ENV['POSLAVU_DATANAME'] && ENV['POSLAVU_KEY'] && ENV['POSLAVU_TOKEN']
5
+ before(:all) { WebMock.allow_net_connect! }
6
+ after(:all) { WebMock.disable_net_connect! }
7
+ let!(:client) { POSLavu::Client.new(ENV['POSLAVU_DATANAME'], ENV['POSLAVU_TOKEN'], ENV['POSLAVU_KEY']) }
8
+ subject { client }
9
+
10
+ describe("tables") do
11
+ let(:response) { client.table(table).page(1,1).to_a }
12
+ subject { response }
13
+
14
+ describe("locations") do
15
+ let(:table) { 'locations' }
16
+ it { should_not be_empty }
17
+
18
+ describe("#first") {
19
+ subject { response.first }
20
+ it { should be_kind_of(POSLavu::Row) }
21
+ its(:keys) { should eq [:id, :title, :address, :city, :state, :zip, :phone, :website, :manager, :taxrate, :menu_id, :ag1, :ag2, :PINwait, :integrateCC, :seat_numbers, :PINwaitM, :country, :monitary_symbol, :left_or_right, :gratuity_label, :auto_send_at_checkout, :allow_resend, :lock_orders, :lock_order_override, :exit_after_save, :exit_after_send, :exit_after_print_check, :exit_after_checkout, :exit_after_void, :admin_PIN_discount, :admin_PIN_void, :lock_net_path, :tab_view, :tabs_and_tables, :hide_tabs, :allow_tab_view_toggle, :display_forced_modifier_prices, :component_package, :ask4email_at_checkout, :component_package2, :allow_custom_items, :default_receipt_print, :product_level, :exit_order_after_save, :exit_order_after_send, :exit_order_after_print_check, :use_direct_printing, :debug_mode, :allow_deposits, :cc_signatures, :gateway, :cc_transtype, :admin_PIN_terminal_set, :admin_PIN_till_report, :ml_type, :ml_un, :ml_pw, :ml_listid, :disable_decimal, :individual_cc_receipts, :server_manage_tips, :day_start_end_time, :require_cvn, :display_order_sent_in_ipod, :print_forced_mods_on_receipt, :print_optional_mods_on_receipt, :return_after_add_item_on_ipod, :kitchen_ticket_font_size, :receipt_font_size, :modifiers_in_red, :market_type, :allow_debug_menu, :customer_cc_receipt, :verify_swipe_amount, :itemize_payments_on_receipt, :debug_console, :allow_cc_returns, :allow_voice_auth, :allow_partial_auth, :allow_signature_tip, :mute_register_bell, :group_equivalent_items, :cc_signatures_ipod, :string_encoding, :tax_inclusion, :rounding_factor, :round_up_or_down, :tax_auto_gratuity, :level_to_open_register, :append_cct_details, :html_email_receipts, :verify_remaining_payment, :multiple_quantities_in_red, :verify_quickpay, :use_language_pack, :print_item_notes_on_receipt, :verify_entered_payment, :other_transactions_open_drawer, :credit_transactions_open_drawer, :allow_to_rate, :gateway_debug, :level_to_open_register_at_checkout, :ask_for_guest_count, :level_to_edit_sent_items, :default_dining_room_background, :admin_PIN_void_payments, :admin_PIN_refund, :order_pad_font, :email, :raster_mode_font_size1, :raster_mode_font_size2, :print_logo_on_receipts, :allow_tax_exempt, :level_to_grant_tax_exempt, :timezone, :clockin_overlay, :item_icon_color, :component_package3, :save_device_console_log, :order_pad_font_ipod, :default_preauth_amount, :decimal_char, :thousands_char, :get_card_info, :bypass_checkout_message, :hide_item_titles, :allow_RFID, :display_seat_course_icons, :cash_transactions_open_drawer, :component_package_code, :vf_enrollment_id] }
22
+ }
23
+ end
24
+
25
+ describe("orders") do
26
+ let(:table) { 'orders' }
27
+ it { should_not be_empty }
28
+
29
+ describe("#first") {
30
+ subject { response.first }
31
+ it { should be_kind_of(POSLavu::Row) }
32
+ its(:keys) { should eq [:id, :order_id, :location, :location_id, :opened, :closed, :subtotal, :taxrate, :tax, :total, :server, :server_id, :tablename, :send_status, :discount, :discount_sh, :gratuity, :gratuity_percent, :card_gratuity, :cash_paid, :card_paid, :gift_certificate, :change_amount, :reopen_refund, :void, :cashier, :cashier_id, :auth_by, :auth_by_id, :guests, :email, :permission, :check_has_printed, :no_of_checks, :card_desc, :transaction_id, :multiple_tax_rates, :tab, :original_id, :deposit_status, :register, :refunded, :refund_notes, :refunded_cc, :refund_notes_cc, :refunded_by, :refunded_by_cc, :cash_tip, :discount_value, :reopened_datetime, :discount_type, :deposit_amount, :subtotal_without_deposit, :togo_status, :togo_phone, :togo_time, :cash_applied, :reopen_datetime, :rounding_amount, :auto_gratuity_is_taxed, :discount_id, :refunded_gc, :register_name, :opening_device, :closing_device, :alt_paid, :alt_refunded, :last_course_sent, :tax_exempt, :reclosed_datetime, :reopening_device, :reclosing_device, :exemption_id, :exemption_name, :recloser, :recloser_id, :void_reason, :alt_tablename, :checked_out, :idiscount_amount, :past_names, :itax, :togo_name, :merges, :active_device, :tabname, :last_modified, :last_mod_device, :discount_info, :last_mod_register_name, :force_closed] }
33
+ }
34
+ end
35
+
36
+ describe("order_contents") do
37
+ let(:table) { 'order_contents' }
38
+ it { should_not be_empty }
39
+
40
+ describe("#first") {
41
+ subject { response.first }
42
+ its(:keys) { should eq [:id, :loc_id, :order_id, :item, :price, :quantity, :options, :special, :modify_price, :print, :check, :seat, :item_id, :printer, :apply_taxrate, :custom_taxrate, :modifier_list_id, :forced_modifier_group_id, :forced_modifiers_price, :course, :print_order, :open_item, :subtotal, :allow_deposit, :deposit_info, :discount_amount, :discount_value, :discount_type, :after_discount, :subtotal_with_mods, :tax_amount, :notes, :total_with_tax, :itax_rate, :itax, :tax_rate1, :tax1, :tax_rate2, :tax2, :tax_rate3, :tax3, :tax_subtotal1, :tax_subtotal2, :tax_subtotal3, :after_gratuity, :void, :discount_id, :server_time, :device_time, :idiscount_id, :idiscount_sh, :idiscount_value, :idiscount_type, :idiscount_amount, :split_factor, :hidden_data1, :hidden_data2, :hidden_data3, :hidden_data4, :tax_inclusion, :tax_name1, :tax_name2, :tax_name3, :sent, :tax_exempt, :exemption_id, :exemption_name, :itax_name, :checked_out, :hidden_data5, :hidden_data6, :price_override, :original_price, :override_id, :auto_saved, :idiscount_info, :category_id] }
43
+ }
44
+ end
45
+
46
+ describe("order_payments") do
47
+ let(:table) { 'order_payments' }
48
+ it { should_not be_empty }
49
+
50
+ describe("#first") {
51
+ subject { response.first }
52
+ its(:keys) { should eq [:id, :order_id, :check, :amount, :card_desc, :transaction_id, :refunded, :refund_notes, :refunded_by, :refund_pnref, :tip_amount, :auth, :loc_id, :processed, :auth_code, :card_type, :datetime, :pay_type, :voided, :void_notes, :voided_by, :void_pnref, :register, :got_response, :transtype, :split_tender_id, :temp_data, :change, :total_collected, :record_no, :server_name, :action, :ref_data, :process_data, :voice_auth, :server_id, :preauth_id, :tip_for_id, :swipe_grade, :batch_no, :register_name, :pay_type_id, :first_four, :mpshc_pid, :server_time, :info, :signature, :info_label, :for_deposit, :more_info, :customer_id, :is_deposit, :device_udid, :internal_id] }
53
+ }
54
+ end
55
+
56
+ describe("menu_groups") do
57
+ let(:table) { 'menu_groups' }
58
+ it { should_not be_empty }
59
+
60
+ describe("#first") {
61
+ subject { response.first }
62
+ its(:keys) { should eq [:id, :menu_id, :group_name, :orderby] }
63
+ }
64
+ end
65
+
66
+ describe("menu_categories") do
67
+ let(:table) { 'menu_categories' }
68
+ it { should_not be_empty }
69
+
70
+ describe("#first") {
71
+ subject { response.first }
72
+ its(:keys) { should eq [:id, :menu_id, :group_id, :name, :image, :description, :active, :print, :last_modified_date, :printer, :modifier_list_id, :apply_taxrate, :custom_taxrate, :forced_modifier_group_id, :print_order, :super_group_id, :tax_inclusion, :ltg_display] }
73
+ }
74
+ end
75
+
76
+ describe("menu_items") do
77
+ let(:table) { 'menu_items' }
78
+ it { should_not be_empty }
79
+
80
+ describe("#first") {
81
+ subject { response.first }
82
+ its(:keys) { should eq [:id, :category_id, :menu_id, :name, :price, :description, :image, :options1, :options2, :options3, :active, :print, :quick_item, :last_modified_date, :printer, :apply_taxrate, :custom_taxrate, :modifier_list_id, :forced_modifier_group_id, :image2, :image3, :misc_content, :ingredients, :open_item, :hidden_value, :hidden_value2, :allow_deposit, :UPC, :hidden_value3, :inv_count, :show_in_app, :super_group_id, :tax_inclusion, :ltg_display] }
83
+ }
84
+ end
85
+
86
+ describe("tables") do
87
+ let(:table) { 'tables' }
88
+ it { should_not be_empty }
89
+
90
+ describe("#first") {
91
+ subject { response.first }
92
+ its(:keys) { should eq [:id, :loc_id, :coord_x, :coord_y, :shapes, :widths, :heights, :names, :title, :rotate, :centerX, :centerY] }
93
+ }
94
+ end
95
+
96
+ describe("clock_punches") do
97
+ let(:table) { 'clock_punches' }
98
+ it { should_not be_empty }
99
+
100
+ describe("#first") {
101
+ subject { response.first }
102
+ its(:keys) { should eq [:id, :location, :location_id, :punch_type, :server, :server_id, :time, :hours, :punched_out, :time_out, :server_time, :server_time_out, :punch_id, :udid_in, :udid_out, :ip_in, :ip_out, :notes] }
103
+ }
104
+ end
105
+
106
+ describe("modifiers") do
107
+ let(:table) { 'modifiers' }
108
+ it { should_not be_empty }
109
+
110
+ describe("#first") {
111
+ subject { response.first }
112
+ its(:keys) { should eq [:title, :cost, :category, :id, :ingredients] }
113
+ }
114
+ end
115
+
116
+ describe("modifiers_used") do
117
+ let(:table) { 'modifiers_used' }
118
+ it { should_not be_empty }
119
+
120
+ describe("#first") {
121
+ subject { response.first }
122
+ its(:keys) { should eq [:id, :loc_id, :order_id, :mod_id, :qty, :type, :row, :cost, :unit_cost] }
123
+ }
124
+ end
125
+
126
+ describe("forced_modifiers") do
127
+ let(:table) { 'forced_modifiers' }
128
+ it { should_not be_empty }
129
+
130
+ describe("#first") {
131
+ subject { response.first }
132
+ its(:keys) { should eq [:title, :cost, :list_id, :id, :detour, :extra, :extra2, :ingredients, :extra3, :extra4, :extra5] }
133
+ }
134
+ end
135
+
136
+ describe("ingredients") do
137
+ let(:table) { 'ingredients' }
138
+ it { should_not be_empty }
139
+
140
+ describe("#first") {
141
+ subject { response.first }
142
+ its(:keys) { should eq [:title, :qty, :unit, :low, :high, :id, :category, :cost, :loc_id] }
143
+ }
144
+ end
145
+
146
+ describe("ingredient_categories") do
147
+ let(:table) { 'ingredient_categories' }
148
+ it { should_not be_empty }
149
+
150
+ describe("#first") {
151
+ subject { response.first }
152
+ its(:keys) { should eq [:title, :description, :id, :loc_id] }
153
+ }
154
+ end
155
+
156
+ describe("ingredient_usage") do
157
+ let(:table) { 'ingredient_usage' }
158
+ it { should_not be_empty }
159
+
160
+ describe("#first") {
161
+ subject { response.first }
162
+ its(:keys) { should eq [:ts, :date, :orderid, :itemid, :ingredientid, :qty, :id, :loc_id, :server_time, :cost, :content_id] }
163
+ }
164
+ end
165
+
166
+ describe("users") do
167
+ let(:table) { 'users' }
168
+ it { should_not be_empty }
169
+
170
+ describe("#first") {
171
+ subject { response.first }
172
+ its(:keys) { should eq [:id, :company_code, :username, :f_name, :l_name, :email, :access_level, :quick_serve, :loc_id, :service_type, :address, :phone, :mobile, :role_id, :deleted_date, :created_date] }
173
+ }
174
+ end
175
+ end
176
+
177
+ else
178
+ it("API credentials") {
179
+ pending "none provided; see README.md"
180
+ }
181
+ end
182
+ end
@@ -0,0 +1,65 @@
1
+ require 'spec_helper'
2
+
3
+ describe POSLavu::Client do
4
+ let(:dataname) { 'dataname' }
5
+ let(:token) { 'token' }
6
+ let(:key) { 'key' }
7
+ let(:client) { POSLavu::Client.new(dataname, token, key) }
8
+ subject { client }
9
+
10
+ describe "invoke" do
11
+ describe "HTTP requests" do
12
+ before {
13
+ poslavu_api_stub
14
+ client.invoke('command', 'abc' => 'def') rescue nil
15
+ }
16
+
17
+ specify("should request with 'dataname' parameter") { WebMock.should have_requested_poslavu_api('dataname' => dataname) }
18
+ specify("should request with 'token' parameter") { WebMock.should have_requested_poslavu_api('token' => token) }
19
+ specify("should request with 'key' parameter") { WebMock.should have_requested_poslavu_api('key' => key) }
20
+ specify("should request with 'cmd' parameter") { WebMock.should have_requested_poslavu_api('cmd' => 'command') }
21
+ specify("should request with hash parameter") { WebMock.should have_requested_poslavu_api('abc' => 'def') }
22
+ specify("should request with User-Agent header") { WebMock.should have_requested_poslavu_api.with(:headers => { 'User-Agent' => "POSLavu Ruby #{POSLavu::VERSION}"}) }
23
+ end
24
+
25
+ describe "error handling" do
26
+ it "raises errors returned as body text" do
27
+ poslavu_api_stub { 'this is an error' }
28
+ lambda { client.invoke('command') }.should raise_exception POSLavu::Client::Error, "this is an error"
29
+ end
30
+
31
+ # not yet observed in the wild
32
+ it "raises errors for 500 responses" do
33
+ poslavu_api_stub.to_return(:status => 500)
34
+ lambda { client.invoke('command') }.should raise_exception POSLavu::Client::Error
35
+ end
36
+
37
+ it "raises errors for timeouts" do
38
+ poslavu_api_stub.to_timeout
39
+ lambda { client.invoke('command') }.should raise_exception POSLavu::Client::Error
40
+ end
41
+ end
42
+
43
+ describe "response parsing" do
44
+ before { poslavu_api_stub { response } }
45
+ subject { client.invoke('command') }
46
+
47
+ describe "empty string" do
48
+ let(:response) { "" }
49
+ it { should be_kind_of Array }
50
+ it { should be_empty }
51
+ end
52
+
53
+ describe "single row" do
54
+ let(:response) { [POSLavu::Row.new(:foo => 'bar', :baz => 'quxx')] }
55
+ it { should be_kind_of Array }
56
+ it("should match the expected value") { subject.should eql response }
57
+ end
58
+
59
+ describe "multiple rows" do
60
+ let(:response) { (1..10).map { |n| POSLavu::Row.new(:counter => n) } }
61
+ it("should match the expected value") { subject.should eql response }
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,138 @@
1
+ describe POSLavu::QueryScope do
2
+ let(:dataname) { 'dataname' }
3
+ let(:token) { 'token' }
4
+ let(:key) { 'key' }
5
+ let(:table_name) { 'table' }
6
+ let(:client) { POSLavu::Client.new(dataname, token, key) }
7
+ let(:table) { client.table(table_name) }
8
+
9
+ it "should be returned by POSLavu::Client#table" do
10
+ table.should be_kind_of POSLavu::QueryScope
11
+ end
12
+
13
+ describe "#filter" do
14
+ before { poslavu_api_stub { POSLavu::Row.new } }
15
+ ['<=', '>=', '<', '>', '=', '<>', '!=', 'LIKE', 'NOT LIKE'].each { |operator|
16
+ specify "filters using #{operator}" do
17
+ table.filter('a', operator, 'b').page(1, 1).each {}
18
+ WebMock.should have_requested_poslavu_api('filters' => '[{"field":"a","operator":"' + operator + '","value1":"b"}]')
19
+ end
20
+ }
21
+
22
+ specify "filters using BETWEEN" do
23
+ table.filter('a', 'BETWEEN', 'b', 'c').page(1, 1).each {}
24
+ WebMock.should have_requested_poslavu_api('filters' => '[{"field":"a","operator":"BETWEEN","value1":"b","value2":"c"}]')
25
+ end
26
+
27
+
28
+ it "chains multiple filters" do
29
+ table.filter('a', '=', 'b').filter('c', '=', 'd').page(1, 1).each {}
30
+ WebMock.should have_requested_poslavu_api('filters' => '[{"field":"a","operator":"=","value1":"b"},{"field":"c","operator":"=","value1":"d"}]')
31
+ end
32
+
33
+ it "preserves pagination when filtering" do
34
+ base = table.page(2, 47)
35
+ filtered = base.filter('field', '=', 'value')
36
+ filtered.instance_variable_get(:@start_record).should eq 47
37
+ filtered.instance_variable_get(:@record_count).should eq 47
38
+ end
39
+ end
40
+
41
+ describe "#where" do
42
+ before { poslavu_api_stub { POSLavu::Row.new } }
43
+
44
+ it "creates proper JSON filter" do
45
+ table.where('a' => 'b', 'c' => 'd').page(1, 1).each {}
46
+ WebMock.should have_requested_poslavu_api('filters' => '[{"field":"a","operator":"=","value1":"b"},{"field":"c","operator":"=","value1":"d"}]')
47
+ end
48
+ end
49
+
50
+ describe "#each" do
51
+ describe "for a single page" do
52
+ let(:page_size) { 40 }
53
+ before { poslavu_api_stub { (1..page_size).map { |n| POSLavu::Row.new('counter' => n) } } }
54
+ subject { table.page(1, page_size) }
55
+
56
+ it "returns rows" do
57
+ collected = []
58
+ subject.each { |value| collected << value }
59
+ collected.size.should eq 40
60
+ end
61
+
62
+ it "memoizes results" do
63
+ subject.each { }
64
+ subject.each { }
65
+ WebMock.should have_requested_poslavu_api.once
66
+ end
67
+
68
+ it "re-requests when chained" do
69
+ subject.each { }
70
+ child = subject.filter('foo', '=', 'bar')
71
+ child.each { }
72
+ WebMock.should have_requested_poslavu_api.twice
73
+ end
74
+
75
+ it "passes filter as JSON" do
76
+ subject.filter('a', '=', 'b').each {}
77
+ WebMock.should have_requested_poslavu_api('filters' => '[{"field":"a","operator":"=","value1":"b"}]')
78
+ end
79
+ end
80
+
81
+ describe "for all pages when there is only one page" do
82
+ before {
83
+ poslavu_api_stub('limit' => '0,100') { (1..50).map { |n| POSLavu::Row.new('counter' => n) } }
84
+ }
85
+
86
+ subject { table }
87
+
88
+ it "returns all results" do
89
+ rows = 0
90
+ subject.each { rows += 1 }
91
+ rows.should eq 50
92
+ end
93
+
94
+ it "requests all pages" do
95
+ subject.each {}
96
+ WebMock.should have_requested_poslavu_api.once
97
+ end
98
+
99
+ it "memoizes results" do
100
+ subject.each {}
101
+ subject.each {}
102
+ WebMock.should have_requested_poslavu_api.once
103
+ end
104
+ end
105
+
106
+ describe "for all pages when there is more than one" do
107
+ let(:full_page) { (1..100).map { |n| POSLavu::Row.new('counter' => n) } }
108
+ let(:half_page) { full_page[0,50] }
109
+
110
+ before {
111
+ poslavu_api_stub('limit' => '0,100') { full_page }
112
+ poslavu_api_stub('limit' => '100,100') { full_page }
113
+ poslavu_api_stub('limit' => '200,100') { half_page }
114
+ }
115
+
116
+ subject { table }
117
+
118
+ it "returns all results" do
119
+ rows = 0
120
+ subject.each { rows += 1 }
121
+ rows.should eq 250
122
+ end
123
+
124
+ it "requests all pages" do
125
+ subject.each {}
126
+ WebMock.should have_requested_poslavu_api.times(3)
127
+ end
128
+
129
+ it "does not memoize results" do
130
+ subject.each {}
131
+ subject.each {}
132
+ WebMock.should have_requested_poslavu_api.times(6)
133
+ end
134
+ end
135
+
136
+ end
137
+
138
+ end
@@ -0,0 +1,56 @@
1
+ require 'spec_helper'
2
+
3
+ describe POSLavu::Row do
4
+ subject { POSLavu::Row.new }
5
+
6
+ it { should be_kind_of Hash }
7
+
8
+ describe "new" do
9
+ subject { POSLavu::Row.new(hash) }
10
+
11
+ describe "hash with symbol key and string value" do
12
+ let(:hash) do { :a => 'b' } end
13
+ its(:keys) { should eql [:a] }
14
+ its(:values) { should eql ['b'] }
15
+ end
16
+
17
+ describe "hash with string key and integer value" do
18
+ let(:hash) do { 'a' => 0 } end
19
+ its(:keys) { should eql [:a] }
20
+ its(:values) { should eql ['0'] }
21
+ end
22
+ end
23
+
24
+ describe "#from_xml" do
25
+ subject { POSLavu::Row.from_xml(string) }
26
+
27
+ describe "<row/>" do
28
+ let(:string) { "<row/>" }
29
+ it { should be_empty }
30
+ end
31
+
32
+ describe "<row><foo>bar</foo></row>" do
33
+ let(:string) { "<row><foo>bar</foo></row>" }
34
+ its(:keys) { should eq [:foo] }
35
+ its(:values) { should eq ['bar'] }
36
+
37
+ it("to_xml should match") {
38
+ subject.to_xml.gsub(/\s/, '').should eq(string.gsub(/\s/, ''))
39
+ }
40
+ end
41
+
42
+ describe "failure cases" do
43
+ def self.should_fail(string)
44
+ subject { lambda { POSLavu::Row.from_xml(string) } }
45
+ it("#{string.inspect} should raise ArgumentError") { should raise_exception ArgumentError }
46
+ end
47
+
48
+ should_fail nil
49
+ should_fail ""
50
+ should_fail "<not_a_row_element/>"
51
+ should_fail "<row/><row/>"
52
+ should_fail "<result><row/></result>"
53
+ end
54
+
55
+ end
56
+ end
@@ -0,0 +1,12 @@
1
+ require "poslavu"
2
+ Bundler.require :development
3
+
4
+ require 'webmock/rspec'
5
+
6
+ Dir[File.expand_path("../support/**/*.rb", __FILE__)].each {|f| require f}
7
+
8
+ RSpec.configure { |config|
9
+ config.include POSLavu::APIStub
10
+ }
11
+
12
+ Dotenv.load
@@ -0,0 +1,29 @@
1
+ module POSLavu::APIStub
2
+ def poslavu_api_stub(params = {}, &block)
3
+ stub = stub_http_request(:post, POSLavu::Client::URL).with(:body => hash_including(params))
4
+
5
+ if block
6
+ stub.to_return { |request|
7
+ response = block.call(request)
8
+
9
+ if response.instance_of?(Hash)
10
+ response
11
+ else
12
+ body = case response
13
+ when Array then response.map(&:to_xml).join # assume array of rows
14
+ when POSLavu::Row then response.to_xml
15
+ else response.to_s
16
+ end
17
+
18
+ { :body => body, :status => 200 }
19
+ end
20
+ }
21
+ end
22
+
23
+ stub
24
+ end
25
+
26
+ def have_requested_poslavu_api(params = {})
27
+ have_requested(:post, POSLavu::Client::URL).with(:body => hash_including(params))
28
+ end
29
+ end
metadata ADDED
@@ -0,0 +1,247 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: poslavu
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Will Glynn
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-10-03 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: nokogiri
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '1.5'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '1.5'
30
+ - !ruby/object:Gem::Dependency
31
+ name: faraday
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: '0.8'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: '0.8'
46
+ - !ruby/object:Gem::Dependency
47
+ name: multi_json
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: '1.3'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '1.3'
62
+ - !ruby/object:Gem::Dependency
63
+ name: bundler
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ version: '1.1'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ version: '1.1'
78
+ - !ruby/object:Gem::Dependency
79
+ name: dotenv
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ~>
84
+ - !ruby/object:Gem::Version
85
+ version: '0.2'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ~>
92
+ - !ruby/object:Gem::Version
93
+ version: '0.2'
94
+ - !ruby/object:Gem::Dependency
95
+ name: guard
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ~>
100
+ - !ruby/object:Gem::Version
101
+ version: '1.4'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ~>
108
+ - !ruby/object:Gem::Version
109
+ version: '1.4'
110
+ - !ruby/object:Gem::Dependency
111
+ name: rspec
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ~>
116
+ - !ruby/object:Gem::Version
117
+ version: '2.11'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ~>
124
+ - !ruby/object:Gem::Version
125
+ version: '2.11'
126
+ - !ruby/object:Gem::Dependency
127
+ name: rake
128
+ requirement: !ruby/object:Gem::Requirement
129
+ none: false
130
+ requirements:
131
+ - - ~>
132
+ - !ruby/object:Gem::Version
133
+ version: 0.9.2
134
+ type: :development
135
+ prerelease: false
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ none: false
138
+ requirements:
139
+ - - ~>
140
+ - !ruby/object:Gem::Version
141
+ version: 0.9.2
142
+ - !ruby/object:Gem::Dependency
143
+ name: webmock
144
+ requirement: !ruby/object:Gem::Requirement
145
+ none: false
146
+ requirements:
147
+ - - ~>
148
+ - !ruby/object:Gem::Version
149
+ version: '1.8'
150
+ type: :development
151
+ prerelease: false
152
+ version_requirements: !ruby/object:Gem::Requirement
153
+ none: false
154
+ requirements:
155
+ - - ~>
156
+ - !ruby/object:Gem::Version
157
+ version: '1.8'
158
+ - !ruby/object:Gem::Dependency
159
+ name: pry
160
+ requirement: !ruby/object:Gem::Requirement
161
+ none: false
162
+ requirements:
163
+ - - ! '>='
164
+ - !ruby/object:Gem::Version
165
+ version: '0'
166
+ type: :development
167
+ prerelease: false
168
+ version_requirements: !ruby/object:Gem::Requirement
169
+ none: false
170
+ requirements:
171
+ - - ! '>='
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ description: POSLavu is a hosted point-of-sale system. The `poslavu` gem provides
175
+ access to the API.
176
+ email:
177
+ - will@willglynn.com
178
+ executables: []
179
+ extensions: []
180
+ extra_rdoc_files: []
181
+ files:
182
+ - .gitignore
183
+ - Gemfile
184
+ - LICENSE
185
+ - README.rdoc
186
+ - Rakefile
187
+ - lib/poslavu.rb
188
+ - lib/poslavu/client.rb
189
+ - lib/poslavu/query_scope.rb
190
+ - lib/poslavu/row.rb
191
+ - lib/poslavu/version.rb
192
+ - poslavu.gemspec
193
+ - spec/aliases_spec.rb
194
+ - spec/live_spec.rb
195
+ - spec/poslavu/client_spec.rb
196
+ - spec/poslavu/query_scope_spec.rb
197
+ - spec/poslavu/row_spec.rb
198
+ - spec/spec_helper.rb
199
+ - spec/support/poslavu_api_stub.rb
200
+ homepage: http://github.com/willglynn/poslavu
201
+ licenses: []
202
+ post_install_message:
203
+ rdoc_options:
204
+ - --title
205
+ - POSLavu API Client
206
+ - --main
207
+ - README.rdoc
208
+ - --exclude
209
+ - spec
210
+ - --exclude
211
+ - Gemfile
212
+ - --exclude
213
+ - Rakefile
214
+ require_paths:
215
+ - lib
216
+ required_ruby_version: !ruby/object:Gem::Requirement
217
+ none: false
218
+ requirements:
219
+ - - ! '>='
220
+ - !ruby/object:Gem::Version
221
+ version: '0'
222
+ segments:
223
+ - 0
224
+ hash: -4247689874090540976
225
+ required_rubygems_version: !ruby/object:Gem::Requirement
226
+ none: false
227
+ requirements:
228
+ - - ! '>='
229
+ - !ruby/object:Gem::Version
230
+ version: '0'
231
+ segments:
232
+ - 0
233
+ hash: -4247689874090540976
234
+ requirements: []
235
+ rubyforge_project:
236
+ rubygems_version: 1.8.24
237
+ signing_key:
238
+ specification_version: 3
239
+ summary: POSLavu API client
240
+ test_files:
241
+ - spec/aliases_spec.rb
242
+ - spec/live_spec.rb
243
+ - spec/poslavu/client_spec.rb
244
+ - spec/poslavu/query_scope_spec.rb
245
+ - spec/poslavu/row_spec.rb
246
+ - spec/spec_helper.rb
247
+ - spec/support/poslavu_api_stub.rb