00000000001// legacy.js — Byte Burrito, 2019–present. Please don't add anything here.
1// (Everyone has added something here.)
2
3const TAX_RATE = 0.0875;
4const MAX_DISTANCE_KM = 8;
5const OPEN_HOUR = 11;
6const CLOSE_HOUR = 23;
7const RUSH_START = 12;
8const RUSH_END = 14;
9
10function isOpen(now) {
11 const hour = now.getHours();
12 return hour >= OPEN_HOUR && hour < CLOSE_HOUR;
13}
14
15function isLunchRush(now) {
16 const hour = now.getHours();
17 return hour >= RUSH_START && hour < RUSH_END;
18}
19
20function formatPrice(cents) {
21 const dollars = Math.floor(cents / 100);
22 const rest = String(cents % 100).padStart(2, "0");
23 return "$" + dollars + "." + rest;
24}
25
26function subtotal(items) {
27 let sum = 0;
28 for (const item of items) {
29 sum += item.price * item.qty;
30 }
31 return sum;
32}
33
34function taxFor(amount) {
35 return Math.round(amount * TAX_RATE);
36}
37
38function totalFor(order) {
39 const base = subtotal(order.items);
40 const tax = taxFor(base);
41 const tip = order.tip || 0;
42 return base + tax + tip;
43}
44
45// DEAD CODE: the fax ordering line was switched off in 2020
46function faxOrder(order) {
47 const sheet = renderFaxSheet(order);
48 faxMachine.send(sheet, "+1-555-0100");
49 log("fax sent", order.id);
50 return true;
51}
52
53function validateOrder(order) {
54 if (!order.items || order.items.length === 0) return "empty";
55 if (order.items.some((i) => i.qty <= 0)) return "bad quantity";
56 if (!order.address) return "no address";
57 if (!isOpen(new Date())) return "closed";
58 return null;
59}
60
61function applyCoupon(order, code) {
62 const coupon = COUPONS[code];
63 if (!coupon) return order;
64 if (coupon.expires < Date.now()) return order;
65 const off = Math.round(subtotal(order.items) * coupon.percent);
66 return { ...order, discount: off };
67}
68
69function studentDiscount(order, user) {
70 if (!user.isStudent) return 0;
71 return Math.round(subtotal(order.items) * 0.1);
72}
73
74// DEAD CODE: loyalty points were replaced by the app in 2021
75function addLoyaltyPoints(user, order) {
76 const points = Math.floor(totalFor(order) / 100);
77 user.points = (user.points || 0) + points;
78 saveUser(user);
79 return points;
80}
81
82function distanceKm(a, b) {
83 const dLat = (b.lat - a.lat) * 111;
84 const dLng = (b.lng - a.lng) * 111 * Math.cos((a.lat * Math.PI) / 180);
85 return Math.sqrt(dLat * dLat + dLng * dLng);
86}
87
88function nearestRider(riders, address) {
89 let best = null;
90 for (const rider of riders) {
91 if (!rider.online) continue;
92 const d = distanceKm(rider.position, address.position);
93 if (d > MAX_DISTANCE_KM) continue;
94 if (!best || d < best.distance) best = { rider, distance: d };
95 }
96 return best;
97}
98
99function assignRider(order, riders) {
100 const match = nearestRider(riders, order.address);
101 if (!match) return refund(order, "No rider nearby, sorry!");
102 order.riderId = match.rider.id;
103 order.eta = Math.ceil(match.distance * 4) + 10;
104 notify(match.rider.phone, "New order: " + order.id);
105 return order;
106}
107
108function etaMinutes(order, now) {
109 const rush = isLunchRush(now) ? 10 : 0;
110 return order.eta + rush;
111}
112
113// DEAD CODE: the pager integration, before the riders had phones
114function pageRider(rider, order) {
115 const message = "ORDER " + order.id + " " + order.address.street;
116 pager.send(rider.pagerId, message.slice(0, 80));
117 return true;
118}
119
120function refund(order, reason) {
121 order.status = "refunded";
122 order.refundReason = reason;
123 payments.refund(order.paymentId, totalFor(order));
124 notify(order.customerPhone, "Refunded: " + reason);
125 return order;
126}
127
128function markDelivered(order, now) {
129 order.status = "delivered";
130 order.deliveredAt = now.toISOString();
131 const late = now - new Date(order.placedAt) > order.eta * 60000;
132 if (late) order.lateBy = Math.round((now - new Date(order.placedAt)) / 60000 - order.eta);
133 return order;
134}
135
136function kitchenTicket(order) {
137 const lines = order.items.map((i) => i.qty + " x " + i.name);
138 lines.unshift("ORDER " + order.id);
139 if (order.notes) lines.push("NOTE: " + order.notes);
140 return lines.join("\n");
141}
142
143function printTicket(order) {
144 const ticket = kitchenTicket(order);
145 printer.print(ticket);
146 log("printed", order.id);
147}
148
149// DEAD CODE: the old receipt printer, replaced by email receipts
150function printReceipt(order) {
151 const lines = order.items.map((i) => i.name + " " + formatPrice(i.price * i.qty));
152 lines.push("TOTAL " + formatPrice(totalFor(order)));
153 receiptPrinter.print(lines.join("\n"));
154}
155
156function emailReceipt(order, user) {
157 const body = renderReceipt(order);
158 mailer.send(user.email, "Your Byte Burrito receipt", body);
159}
160
161function menuItem(id) {
162 const item = MENU.find((m) => m.id === id);
163 if (!item) throw new Error("not on the menu: " + id);
164 return item;
165}
166
167function isSpicy(id) {
168 return menuItem(id).spicy === true;
169}
170
171function happyHourPrice(item, now) {
172 const hour = now.getHours();
173 if (hour >= 17 && hour < 19) return Math.round(item.price * 0.8);
174 return item.price;
175}
176
177function priceList(now) {
178 return MENU.map((item) => ({
179 id: item.id,
180 name: item.name,
181 price: formatPrice(happyHourPrice(item, now)),
182 }));
183}
184
185// DEAD CODE: the 2019 "burrito of the month", never shipped
186function burritoOfTheMonth(month) {
187 const picks = ["al-pastor", "veggie", "carnitas", "birria"];
188 return menuItem(picks[month % picks.length]);
189}
190
191function placeOrder(cart, user, now) {
192 const order = { id: newId(), items: cart.items, address: user.address, placedAt: now.toISOString() };
193 const fee = deliveryFee(order, distanceKm(KITCHEN, order.address));
194 const problem = validateOrder(order);
195 if (problem) throw new Error(problem);
196 order.discount = studentDiscount(order, user);
197 order.total = totalFor(order) - order.discount + fee;
198 return order;
199}
200
201function deliveryFee(order, km) {
202 const base = km <= 2 ? 0 : 1.5;
203 const perKm = km > 2 ? (km - 2) * 0.4 : 0;
204 const rush = isLunchRush(new Date()) ? 1 : 0;
205 return Math.round((base + perKm + rush) * 100) / 100;
206}
207
208function newId() {
209 return "BB-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 6);
210}
211
212function log(event, ...details) {
213 console.log("[" + new Date().toISOString() + "] " + event, ...details);
214}
215
216function notify(phone, text) {
217 if (!phone) return;
218 sms.send(phone, text.slice(0, 160));
219}
220
221function saveUser(user) {
222 db.users.put(user.id, user);
223}
224
225function loadUser(id) {
226 return db.users.get(id);
227}
228
229function saveOrder(order) {
230 db.orders.put(order.id, order);
231 log("saved", order.id);
232}
233
234function loadOrder(id) {
235 return db.orders.get(id);
236}
237
238function ordersToday(now) {
239 const day = now.toISOString().slice(0, 10);
240 return db.orders.all().filter((o) => o.placedAt.startsWith(day));
241}
242
243function revenueToday(now) {
244 return ordersToday(now).reduce((sum, o) => sum + o.total, 0);
245}
246
247function busiestHour(now) {
248 const counts = new Array(24).fill(0);
249 for (const o of ordersToday(now)) counts[new Date(o.placedAt).getHours()]++;
250 return counts.indexOf(Math.max(...counts));
251}
252
253// DEAD CODE: the CSV export the accountant asked for, then never used
254function exportCsv(orders) {
255 const rows = orders.map((o) => [o.id, o.total, o.status].join(","));
256 rows.unshift("id,total,status");
257 return rows.join("\n");
258}
259
260function ridersOnline(riders) {
261 return riders.filter((r) => r.online).length;
262}
263
264function riderReport(riders, now) {
265 const online = ridersOnline(riders);
266 const rush = isLunchRush(now);
267 return { online, rush, short: rush && online < 6 };
268}
269
270function weatherDelay(weather) {
271 if (weather === "rain") return 8;
272 if (weather === "snow") return 20;
273 return 0;
274}
275
276function adjustEta(order, weather) {
277 order.eta = order.eta + weatherDelay(weather);
278 return order;
279}
280
281function cancelOrder(order, reason) {
282 if (order.status === "delivered") return order;
283 order.status = "cancelled";
284 return refund(order, reason);
285}
286
287function reorder(previous, user, now) {
288 const cart = { items: previous.items.map((i) => ({ ...i })) };
289 return placeOrder(cart, user, now);
290}
291
292function favourite(user) {
293 const counts = {};
294 for (const o of db.orders.byUser(user.id)) {
295 for (const i of o.items) counts[i.id] = (counts[i.id] || 0) + i.qty;
296 }
297 return Object.keys(counts).sort((a, b) => counts[b] - counts[a])[0];
298}
299
300module.exports = {
301 isOpen,
302 placeOrder,
303 assignRider,
304 markDelivered,
305 printTicket,
306 emailReceipt,
307 priceList,
308 revenueToday,
309 riderReport,
310 cancelOrder,
311 reorder,
312};