[object Object]

← 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

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 →