# Kings & Queens Unified Support Calendar and Sam Booking Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Add dated English-support slots, capacity, 12-hour transfer holds, receipt review, and WhatsApp booking through Sam inside Ale's existing unified calendar.

**Architecture:** PocketBase stores support slots and bookings and enforces conflicts/capacity transactionally. The existing teacher calendar merges normal classes and support slots without migrating `classes`. n8n adds narrowly scoped Sam tools that call authenticated PocketBase routes; Ale remains the only person who approves transfer receipts.

**Tech Stack:** PocketBase JSVM hooks and SQLite transactions, static React/Babel UI, Node.js assertion tests, Python 3 schema/deploy scripts, n8n workflow API, Chatwoot, Evolution API.

---

**Dependency:** Complete and verify `2026-06-21-kq-otp-familias-implementation.md` first. It establishes hooks, safe secrets, `KQ_TOOL_TOKEN`, and the test/deploy pattern used here.

## File map

- Create `Edd-OS/projects/kings-and-queens/app/support-calendar.js`: pure event merge, occupancy, copy-week, and cancellation rules.
- Create `Edd-OS/projects/kings-and-queens/app/support-calendar.test.mjs`: calendar domain tests.
- Modify `Edd-OS/projects/kings-and-queens/app/teacher.html:1273-1364,2037-2220`: unified year/month calendar, slots, settings, and receipt review.
- Create `Edd-OS/projects/kings-and-queens/scripts/setup_support_booking.py`: idempotent support schema.
- Create `Edd-OS/projects/kings-and-queens/scripts/test_setup_support_booking.py`: schema contract tests.
- Create `Edd-OS/projects/kings-and-queens/backend/pb_hooks/support-utils.js`: conflict, capacity, hold, and cancellation rules.
- Create `Edd-OS/projects/kings-and-queens/backend/pb_hooks/support-utils.test.mjs`: backend domain tests.
- Modify `Edd-OS/projects/kings-and-queens/backend/pb_hooks/main.pb.js`: support routes and expiry cron.
- Create `Edd-OS/projects/kings-and-queens/backend/verify_support_booking.py`: API integration smoke test.
- Create `Edd-OS/projects/kings-and-queens/whatsapp-agent/business-context/kq-support-school.md`: approved conversational policy.
- Modify `Edd-OS/projects/kings-and-queens/whatsapp-agent/system-prompt.md`: support booking behavior and price exception.
- Create `Edd-OS/projects/kings-and-queens/whatsapp-agent/workflows/deploy_support_workflows.py`: idempotent n8n workflow/tool patcher.
- Create `Edd-OS/projects/kings-and-queens/whatsapp-agent/workflows/test_deploy_support_workflows.py`: workflow JSON contract tests.
- Modify `Edd-OS/server/docker-compose.yml`: expose `KQ_TOOL_TOKEN` to n8n and the internal confirmation webhook URL to KQ PocketBase without embedding secrets.

## Task 1: Define calendar and booking domain rules

**Files:**
- Create: `Edd-OS/projects/kings-and-queens/app/support-calendar.js`
- Create: `Edd-OS/projects/kings-and-queens/app/support-calendar.test.mjs`
- Create: `Edd-OS/projects/kings-and-queens/backend/pb_hooks/support-utils.js`
- Create: `Edd-OS/projects/kings-and-queens/backend/pb_hooks/support-utils.test.mjs`

- [ ] **Step 1: Write frontend domain tests**

```javascript
// app/support-calendar.test.mjs
import assert from 'node:assert/strict';
import calendar from './support-calendar.js';

const bookings = [
  { slot_id: 's1', status: 'held', hold_expires_at: '2026-06-21T18:00:00Z' },
  { slot_id: 's1', status: 'confirmed' },
  { slot_id: 's1', status: 'cancelled' },
];
assert.equal(calendar.occupiedSeats('s1', bookings, '2026-06-21T17:00:00Z'), 2);
assert.equal(calendar.remainingSeats({ id: 's1', capacity: 2 }, bookings, '2026-06-21T17:00:00Z'), 0);
assert.equal(calendar.overlaps('17:00', '18:00', '18:00', '19:00'), false);
assert.equal(calendar.overlaps('17:00', '18:00', '17:30', '18:30'), true);
assert.deepEqual(calendar.cancellationOutcome('2026-06-23T18:00:00Z', '2026-06-21T17:00:00Z'), { credit: true });
assert.deepEqual(calendar.cancellationOutcome('2026-06-22T12:00:00Z', '2026-06-21T17:00:00Z'), { credit: false });
assert.equal(calendar.copyDate('2026-06-15', '2026-06-22', '2026-06-17'), '2026-06-24');
console.log('support calendar rules ok');
```

