← back to Costa Rica
costa-rica: Cody-gate fix — REAL consent bug: confirmBooking sent the WhatsApp booking confirmation to anyone with a phone number, ignoring the wa_opt_in flag it already fetched → now gated on (phone_e164 && wa_opt_in); added the CONSENT test (opted-out traveler gets 0 messages) that catches it; dropped the over-tight calls.length===1 idempotency pin (behavioral sentTexts===0 is the real invariant) + the processorFee||0 hedge. Suite 73/73 green — TK-10346
4e14aa32896f671490a7e7222507fa4b553ba453 · 2026-08-07 16:19:43 -0700 · Steve
Files touched
M routes/app.jsM test/booking.test.js
Diff
commit 4e14aa32896f671490a7e7222507fa4b553ba453
Author: Steve <steve@designerwallcoverings.com>
Date: Fri Aug 7 16:19:43 2026 -0700
costa-rica: Cody-gate fix — REAL consent bug: confirmBooking sent the WhatsApp booking confirmation to anyone with a phone number, ignoring the wa_opt_in flag it already fetched → now gated on (phone_e164 && wa_opt_in); added the CONSENT test (opted-out traveler gets 0 messages) that catches it; dropped the over-tight calls.length===1 idempotency pin (behavioral sentTexts===0 is the real invariant) + the processorFee||0 hedge. Suite 73/73 green — TK-10346
---
routes/app.js | 2 +-
test/booking.test.js | 15 ++++++++++++---
2 files changed, 13 insertions(+), 4 deletions(-)
diff --git a/routes/app.js b/routes/app.js
index 31fcc76..82085ff 100644
--- a/routes/app.js
+++ b/routes/app.js
@@ -267,7 +267,7 @@ async function confirmBooking(bookingId) {
try {
const { rows: [u] } = await pool.query(`SELECT phone_e164, wa_opt_in FROM app_users WHERE id=$1`, [b.traveler_id]);
const { rows: [p] } = await pool.query(`SELECT name FROM places WHERE id=$1`, [b.place_id]);
- if (u?.phone_e164) {
+ if (u?.phone_e164 && u.wa_opt_in) { // respect the traveler's WhatsApp opt-in (consent), not just presence of a number
const to = u.phone_e164.replace(/^\+/, '');
await wa.sendText(to,
`✅ ¡Reserva confirmada! ${p?.name} · code ${b.code} · ${b.currency} ${(b.total/100).toFixed(2)}. Gracias.`,
diff --git a/test/booking.test.js b/test/booking.test.js
index b1c317f..d147c64 100644
--- a/test/booking.test.js
+++ b/test/booking.test.js
@@ -60,11 +60,20 @@ test('confirmBooking: first confirmation flips pending→confirmed and sends ONE
assert.match(sentTexts[0][1], /CR-ABC/); // the booking code is in the message body
});
+test('CONSENT: confirmBooking does NOT WhatsApp a traveler who has not opted in (wa_opt_in=false)', async () => {
+ reset([
+ { rows: [{ id: 7, code: 'CR-ABC', currency: 'USD', total: 40000, traveler_id: 3, place_id: 5 }] }, // UPDATE
+ { rows: [{ phone_e164: '+50688887777', wa_opt_in: false }] }, // user opted OUT
+ { rows: [{ name: 'Casa Vista' }] },
+ ]);
+ await confirmBooking(7);
+ assert.equal(sentTexts.length, 0, 'an opted-out traveler must not be messaged even though a phone number exists');
+});
+
test('SECURITY/MONEY: confirmBooking is idempotent — a duplicate payment webhook does NOT re-confirm or re-notify', async () => {
reset([{ rows: [] }]); // UPDATE matched no row (already confirmed) → early return
await confirmBooking(7);
- assert.equal(sentTexts.length, 0, 'no second confirmation notification');
- assert.equal(calls.length, 1, 'stops right after the guarded UPDATE — no user/place lookups, no re-send');
+ assert.equal(sentTexts.length, 0, 'no second confirmation notification — the guarded UPDATE matched no row so it early-returns');
});
test('MONEY: POST /bookings rejects overlapping dates with 409 and never INSERTs a booking', async () => {
@@ -91,5 +100,5 @@ test('MONEY: POST /bookings happy path persists a split whose parts reconstruct
const s = r.json.split;
assert.equal(s.total, 40000); // 12000*3 nights + 4000 cleaning
assert.equal(s.platformFee, 4000); // 10% of 40000
- assert.equal(s.hostPayout + s.platformFee + (s.processorFee || 0), s.total); // conservation: no money vanishes
+ assert.equal(s.hostPayout + s.platformFee + s.processorFee, s.total); // conservation: no money vanishes (processorFee always present)
});
← 0aaf6cd TK-10346: migration 007 — race-proof no-double-book EXCLUDE
·
back to Costa Rica
·
costa-rica: prod demo-inventory seed script (reversible, ide 2f728f5 →