noosh24 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Noosh24
4
+ class Page
5
+ include Enumerable
6
+
7
+ attr_reader :items, :total, :limit, :offset
8
+
9
+ def initialize(items:, total:, limit:, offset:)
10
+ @items = items.freeze
11
+ @total = total
12
+ @limit = limit
13
+ @offset = offset
14
+ freeze
15
+ end
16
+
17
+ def has_more?
18
+ !items.empty? && offset + items.size < total
19
+ end
20
+
21
+ def next_offset
22
+ offset + items.size
23
+ end
24
+
25
+ def each(&)
26
+ return enum_for(:each) { items.size } unless block_given?
27
+
28
+ items.each(&)
29
+ self
30
+ end
31
+
32
+ def size
33
+ items.size
34
+ end
35
+ alias length size
36
+
37
+ def empty?
38
+ items.empty?
39
+ end
40
+
41
+ def ==(other)
42
+ other.is_a?(Page) && [other.items, other.total, other.limit, other.offset] == [items, total, limit, offset]
43
+ end
44
+ alias eql? ==
45
+
46
+ def hash
47
+ [self.class, items, total, limit, offset].hash
48
+ end
49
+
50
+ def inspect
51
+ "#<#{self.class.name} items=#{items.size} total=#{total} limit=#{limit} offset=#{offset}>"
52
+ end
53
+ end
54
+
55
+ CreateReservationResult = Data.define(:reservation, :created) do
56
+ alias_method :created?, :created
57
+ end
58
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Noosh24
4
+ PreOrderLine = Data.define(:item_id, :qty, :note) do
5
+ def self.coerce(line)
6
+ return line if line.is_a?(PreOrderLine)
7
+ raise ArgumentError, "A pre-order line is a PreOrderLine or a Hash" unless line.is_a?(Hash)
8
+
9
+ fields = line.transform_keys(&:to_s)
10
+ new(item_id: fields.fetch("item_id") { fields.fetch("itemId") }, qty: fields.fetch("qty"), note: fields["note"])
11
+ end
12
+
13
+ def initialize(item_id:, qty:, note: nil)
14
+ super
15
+ end
16
+
17
+ def to_body
18
+ body = { "itemId" => item_id, "qty" => qty }
19
+ body["note"] = note unless note.nil?
20
+ body
21
+ end
22
+ end
23
+
24
+ NewReservation = Data.define(
25
+ :date, :time, :guests, :guest_name, :phone, :email, :table_id, :zone, :special_request,
26
+ :allergy_note, :birthday, :child_chair, :pre_order, :external_ref
27
+ ) do
28
+ def self.coerce(reservation)
29
+ return reservation if reservation.is_a?(NewReservation)
30
+ raise ArgumentError, "A reservation is a NewReservation or a Hash" unless reservation.is_a?(Hash)
31
+
32
+ new(**reservation.transform_keys(&:to_sym))
33
+ end
34
+
35
+ def initialize(date:, time:, guests:, guest_name:, phone:, email: nil, table_id: nil, zone: nil,
36
+ special_request: nil, allergy_note: nil, birthday: nil, child_chair: nil, pre_order: nil,
37
+ external_ref: nil)
38
+ lines = pre_order&.map { |line| PreOrderLine.coerce(line) }&.freeze
39
+ super(date:, time:, guests:, guest_name:, phone:, email:, table_id:, zone:, special_request:,
40
+ allergy_note:, birthday:, child_chair:, pre_order: lines, external_ref:)
41
+ end
42
+
43
+ def idempotent?
44
+ !external_ref.to_s.strip.empty?
45
+ end
46
+
47
+ def to_body
48
+ body = {
49
+ "date" => Convert.date_param(date),
50
+ "time" => time,
51
+ "guests" => guests,
52
+ "guestName" => guest_name,
53
+ "phone" => phone
54
+ }
55
+ {
56
+ "email" => email, "tableId" => table_id, "zone" => zone, "specialRequest" => special_request,
57
+ "allergyNote" => allergy_note, "birthday" => birthday, "childChair" => child_chair
58
+ }.each { |key, value| body[key] = value unless value.nil? }
59
+ body["preOrder"] = pre_order.map(&:to_body) unless pre_order.nil?
60
+ body["externalRef"] = external_ref unless external_ref.nil?
61
+ body
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,247 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Noosh24
4
+ class Resource
5
+ def initialize(client)
6
+ @client = client
7
+ end
8
+
9
+ def inspect
10
+ "#<#{self.class.name}>"
11
+ end
12
+
13
+ private
14
+
15
+ def path_id(value)
16
+ raise ArgumentError, "An id is required" unless value.is_a?(String) && !value.strip.empty?
17
+
18
+ Client.escape(value)
19
+ end
20
+
21
+ def list_query(limit:, offset:, sort:, where:, expand: nil)
22
+ query = {}
23
+ query["limit"] = Integer(limit) unless limit.nil?
24
+ query["offset"] = Integer(offset) unless offset.nil?
25
+ sorts = strings(sort)
26
+ query["sort"] = sorts unless sorts.empty?
27
+ text = where.to_s
28
+ query["where"] = text unless text.empty?
29
+ expands = strings(expand)
30
+ query["expand"] = expands unless expands.empty?
31
+ query
32
+ end
33
+
34
+ def strings(value)
35
+ Array(value).map(&:to_s).reject(&:empty?)
36
+ end
37
+
38
+ def fetch_page(path, model, query)
39
+ _, data = @client.request("GET", path, query: query)
40
+ data = Convert.mapping(data)
41
+ items = Convert.list_of(data["items"], model)
42
+ Page.new(
43
+ items: items,
44
+ total: Convert.int(data["total"]) || items.size,
45
+ limit: Convert.int(data["limit"]) || query.fetch("limit", items.size),
46
+ offset: Convert.int(data["offset"]) || query.fetch("offset", 0)
47
+ )
48
+ end
49
+
50
+ def paginate(path, model, query, page_size, &block)
51
+ query = { "limit" => page_size, "offset" => 0 }.merge(query)
52
+ enum = Enumerator.new do |yielder|
53
+ cursor = query.dup
54
+ loop do
55
+ page = fetch_page(path, model, cursor)
56
+ page.items.each { |item| yielder << item }
57
+ break unless page.has_more?
58
+
59
+ cursor["offset"] = page.next_offset
60
+ end
61
+ end
62
+ return enum unless block
63
+
64
+ enum.each(&block)
65
+ nil
66
+ end
67
+
68
+ def fetch_all(path, model)
69
+ _, data = @client.request("GET", path)
70
+ Convert.list_of(Convert.mapping(data)["items"], model)
71
+ end
72
+
73
+ def fetch_one(path, model, query = nil)
74
+ _, data = @client.request("GET", path, query: query)
75
+ model.from_hash(data)
76
+ end
77
+ end
78
+
79
+ class Menu < Resource
80
+ def list_categories
81
+ fetch_all("/menu/categories", Category)
82
+ end
83
+
84
+ def list_products(limit: nil, offset: nil, sort: nil, where: nil, expand: nil)
85
+ fetch_page("/menu/products", Product, list_query(limit:, offset:, sort:, where:, expand:))
86
+ end
87
+
88
+ def iterate_products(limit: nil, offset: nil, sort: nil, where: nil, expand: nil, &)
89
+ page_size = strings(expand).empty? ? 100 : 50
90
+ paginate("/menu/products", Product, list_query(limit:, offset:, sort:, where:, expand:), page_size, &)
91
+ end
92
+
93
+ def get_product(product_id, expand: nil)
94
+ expands = strings(expand)
95
+ fetch_one("/menu/products/#{path_id(product_id)}", Product, expands.empty? ? nil : { "expand" => expands })
96
+ end
97
+
98
+ def get_stop_list
99
+ fetch_all("/menu/stop-list", StopListItem)
100
+ end
101
+ end
102
+
103
+ class Reservations < Resource
104
+ def list(limit: nil, offset: nil, sort: nil, where: nil)
105
+ fetch_page("/reservations", Reservation, list_query(limit:, offset:, sort:, where:))
106
+ end
107
+
108
+ def iterate(limit: nil, offset: nil, sort: nil, where: nil, &)
109
+ paginate("/reservations", Reservation, list_query(limit:, offset:, sort:, where:), 100, &)
110
+ end
111
+
112
+ def get(reservation_id)
113
+ fetch_one("/reservations/#{path_id(reservation_id)}", Reservation)
114
+ end
115
+
116
+ def availability(date, time, guests: nil)
117
+ query = { "date" => Convert.date_param(date), "time" => time, "guests" => guests }
118
+ fetch_one("/reservations/availability", TableAvailability, query)
119
+ end
120
+
121
+ def create(reservation = nil, **fields)
122
+ booking = NewReservation.coerce(reservation.nil? ? fields : reservation)
123
+ status, data = @client.request("POST", "/reservations", body: booking.to_body, retryable: booking.idempotent?)
124
+ CreateReservationResult.new(reservation: Reservation.from_hash(data), created: status == 201)
125
+ end
126
+
127
+ def cancel(reservation_id)
128
+ _, data = @client.request("POST", "/reservations/#{path_id(reservation_id)}/cancel")
129
+ Reservation.from_hash(data)
130
+ end
131
+ end
132
+
133
+ class Orders < Resource
134
+ def list(limit: nil, offset: nil, sort: nil, where: nil)
135
+ fetch_page("/orders", Order, list_query(limit:, offset:, sort:, where:))
136
+ end
137
+
138
+ def iterate(limit: nil, offset: nil, sort: nil, where: nil, &)
139
+ paginate("/orders", Order, list_query(limit:, offset:, sort:, where:), 100, &)
140
+ end
141
+
142
+ def get(order_id)
143
+ fetch_one("/orders/#{path_id(order_id)}", Order)
144
+ end
145
+
146
+ def list_bills(limit: nil, offset: nil, sort: nil, where: nil)
147
+ fetch_page("/bills", Bill, list_query(limit:, offset:, sort:, where:))
148
+ end
149
+
150
+ def iterate_bills(limit: nil, offset: nil, sort: nil, where: nil, &)
151
+ paginate("/bills", Bill, list_query(limit:, offset:, sort:, where:), 50, &)
152
+ end
153
+
154
+ def get_bill(bill_id)
155
+ fetch_one("/bills/#{path_id(bill_id)}", Bill)
156
+ end
157
+ end
158
+
159
+ class Payments < Resource
160
+ def list(limit: nil, offset: nil, sort: nil, where: nil)
161
+ fetch_page("/payments", Payment, list_query(limit:, offset:, sort:, where:))
162
+ end
163
+
164
+ def iterate(limit: nil, offset: nil, sort: nil, where: nil, &)
165
+ paginate("/payments", Payment, list_query(limit:, offset:, sort:, where:), 100, &)
166
+ end
167
+
168
+ def get(payment_id)
169
+ fetch_one("/payments/#{path_id(payment_id)}", Payment)
170
+ end
171
+
172
+ def z_report(date = nil)
173
+ _, data = @client.request("GET", "/reports/z", query: date.nil? ? nil : { "date" => Convert.date_param(date) })
174
+ Convert.mapping(data)
175
+ end
176
+ end
177
+
178
+ class Tables < Resource
179
+ def list
180
+ fetch_all("/tables", Table)
181
+ end
182
+
183
+ def list_halls
184
+ fetch_all("/halls", Hall)
185
+ end
186
+ end
187
+
188
+ class Customers < Resource
189
+ def list(limit: nil, offset: nil, sort: nil, where: nil)
190
+ fetch_page("/customers", Customer, list_query(limit:, offset:, sort:, where:))
191
+ end
192
+
193
+ def iterate(limit: nil, offset: nil, sort: nil, where: nil, &)
194
+ paginate("/customers", Customer, list_query(limit:, offset:, sort:, where:), 100, &)
195
+ end
196
+
197
+ def get(customer_id)
198
+ fetch_one("/customers/#{path_id(customer_id)}", Customer)
199
+ end
200
+ end
201
+
202
+ class Loyalty < Resource
203
+ def get_program
204
+ fetch_one("/loyalty/program", LoyaltyProgram)
205
+ end
206
+
207
+ def list_cards(limit: nil, offset: nil, sort: nil, where: nil)
208
+ fetch_page("/loyalty/cards", LoyaltyCard, list_query(limit:, offset:, sort:, where:))
209
+ end
210
+
211
+ def iterate_cards(limit: nil, offset: nil, sort: nil, where: nil, &)
212
+ paginate("/loyalty/cards", LoyaltyCard, list_query(limit:, offset:, sort:, where:), 100, &)
213
+ end
214
+ end
215
+
216
+ class Stock < Resource
217
+ def list_ingredients(limit: nil, offset: nil, sort: nil, where: nil)
218
+ fetch_page("/stock/ingredients", Ingredient, list_query(limit:, offset:, sort:, where:))
219
+ end
220
+
221
+ def iterate_ingredients(limit: nil, offset: nil, sort: nil, where: nil, &)
222
+ paginate("/stock/ingredients", Ingredient, list_query(limit:, offset:, sort:, where:), 100, &)
223
+ end
224
+
225
+ def list_moves(limit: nil, offset: nil, sort: nil, where: nil)
226
+ fetch_page("/stock/moves", StockMove, list_query(limit:, offset:, sort:, where:))
227
+ end
228
+
229
+ def iterate_moves(limit: nil, offset: nil, sort: nil, where: nil, &)
230
+ paginate("/stock/moves", StockMove, list_query(limit:, offset:, sort:, where:), 100, &)
231
+ end
232
+
233
+ def list_stores
234
+ fetch_all("/stock/stores", StockStore)
235
+ end
236
+ end
237
+
238
+ class Reviews < Resource
239
+ def list(limit: nil, offset: nil, sort: nil, where: nil)
240
+ fetch_page("/reviews", Review, list_query(limit:, offset:, sort:, where:))
241
+ end
242
+
243
+ def iterate(limit: nil, offset: nil, sort: nil, where: nil, &)
244
+ paginate("/reviews", Review, list_query(limit:, offset:, sort:, where:), 100, &)
245
+ end
246
+ end
247
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Noosh24
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Noosh24
4
+ class Where
5
+ FIELD = /\A[A-Za-z][A-Za-z0-9_.]*\z/
6
+
7
+ def self.literal(value)
8
+ case value
9
+ when true then "true"
10
+ when false then "false"
11
+ when Integer then value.to_s
12
+ when Float then number(value)
13
+ when String then "'#{value.gsub("'", "''")}'"
14
+ when Time then "'#{timestamp(value)}'"
15
+ when DateTime then "'#{timestamp(value.to_time)}'"
16
+ when Date then "'#{value.iso8601}'"
17
+ when nil then raise ArgumentError, "where does not take nil"
18
+ else raise ArgumentError, "where takes strings, numbers, booleans, dates and times, not #{value.class}"
19
+ end
20
+ end
21
+
22
+ def self.number(value)
23
+ raise ArgumentError, "Numbers in where must be finite" unless value.finite?
24
+
25
+ value == value.to_i ? value.to_i.to_s : value.to_s
26
+ end
27
+
28
+ def self.timestamp(time)
29
+ return time.iso8601 if time.nsec.zero?
30
+
31
+ time.iso8601((time.nsec % 1000).zero? ? 6 : 9)
32
+ end
33
+
34
+ private_class_method :number, :timestamp
35
+
36
+ def initialize
37
+ @conditions = []
38
+ end
39
+
40
+ def eq(field, value) = add(field, "=", value)
41
+
42
+ def ne(field, value) = add(field, "!=", value)
43
+
44
+ def gt(field, value) = add(field, ">", value)
45
+
46
+ def gte(field, value) = add(field, ">=", value)
47
+
48
+ def lt(field, value) = add(field, "<", value)
49
+
50
+ def lte(field, value) = add(field, "<=", value)
51
+
52
+ def in(field, values)
53
+ items = Array(values)
54
+ raise ArgumentError, "in needs at least one value" if items.empty?
55
+
56
+ @conditions << "#{check(field)} in (#{items.map { |item| self.class.literal(item) }.join(', ')})"
57
+ self
58
+ end
59
+
60
+ def empty?
61
+ @conditions.empty?
62
+ end
63
+
64
+ def to_s
65
+ @conditions.join(" AND ")
66
+ end
67
+
68
+ def inspect
69
+ "#<#{self.class.name} #{to_s.inspect}>"
70
+ end
71
+
72
+ private
73
+
74
+ def add(field, operator, value)
75
+ @conditions << "#{check(field)} #{operator} #{self.class.literal(value)}"
76
+ self
77
+ end
78
+
79
+ def check(field)
80
+ raise ArgumentError, "#{field.inspect} is not a field name" unless field.is_a?(String) && FIELD.match?(field)
81
+
82
+ field
83
+ end
84
+ end
85
+
86
+ def self.where
87
+ Where.new
88
+ end
89
+ end
data/lib/noosh24.rb ADDED
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+ require "json"
5
+ require "net/http"
6
+ require "openssl"
7
+ require "time"
8
+ require "uri"
9
+
10
+ require_relative "noosh24/version"
11
+ require_relative "noosh24/errors"
12
+ require_relative "noosh24/convert"
13
+ require_relative "noosh24/models"
14
+ require_relative "noosh24/page"
15
+ require_relative "noosh24/where"
16
+ require_relative "noosh24/reservation_input"
17
+ require_relative "noosh24/resources"
18
+ require_relative "noosh24/client"
metadata ADDED
@@ -0,0 +1,56 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: noosh24
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Noosh24
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: Read a Noosh24 venue's menu, reservations, orders, payments, tables,
13
+ customers, loyalty, stock and reviews, and create or cancel reservations.
14
+ executables: []
15
+ extensions: []
16
+ extra_rdoc_files: []
17
+ files:
18
+ - LICENSE
19
+ - README.md
20
+ - lib/noosh24.rb
21
+ - lib/noosh24/client.rb
22
+ - lib/noosh24/convert.rb
23
+ - lib/noosh24/errors.rb
24
+ - lib/noosh24/models.rb
25
+ - lib/noosh24/page.rb
26
+ - lib/noosh24/reservation_input.rb
27
+ - lib/noosh24/resources.rb
28
+ - lib/noosh24/version.rb
29
+ - lib/noosh24/where.rb
30
+ homepage: https://developer.noosh24.com
31
+ licenses:
32
+ - MIT
33
+ metadata:
34
+ homepage_uri: https://developer.noosh24.com
35
+ documentation_uri: https://developer.noosh24.com/reference
36
+ source_code_uri: https://github.com/noosh24/sdks/tree/main/ruby
37
+ bug_tracker_uri: https://github.com/noosh24/sdks/issues
38
+ rubygems_mfa_required: 'true'
39
+ rdoc_options: []
40
+ require_paths:
41
+ - lib
42
+ required_ruby_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '3.3'
47
+ required_rubygems_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: '0'
52
+ requirements: []
53
+ rubygems_version: 4.0.20
54
+ specification_version: 4
55
+ summary: Official Ruby client for the Noosh24 Open API
56
+ test_files: []