- [ ] **Step 2: Write backend domain tests**

```javascript
// backend/pb_hooks/support-utils.test.mjs
import assert from 'node:assert/strict';
import support from './support-utils.js';

assert.equal(support.consumesSeat({ status: 'held', hold_expires_at: '2026-06-21T18:00:00Z' }, '2026-06-21T17:00:00Z'), true);
assert.equal(support.consumesSeat({ status: 'held', hold_expires_at: '2026-06-21T16:00:00Z' }, '2026-06-21T17:00:00Z'), false);
assert.equal(support.consumesSeat({ status: 'payment_review' }, '2026-06-21T17:00:00Z'), true);
assert.equal(support.consumesSeat({ status: 'rejected' }, '2026-06-21T17:00:00Z'), false);
assert.equal(support.holdExpiry('2026-06-21T12:00:00Z'), '2026-06-22T00:00:00.000Z');
assert.equal(support.bookingKey(525, 'slot123'), '525:slot123:hold');
console.log('support backend rules ok');
```

- [ ] **Step 3: Run tests and verify missing modules**

Run:

```bash
cd /home/edd/Proyectos/Edd-OS/projects/kings-and-queens
node app/support-calendar.test.mjs
node backend/pb_hooks/support-utils.test.mjs
```

Expected: both fail with missing modules.

- [ ] **Step 4: Implement the browser helper**

```javascript
// app/support-calendar.js
(function (root, factory) {
  const api = factory();
  if (typeof module === 'object' && module.exports) module.exports = api;
  if (root) root.KQSupportCalendar = api;
})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
  const seatStatuses = new Set(['held', 'payment_review', 'confirmed']);
  function consumesSeat(booking, nowIso) {
    if (!seatStatuses.has(booking.status)) return false;
    return booking.status !== 'held' || !booking.hold_expires_at || booking.hold_expires_at > nowIso;
  }
  function occupiedSeats(slotId, bookings, nowIso) {
    return bookings.filter(b => b.slot_id === slotId && consumesSeat(b, nowIso)).length;
  }
  function remainingSeats(slot, bookings, nowIso) {
    return Math.max(0, Number(slot.capacity || 2) - occupiedSeats(slot.id, bookings, nowIso));
  }
  function overlaps(aStart, aEnd, bStart, bEnd) { return aStart < bEnd && bStart < aEnd; }
  function cancellationOutcome(classIso, nowIso) {
    return { credit: new Date(classIso).getTime() - new Date(nowIso).getTime() > 24 * 60 * 60 * 1000 };
  }
  function copyDate(sourceMonday, targetMonday, sourceDate) {
    const offset = Math.round((new Date(`${sourceDate}T12:00:00Z`) - new Date(`${sourceMonday}T12:00:00Z`)) / 86400000);
    const target = new Date(`${targetMonday}T12:00:00Z`);
    target.setUTCDate(target.getUTCDate() + offset);
    return target.toISOString().slice(0, 10);
  }
  function mergeEvents(classes, slots) {
    return [
      ...classes.map(record => ({ ...record, calendar_kind: 'class' })),
      ...slots.map(record => ({ ...record, calendar_kind: 'support' })),
    ];
  }
  return { consumesSeat, occupiedSeats, remainingSeats, overlaps, cancellationOutcome, copyDate, mergeEvents };
});
```

- [ ] **Step 5: Implement backend helper with the same invariants**

```javascript
// backend/pb_hooks/support-utils.js
module.exports = {
  consumesSeat(booking, nowIso) {
    if (["payment_review", "confirmed"].includes(booking.status)) return true;
    return booking.status === "held" && (!booking.hold_expires_at || booking.hold_expires_at > nowIso);
  },
  holdExpiry(nowIso) {
    return new Date(new Date(nowIso).getTime() + 12 * 60 * 60 * 1000).toISOString();
  },
  bookingKey(conversationId, slotId) {
    return `${conversationId}:${slotId}:hold`;
  },
  overlaps(aStart, aEnd, bStart, bEnd) {
    return aStart < bEnd && bStart < aEnd;
  },
  getsCredit(classIso, nowIso) {
    return new Date(classIso).getTime() - new Date(nowIso).getTime() > 24 * 60 * 60 * 1000;
  },
};
```

- [ ] **Step 6: Run both tests**

Run: `node app/support-calendar.test.mjs && node backend/pb_hooks/support-utils.test.mjs`

