@booklib/skills 1.6.0 → 1.7.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.
- package/agents/architecture-reviewer.md +136 -0
- package/agents/booklib-reviewer.md +90 -0
- package/agents/data-reviewer.md +107 -0
- package/agents/jvm-reviewer.md +146 -0
- package/agents/python-reviewer.md +128 -0
- package/agents/rust-reviewer.md +115 -0
- package/agents/ts-reviewer.md +110 -0
- package/agents/ui-reviewer.md +117 -0
- package/bin/skills.js +43 -6
- package/package.json +1 -1
- package/benchmark/devto-post.md +0 -178
- package/benchmark/order-processing.original.js +0 -158
- package/benchmark/order-processing.pr-toolkit.js +0 -181
- package/benchmark/order-processing.skill-router.js +0 -271
- package/benchmark/review-report.md +0 -129
- package/demo.gif +0 -0
- package/demo.tape +0 -40
- package/docs/index.html +0 -411
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
// order processing thing
|
|
2
|
-
// ORIGINAL — intentionally bad code. Do not use in production.
|
|
3
|
-
|
|
4
|
-
var db = require('./db')
|
|
5
|
-
var mailer = require('./mailer')
|
|
6
|
-
|
|
7
|
-
var discount = 0.1
|
|
8
|
-
var TAX = 0.23
|
|
9
|
-
var items = []
|
|
10
|
-
var total = 0
|
|
11
|
-
var usr = null
|
|
12
|
-
|
|
13
|
-
function process(o, u, pay, s) {
|
|
14
|
-
usr = u
|
|
15
|
-
if (u != null) {
|
|
16
|
-
if (u.active == true) {
|
|
17
|
-
if (o != null) {
|
|
18
|
-
if (o.items != null && o.items.length > 0) {
|
|
19
|
-
var t = 0
|
|
20
|
-
for (var i = 0; i < o.items.length; i++) {
|
|
21
|
-
var item = o.items[i]
|
|
22
|
-
if (item.qty > 0) {
|
|
23
|
-
if (item.price > 0) {
|
|
24
|
-
t = t + (item.qty * item.price)
|
|
25
|
-
items.push(item)
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
if (u.type == "premium") {
|
|
30
|
-
t = t - (t * 0.1)
|
|
31
|
-
}
|
|
32
|
-
if (u.type == "vip") {
|
|
33
|
-
t = t - (t * 0.2)
|
|
34
|
-
}
|
|
35
|
-
if (u.type == "staff") {
|
|
36
|
-
t = t - (t * 0.5)
|
|
37
|
-
}
|
|
38
|
-
total = t + (t * TAX)
|
|
39
|
-
if (pay == "card") {
|
|
40
|
-
var res = chargeCard(u.card, total)
|
|
41
|
-
if (res == true) {
|
|
42
|
-
var q = "INSERT INTO orders VALUES ('" + o.id + "', '" + u.id + "', " + total + ", 'paid')"
|
|
43
|
-
db.query(q)
|
|
44
|
-
mailer.send(u.email, "Order confirmed", "ur order is confirmed lol total: " + total)
|
|
45
|
-
s.orders++
|
|
46
|
-
s.revenue = s.revenue + total
|
|
47
|
-
return true
|
|
48
|
-
} else {
|
|
49
|
-
return false
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
if (pay == "paypal") {
|
|
53
|
-
var res2 = chargePaypal(u.paypal, total)
|
|
54
|
-
if (res2 == true) {
|
|
55
|
-
var q2 = "INSERT INTO orders VALUES ('" + o.id + "', '" + u.id + "', " + total + ", 'paid')"
|
|
56
|
-
db.query(q2)
|
|
57
|
-
mailer.send(u.email, "Order confirmed", "ur order is confirmed lol total: " + total)
|
|
58
|
-
s.orders++
|
|
59
|
-
s.revenue = s.revenue + total
|
|
60
|
-
return true
|
|
61
|
-
} else {
|
|
62
|
-
return false
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
if (pay == "crypto") {
|
|
66
|
-
// TODO: implement this someday
|
|
67
|
-
return false
|
|
68
|
-
}
|
|
69
|
-
} else {
|
|
70
|
-
return false
|
|
71
|
-
}
|
|
72
|
-
} else {
|
|
73
|
-
return false
|
|
74
|
-
}
|
|
75
|
-
} else {
|
|
76
|
-
return false
|
|
77
|
-
}
|
|
78
|
-
} else {
|
|
79
|
-
return false
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function chargeCard(card, amt) {
|
|
84
|
-
// just assume it works
|
|
85
|
-
console.log("charging card " + card + " for " + amt)
|
|
86
|
-
return true
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function chargePaypal(pp, amt) {
|
|
90
|
-
console.log("paypal " + pp + " " + amt)
|
|
91
|
-
return true
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// get user orders
|
|
95
|
-
function getOrds(uid) {
|
|
96
|
-
var q = "SELECT * FROM orders WHERE user_id = '" + uid + "'"
|
|
97
|
-
return db.query(q)
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// cancel
|
|
101
|
-
function cancel(oid, uid, rsn) {
|
|
102
|
-
var q = "SELECT * FROM orders WHERE id = '" + oid + "'"
|
|
103
|
-
var ord = db.query(q)
|
|
104
|
-
if (ord != null) {
|
|
105
|
-
if (ord.user_id == uid) {
|
|
106
|
-
if (ord.status != "cancelled") {
|
|
107
|
-
if (ord.status != "shipped") {
|
|
108
|
-
if (ord.status != "delivered") {
|
|
109
|
-
var q2 = "UPDATE orders SET status = 'cancelled', reason = '" + rsn + "' WHERE id = '" + oid + "'"
|
|
110
|
-
db.query(q2)
|
|
111
|
-
mailer.send(usr.email, "Cancelled", "ok cancelled")
|
|
112
|
-
return true
|
|
113
|
-
} else {
|
|
114
|
-
return false
|
|
115
|
-
}
|
|
116
|
-
} else {
|
|
117
|
-
return false
|
|
118
|
-
}
|
|
119
|
-
} else {
|
|
120
|
-
return false
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
return false
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// stats thing used everywhere
|
|
128
|
-
var stats = {
|
|
129
|
-
orders: 0,
|
|
130
|
-
revenue: 0,
|
|
131
|
-
cancelled: 0
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function getStats() {
|
|
135
|
-
return eval("stats")
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// some random util shoved in here
|
|
139
|
-
function formatMoney(n) {
|
|
140
|
-
return "$" + Math.round(n * 100) / 100
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// also does refunds i guess
|
|
144
|
-
function refund(oid) {
|
|
145
|
-
var q = "SELECT * FROM orders WHERE id = '" + oid + "'"
|
|
146
|
-
var ord = db.query(q)
|
|
147
|
-
if (ord.status == "paid") {
|
|
148
|
-
// refund the money somehow
|
|
149
|
-
console.log("refunding " + ord.total)
|
|
150
|
-
var q2 = "UPDATE orders SET status = 'refunded' WHERE id = '" + oid + "'"
|
|
151
|
-
db.query(q2)
|
|
152
|
-
stats.revenue = stats.revenue - ord.total
|
|
153
|
-
stats.cancelled++
|
|
154
|
-
mailer.send(usr.email, "Refund", "u got ur money back: " + formatMoney(ord.total))
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
module.exports = { process, getOrds, cancel, getStats, refund }
|
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
// code-after-native.js
|
|
2
|
-
// Rewritten applying all findings from: pr-review-toolkit:code-reviewer
|
|
3
|
-
// Fixes applied (by issue #):
|
|
4
|
-
// #1 Parameterised queries — no SQL injection
|
|
5
|
-
// #2 eval("stats") → return stats
|
|
6
|
-
// #3 No module-level mutable state — all state is local or injected
|
|
7
|
-
// #4 items array removed from module scope
|
|
8
|
-
// #5 null check before ord.status in refund()
|
|
9
|
-
// #6 Card data not logged to console (PCI)
|
|
10
|
-
// #7 Unknown payment method throws, not silently returns undefined
|
|
11
|
-
// #8 Shared finalizeOrder() — no duplicated block
|
|
12
|
-
// #9 discount variable removed; named constants used
|
|
13
|
-
// #10 === / !== throughout, no loose equality
|
|
14
|
-
// #11/#12 cancel/refund look up user email from order, not global usr
|
|
15
|
-
// #13 Single stats object — no two-object inconsistency
|
|
16
|
-
// #14 cancel() updates stats.cancelled on success
|
|
17
|
-
// #15 try/catch around db and mailer calls
|
|
18
|
-
// #16 const/let throughout, no var
|
|
19
|
-
// #17 Descriptive parameter names
|
|
20
|
-
|
|
21
|
-
'use strict';
|
|
22
|
-
|
|
23
|
-
const db = require('./db');
|
|
24
|
-
const mailer = require('./mailer');
|
|
25
|
-
|
|
26
|
-
const TAX_RATE = 0.23;
|
|
27
|
-
const DISCOUNT_PREMIUM = 0.10;
|
|
28
|
-
const DISCOUNT_VIP = 0.20;
|
|
29
|
-
const DISCOUNT_STAFF = 0.50;
|
|
30
|
-
const NON_CANCELLABLE = ['cancelled', 'shipped', 'delivered'];
|
|
31
|
-
|
|
32
|
-
// Module-level stats — single source of truth (#13)
|
|
33
|
-
const stats = { orders: 0, revenue: 0, cancelled: 0 };
|
|
34
|
-
|
|
35
|
-
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
36
|
-
|
|
37
|
-
// Renamed from process() — avoids shadowing Node's global `process` (#17, #13)
|
|
38
|
-
async function placeOrder(order, user, paymentMethod, statsRef) {
|
|
39
|
-
// Guard clauses replace pyramid nesting (#10)
|
|
40
|
-
if (!user || !user.active) return false;
|
|
41
|
-
if (!order || !order.items || order.items.length === 0) return false;
|
|
42
|
-
|
|
43
|
-
// Local variables — no module-level state (#3, #4)
|
|
44
|
-
const validItems = order.items.filter(item => item.qty > 0 && item.price > 0);
|
|
45
|
-
if (validItems.length === 0) return false;
|
|
46
|
-
|
|
47
|
-
let subtotal = validItems.reduce((sum, item) => sum + item.qty * item.price, 0);
|
|
48
|
-
|
|
49
|
-
// Named constants replace magic numbers (#9)
|
|
50
|
-
if (user.type === 'premium') subtotal *= (1 - DISCOUNT_PREMIUM);
|
|
51
|
-
else if (user.type === 'vip') subtotal *= (1 - DISCOUNT_VIP);
|
|
52
|
-
else if (user.type === 'staff') subtotal *= (1 - DISCOUNT_STAFF);
|
|
53
|
-
|
|
54
|
-
const total = subtotal * (1 + TAX_RATE);
|
|
55
|
-
|
|
56
|
-
// Dispatch — unknown method throws, not silently undefined (#7)
|
|
57
|
-
const charged = await chargePayment(paymentMethod, user, total);
|
|
58
|
-
if (!charged) return false;
|
|
59
|
-
|
|
60
|
-
// Shared confirmation — duplicated block eliminated (#8)
|
|
61
|
-
await finalizeOrder(order, user, total, statsRef || stats);
|
|
62
|
-
return true;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
async function getOrders(userId) {
|
|
66
|
-
// Parameterised query (#1)
|
|
67
|
-
return db.query('SELECT * FROM orders WHERE user_id = $1', [userId]);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async function cancelOrder(orderId, userId, reason) {
|
|
71
|
-
try {
|
|
72
|
-
// Parameterised (#1)
|
|
73
|
-
const order = await db.query('SELECT * FROM orders WHERE id = $1', [orderId]);
|
|
74
|
-
|
|
75
|
-
// Null check (#5); strict equality (#10)
|
|
76
|
-
if (!order) return false;
|
|
77
|
-
if (order.user_id !== userId) return false;
|
|
78
|
-
if (NON_CANCELLABLE.includes(order.status)) return false;
|
|
79
|
-
|
|
80
|
-
await db.query(
|
|
81
|
-
'UPDATE orders SET status = $1, reason = $2 WHERE id = $3',
|
|
82
|
-
['cancelled', reason, orderId],
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
// Use order's own email — not global usr (#11)
|
|
86
|
-
mailer.send(order.user_email, 'Order cancelled', 'Your order has been cancelled.');
|
|
87
|
-
|
|
88
|
-
// Stats update on cancel (#14)
|
|
89
|
-
stats.cancelled++;
|
|
90
|
-
stats.revenue -= order.total;
|
|
91
|
-
|
|
92
|
-
return true;
|
|
93
|
-
} catch (err) {
|
|
94
|
-
// Error handling (#15)
|
|
95
|
-
console.error('cancelOrder failed:', err.message);
|
|
96
|
-
return false;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
async function refundOrder(orderId) {
|
|
101
|
-
try {
|
|
102
|
-
const order = await db.query('SELECT * FROM orders WHERE id = $1', [orderId]);
|
|
103
|
-
|
|
104
|
-
// Null check added — was missing, caused TypeError crash (#5)
|
|
105
|
-
if (!order) return false;
|
|
106
|
-
if (order.status !== 'paid') return false;
|
|
107
|
-
|
|
108
|
-
await db.query(
|
|
109
|
-
'UPDATE orders SET status = $1 WHERE id = $2',
|
|
110
|
-
['refunded', orderId],
|
|
111
|
-
);
|
|
112
|
-
|
|
113
|
-
stats.revenue -= order.total;
|
|
114
|
-
stats.cancelled++;
|
|
115
|
-
|
|
116
|
-
// Use order's own email — not global usr (#12)
|
|
117
|
-
mailer.send(
|
|
118
|
-
order.user_email,
|
|
119
|
-
'Refund processed',
|
|
120
|
-
`Your refund of ${formatMoney(order.total)} is on its way.`,
|
|
121
|
-
);
|
|
122
|
-
|
|
123
|
-
return true;
|
|
124
|
-
} catch (err) {
|
|
125
|
-
console.error('refundOrder failed:', err.message);
|
|
126
|
-
return false;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function getStats() {
|
|
131
|
-
return { ...stats }; // eval("stats") → just return the object (#2); defensive copy (#13)
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// ─── Private helpers ─────────────────────────────────────────────────────────
|
|
135
|
-
|
|
136
|
-
async function chargePayment(method, user, total) {
|
|
137
|
-
if (method === 'card') return chargeCard(user.card, total);
|
|
138
|
-
if (method === 'paypal') return chargePaypal(user.paypal, total);
|
|
139
|
-
if (method === 'crypto') throw new Error('Crypto payments are not yet supported');
|
|
140
|
-
// Unknown method throws — no silent undefined return (#7)
|
|
141
|
-
throw new Error(`Unknown payment method: ${method}`);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Shared — eliminates the duplicated card/paypal block (#8)
|
|
145
|
-
async function finalizeOrder(order, user, total, statsRef) {
|
|
146
|
-
// Parameterised INSERT (#1)
|
|
147
|
-
await db.query(
|
|
148
|
-
'INSERT INTO orders (id, user_id, total, status) VALUES ($1, $2, $3, $4)',
|
|
149
|
-
[order.id, user.id, total, 'paid'],
|
|
150
|
-
);
|
|
151
|
-
mailer.send(
|
|
152
|
-
user.email,
|
|
153
|
-
'Order confirmed',
|
|
154
|
-
`Your order has been confirmed. Total: ${formatMoney(total)}`,
|
|
155
|
-
);
|
|
156
|
-
statsRef.orders++;
|
|
157
|
-
statsRef.revenue += total;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function chargeCard(cardToken, amount) {
|
|
161
|
-
// Do NOT log card details — PCI-DSS (#6)
|
|
162
|
-
console.log(`Charging card (ending ...${String(cardToken).slice(-4)}) for ${formatMoney(amount)}`);
|
|
163
|
-
return true; // TODO: integrate real payment gateway
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function chargePaypal(account, amount) {
|
|
167
|
-
console.log(`Charging PayPal ${account} for ${formatMoney(amount)}`);
|
|
168
|
-
return true; // TODO: integrate PayPal SDK
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function formatMoney(amount) {
|
|
172
|
-
return `$${(Math.round(amount * 100) / 100).toFixed(2)}`;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
module.exports = {
|
|
176
|
-
placeOrder,
|
|
177
|
-
getOrders,
|
|
178
|
-
cancelOrder,
|
|
179
|
-
refundOrder,
|
|
180
|
-
getStats,
|
|
181
|
-
};
|
|
@@ -1,271 +0,0 @@
|
|
|
1
|
-
// code-after-custom.js
|
|
2
|
-
// Rewritten applying: clean-code-reviewer + design-patterns (via skill-router)
|
|
3
|
-
//
|
|
4
|
-
// Patterns applied (design-patterns skill):
|
|
5
|
-
// Strategy — payment methods (GoF: encapsulate interchangeable algorithms)
|
|
6
|
-
// Strategy — discount tiers (GoF: data-driven, open for extension)
|
|
7
|
-
// State — order lifecycle (GoF: each status owns its valid transitions)
|
|
8
|
-
// Singleton — stats (GoF: deliberate, clean, no accidental shared state)
|
|
9
|
-
// Observer — side effects (GoF: email/stats decoupled from business logic)
|
|
10
|
-
// Facade — module API (GoF: single clean surface, internals hidden)
|
|
11
|
-
// Template Method — validation (GoF: fixed skeleton, variable steps)
|
|
12
|
-
//
|
|
13
|
-
// Principles applied (clean-code-reviewer skill):
|
|
14
|
-
// N1/N2 — intention-revealing names at the right abstraction level
|
|
15
|
-
// F1/F2 — small functions, one responsibility each
|
|
16
|
-
// G5 — DRY: no duplicated confirmation block
|
|
17
|
-
// G25 — named constants, no magic numbers
|
|
18
|
-
// G23 — data-driven lookup, not if-chain
|
|
19
|
-
// G28 — encapsulated predicates
|
|
20
|
-
// G30 — functions do one thing
|
|
21
|
-
// Ch.7 — throw with context, never silent return false
|
|
22
|
-
// G4 — parameterised queries, no eval, no safety overrides
|
|
23
|
-
|
|
24
|
-
'use strict';
|
|
25
|
-
|
|
26
|
-
const db = require('./db');
|
|
27
|
-
const mailer = require('./mailer');
|
|
28
|
-
|
|
29
|
-
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
30
|
-
|
|
31
|
-
const TAX_RATE = 0.23;
|
|
32
|
-
|
|
33
|
-
// Strategy: discount tiers — add a new tier here, nothing else changes (G23, G25)
|
|
34
|
-
const DISCOUNT_BY_TIER = Object.freeze({
|
|
35
|
-
standard: 0,
|
|
36
|
-
premium: 0.10,
|
|
37
|
-
vip: 0.20,
|
|
38
|
-
staff: 0.50,
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
// Strategy: payment processors — add a new method here, nothing else changes
|
|
42
|
-
const PAYMENT_PROCESSORS = Object.freeze({
|
|
43
|
-
card: {
|
|
44
|
-
charge(user, amount) {
|
|
45
|
-
// TODO: integrate real payment gateway
|
|
46
|
-
console.log(`Charging card (...${String(user.card).slice(-4)}) for ${formatMoney(amount)}`);
|
|
47
|
-
return true;
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
paypal: {
|
|
51
|
-
charge(user, amount) {
|
|
52
|
-
// TODO: integrate PayPal SDK
|
|
53
|
-
console.log(`Charging PayPal ${user.paypal} for ${formatMoney(amount)}`);
|
|
54
|
-
return true;
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
crypto: {
|
|
58
|
-
charge() {
|
|
59
|
-
throw new Error('Crypto payments are not yet implemented');
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
// ─── State: order lifecycle ───────────────────────────────────────────────────
|
|
65
|
-
// Each state owns its own transition rules.
|
|
66
|
-
// Adding a new status = adding one object. No existing guards change.
|
|
67
|
-
|
|
68
|
-
const ORDER_STATES = Object.freeze({
|
|
69
|
-
pending: {
|
|
70
|
-
canCancel: () => true,
|
|
71
|
-
canRefund: () => false,
|
|
72
|
-
},
|
|
73
|
-
paid: {
|
|
74
|
-
canCancel: () => true,
|
|
75
|
-
canRefund: () => true,
|
|
76
|
-
},
|
|
77
|
-
shipped: {
|
|
78
|
-
canCancel: () => false,
|
|
79
|
-
canRefund: () => false,
|
|
80
|
-
},
|
|
81
|
-
delivered: {
|
|
82
|
-
canCancel: () => false,
|
|
83
|
-
canRefund: () => true,
|
|
84
|
-
},
|
|
85
|
-
cancelled: {
|
|
86
|
-
canCancel: () => false,
|
|
87
|
-
canRefund: () => false,
|
|
88
|
-
},
|
|
89
|
-
refunded: {
|
|
90
|
-
canCancel: () => false,
|
|
91
|
-
canRefund: () => false,
|
|
92
|
-
},
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
function getOrderState(status) {
|
|
96
|
-
const state = ORDER_STATES[status];
|
|
97
|
-
if (!state) throw new Error(`Unknown order status: ${status}`);
|
|
98
|
-
return state;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// ─── Singleton: stats ─────────────────────────────────────────────────────────
|
|
102
|
-
// Deliberate, clean singleton. Not an accidental module-scope variable.
|
|
103
|
-
|
|
104
|
-
const Stats = (() => {
|
|
105
|
-
const data = { orders: 0, revenue: 0, cancelled: 0 };
|
|
106
|
-
return {
|
|
107
|
-
recordPlaced(total) { data.orders++; data.revenue += total; },
|
|
108
|
-
recordCancelled(total) { data.cancelled++; data.revenue -= total; },
|
|
109
|
-
recordRefunded(total) { data.cancelled++; data.revenue -= total; },
|
|
110
|
-
snapshot() { return { ...data }; },
|
|
111
|
-
};
|
|
112
|
-
})();
|
|
113
|
-
|
|
114
|
-
// ─── Observer: event bus ──────────────────────────────────────────────────────
|
|
115
|
-
// Business logic emits events. Side effects (email, stats) register as listeners.
|
|
116
|
-
// Adding SMS or audit log = one new listener. Core functions unchanged.
|
|
117
|
-
|
|
118
|
-
const EventBus = (() => {
|
|
119
|
-
const listeners = {};
|
|
120
|
-
return {
|
|
121
|
-
on(event, fn) { (listeners[event] = listeners[event] || []).push(fn); },
|
|
122
|
-
emit(event, payload) { (listeners[event] || []).forEach(fn => fn(payload)); },
|
|
123
|
-
};
|
|
124
|
-
})();
|
|
125
|
-
|
|
126
|
-
// Register observers at startup — not inside business functions
|
|
127
|
-
EventBus.on('order.placed', ({ user, total }) => {
|
|
128
|
-
mailer.send(user.email, 'Order confirmed', `Your order total is ${formatMoney(total)}.`);
|
|
129
|
-
Stats.recordPlaced(total);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
EventBus.on('order.cancelled', ({ order, total }) => {
|
|
133
|
-
mailer.send(order.user_email, 'Order cancelled', 'Your order has been cancelled.');
|
|
134
|
-
Stats.recordCancelled(total);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
EventBus.on('order.refunded', ({ order, total }) => {
|
|
138
|
-
mailer.send(order.user_email, 'Refund processed', `Your refund of ${formatMoney(total)} is on its way.`);
|
|
139
|
-
Stats.recordRefunded(total);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
// ─── Validation (Template Method skeleton) ────────────────────────────────────
|
|
143
|
-
// Fixed skeleton: check preconditions, then run action. (G28, Ch.7)
|
|
144
|
-
|
|
145
|
-
function assertActiveUser(user) {
|
|
146
|
-
if (!user) throw new Error('user is required');
|
|
147
|
-
if (!user.active) throw new Error(`User ${user.id} is not active`);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function assertValidOrder(order) {
|
|
151
|
-
if (!order?.items?.length) throw new Error('order must contain at least one item');
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function assertSupportedPayment(method) {
|
|
155
|
-
if (!PAYMENT_PROCESSORS[method]) throw new Error(`Unsupported payment method: ${method}`);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// ─── Calculation (F1: one responsibility each) ────────────────────────────────
|
|
159
|
-
|
|
160
|
-
function calculateSubtotal(items) {
|
|
161
|
-
return items
|
|
162
|
-
.filter(item => item.qty > 0 && item.price > 0)
|
|
163
|
-
.reduce((sum, item) => sum + item.qty * item.price, 0);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function applyTierDiscount(amount, userTier) {
|
|
167
|
-
const rate = DISCOUNT_BY_TIER[userTier] ?? 0; // G25: named table, not magic number
|
|
168
|
-
return amount * (1 - rate);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function applyTax(amount) {
|
|
172
|
-
return amount * (1 + TAX_RATE);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// ─── Persistence ──────────────────────────────────────────────────────────────
|
|
176
|
-
// Each function does one thing. Parameterised queries throughout (G4).
|
|
177
|
-
|
|
178
|
-
async function persistOrder(order, user, total) {
|
|
179
|
-
await db.query(
|
|
180
|
-
'INSERT INTO orders (id, user_id, total, status) VALUES (?, ?, ?, ?)',
|
|
181
|
-
[order.id, user.id, total, 'paid'],
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
async function fetchOrder(orderId) {
|
|
186
|
-
const order = await db.query('SELECT * FROM orders WHERE id = ?', [orderId]);
|
|
187
|
-
if (!order) throw new Error(`Order not found: ${orderId}`);
|
|
188
|
-
return order;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
async function persistCancellation(orderId, reason) {
|
|
192
|
-
await db.query(
|
|
193
|
-
'UPDATE orders SET status = ?, reason = ? WHERE id = ?',
|
|
194
|
-
['cancelled', reason, orderId],
|
|
195
|
-
);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
async function persistRefund(orderId) {
|
|
199
|
-
await db.query('UPDATE orders SET status = ? WHERE id = ?', ['refunded', orderId]);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// ─── Formatting ───────────────────────────────────────────────────────────────
|
|
203
|
-
|
|
204
|
-
function formatMoney(amount) {
|
|
205
|
-
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// ─── Facade: public API ───────────────────────────────────────────────────────
|
|
209
|
-
// Callers import one clean surface. All internals (strategies, state,
|
|
210
|
-
// observer, singleton) are hidden and can evolve independently.
|
|
211
|
-
|
|
212
|
-
async function processOrder(order, user, paymentMethod) {
|
|
213
|
-
// Template Method: validate → calculate → charge → persist → notify (via Observer)
|
|
214
|
-
assertActiveUser(user);
|
|
215
|
-
assertValidOrder(order);
|
|
216
|
-
assertSupportedPayment(paymentMethod);
|
|
217
|
-
|
|
218
|
-
const subtotal = calculateSubtotal(order.items);
|
|
219
|
-
const discounted = applyTierDiscount(subtotal, user.type);
|
|
220
|
-
const total = applyTax(discounted);
|
|
221
|
-
|
|
222
|
-
// Strategy pattern: dispatch to the right processor, one confirmation path
|
|
223
|
-
PAYMENT_PROCESSORS[paymentMethod].charge(user, total);
|
|
224
|
-
await persistOrder(order, user, total);
|
|
225
|
-
|
|
226
|
-
// Observer: emit event — email and stats handled by listeners, not here
|
|
227
|
-
EventBus.emit('order.placed', { user, order, total });
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
async function getUserOrders(userId) {
|
|
231
|
-
return db.query('SELECT * FROM orders WHERE user_id = ?', [userId]);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
async function cancelOrder(orderId, userId, reason) {
|
|
235
|
-
const order = await fetchOrder(orderId);
|
|
236
|
-
|
|
237
|
-
if (order.userId !== userId) throw new Error('Order does not belong to this user');
|
|
238
|
-
|
|
239
|
-
// State pattern: the status object decides whether cancellation is legal
|
|
240
|
-
if (!getOrderState(order.status).canCancel()) {
|
|
241
|
-
throw new Error(`Order ${orderId} cannot be cancelled (status: ${order.status})`);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
await persistCancellation(orderId, reason);
|
|
245
|
-
EventBus.emit('order.cancelled', { order, total: order.total });
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
async function refundOrder(orderId) {
|
|
249
|
-
const order = await fetchOrder(orderId);
|
|
250
|
-
|
|
251
|
-
// State pattern: the status object decides whether refund is legal
|
|
252
|
-
if (!getOrderState(order.status).canRefund()) {
|
|
253
|
-
throw new Error(`Order ${orderId} cannot be refunded (status: ${order.status})`);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
await persistRefund(orderId);
|
|
257
|
-
EventBus.emit('order.refunded', { order, total: order.total });
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function getStats() {
|
|
261
|
-
return Stats.snapshot(); // Singleton: clean access, defensive copy
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// Facade export: one coherent interface
|
|
265
|
-
module.exports = {
|
|
266
|
-
processOrder,
|
|
267
|
-
getUserOrders,
|
|
268
|
-
cancelOrder,
|
|
269
|
-
refundOrder,
|
|
270
|
-
getStats,
|
|
271
|
-
};
|