Learn Practice Daily golf Reference
Navigation·Lesson 5 of 5·11 min·0/5 exercisesPro

The outage

The whole of track eleven at 3:40 a.m.: a guided tour of four functions with ribbons, two fixes, then a second look where every way back is yours to choose.

Scene

Wednesday, 3:40 a.m. The pager again. Customers who press Cancel twice are being refunded twice, and the incident log names four functions: cancelOrder, refund, notify, saveOrder, scattered across legacy.js.

You'll read them in the order the log lists them, go back and forth between them, then fix two of them. Every way back you know, half asleep.

From memory

Warm-up from earlier chapters

Two from earlier chapters, from memory, before the new idea. Two wrong tries show the answer.

Highlight this bracket, its partner and everything between.
press the keys…2 keys
1 of 2
Highlight this bracket, its partner and everything between.

Every way back, on one card

Four ways around the book tonight. Use whichever you remember first: a way that works is a good way.

The place…Leave itGo back
you saved (a ribbon)ma'a the line, `a the exact word
you opened (the trail)any jumpCtrl-o back, Ctrl-i forward; '' or `` for the last one
you wrote (the changes)any changeg; back, g, forward; gi to pick up the pen
at the other end of a pair%
Boss round

Stage 1 · The trail

3:42. The guided tour. Visit the four functions in the order the incident log lists them, leaving a ribbon at each before you set off for the next: cancelOrder (ma), refund (mb), notify (mc), saveOrder (md). Then back to refund, to cancelOrder, and to saveOrder again.

1 of 7Move the cursor onto the on line 239, below the screen
1// 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
45function validateOrder(order) {
46 if (!order.items || order.items.length === 0) return "empty";
47 if (order.items.some((i) => i.qty <= 0)) return "bad quantity";
48 if (!order.address) return "no address";
49 if (!isOpen(new Date())) return "closed";
50 return null;
51}
52
53function applyCoupon(order, code) {
54 const coupon = COUPONS[code];
55 if (!coupon) return order;
56 if (coupon.expires < Date.now()) return order;
57 const off = Math.round(subtotal(order.items) * coupon.percent);
58 return { ...order, discount: off };
59}
60
61function studentDiscount(order, user) {
62 if (!user.isStudent) return 0;
63 return Math.round(subtotal(order.items) * 0.1);
64}
65
66function distanceKm(a, b) {
67 const dLat = (b.lat - a.lat) * 111;
68 const dLng = (b.lng - a.lng) * 111 * Math.cos((a.lat * Math.PI) / 180);
69 return Math.sqrt(dLat * dLat + dLng * dLng);
70}
71
72function nearestRider(riders, address) {
73 let best = null;
74 for (const rider of riders) {
75 if (!rider.online) continue;
76 const d = distanceKm(rider.position, address.position);
77 if (d > MAX_DISTANCE_KM) continue;
78 if (!best || d < best.distance) best = { rider, distance: d };
79 }
80 return best;
81}
82
83function assignRider(order, riders) {
84 const match = nearestRider(riders, order.address);
85 if (!match) return refund(order, "No rider nearby, sorry!");
86 order.riderId = match.rider.id;
87 order.eta = Math.ceil(match.distance * 4) + 10;
88 notify(match.rider.phone, "New order: " + order.id);
89 return order;
90}
91
92function etaMinutes(order, now) {
93 const rush = isLunchRush(now) ? 10 : 0;
94 return order.eta + rush;
95}
96
97function refund(order, reason) {
98 order.status = "refunded";
99 order.refundReason = reason;
100 payments.refund(order.paymentId, totalFor(order));
101 notify(order.customerPhone, "Refunded: " + reason);
102 return order;
103}
104
105function markDelivered(order, now) {
106 order.status = "delivered";
107 order.deliveredAt = now.toISOString();
108 const late = now - new Date(order.placedAt) > order.eta * 60000;
109 if (late) order.lateBy = Math.round((now - new Date(order.placedAt)) / 60000 - order.eta);
110 return order;
111}
112
113function kitchenTicket(order) {
114 const lines = order.items.map((i) => i.qty + " x " + i.name);
115 lines.unshift("ORDER " + order.id);
116 if (order.notes) lines.push("NOTE: " + order.notes);
117 return lines.join("\n");
118}
119
120function printTicket(order) {
121 const ticket = kitchenTicket(order);
122 printer.print(ticket);
123 log("printed", order.id);
124}
125
126function emailReceipt(order, user) {
127 const body = renderReceipt(order);
128 mailer.send(user.email, "Your Byte Burrito receipt", body);
129}
130
131function menuItem(id) {
132 const item = MENU.find((m) => m.id === id);
133 if (!item) throw new Error("not on the menu: " + id);
134 return item;
135}
136
137function isSpicy(id) {
138 return menuItem(id).spicy === true;
139}
140
141function happyHourPrice(item, now) {
142 const hour = now.getHours();
143 if (hour >= 17 && hour < 19) return Math.round(item.price * 0.8);
144 return item.price;
145}
146
147function priceList(now) {
148 return MENU.map((item) => ({
149 id: item.id,
150 name: item.name,
151 price: formatPrice(happyHourPrice(item, now)),
152 }));
153}
154
155function placeOrder(cart, user, now) {
156 const order = { id: newId(), items: cart.items, address: user.address, placedAt: now.toISOString() };
157 const fee = deliveryFee(order, distanceKm(KITCHEN, order.address));
158 const problem = validateOrder(order);
159 if (problem) throw new Error(problem);
160 order.discount = studentDiscount(order, user);
161 order.total = totalFor(order) - order.discount + fee;
162 return order;
163}
164
165function deliveryFee(order, km) {
166 const base = km <= 2 ? 0 : 1.5;
167 const perKm = km > 2 ? (km - 2) * 0.4 : 0;
168 const rush = isLunchRush(new Date()) ? 1 : 0;
169 return Math.round((base + perKm + rush) * 100) / 100;
170}
171
172function newId() {
173 return "BB-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 6);
174}
175
176function log(event, ...details) {
177 console.log("[" + new Date().toISOString() + "] " + event, ...details);
178}
179
180function notify(phone, text) {
181 if (!phone) return;
182 sms.send(phone, text.slice(0, 160));
183}
184
185function saveUser(user) {
186 db.users.put(user.id, user);
187}
188
189function loadUser(id) {
190 return db.users.get(id);
191}
192
193function saveOrder(order) {
194 db.orders.put(order.id, order);
195 log("saved", order.id);
196}
197
198function loadOrder(id) {
199 return db.orders.get(id);
200}
201
202function ordersToday(now) {
203 const day = now.toISOString().slice(0, 10);
204 return db.orders.all().filter((o) => o.placedAt.startsWith(day));
205}
206
207function revenueToday(now) {
208 return ordersToday(now).reduce((sum, o) => sum + o.total, 0);
209}
210
211function busiestHour(now) {
212 const counts = new Array(24).fill(0);
213 for (const o of ordersToday(now)) counts[new Date(o.placedAt).getHours()]++;
214 return counts.indexOf(Math.max(...counts));
215}
216
217function ridersOnline(riders) {
218 return riders.filter((r) => r.online).length;
219}
220
221function riderReport(riders, now) {
222 const online = ridersOnline(riders);
223 const rush = isLunchRush(now);
224 return { online, rush, short: rush && online < 6 };
225}
226
227function weatherDelay(weather) {
228 if (weather === "rain") return 8;
229 if (weather === "snow") return 20;
230 return 0;
231}
232
233function adjustEta(order, weather) {
234 order.eta = order.eta + weatherDelay(weather);
235 return order;
236}
237
238function cancelOrder(order, reason) {
239 if (order.status === "delivered") return order;
240 order.status = "cancelled";
241 return refund(order, reason);
242}
243
244function reorder(previous, user, now) {
245 const cart = { items: previous.items.map((i) => ({ ...i })) };
246 return placeOrder(cart, user, now);
247}
248
249function favourite(user) {
250 const counts = {};
251 for (const o of db.orders.byUser(user.id)) {
252 for (const i of o.items) counts[i.id] = (counts[i.id] || 0) + i.qty;
253 }
254 return Object.keys(counts).sort((a, b) => counts[b] - counts[a])[0];
255}
256
257module.exports = {
258 isOpen,
259 placeOrder,
260 assignRider,
261 markDelivered,
262 printTicket,
263 emailReceipt,
264 priceList,
265 revenueToday,
266 riderReport,
267 cancelOrder,
268 reorder,
269};
NORMALLoading the editor…1/70.0s1:1