Expected: both success messages.

- [ ] **Step 7: Commit**

```bash
git add Edd-OS/projects/kings-and-queens/app/support-calendar* Edd-OS/projects/kings-and-queens/backend/pb_hooks/support-utils*
git commit -m "test(kq): define support booking rules"
```

## Task 2: Create support collections and defaults

**Files:**
- Create: `Edd-OS/projects/kings-and-queens/scripts/setup_support_booking.py`
- Create: `Edd-OS/projects/kings-and-queens/scripts/test_setup_support_booking.py`

- [ ] **Step 1: Write schema tests**

```python
# scripts/test_setup_support_booking.py
import unittest
from setup_support_booking import build_schema, default_settings


class SupportSchemaTests(unittest.TestCase):
    def test_collections_and_statuses(self):
        schema = build_schema()
        self.assertEqual(set(schema), {"support_settings", "support_slots", "support_bookings"})
        statuses = next(f for f in schema["support_bookings"]["fields"] if f["name"] == "status")["values"]
        self.assertEqual(statuses, ["held", "payment_review", "confirmed", "rejected", "cancelled", "expired", "credit"])

    def test_defaults(self):
        self.assertEqual(default_settings()["default_capacity"], 2)
        self.assertEqual(default_settings()["default_deposit"], 5000)
        self.assertEqual(default_settings()["hold_hours"], 12)
        self.assertEqual(default_settings()["cancel_credit_hours"], 24)


if __name__ == "__main__":
    unittest.main()
```

- [ ] **Step 2: Verify failure**

Run: `cd scripts && python3 -m unittest -v test_setup_support_booking.py`

Expected: missing module.

- [ ] **Step 3: Implement schema definitions**

`setup_support_booking.py` must use the environment-driven PocketBase client pattern from `setup_billing.py` and expose:

```python
def default_settings():
    return {
        "default_capacity": 2,
        "default_deposit": 5000,
        "hold_hours": 12,
        "cancel_credit_hours": 24,
        "transfer_alias": "",
        "transfer_instructions": "Transferí la seña y enviá el comprobante por este chat.",
    }


def build_schema():
    teacher = "@request.auth.id != '' && @request.auth.rol = 'teacher'"
    return {
        "support_settings": {"fields": [
            {"name": "default_capacity", "type": "number", "required": True},
            {"name": "default_deposit", "type": "number", "required": True},
            {"name": "hold_hours", "type": "number", "required": True},
            {"name": "cancel_credit_hours", "type": "number", "required": True},
            {"name": "transfer_alias", "type": "text", "required": False},
            {"name": "transfer_instructions", "type": "text", "required": False},
        ], "rules": [teacher, teacher, None, teacher, None]},
        "support_slots": {"fields": [
            {"name": "date", "type": "text", "required": True},
            {"name": "time_start", "type": "text", "required": True},
            {"name": "time_end", "type": "text", "required": True},
            {"name": "status", "type": "select", "required": True, "maxSelect": 1, "values": ["open", "suspended"]},
            {"name": "capacity", "type": "number", "required": True},
            {"name": "deposit_amount", "type": "number", "required": True},
            {"name": "notes", "type": "text", "required": False},
        ], "rules": [teacher, teacher, None, None, None]},
        "support_bookings": {"fields": [
            {"name": "slot_id", "type": "relation", "required": True, "collectionId": "support_slots", "maxSelect": 1},
            {"name": "student_name", "type": "text", "required": True},
            {"name": "contact_phone", "type": "text", "required": True},
            {"name": "guardian_name", "type": "text", "required": False},
            {"name": "education_level", "type": "select", "required": True, "maxSelect": 1, "values": ["primary", "secondary"]},
            {"name": "school_year", "type": "text", "required": True},
            {"name": "topic", "type": "text", "required": True},
            {"name": "conversation_id", "type": "number", "required": True},
            {"name": "status", "type": "select", "required": True, "maxSelect": 1, "values": ["held", "payment_review", "confirmed", "rejected", "cancelled", "expired", "credit"]},
            {"name": "hold_expires_at", "type": "date", "required": False},
            {"name": "deposit_amount", "type": "number", "required": True},
            {"name": "receipt", "type": "file", "required": False, "maxSelect": 1, "maxSize": 5242880, "mimeTypes": ["image/jpeg", "image/png", "application/pdf"]},
            {"name": "idempotency_key", "type": "text", "required": True},
            {"name": "credit_used", "type": "bool", "required": False},
            {"name": "confirmed_at", "type": "date", "required": False},
            {"name": "confirmation_sent_at", "type": "date", "required": False},
        ], "rules": [teacher, teacher, None, None, None], "indexes": [
            "CREATE UNIQUE INDEX idx_support_booking_key ON support_bookings (idempotency_key)"
        ]},
    }
```

