-
Notifications
You must be signed in to change notification settings - Fork 0
/
model.py
454 lines (347 loc) · 18.2 KB
/
model.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
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
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
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
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
from Classes import Statics as local_data
from dataclasses import asdict, dataclass
from Classes.Statics import Reservation
from datetime import datetime, timezone
from collections import defaultdict
from flask import current_app as app
from flask_pymongo import PyMongo
from tabulate import tabulate
import bcrypt
import hashlib
categories = ["Coffee", "Specialty Drinks", "Sandwiches", "Desserts"]
collections = ["menu", "receipts", "admin"]
def start_db()->"Database":
"""Starts a connection to the
website's MongoDB database.
Returns:
Database: A MongoDB database object.
"""
mongo = PyMongo(app)
db = mongo.db
return db
def encrypt_pswd(password:str)->bytes:
"""Encrypts a password with bcrypt
and returns bcrypt's representation
of the hash.
Args:
password (str): The password to hash, as a string.
Returns:
bytes: The hashed password in bytes, encoded by bcrypt.
"""
password = password.encode('utf-8')
salt = bcrypt.gensalt()
return bcrypt.hashpw(password, salt)
def reset_menu_collection()->None:
"""Resets the menu collection by
clearing all its documents out
and re-uploading the locally stored
menu items. These initial items
are later loaded onto the website.
"""
#starts the db
db = start_db()
#access the menu collection
db_menu = db.menu
db_menu.delete_many({})
#inserts the menu field into the mongodb database
for item_obj in local_data.menu.values():
db_menu.insert_one(asdict(item_obj))
def reset_receipts_collection()->None:
"""Resets the receipt collection by
clearing all its documents out.
"""
db = start_db()
db.receipt.delete_many({})
@dataclass
class ShoppingCart:
def __init__(self,name:str)->None:
"""Initializes an instance of the customers' shopping cart.
Args:
name (str): The customers' name.
Raises:
TypeError: Raised if the parameters passed by the customer aren't a string.
ValueError: Raised if the name isn't a valid string: empty or containing non-alphabetical characters.
"""
if type(name) != str:
raise TypeError("Customer name must be a string!")
if not name or not name.strip():
raise ValueError("Customer name cannot be empty!")
for names in name.split():
if not names.isalpha():
raise ValueError("Customer name must not have special characters or numbers!")
self.name = name
self.cart = defaultdict(int) #keys are strings of the menu item and values are the amount of orders of that item
self.reservation = None
self.subtotal = 0
def __str__(self)->str:
"""Generates a string, printed to the command line, of the customers' current shopping cart.
If there are food items in the cart, displays their: names, quantity, price,
and finally, the current subtotal and the current reservation status.
Returns:
str: A string representing the state of every attribute in the shopping cart.
"""
cart_string = "\n\n"
menu = start_db().menu.find({})
if len(self.cart) < 1:
table = f"No menu items currently in {self.name}'s cart."
else:
menu_rows = []
for item in menu: #creates each row in the table
quantity = self.cart.get(item['name'].lower())
if quantity is None:
continue
menu_rows.append([item['name'], quantity, f"${item['price']:.2f}"]) #item name, amount, and price
table = tabulate(menu_rows, headers=[self.name, "Amount", "Price"], stralign="left", numalign="center") #specifies the headers for each column
cart_string += table + "\n\n" + f"Current subtotal: ${self.subtotal:.2f}" + "\n" + f"Reservation: {self.reservation}" + "\n\n" #format string
return cart_string
def add_items(self, item:str, amount_to_add:int=1)->None:
"""Adds a food item to the shopping cart along with the quantity the user desires to order.
Args:
item (str): The food item to add into the cart
amount_to_add (int, optional): The quantity of the aforementioned item to add. Defaults to 1.
Raises:
TypeError: Raised if the food item to add isn't a string or if the quantity to add isn't an integer.
ValueError: Raised if the food item isn't in the menu or if the customer attempts to add more than the minimum
and maximum allowed quantities of a food item.
"""
db = start_db()
if type(item) != str:
raise TypeError("Menu item must be a valid string!")
original_string = item #needed so that the error string returned isn't formatted
item = item.lower()
item_record = db.menu.find_one({"name":original_string})
if not item:
raise ValueError(f"{original_string} does not exist on the menu!")
if type(amount_to_add) != int:
raise TypeError("Amount must be a whole integer!")
if amount_to_add < 1 or amount_to_add > 10:
raise ValueError("Attempted to add too many items in the cart!")
# verify that the amount to be added + the amount in the cart doesn't exceed the max allowed
if self.cart.get(item) and self.cart.get(item) + amount_to_add > 10:
raise ValueError("You can't place more than 10 orders of an item!")
self.cart[item] += amount_to_add
self.subtotal += item_record['price']*amount_to_add #update the subtotal of the shopping cart as items are added or removed (no tax)
def remove_items(self, item:str, amount_to_remove:int=1)->None:
"""Removes the given quantities of an item from the shopping cart.
Args:
item (str): The food item to remove from the customers' cart.
amount_to_remove (int, optional): The amount of items to remove from the cart. Defaults to 1.
Raises:
TypeError: Raised if the food item to remove from the cart isn't a string or if the amount of items to remove isn't
an integer.
ValueError: Raised if the food item isn't present in the cart, if the customer attemps to remove more than
the maximum or minimum allowed, or if the amount of food items to remove all is greater than the
current amount of items in the cart.
"""
if type(item) != str: # Verifying if item to add is valid
raise TypeError(f"The item to be removed must be a string!")
original_string = item
item = item.lower()
if item not in self.cart:
raise ValueError(f"{original_string} is not currently in the cart!")
# Verifying if the amount to remove is valid and its edge cases
amount_in_cart = self.cart.get(item)
if type(amount_to_remove) != int:
raise TypeError("The amount of items to be removed must be a whole integer!")
if amount_to_remove < 1 or amount_to_remove > 10:
raise ValueError("Can't remove more items than the maximum or minimum allowed!")
if amount_to_remove > amount_in_cart:
raise ValueError("Can't remove more items than currently present in the cart!")
item_price = start_db().menu.find_one({"name":item}).price
if amount_to_remove == amount_in_cart:
del self.cart[item]
else:
self.cart[item] -= amount_to_remove
self.subtotal -= item_price*amount_to_remove
def set_reservation(self, day:str, hour:str, meridiem:str)->None:
"""Sets a cafe reservation on the given date.
Args:
day (str): The day of the week to set the reservation on.
hour (str): The hour of the day, between 1 and 12, to set the reservation on.
meridiem (str): The meridiem, AM or PM, on which the reservation will be set.
"""
self.reservation = Reservation(day, hour, meridiem)
def clear_reservation(self)->None:
"""If a reservation has been set by the customer, clears it out.
Raises:
ValueError: Raised if the customer attemps to clear an empty reservation.
"""
if self.reservation:
self.reservation = None
else:
raise ValueError("You can't clear empty reservations!")
class Receipt:
"""
A class that represents the customers' order receipt. Based
on their final order after payment, contains all information
based on their order info: the items and quantities purchased,
the reservation date if any was made, the customers' name, and
their subtotal.
Attributes:
items: A dictionary containing each food item from their order, as string keys, and their quantities as integer
values.
reservation: A reservation object containing the date of the reservation, or None if a reservation wasn't set.
name: A string, representing a valid customer name.
subtotal: A float which represents the final subtotal. Items ordered cannot be changed.
tax_percent: A float which represents the sales tax rate for the cafe.
"""
def __init__(self, food_items:dict, reservation:'Reservation', name:str, subtotal:float)->None:
"""Initializes an instance of a receipt object.
Args:
items (dict): Contains the items in the final order as key strings and their quantities as integer values.
reservation (Reservation): Contains the date of the reservation. None if a reservation wasn't set.
name (str): Represents the customers' name.
subtotal (float): Contains the final subtotal. Items ordered cannot be changed.
Raises:
TypeError: Raised if the food items aren't in a dictionary, if the reservation is not a valid Reservation object,
the customer name isn't a string, or the subtotal isn't a float or integer.
ValueError: Raised if the food items' dictionary is empty or contains invalid keys or values, the customer
name contains non alphanumeric characters or is empty, or if the subtotal is less than
or equals to zero. All orders must have a positive, non zero subtotal.
"""
if type(food_items) not in [dict, defaultdict]:
raise TypeError(f"Items must be a dictionary. Given type: {type(food_items)}")
if type(reservation) is not Reservation and reservation is not None: #Reservations can be None
raise TypeError(f"Error: Reservations must be Reservation type. Given type: {type(reservation)}")
if type(name) is not str:
raise TypeError(f"Error: name must be a string. Given type: {type(name)}")
if type(subtotal) not in [int, float]:
raise TypeError(f"Error: Total is not a number type. Given type: {type(subtotal)}")
if not food_items or None in food_items.values() or sum(1 for amount in food_items.values() if amount < 0):
raise ValueError("Error: Cannot generate receipt of no items.")
if not name or not name.split():
raise ValueError("Error: Customer name must not be empty")
for letter in name.split():
if not letter.isalpha() or letter.isspace():
raise ValueError("Error: Customer name must not have special characters or numbers.")
if subtotal <= 0:
raise ValueError("Cannot make a receipt for a purchase with no total or a purchase with a sub zero total!")
self.food_items = food_items
self.reservation = reservation
self.name = name
self.subtotal = subtotal
self.tax_percent = 10.25/100
def tax(self)->float:
"""Calculates the sales tax amount for the given orders' subtotal.
Returns:
float: The sales tax of the customers' order, rounded up to two decimal places.
"""
return round(self.subtotal * self.tax_percent, 2)
def total(self)->float:
"""Calculates the customers' final order total. By definition,
the final total is the sum of the order subtotal combined
with its corresponding sales tax.
Returns:
float: The final total of the customers' order.
"""
return self.subtotal + self.tax()
def receipt_number(self)->str:
"""Generates a 10 digit receipt number for the current order.
Every receipt must have a unique, corresponding receipt number
which can be recreated given the customers' order info.
Generated by taking the first four digits of the customers'
name hash, the first three digits of the total order's value in
pennies, and the number of unique items ordered.
Returns:
str: The current receipts' unique receipt number, as a string.
"""
def simple_hash(name:str)->str:
"""Calculates a simple hash given a string using RSA's MD5
algorithm. Unlike the built in hash function, this guarantees
a constant hash across sessions.
Args:
name (str): The input string to be hashed. For the receipt's purposes, it's the customers' name.
Returns:
str: A string representing the first four digits of the resulting hash.
"""
name = name.encode("utf-8") #encode for string hashing
md5_hash = hashlib.md5()
md5_hash.update(name)
return str(int(md5_hash.hexdigest(), 16))[:4] #converts to hex, converts to the equivalent int, gets first four digits
def to_pennies(order_total:float)->str:
"""Converts the order's total from USD to pennies.
Since the cheapest item in the menu is a dollar,
this is always guaranteed to be at least three digits
long.
Args:
order_total (float): The final orders' total, in USD.
Returns:
str: The final orders' total in pennies, as a string.
"""
return str(order_total * 100)[:3] #converting usd to pennies and getting the first three digits
def first_three_length_cart(items_ordered:int)->str:
"""Calculates the final three digits in the receipt number
by getting the amount of unique items ordered.
If this amount is less than three, fills in the
remaining digits to the left with zeros.
Args:
items_ordered (int): The number of unique items ordered, not their quantity.
Returns:
str: The number of unique items ordered, formatted as a string. If this amount is less
than three, fills in the remaining spots with zeros.
"""
items_ordered = str(items_ordered)
if len(items_ordered) < 3:
items_ordered = items_ordered.zfill(3) #fill as many remaining spots with zero (1 or 2 zeros)
return items_ordered[:3]
#calculates the receipt number
name_hash = simple_hash(self.name)
pennies = to_pennies(self.total())
length = first_three_length_cart(len(self.food_items))
return name_hash + pennies + length
def generate_receipt(self)->None:
"""Writes the final receipt to a text file within the current directory.
Displays the receipt's attributes, orders, and more such as:
items ordered with their quantities, the totals, the receipt number,
the time at which the order was placed, the customer's name,
and the reservation date.
"""
receipt_string = "CAFFÈ ÉTOILÉ\n"
menu = start_db().menu.find({})
receipt_rows = []
for item in menu:
if item['name'].lower() not in self.food_items:
continue
quantity = self.food_items[item['name'].lower()]
receipt_rows.append([item['name'], quantity, f"${float(item['price']):.2f}"]) #item name, amount, and price
# for food_order, quantity in self.food_items.items():
# receipt_rows.append([menu.get(food_order.lower()).name, quantity, f"${menu.get(food_order.lower()).price:.2f}"]) #item name, amount, and price
receipt_body = tabulate(
receipt_rows,
headers=["Items", "Amount", "Price"],
stralign="left",
numalign="center",
tablefmt="psql"
)
receipt_string += receipt_body + "\n\n"
#Price calculations
to_pay_rows = [["Subtotal: ", f"${self.subtotal:.2f}"], ["Tax: ", f"${self.tax():.2f}"], ["Total: ", f"${self.total():.2f}"]]
payments = tabulate(
to_pay_rows,
stralign="left",
numalign="left",
tablefmt="simple"
)
utc_now = datetime.now(timezone.utc) #gets the current time in utc
local_time_obj = utc_now.astimezone() #gets the specific utc timezone (from the computer system)
time_string = local_time_obj.strftime("%m-%d-%Y at %I:%M %p") #get current/local time as 12-hour string
receipt_string += payments + "\n\n" + f"Customer: {self.name}\n" + f"Receipt Number: #{self.receipt_number()}\n" + f"Reservation: {self.reservation}\n" \
f"Time Generated: {time_string}\n\n" + "Thanks for stopping by!"
with open("receipt.txt", "w", encoding='utf-8') as receipt_file: #write receipt to text file, utf-8 for special characters
receipt_file.write(receipt_string)
def json_to_cart(json:dict) -> ShoppingCart:
cart = ShoppingCart(json.get('name','user'))
cart.cart = defaultdict(int,json['cart'])
cart.reservation = json['reservation']
cart.subtotal = json['subtotal']
return cart
def receipt_to_json(receipt: Receipt) -> dict:
return {
"name" : receipt.name,
"subtotal" : receipt.subtotal,
"total" : receipt.total(),
"receipt_number" : receipt.receipt_number(),
"food_items" : receipt.food_items,
"tax" : receipt.tax(),
"reservation" : str(receipt.reservation)
}