This exercise needs a keyboard. Read on, and play it at your computer.

Boss round

Stage 2 · The fix

3:50. In refund, a guard on a new first line, indented like the lines below: if (order.status === "refunded") return order;. In cancelOrder, only orders without a status yet can be cancelled: if (order.status === "delivered") becomes if (order.status). Then back to where you stopped typing, and sign the guard at the end of its line: // on call, 3:58 a.m.

Stuck? Show a hint
Pro
The rest of this lesson is in Pro

The first exercise of every lesson is free. Pro unlocks the rest, every track, for good.

See Pro

This exercise needs a keyboard. Read on, and play it at your computer.

Challenge

Stage 3 · The second look

4:00. Before you deploy, one more look round, and this time no one tells you which keys. Three places to get back to; take any way you like.

Stuck? Show a hint
Pro
The rest of this lesson is in Pro

The first exercise of every lesson is free. Pro unlocks the rest, every track, for good.

See Pro

This exercise needs a keyboard. Read on, and play it at your computer.

4:03 a.m., the incident replayedFinish the exercise above to see it.
From memory

The whole track, from memory

Seven situations from tonight.

Leave ribbon a here.
scroll here, then press the keys2 keys
1 of 7
Leave ribbon a here.

Extra practice optional

The chapter is done. These rounds keep going as long as you like: stop whenever it stops being useful.

Endless

Laps of the long book

Mixed movement practice: endless stops anywhere in legacy.js, reached with line numbers, paragraphs and the screen keys from the vertical track. It doesn't ask for ribbons or the trail, because those need somewhere you meant to come back to.

Pro
The rest of this lesson is in Pro

The first exercise of every lesson is free. Pro unlocks the rest, every track, for good.

See Pro

This exercise needs a keyboard. Read on, and play it at your computer.