The applier must create or extend collections idempotently and seed exactly one `support_settings` record when none exists.

- [ ] **Step 4: Run tests and compile**

Run: `python3 -m unittest -v test_setup_support_booking.py && python3 -m py_compile setup_support_booking.py`

Expected: two tests pass; compile is silent.

- [ ] **Step 5: Commit**

```bash
git add Edd-OS/projects/kings-and-queens/scripts/setup_support_booking.py Edd-OS/projects/kings-and-queens/scripts/test_setup_support_booking.py
git commit -m "feat(kq): define support calendar schema"
```

## Task 3: Implement transactional support APIs

**Files:**
- Modify: `Edd-OS/projects/kings-and-queens/backend/pb_hooks/main.pb.js`
- Create: `Edd-OS/projects/kings-and-queens/backend/verify_support_booking.py`

- [ ] **Step 1: Add a teacher-or-tool authentication helper**

Inside each handler, require `auth-utils.js` and `support-utils.js`. Tool routes must compare `e.request.header.get('X-KQ-Tool-Token')` with `$os.getenv('KQ_TOOL_TOKEN')`; panel routes must require `e.auth.getString('rol') === 'teacher'`. Return `403` without indicating which credential failed.

- [ ] **Step 2: Implement slot search**

`POST /api/kq/support/search` accepts `{preferred_days, preferred_time}` and returns at most three future `open` slots with computed remaining capacity. Exclude slots overlapping any non-cancelled normal `classes` record. Sort by `date,time_start`.

Return:

```json
{"slots":[{"id":"slot-id","date":"2026-06-24","time_start":"17:00","time_end":"18:00","remaining":2,"deposit_amount":5000}]}
```

- [ ] **Step 3: Implement atomic hold creation**

`POST /api/kq/support/hold` accepts the approved student fields plus `slot_id`, `contact_phone`, and `conversation_id`. In `$app.runInTransaction`:

1. Reuse an existing booking with the same idempotency key.
2. Reload the slot.
3. Reject `suspended`, past, overlapping, or full slots.
4. Count only seat-consuming bookings.
5. Create `held` with `hold_expires_at = now + 12 hours` and the slot's deposit.

Return the booking code, expiry, transfer alias/instructions, and deposit. Never hardcode the alias or amount in Sam's prompt.

- [ ] **Step 4: Implement receipt capture**

`POST /api/kq/support/receipt` accepts `{conversation_id,receipt_url}`. Find the newest unexpired `held` booking for that conversation, allow only HTTPS URLs from the configured Chatwoot host, download with `$filesystem.fileFromURL(receipt_url, 15)`, assign the file, and set status `payment_review`. The file field enforces JPEG/PNG/PDF and 5 MB.

- [ ] **Step 5: Implement teacher review**

`POST /api/kq/support/review` accepts `{booking_id,decision}` from an authenticated teacher. In a transaction:

```javascript
if (decision === "approve") {
  booking.set("status", "confirmed");
  booking.set("confirmed_at", new Date().toISOString());
} else {
  booking.set("status", "rejected");
}
txApp.save(booking);
```

On approval, set `confirmation_sent_at` before calling an n8n webhook carrying booking ID, conversation ID, phone, student, date, and time. Send `X-KQ-Tool-Token` with the webhook. This is an at-most-once dispatch: a webhook failure alerts Ale for manual follow-up and is not retried automatically.

- [ ] **Step 6: Implement teacher slot/settings operations**

Add authenticated-teacher routes:

- `POST /api/kq/support/slot` creates or updates a slot after checking date/time validity, capacity not below occupied seats, and overlap against normal classes and support slots.
- `POST /api/kq/support/copy-week` accepts source/target Mondays, previews mapped dates, rejects conflicts, and creates independent records transactionally.
- `POST /api/kq/support/settings` updates only default capacity, default deposit, transfer alias, and transfer instructions.

Return `409` with a human-readable conflict message and do not partially write a copied week.

- [ ] **Step 7: Implement cancellation and reprogramming**

`POST /api/kq/support/cancel` uses booking code plus contact phone. More than 24 hours before the slot sets `credit`; otherwise `cancelled`. `POST /api/kq/support/reschedule` requires a credit booking with `credit_used=false`, reserves the target slot atomically, and marks the credit used. Repeated calls return the same result.

- [ ] **Step 8: Add hold expiry cron**

```javascript
cronAdd("kq_support_hold_expiry", "*/10 * * * *", () => {
  const now = new Date().toISOString();
  try {
    const expired = $app.findRecordsByFilter(
      "support_bookings", "status = 'held' && hold_expires_at < {:now}", "+id", 500, 0, { now }
    );
    expired.forEach(record => { record.set("status", "expired"); $app.save(record); });
  } catch (err) {
    $app.logger().error("support hold expiry failed", "err", String(err));
  }
});
```

- [ ] **Step 9: Write API smoke verification**

`verify_support_booking.py` must call search with `X-KQ-Tool-Token`, assert at most three ordered slots, hold the same test slot twice and assert the same booking ID, and verify an unknown slot returns `404` or `409`. Read token and URL from environment; never print the token.

- [ ] **Step 10: Run tests and commit**

```bash
node backend/pb_hooks/support-utils.test.mjs
python3 -m py_compile backend/verify_support_booking.py
git add Edd-OS/projects/kings-and-queens/backend
git commit -m "feat(kq): add transactional support booking API"
```

## Task 4: Merge support slots into Ale's existing calendar

**Files:**
- Modify: `Edd-OS/projects/kings-and-queens/app/teacher.html:1273-1364,2037-2220`
- Test: `Edd-OS/projects/kings-and-queens/app/support-calendar.test.mjs`

- [ ] **Step 1: Load the helper and support records**

Add `<script src="support-calendar.js"></script>`. In `TeacherApp`, add:

```javascript
const [supportSlots, setSupportSlots] = useState([]);
const [supportBookings, setSupportBookings] = useState([]);
const [supportSettings, setSupportSettings] = useState(null);
```

Extend `loadAll()` with `support_slots`, `support_bookings`, and the first `support_settings` record. Teacher rules permit these reads.

- [ ] **Step 2: Make `CalendarView` accept both event types**

Replace `classesForDay` with:

```javascript
const calendarEvents = useMemo(
  () => KQSupportCalendar.mergeEvents(classes, supportSlots),
  [classes, supportSlots]
);
function eventsForDay(iso) {
  return calendarEvents.filter(event => event.date === iso)
    .sort((a, b) => (a.time_start || '').localeCompare(b.time_start || ''));
}
```

Normal classes retain their current pills. Support slots use a teal pill labeled `Apoyo · ocupados/capacidad`; full is derived, not stored.

- [ ] **Step 3: Add month/year controls without a separate calendar**

Add `viewMode` (`month` or `year`). The year view renders 12 compact month grids from the same `calendarEvents`; clicking a month opens the existing month view. Keep the current `Clases` tab and rename its heading to `Calendario`. Do not add an “Apoyo escolar” calendar tab.

- [ ] **Step 4: Add the unified create menu**

Clicking a date opens two actions: `Nueva clase` and `Horario de apoyo`. `Nueva clase` keeps the current `ClassModal`; `Horario de apoyo` opens `SupportSlotModal` with defaults from `supportSettings`.

- [ ] **Step 5: Add copy-week behavior**

The calendar action `Copiar semana` selects source Monday and target Monday, previews generated dates via `KQSupportCalendar.copyDate`, rejects conflicts against normal classes and support slots, then creates independent support slot records through a teacher backend route.

- [ ] **Step 6: Run domain and source checks**

Run:

```bash
node app/support-calendar.test.mjs
grep -n "mergeEvents\|viewMode\|Copiar semana\|Horario de apoyo" app/teacher.html
```

Expected: tests pass and all unified-calendar hooks are present.

- [ ] **Step 7: Commit**

```bash
git add Edd-OS/projects/kings-and-queens/app/teacher.html Edd-OS/projects/kings-and-queens/app/support-calendar.js Edd-OS/projects/kings-and-queens/app/support-calendar.test.mjs
git commit -m "feat(kq): unify classes and support calendar"
```

## Task 5: Add slot editing and receipt review to the calendar

**Files:**
- Modify: `Edd-OS/projects/kings-and-queens/app/teacher.html`

- [ ] **Step 1: Implement `SupportSlotModal`**

Fields: date, fixed start/end, capacity default 2, deposit default 5000, status open/suspended, internal notes. Save through the teacher backend route so class conflicts are validated server-side. Show current occupancy and prevent reducing capacity below occupied seats.

- [ ] **Step 2: Implement the slot detail panel**

On a support pill, show confirmed, payment-review, active hold, and expired bookings. Each row includes student, responsible, primary/secondary, grade/year, topic, phone, state, and hold expiry.

- [ ] **Step 3: Implement receipt review**

For `payment_review`, show the authenticated PocketBase file URL and buttons:

```javascript
async function reviewBooking(bookingId, decision) {
  const response = await fetch(`${PB_URL}/api/kq/support/review`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', Authorization: pb.authStore.token },
    body: JSON.stringify({ booking_id: bookingId, decision }),
  });
  const data = await response.json();
  if (!response.ok) throw new Error(data.message || 'No se pudo revisar la seña');
  setSupportBookings(items => items.map(item => item.id === data.booking.id ? data.booking : item));
}
```

Approval changes to confirmed and triggers Sam. Rejection releases the seat and sends no automatic customer message.

- [ ] **Step 4: Implement support settings editor**

In the same calendar screen, add a compact `Configuración` panel for capacity default, deposit default, hold hours fixed at 12, cancellation threshold fixed at 24, transfer alias, and transfer instructions. Only capacity, deposit, alias, and instructions are editable in this scope.

- [ ] **Step 5: Verify UI source and regression tests**

Run:

```bash
node app/billing.test.mjs
node app/support-calendar.test.mjs
grep -n "SupportSlotModal\|reviewBooking\|transfer_alias\|payment_review" app/teacher.html
```

Expected: tests pass and all panel contracts exist.

- [ ] **Step 6: Commit**

```bash
git add Edd-OS/projects/kings-and-queens/app/teacher.html
git commit -m "feat(kq): add support slot and receipt management"
```

## Task 6: Add Sam's support-school tools without recreating the live agent

**Files:**
- Create: `Edd-OS/projects/kings-and-queens/whatsapp-agent/business-context/kq-support-school.md`
- Modify: `Edd-OS/projects/kings-and-queens/whatsapp-agent/system-prompt.md`
- Create: `Edd-OS/projects/kings-and-queens/whatsapp-agent/workflows/deploy_support_workflows.py`
- Create: `Edd-OS/projects/kings-and-queens/whatsapp-agent/workflows/test_deploy_support_workflows.py`
- Modify: `Edd-OS/server/docker-compose.yml` n8n environment

- [ ] **Step 1: Write workflow-builder tests**

```python
# whatsapp-agent/workflows/test_deploy_support_workflows.py
import unittest
from deploy_support_workflows import CONFIRMATION_WORKFLOW_NAME, TOOL_SPECS, build_tool_workflow


class WorkflowBuilderTests(unittest.TestCase):
    def test_all_approved_tools_exist(self):
        self.assertEqual(set(TOOL_SPECS), {
            "search_support_slots", "hold_support_slot", "attach_support_receipt",
            "cancel_support_booking", "reschedule_support_booking",
        })

    def test_tool_uses_server_token_expression(self):
        wf = build_tool_workflow("search_support_slots", "https://db.kingsandqueens.com.ar")
        request = next(node for node in wf["nodes"] if node["type"] == "n8n-nodes-base.httpRequest")
        rendered = str(request["parameters"])
        self.assertIn("KQ_TOOL_TOKEN", rendered)
        self.assertNotIn("edd_secret", rendered)

    def test_confirmation_is_webhook_not_ai_tool(self):
        self.assertEqual(CONFIRMATION_WORKFLOW_NAME, "kq-support-confirmation")


if __name__ == "__main__":
    unittest.main()
```

- [ ] **Step 2: Write the approved business context**

`kq-support-school.md` must state: English only; primary/secondary; required student fields; three-slot maximum; fixed slots; capacity from tool; 12-hour hold; transfer deposit returned by tool; receipt required; Ale approves; confirmation only; >24-hour one-time credit; <24-hour forfeiture; no reminders; no automatic suspension workflow.

- [ ] **Step 3: Update Sam's prompt with a narrow price exception**

Add:

```markdown
## Apoyo escolar de inglés
- Usá herramientas para horarios, cupos, seña y reservas. Nunca inventes disponibilidad.
- Podés informar únicamente el importe de seña devuelto por hold_support_slot.
- Para otros cursos sigue prohibido dar precios sin derivar a Ale.
- Pedí nombre, primario/secundario, grado o año, tema y preferencia horaria.
- Mostrá como máximo las 3 opciones devueltas por search_support_slots.
- No confirmes pago: el comprobante queda en revisión de Ale.
```

- [ ] **Step 4: Implement idempotent tool workflow generation**

`deploy_support_workflows.py` must define each tool name, endpoint, fields, and description. `build_tool_workflow()` creates an Execute Workflow Trigger connected to an HTTP Request that posts JSON to PocketBase with:

```python
"headerParameters": {"parameters": [
    {"name": "X-KQ-Tool-Token", "value": "={{ $env.KQ_TOOL_TOKEN }}"},
    {"name": "Content-Type", "value": "application/json"},
]}
```

Use the n8n REST API to upsert by exact name. Fetch the current `kq-chatwoot-agent` instead of recreating it. Add five `toolWorkflow` nodes, with connections:

```python
connections[tool_name] = {
    "ai_tool": [[{"node": "Sam - AI Agent", "type": "ai_tool", "index": tool_index}]]
}
```

Compute `tool_index` from the highest existing `ai_tool` index and append sequentially; never renumber existing tools. Preserve all existing nodes, credentials, IDs, webhook paths, memory, and active state. Append the approved prompt block once using markers `## KQ_SUPPORT_START` and `## KQ_SUPPORT_END`.

- [ ] **Step 5: Build the non-tool confirmation workflow**

Create/update `kq-support-confirmation` with webhook path `kq-support-confirmed`. Its first Code node must compare the incoming `x-kq-tool-token` header against `$env.KQ_TOOL_TOKEN` and reject mismatches. It then posts this message into the existing Chatwoot conversation so delivery uses Sam's inbox/number:

```javascript
const message = `✅ Seña confirmada para ${$json.student_name}.\n📅 ${$json.date} de ${$json.time_start} a ${$json.time_end}.\nTu lugar quedó reservado.`;
return [{ json: { ...$json, message } }];
```

The HTTP Request posts to `http://chatwoot-web:3000/api/v1/accounts/4/conversations/{conversation_id}/messages` with `api_access_token={{$env.CHATWOOT_ADMIN_API_TOKEN}}`. This workflow is not attached to the AI Agent and sends no reminders.

- [ ] **Step 6: Wire runtime variables**

In the `n8n` service environment ensure:

```yaml
      - KQ_TOOL_TOKEN=${KQ_TOOL_TOKEN}
      - CHATWOOT_ADMIN_API_TOKEN=${CHATWOOT_ADMIN_API_TOKEN}
```

In `kq-pocketbase` add:

```yaml
      - KQ_CONFIRMATION_WEBHOOK_URL=http://eddos-n8n:5678/webhook/kq-support-confirmed
```

Do not embed either secret in workflow JSON or Python source.

- [ ] **Step 7: Run tests**

Run:

```bash
cd /home/edd/Proyectos/Edd-OS/projects/kings-and-queens/whatsapp-agent/workflows
python3 -m unittest -v test_deploy_support_workflows.py
python3 -m py_compile deploy_support_workflows.py
```

Expected: three tests pass and compile is silent.

- [ ] **Step 8: Commit**

```bash
git add Edd-OS/projects/kings-and-queens/whatsapp-agent Edd-OS/server/docker-compose.yml
git commit -m "feat(kq): add Sam support booking tools"
```

## Task 7: Add integration and concurrency verification

**Files:**
- Modify: `Edd-OS/projects/kings-and-queens/backend/verify_support_booking.py`
- Create: `Edd-OS/projects/kings-and-queens/backend/test_support_concurrency.py`

- [ ] **Step 1: Write the concurrent last-seat test**

Create a temporary slot with capacity 1 using teacher credentials, then use `concurrent.futures.ThreadPoolExecutor(max_workers=2)` to post two different conversations to `/api/kq/support/hold`. Assert one response is `200` and the other `409`, and query the collection to assert exactly one seat-consuming booking.

```python
with ThreadPoolExecutor(max_workers=2) as pool:
    results = list(pool.map(lambda cid: hold(slot_id, cid), [900001, 900002]))
assert sorted(status for status, _ in results) == [200, 409], results
```

- [ ] **Step 2: Add expiry and review tests**

Create an expired `held` fixture and invoke the expiry logic through a test-only teacher route enabled only when `KQ_TEST_MODE=true`; assert status `expired`. Create `payment_review`, advance beyond 12 hours, and assert it still consumes a seat. Approve it and assert one confirmation webhook event.

- [ ] **Step 3: Add cancellation tests**

Create one booking 25 hours ahead and one 23 hours ahead. Cancel both through the tool route. Assert `credit`/`credit_used=false` for the first and `cancelled` for the second. Reprogram the credit once and assert a second attempt returns `409`.

- [ ] **Step 4: Run all automated checks**

Run:

```bash
cd /home/edd/Proyectos/Edd-OS/projects/kings-and-queens
node app/billing.test.mjs
node app/auth-family.test.mjs
node app/support-calendar.test.mjs
node backend/pb_hooks/auth-utils.test.mjs
node backend/pb_hooks/support-utils.test.mjs
(cd scripts && python3 -m unittest -v test_setup_auth_family.py test_setup_support_booking.py)
python3 backend/test_support_concurrency.py
```

Expected: every test passes; concurrency result is one `200`, one `409`.

- [ ] **Step 5: Commit**

```bash
git add Edd-OS/projects/kings-and-queens/backend
git commit -m "test(kq): cover support booking integration"
```

## Task 8: Deploy and verify the complete WhatsApp booking flow

**Files:**
- Deploy tracked files from Tasks 1-7
- Modify ignored runtime state only through approved scripts/API

- [ ] **Step 1: Back up live sources of truth**

Run:

```bash
cd /home/edd/Proyectos/Edd-OS/server
mkdir -p backups/manual-kq-support
docker cp eddos-kq-pocketbase:/pb_data/data.db backups/manual-kq-support/data-before-support.db
docker exec n8n-db pg_dump -U n8n_user -d n8n -t workflow_entity -t workflow_history > backups/manual-kq-support/n8n-workflows-before-support.sql
```

Expected: both backups exist and are non-empty.

- [ ] **Step 2: Apply schema and recreate required services**

Run:

```bash
cd /home/edd/Proyectos/Edd-OS/server
set -a
. ./.env
set +a
cd ../projects/kings-and-queens
KQ_PB_URL=http://127.0.0.1:8091 python3 scripts/setup_support_booking.py
cd ../../../server
docker compose up -d --force-recreate kq-pocketbase n8n
docker compose ps kq-pocketbase n8n
```

Expected: schema reports created/current; both services running.

- [ ] **Step 3: Deploy Sam tools against the current live workflow**

Run:

```bash
cd /home/edd/Proyectos/Edd-OS/projects/kings-and-queens/whatsapp-agent/workflows
set -a
. /home/edd/Proyectos/Edd-OS/server/.env
set +a
N8N_BASE_URL=http://127.0.0.1:5678 python3 deploy_support_workflows.py
```

Expected: five tool workflows upserted; `kq-chatwoot-agent` patched once and remains active.

- [ ] **Step 4: Verify live n8n structure**

Query `workflow_entity` and assert:

- `kq-chatwoot-agent` is active;
- five tool nodes are connected to `Sam - AI Agent` as `ai_tool`;
- the support prompt marker occurs once;
- no existing Chatwoot webhook, memory, audio, image, escalation, or credential reference was removed.

- [ ] **Step 5: Verify calendar behavior in browser**

Open the production teacher account and verify:

1. Existing classes are unchanged.
2. Month and year views show the same unified events.
3. Create two support slots with default capacity 2 and deposit ARS 5.000.
4. Edit one slot to a different capacity/deposit.
5. Copy a week and edit one copied date without changing the source.
6. Confirm overlapping normal/support events are rejected.

- [ ] **Step 6: Run a full WhatsApp booking with test data**

From `KQ_TEST_PHONE`, ask Sam for primary-school English support, provide grade/year, topic, and preferred time. Verify Sam shows at most three live slots. Choose one, verify the 12-hour hold and transfer instructions, send a JPEG receipt, and confirm the booking appears as `payment_review` in Ale's calendar.

- [ ] **Step 7: Approve the receipt and verify the only automatic message**

Approve in the app. Verify:

- status becomes `confirmed`;
- occupied seats increment once;
- one confirmation arrives from Sam's number;
- no reminder is scheduled;
- replaying the webhook sends no duplicate.

- [ ] **Step 8: Final health and regression checks**

Run:

```bash
curl -fsS http://127.0.0.1:8091/api/health
curl -fsS http://127.0.0.1:5678/healthz
curl -fsSI -H 'Host: www.kingsandqueens.com.ar' http://127.0.0.1:8080/teacher.html
docker logs --since 15m eddos-kq-pocketbase 2>&1 | grep -Ei 'error|panic|syntax' || true
docker logs --since 15m eddos-n8n 2>&1 | grep -Ei 'error|panic' || true
```

Expected: health endpoints succeed, Nginx returns `200`, and no new relevant errors appear.

- [ ] **Step 9: Preserve evidence and commit only tracked changes**

Record test output and production verification in the implementation handoff. Never commit `.env`, database copies, receipts, customer phone numbers, or workflow dumps containing credentials.
