Qontak | Chatbot | AI Agent — Native Integrations — Phase 1: Google Calendar — In-Conversation Booking
Google Calendar provider — Phase 1 PRD under the Native Integration ANCHOR.
Google Calendar is an OAuth provider of the Native Integration umbrella. Imported from
Confluence on 2026-06-17.
Code status: Google OAuth2 + Calendar resource provider are shipped (BOT-4278, BOT-4292);
the in-conversation booking action + config drawer are not yet in production chatbot-fe.
NEW PRD — Phase 1 of Native Integrations. Parent ANCHOR: Qontak | Chatbot | AI Agent — Native Integrations — ANCHOR · AI-Readiness: 9.7+ / 10.0 — READY (DRAFT pending PM final review).
First initiative under new AI SDLC workflow — UI generated by AI coding agent via MCP Pixel, no Figma design.
🎨 Designer Review Notes — for Wulan & Rizky
Review owner: Wulan Febyazzahra + Rizky Surur (UI Reviewers + MCP Pixel Prompt Curators)
Review gate: Internal Alpha (S6c) — MCP Pixel output must pass sign-off before Closed Beta
Reference companion page: Wireframes + MCP Pixel Prompts — every screen + Mekari Pixel prompt block
Sections you need to review
| Section | What to review | Why |
|---|
| S11 — New Features (UI) 🎨 | UI Generation Approach (MCP Pixel pattern), Action drawer IA + 4 canonical drawer states, Screen 3 (per-action details form, Pixel MCP-only spec), N2 inline OAuth status row state machine, brand-asset guard | This is the design source of truth — MCP Pixel reads from S11 prose |
| GCAL-S01 🎨 — Connect AI Agent to Google Calendar via OAuth | UI Reference, all UI States (Empty / Loading / Error / Success), Permission Model | Frontend story — admin-facing OAuth flow surfaced via N2 inline OAuth status row inside Screen 3 (Action details form) |
| GCAL-S02 🎨 — Configure a Calendar template | UI Reference for the Action details form (Screen 3, extends E7) — Calendar dropdown field + all template fields with MpFormControl validation, empty/loading/error/success states for the form, attached-action row in the populated Actions tab (Screen 1, extends E3) | Frontend story — largest form surface in P1; one booking template = one attached action row, no separate template list surface |
| GCAL-S05 🎨 — Handle token refresh failure / re-authorize | UI Reference, N2 inline OAuth status row state transitions (connected → connection-lost → re-auth → connected) and the Re-authorize UX inside Screen 3 (Action details form) | Frontend story — error recovery UX |
| GCAL-S06 🎨 — Cascade delete AI Agent + tokens + templates | UI Reference — parent AI Agent delete modal (Screen 5, owned by parent AI Agent PRD) extended with cascadeWarnings prop for Calendar-specific copy | Frontend story — extends existing parent AI Agent delete modal |
Stories you do NOT need to review (backend-only — no UI)
| Story | Why no designer review |
|---|
| GCAL-S03 — Agent looks up available slots | Backend behavior; status reflected via N2 inline OAuth status row inside Screen 3 (reviewed in S05) |
| GCAL-S04 — Agent creates calendar event + sends invite | Backend behavior; success/failure reflected via N2 inline OAuth status row |
| GCAL-S07 — Auto-refresh OAuth access token | Backend behavior; failure cascades to N2 inline OAuth status row inside Screen 3 (reviewed in S05) |
What "review" means in practice (Internal Alpha gate)
- Run MCP Pixel against each prompt in the Wireframes page → get generated Vue 3 code
- Validate output uses correct
Mp* components, v2.4 Default tokens, dark mode renders
- If MCP Pixel output drifts from Mekari Pixel patterns → refine the S11 prompt content + regenerate
- Sign off in S6c Internal Alpha gate before Closed Beta opens
- If quality fails: fall back to manual UI build by Hadiningbot in Phase 1.5 (per S9 Risk #6)
| Field | Value |
|---|
| PM | Dimas Fauzi Hidayat |
| UI Reviewers + MCP Pixel Prompt Curators | Wulan Febyazzahra, Rizky Surur |
| Product Ops | Devina Amalia |
| Tech Lead | Eko Aprianto |
| PRD Version | 1.11 |
| Status | DRAFT |
| PRD Type | NEW |
| Epic | TBC (S9 #5) |
| Squad | Hadiningbot |
| RFC Link | RFC — Google Sheets Workflow Nodes (Eko Aprianto, 2026-05-19, DRAFT) — canonical BE source of truth for credential storage + OAuth refresh + HTTP client + node_registries + lookup resources + feature flag patterns. Google Calendar MUST mirror these patterns. |
| Figma Master | N/A — UI generated via MCP Pixel by AI coding agent. See S11 §UI Generation Approach. |
| Anchor | Yes — parent ANCHOR |
| Labels | epic:qontak-chatbot | module:ai-agent | feature:native-integrations |
| Last Updated | 2026-05-22 |
S1 — One-liner + Problem
One-liner: Enable Qontak chatbot builders to grant their AI Agent direct access to Google Calendar, so the agent can book appointments end-to-end inside customer conversations.
Problem: Today, when a Qontak AI Agent collects a high-intent lead inside a chat — a customer asking to book a test drive, schedule a property viewing, request a consultation, set up a demo, or reserve a service slot — the agent cannot complete the booking inside the conversation. The agent has to hand off to a human who manually opens Google Calendar, finds an open slot, creates the event, and emails the customer back; this handoff loses ~40-60% of hot leads to drop-off (Assumption — needs validation in S9). Without a native Google Calendar action, AI Agent's value caps at "qualify the lead" instead of "close the booking" — and Qontak cannot demonstrate the AI Agent as a true conversion engine across its cross-vertical customer base. Anchor signal: Citroen (automotive) shared this need explicitly; the same conversion-moment pain exists across real estate, healthcare, banking, B2B SaaS, professional services, and F&B.
S2 — Target Users + Personas
| Persona | Role | Goal | Pain | Workaround |
|---|
| B — CS Ops Lead / Bot Manager (Primary) | CS Ops Lead or Bot Manager — industry-agnostic. | Convert high-intent inbound conversations into booked appointments end-to-end inside the chat. | "Qualify but don't close" gap. 5-30 min lag → 40-60% drop-off. | Manual chat-to-calendar handoff OR custom API Integration. |
| C — Mekari Implementation Consultant (Primary) | Internal Mekari consultant delivering paid implementation across customers. | Deliver a working template in onboarding hours. | Every onboarding repeats the same Calendar API Integration setup. | Custom integration per customer; engineering review per implementation. |
| A — Qontak Admin (Non-Tech) (Secondary) | General Qontak admin at SME/mid-market. Frequently non-technical. | "I bought Qontak — I expect my AI Agent to talk to my team's Google Calendar without me reading API docs." | OAuth + Calendar API via API Integration assumes admin can debug 4xx errors. | Most don't attempt; few escalate via support. |
Design constraint inherited from Persona A: No knowledge of OAuth scopes, refresh tokens, or Google API mechanics may be required.
S3 — Non-Goals
| # | Non-Goal | Where deferred |
|---|
| 1 | Other Google Workspace products (Sheets, Drive, Gmail, Docs) | Phase 2+ |
| 2 | Other calendar providers (Outlook/365, Apple, Calendly) | Phase 4+ |
| 3 | Per-rep / round-robin / per-team calendar assignment | Phase 1.5 or P2 |
| 4 | Reschedule + cancel actions by agent | Phase 1.5 |
| 5 | Two-way sync from Google Calendar back to Qontak | Future |
| 6 | Recurring events | Future |
| 7 | Group bookings | Future |
| 8 | Customer-facing embedded calendar widget | Future |
| 9 | Custom WhatsApp/SMS reminders from Qontak | Cross-channel reminder is its own feature |
| 10 | Data residency / Google data location compliance | Customer-owned |
| 11 | AI Agent LLM / runtime behavior changes | Parent AI Agent runtime PRD |
S4 — Constraints
| Constraint | Spec |
|---|
| Platform | Web only — Chrome, Firefox, Edge (latest stable). No mobile app for config. |
| Performance — OAuth consent flow | Redirect to Google + return ≤ 10s p95 |
| Performance — slot lookup | ≤ 1.5s p95 |
| Performance — create event + send invite | ≤ 2s p95 |
| Performance — token refresh (proactive) | At 50 minutes into 60-minute TTL. Background, non-blocking. |
| Performance — token refresh (reactive) | On 401 mid-call, refresh + retry once ≤ 500ms p95 |
| Performance — concurrent refresh safety | Single-flight per agent_id; ≤500ms wait + reuse |
| Data — slot lookup window | Default 7 days; configurable 1-30 days per template |
| Data — max templates per customer | 10 |
| Data — max bookings/agent/day | No soft cap (Google quota: 60 writes/min/user) |
| Data — credential storage | 1 access + refresh token pair per AI Agent. Encrypted at rest. |
| OAuth scopes | https://www.googleapis.com/auth/calendar.events (least privilege) |
| OAuth redirect URI | https://chat.qontak.com/oauth/google/callback (TBD final by Eko) |
| Plan scope | AI Agent feature + Native Integrations add-on SKU (compound) |
| Feature flag | google_calendar_native_integration_enabled | OFF | per-company |
Role Matrix
| Operation | CAN | CANNOT | Unauthorized behavior |
|---|
| Configure Google Calendar action | Admin, Spv | Agent, Read-only, Billing-only | Tab hidden, no 403 |
| Authorize via OAuth (Google) | Customer's Google admin | Anyone without those rights | Governed by Google OAuth |
| Edit template / re-consent | Admin, Spv | Other roles | — |
| Delete AI Agent with attached Calendar | Admin, Spv | Other roles | — |
S4.7 — Data Lifecycle
| Artifact | Retention | Cleanup Trigger | User-Visible Effect |
|---|
OAuth access_token | TTL ~1hr | Auto-refresh via GCAL-S07 | None |
OAuth refresh_token | Lifetime of AI Agent OR until revoked | Customer revokes OR DELETE AI Agent → cascade purge | Banner: "Re-authorize Google Calendar" |
| OAuth state / nonce | 10 min | TTL on Redis | None |
| Per-customer template config | Lifetime of AI Agent | DELETE AI Agent → cascade purge | Templates removed |
| Booking event metadata cache | 24h | Cron | None |
S5 — Rollout
| Stage | Audience | Duration | Window | Goal |
|---|
| Internal Alpha | 2 internal accounts | 1 week | Mid-May 2026 | OAuth + 3 actions + auto-refresh validated. 🎨 MCP Pixel UI review passed by Wulan/Rizky. |
| Closed Beta | Citroen + 2–3 friendly customers TBD | 4 weeks | Late May → end of June 2026 | Booking failure rate ≤ 3%; ≥1 customer per vertical. |
| Open Beta | All Native Integrations add-on purchasers | 3 weeks | July 2026 | Booking failure rate ≤ 2% sustained 2 weeks. |
| GA | Plan + add-on. PMM launch. | Ongoing — Q3 2026 | — | — |
Feature flag: google_calendar_native_integration_enabled (S4). Backward compatible: existing AI Agents unaffected. Migration: none.
S6 — Observability
| # | Event Name | Trigger | Properties |
|---|
| 1 | calendar_template_configured | Admin creates/edits/deletes template | company_id, agent_id, template_id, action, template_count |
| 2 | calendar_oauth_initiated | Admin clicks Connect | company_id, user_id, agent_id, scope_requested |
| 3 | calendar_oauth_completed | Google returns auth + token stored | company_id, agent_id, latency_ms |
| 4 | calendar_oauth_failed | OAuth fails | company_id, agent_id, failure_reason, error_code |
| 5 | calendar_token_refresh_failed | Refresh fails — security signal | company_id, agent_id, failure_reason |
| 5b | calendar_token_refreshed | Refresh succeeds | company_id, agent_id, refresh_type, latency_ms |
| 6 | calendar_slot_lookup | Agent invokes list_slots | company_id, agent_id, template_id, slot_count_returned, status, latency_ms |
| 7 | calendar_booking_created ⭐ | Agent creates event — conversion event | company_id, agent_id, template_id, event_id, attendee_count, time_to_book_sec |
| 8 | calendar_booking_failed | create_event fails | company_id, agent_id, template_id, failure_reason |
| 9 | calendar_invite_sent | Calendar invite delivered | company_id, agent_id, event_id, invite_status |
Dashboard owner: Hadiningbot squad (PM: Dimas; Tech Lead: Eko Aprianto)
Alerts
| Condition | Threshold | Routing |
|---|
calendar_booking_failed rate | 5% in 1hr | PagerDuty — Hadiningbot |
calendar_oauth_failed rate | 10% in 1hr | Slack #hadiningbot-alerts |
calendar_token_refresh_failed per company | 5% per company in 24hr | Slack #hadiningbot-alerts |
| Slot lookup p95 latency | 1.5s sustained 30min | Slack #hadiningbot-alerts |
| Booking p95 latency | 2s sustained 30min | Slack #hadiningbot-alerts |
S6.5 — Monitoring Cadence
Weekly review for first 4 weeks post-GA, then monthly. Owner: Dimas + Eko. Focus: booking funnel (6 → 7), failure rate, token refresh per company, time-to-book.
Google Calendar BE infrastructure follows the canonical pattern already used by mekari_qontak_crm. This subsection lists the new + modified files and the deliverables owned by the BE team. The pattern itself is defined in the RFC — this PRD does NOT duplicate the pattern content, only declares which files mirror it.
New files (Phase 1):
| File | Mirrors | Purpose |
|---|
app/core/repositories/node_executions/nodes/google_calendar/execute.rb | Repositories::NodeExecutions::Nodes::MekariQontakCrm::Execute | Executor for the Google Calendar booking action. Uses process_path for path interpolation. |
app/core/repositories/node_resources/google_calendar/calendar_http_client.rb | MekariQontakCrm::CrmHttpClient (#request_with_auth) | HTTP client wrapping every Google Calendar API call. Implements refresh-on-401 via https://oauth2.googleapis.com/token. |
app/core/repositories/node_resources/google_calendar/lookup_resources.rb | Repositories::NodeResources::MekariQontakCrm::LookupResources | Exposes resource_key='calendar' — dropdown source for the Calendar field in Screen 3 Action details form. |
db/seeds/google_calendar_nodes.rb | RFC §6 Sheets seed examples | Idempotent seed for one node_registries row (Calendar booking action) with full properties JSON. |
| Specs (rspec) | mekari_qontak_crm specs | Cover: executor happy path, lookup happy path, refresh-on-401, seed idempotency. |
Modified files (Phase 1):
| File | Modification |
|---|
app/core/repositories/node_executions/node_type_registry.rb | Register 'google_calendar' => 'GoogleCalendar::Execute' |
app/api/frontend_service/v1/node_resources/use_cases/lookup_resources.rb | Add GoogleCalendar to the PROVIDER_MODULES constant |
Deliverable owned by BE team (follow-up RFC): The exact node_registries.properties JSON for the Calendar booking action is a follow-up RFC draft owned by Eko + PM, mirroring RFC §6 Sheets format. The PRD records that the schema follows the canonical pattern — exact field-by-field JSON does NOT need to be published inline in this PRD. The 8 form fields (calendar_id, duration_minutes, lookup_window_days, event_title_pattern, location, event_description, attendees_customer_email_field, attendees_always_cc) are specified in Wireframes v4.2 Screen 3 and are the source of truth for the properties schema.
S6b — Success Metrics
| Category | Metric | Definition | Baseline | Target |
|---|
| Conversion ⭐ | Booking conversion rate inside chat | (booking_created / triggered conversations) × 100 | 40-60% via handoff (Assumption) | ≥65% within 60d of GA |
| Conversion | Time-to-book | p50 from first slot_lookup → event_id | Several minutes to hours | ≤30 seconds median |
| Adoption | % customers configuring ≥1 template within 60d | (customers w/ template_configured / add-on customers) × 100 | 0% | ≥50% within 60d |
| Adoption | Avg bookings per active customer per week | Mean per company_id | N/A | ≥20/week within 90d |
| Quality | Booking success rate | created / (created + failed) × 100 | N/A | ≥98% within 60d |
| Quality | OAuth completion rate | oauth_completed / oauth_initiated × 100 | N/A | ≥85% within 60d |
| Efficiency | Implementation hours saved per onboarding | Self-reported, n=10 | TBD (S9 #4) | ≥6 hours saved |
S6c — Stage Gates
| Stage | Audience | Duration | Success Gate | Owner |
|---|
| Internal Alpha | 2 internal accounts | 1 week | 0 P0/P1 · End-to-end OAuth + 3 actions + auto-refresh by all 3 personas · booking_failed rate = 0% (n≥10) · All 10 S6 events firing · 🎨 MCP Pixel UI review passed by Wulan/Rizky | PM + QA + Eko + Wulan/Rizky |
| Closed Beta | Citroen + 2-3 friendlies | 4 weeks | Booking success ≥ 95% · ≥1 customer per vertical · OAuth completion ≥ 75% · No P0/P1 | PM + CSM |
| Open Beta | All add-on purchasers | 3 weeks | booking_failed ≤ 2% sustained · Conversion ≥ 55% sustained · ≥10 customers configured · Time-to-book ≤ 30 sec | Eko + PM |
| GA | All eligible | Ongoing — Q3 2026 | All Open Beta gates sustained 2 wks · PMM launch approved · Implementation team trained | PM + PMM (Yosephine Dhisaclara) + Implementation Lead |
S7 — Dependencies
| # | Dependency | Owning Team | Deliverable | Blocking? |
|---|
| 1 | Google Cloud Console OAuth client | Eko / Platform Eng | New OAuth 2.0 client ID with calendar.events scope | YES |
| 2 | Google Sensitive Scope verification | Eko + Google review | Approved before Open Beta. Test mode (100-user) for Closed Beta. | YES (Open Beta+); NO (earlier) |
| 3 | Encrypted token storage | Hadiningbot | Secret store wired into AI Agent record | YES |
| 4 | Qontak AI Agent config UI extension | Hadiningbot | Config page extended | YES |
| 5 | Native Integrations add-on SKU | Devina + Sales Ops | SKU + entitlement API. ⚠️ Not Started (Risk #12) | YES (Open Beta); NO (Internal Alpha) |
| 6 | Feature Flag service | Platform Eng | google_calendar_native_integration_enabled registered | NO |
| 7 | Qontak RBAC | Platform Eng | Role identification in session | NO |
| 8 | Implementation team training | Implementation Lead + Devina | Internal docs + per-vertical templates | YES (GA only) |
Graceful degradation: Runtime fallback is owned by parent AI Agent runtime layer. This PRD emits calendar_booking_failed with failure_reason so upstream can detect and fallback.
S8 — Decisions + Alternatives Rejected
S8a — Decisions Made
| Date | Decision | Rationale |
|---|
| 2026-05-06 | Phase 1 = Google Calendar (not Sheets) | Stronger demo + OAuth reusable for Phase 2 Sheets |
| 2026-05-06 | OAuth scope = calendar.events | Least privilege; smaller Google verification scope |
| 2026-05-06 | Single shared calendar per company in P1 | Per-rep deferred |
| 2026-05-06 | 3 actions: list_slots + create_event + send_invite | Tight closed loop |
| 2026-05-06 | Per-customer template config, max 10 | Supports multi-vertical |
| 2026-05-06 | Slot window default 7 days, configurable 1-30 | Industry-agnostic |
| 2026-05-06 | No soft cap on bookings/agent/day | Aligned with user SOP |
| 2026-05-06 | Compound entitlement | Matches Mekari Action |
| 2026-05-06 | Single Native Integrations SKU covers all providers | Simpler upsell |
| 2026-05-06 | Web only — no mobile config | All personas configure on web |
| 2026-05-06 | One-way write only | Webhook listener = separate architecture |
| 2026-05-06 | Industry-agnostic framing | Qontak's ICP is cross-industry |
| 2026-05-06 | Standard rollout naming | Matches Mekari Action |
| 2026-05-06 | Primary KPI = 65% conversion | Realistic improvement target |
| 2026-05-07 | OAuth token refresh = proactive + reactive + single-flight | Daily-use feature cannot tolerate hourly silent failures |
| 2026-05-07 | UI generated via MCP Pixel, no Figma | First AI SDLC initiative |
| 2026-05-22 | BE infrastructure mirrors mekari_qontak_crm per RFC Google Sheets Workflow Nodes. Credential storage canonical = organization_connections table (code='google_calendar', auth_type='oauth2', auth_data = lockbox-encrypted JSON of access/refresh/expires_at). Refresh-on-401 canonical = MekariQontakCrm::CrmHttpClient#request_with_auth pattern with token endpoint https://oauth2.googleapis.com/token. Single credential per org per provider for v1. Credential resolved by HTTP client using org_id + provider code, NOT a user-facing property. Calendar dropdown lookup follows resource_types pattern with provider='google_calendar', resource_key='calendar'. | Reusing an existing, proven BE pattern (already used by mekari_qontak_crm) reduces implementation risk and standardizes the credential layer across all native integrations (Calendar, Sheets, Salesforce, Zoho). |
S8b — Alternatives Rejected
| Date | Alternative | Why Rejected |
|---|
| 2026-05-06 | Phase 1 = Google Sheets | Booking is stronger demo |
| 2026-05-06 | Calendar.full OAuth scope | Over-broad |
| 2026-05-06 | Per-rep/round-robin in P1 | ~3x effort |
| 2026-05-06 | Reschedule + cancel in P1 | Phase 1.5 |
| 2026-05-06 | Two-way sync | Infrastructure not in scope |
| 2026-05-06 | Recurring events | Not worth P1 |
| 2026-05-06 | Customer-facing embedded calendar widget | Chat-native faster |
| 2026-05-06 | Custom WhatsApp/SMS reminders | Cross-channel = own feature |
| 2026-05-06 | Single-template-per-customer | Real estate needs multi-template |
| 2026-05-06 | "Test Drive Booking" naming | Too automotive-specific |
| 2026-05-06 | Per-provider SKU | Fragments pricing |
| 2026-05-06 | Free for any AI Agent customer | Leaves demand on table |
| 2026-05-06 | 75% conversion target | 65% is realistic |
| 2026-05-06 | Cross-product upsell metric in S6b | Track informally |
| 2026-05-07 | Reactive-only token refresh | 200-500ms latency spike every 60min |
| 2026-05-07 | Manual Figma design handoff | First AI SDLC initiative |
S9 — Open Questions / Risks
| # | Type | Question / Risk | Owner | Deadline | Mitigation |
|---|
| 1 | Open Q | Final OAuth redirect URI | Eko | Before Internal Alpha | — |
| 2 | Open Q | Google Cloud Console project — new or extend? | Eko + Platform | Before Internal Alpha | — |
| 3 | Open Q | Closed Beta customer selection | Dimas + CSM | End of Internal Alpha | — |
| 4 | Open Q | Implementation Consultant baseline hours | Devina + Implementation Lead | 2026-06-15 | — |
| 5 | Open Q | Epic key | Dimas | Before Internal Alpha | — |
| 6 | Risk 🎨 | MCP Pixel output quality not yet validated | Wulan/Rizky + Eko | End of Internal Alpha | Internal Alpha UI review gate. Fallback to manual UI build in Phase 1.5. |
| 7 | Assumption | Baseline 40-60% drop-off | Dimas + Citroen | End of Closed Beta | — |
| 8 | Risk | Google Sensitive Scope verification 2-6 weeks | Eko + Platform | Before Open Beta | Test mode covers Closed Beta. Submit by 2026-05-15. |
| 9 | Risk | Customer's Google admin can revoke OAuth | Eko | At GA | S6 #5 detects; banner Re-authorize; graceful agent fallback. |
| 10 | Risk | Google Calendar API quota (60 writes/min/user) | Eko | At GA | Google's quota-increase form; token bucket per customer. |
| 11 | Risk | Single shared calendar limit | PM | Before Closed Beta | Customer docs; accelerate Phase 1.5 if 2+ customers push. |
| 12 | Risk | Native Integrations add-on SKU not started | Devina + Sales Ops | 2026-06-15 | Internal Alpha + Closed Beta hardcode bypass. SKU before Open Beta. |
| 13 | Open Q | Frontend confirms select + slider + textarea HTML elements exist in the design system for the Calendar booking form per node_registries.properties schema (mirrors RFC §10 question for Sheets). | Wulan + Rizky | Before Internal Alpha BE seed merge | — |
| 14 | Open Q | OAuth scope userinfo.email may be needed in addition to calendar.events to render the connected Google email in the N2 inline OAuth status row's connected state. PM/BE to confirm. | Dimas + Eko | Before Internal Alpha | — |
| 15 | Open Q | Single credential per org per provider for v1 (no per-team or per-AI-agent credential isolation in Phase 1). Mirrors RFC §3 Out-of-scope for Sheets. PM confirmed in Native Integrations ANCHOR S8a 2026-04-27 ("1 credential per AI Agent"). Flag: the ANCHOR's "per AI Agent" framing and the RFC's "per org per provider" framing are not identical — needs follow-up alignment between PM + Eko + ANCHOR owner before Internal Alpha to confirm v1 ships with the org-level credential and the ANCHOR's "per AI Agent" framing is treated as a future-phase enhancement (or vice versa). | Dimas + Eko | Before Internal Alpha | — |
🎨 S11 — UI Specification — DESIGNER REVIEW REQUIRED
🎨 Wulan + Rizky: This entire section is your review surface. Canonical source of truth: Wireframes + MCP Pixel Prompts v4.2 (Confluence rev 6, Figma 2026-05-22). All IA, drawer states, and per-screen MCP Pixel prompt blocks live there. This section summarises the IA contract and the verification tracker.
v1.8 — IA correction (2026-05-22) [carried forward through v1.10 — canonical source is Wireframes v4.2]. Aligns with Native Integrations ANCHOR v1.2 and Mekari Action ANCHOR v1.3 S8a decisions of 2026-05-22. Cancels the v1.6/v1.7 invention of a new "Native integrations" sibling group. Native rows are additional rows inside the existing Other integration group.
§UI Approach — Existing Figma Reuse, native row under Other integration
Per PM decision (a): one booking template = one attached action. No separate Google Calendar config surface. No multi-template management screen. No TemplateEditorModal. No standalone CalendarStatusBanner. Calendar concepts (calendar picker, duration, lookup window, attendees) collapse into the single existing per-action details form schema that every other Mekari Qontak Bot action uses — generated by MCP Pixel from a prose prompt, no Figma node required.
| Field | Value |
|---|
| Feature | Google Calendar action — In-conversation booking |
| URL | /chatbot/ai-agent/{agent_id}/config |
| Access | Admin, Supervisor (per S4) |
| Design source | Existing Figma reuse for drawer (E2–E7) + per-action details form via MCP Pixel (Pixel MCP-only spec, no Figma node) |
| Canonical wireframes | Wireframes v4.2 — Figma 2026-05-22 |
§Action drawer — canonical IA (Figma 2026-05-22)
The right-side single-column Action drawer contains 5 groups, in this fixed order:
| # | Group | Phase 1 contribution |
|---|
| 1 | Mekari Qontak | unchanged |
| 2 | Mekari Talenta | unchanged |
| 3 | Mekari Jurnal | unchanged |
| 4 | Mekari Desty | unchanged |
| 5 | Other integration | Phase 1 adds exactly one row: Google Calendar. Future phases add Sheets, Salesforce, Zoho as additional rows in this same group — never as new groups. |
Hard NOs (do not regress to these — flagged here because v1.6/v1.7 of this PRD got them wrong):
- ❌ Do NOT add a new top-level group called "Native integrations" between Mekari Desty and Other integration.
- ❌ Do NOT nest native rows under a "Mekari Action" namespace.
- ❌ Do NOT introduce a two-column drawer with an internal preview panel (logo + name + description +
[Configure] CTA + "Coming soon" rail). The drawer is single-column, right-side.
- ❌ Do NOT use
MpIcon name="calendar" or any placeholder component name in MCP Pixel prompts (code-gen treats placeholders as instructions).
§Drawer states — 4 canonical P0 states (from Figma 2026-05-22)
| State | Description | Source |
|---|
| Collapse / expand (default) | Groups render in fixed order. Each group header is collapsible. Other integration shows the Google Calendar row in Phase 1. | Figma 2026-05-22 |
| Scrolled | Long-list scroll behavior inside the single-column drawer; group headers can sticky per Mekari Pixel pattern. | Figma 2026-05-22 |
| Search found | Flat list across all 5 groups — group headers suppressed. Each result row carries its brand icon. | Figma 2026-05-22 |
| Search not found | Centered message: "{query}" not found / Recheck the keywords you have typed and try searching again. | Figma 2026-05-22 |
Runtime-only states (P1, V8 pending — NOT in canonical Figma)
| State | Status | Notes |
|---|
| Loading | P1 — V8 pending | Skeleton rows while drawer fetches groups + rows. Not in canonical Figma; pending design pass before Open Beta. |
| Failed-to-load | P1 — V8 pending | Drawer-level error + Retry. Not in canonical Figma; pending design pass before Open Beta. |
§Screens — what is Figma-canonical vs MCP Pixel-only
| Screen | Source | Notes |
|---|
| Screen 0 — Actions tab empty/populated | Existing Figma (E2/E3) | Reused as-is. |
| Screen 1 — Action drawer (4 states above) | Canonical Figma 2026-05-22 (V1 ✅ RESOLVED) | Single-column right-side drawer. Wireframes v4.2 carries the prompt block. |
| Screen 2 — Search found / Search not found | Canonical Figma 2026-05-22 | See drawer states. |
Screen 3 — Per-action details form (the screen rendered after clicking Google Calendar in the drawer) | Pixel MCP-only spec — no Figma node required. | The MCP Pixel prompt IS the spec. This is the form surface where the admin completes the calendar template (calendar picker, duration, lookup window, event title pattern, attendees mapping, OAuth status row) — all generated from prose. V2 dropped for this screen. |
| Screen 4 — OAuth modal / connect overlay | Pixel MCP-only spec | Prompt block in Wireframes v4.2. |
| Screen 5 — Connection-Lost banner + Re-authorize | Pixel MCP-only spec | Prompt block in Wireframes v4.2. |
| Screen 6 — Cascade delete confirmation modal | Existing modal + Calendar-specific copy | Prompt block in Wireframes v4.2. |
§Brand asset guard (HARD RULE — do not violate)
Every brand SVG slot in any MCP Pixel prompt — including the Google Calendar row in the drawer, the brand glyph in Screen 4 OAuth modal, and any other native row added by future phases — must be authored as:
[BRAND_SVG_PENDING_V4 — DO NOT CODE-GEN WITHOUT REPLACING]
Code-gen of any prompt containing a BRAND_SVG_PENDING_V4 marker is forbidden until V4 is closed. Never substitute placeholder component names such as MpIcon name="calendar", MpIcon name="google", <icon>, or similar — code-gen treats placeholders as literal instructions and silently ships the wrong asset. Wulan + Rizky own V4 closure (Google Calendar brand SVG path + hex colors). On V4 closure, replace every [BRAND_SVG_PENDING_V4 …] marker with the actual inlined SVG, then code-gen can proceed.
| State | Visual | Action |
|---|
| not-connected | Brand glyph ([BRAND_SVG_PENDING_V4 — DO NOT CODE-GEN WITHOUT REPLACING]) + body "Connect to Google Calendar to activate this action" | Primary [Connect Google Calendar] button right-aligned |
| connected | Brand glyph + MpAvatar (user's Google photo if avail) + email + success check | Tertiary [Re-authorize] + [Disconnect] right-aligned |
| connection-lost | MpBanner variant="danger" — "Connection lost — bookings paused." | Primary [Re-authorize Google Calendar] |
| quota-exceeded | MpBanner variant="warning" — "Google API quota exceeded — bookings may be delayed." | Tertiary [View quota usage] + primary [Request quota increase] |
Outstanding verifications (V1–V8) — see Wireframes v4.2 for full tracker
| # | Verification | Owner | Status |
|---|
| V1 | Canonical drawer container pattern (single-column right-side; 5 groups, Other integration is the 5th) | Wulan + Rizky | ✅ RESOLVED 2026-05-22 — Figma confirms single-column right-side drawer; native row lives under Other integration. |
| V2 | Per-screen Figma node IDs (Screens 0–2 + sub-screens; Screens 3–5 = Pixel MCP-only, no Figma node) | Wulan + Rizky | Open for Screens 0–2 only. Screen 3 explicitly dropped (Pixel MCP-only spec). |
| V3 | Live Mp* prop/slot names from @mekari/pixel3 — Pixel API verification via https://ai.mekari.design/mcp (Mekari's canonical design-system MCP) | FE + Wulan/Rizky | Open |
| V4 | Google Calendar brand SVG path + hex colors — closes the [BRAND_SVG_PENDING_V4] guard | Wulan + Rizky | Open — blocks code-gen |
| V5 | Empty-state illustration asset (Screen 0) | Wulan + Rizky | Open |
| V6 | Microcopy review — all literal strings flagged engineering-draft | Content Design + Yosephine | Open |
| V7 | Locale set (id-ID + en) | PM | Open |
| V8 | Drawer runtime states (Loading, Failed-to-load) — design pass before Open Beta | Wulan + Rizky | NEW — Open |
References: Canonical IA + drawer states + per-screen MCP Pixel prompts → Wireframes v4.2. IA contract + S8a decisions → Native Integrations ANCHOR v1.2 and Mekari Action ANCHOR v1.3.
S12 — API & Webhook Behavior
| # | Behavior | Triggered by | Expected | Failure |
|---|
| 1 | Initiate OAuth | Admin clicks Connect | Qontak returns Google OAuth URL with state + scope | State mismatch / user cancel → calendar_oauth_failed |
| 2 | Exchange auth code for tokens | Google redirect with ?code=...&state=... | Validate state. Tokens encrypted + bound to agent_id. calendar_oauth_completed. | Storage error → 500 + ops alert |
| 3 | Refresh access token — proactive + reactive | Proactive: BG job at 50min TTL. Reactive: Google 401 mid-call. | POST token endpoint. Single-flight per agent_id. Atomic replacement. calendar_token_refreshed. | Refresh fails → calendar_token_refresh_failed fires; banner → Connection Lost; cascade to GCAL-S05. |
| 4 | List available slots | Agent invokes list_slots(template_id, optional_date_hint) | Decrypt token. GET freeBusy. Compute open slots. calendar_slot_lookup fires. | 5xx → status=failed. 429 → backoff. |
| 5 | Create calendar event + invite | Agent invokes create_event(template_id, slot_start, customer_data) | POST events with sendUpdates=all. Google sends invites. calendar_booking_created + calendar_invite_sent fire. | 5xx → calendar_booking_failed. 401 → refresh + retry. 429 → backoff. Conflict → slot_conflict. |
| 6 | Cascade delete on AI Agent deletion | Admin deletes AI Agent | Delete agent. Purge encrypted tokens. Purge templates. Tokens on Google's side NOT revoked. | Partial cascade → log alert; orphan purged by nightly cron |
| 7 | Disconnect (without deleting agent) | Admin clicks Disconnect | Purge tokens locally. Banner → Not Connected. Templates remain. | Storage error → log alert |
Google Calendar credentials follow the canonical pattern already used by mekari_qontak_crm. Encoded BE-canonical values below.
A. Credential storage
| Property | Value |
|---|
| Table | organization_connections |
category | 'credential' |
code | 'google_calendar' |
auth_type | 'oauth2' |
auth_data | Lockbox-encrypted JSON { access_token, refresh_token, expires_at }. Never logged. |
| OAuth scopes | https://www.googleapis.com/auth/calendar.events (narrow). If user email is required to render in the N2 inline OAuth status row's connected state, also https://www.googleapis.com/auth/userinfo.email (PM to confirm — flagged as Open Question in S9). |
| Token endpoint | https://oauth2.googleapis.com/token |
| Per-org cardinality | Single credential per org per provider (v1). One row in organization_connections per org with code='google_calendar'. Re-connecting overwrites the existing row in place. |
| Resolution | Credential is resolved by the HTTP client using org_id + provider code. The credential is NOT a user-facing field — the user does NOT pick "which credential" in the form. The N2 inline OAuth status row in Wireframes v4.2 Screen 3 IS the credential UI. |
B. HTTP client (refresh-on-401)
New file: app/core/repositories/node_resources/google_calendar/calendar_http_client.rb.
Mirrors MekariQontakCrm::CrmHttpClient#request_with_auth. Refresh-on-401 behavior:
- On any 401 from Google Calendar API, attempt token refresh via
POST https://oauth2.googleapis.com/token with grant_type=refresh_token + stored refresh_token.
- On refresh success: update
organization_connections.auth_data with new access_token + new expires_at. Retry the original request once.
- On refresh failure (invalid_grant, network error, scope revoked, etc.): mark credential as
connection-lost, trigger N2 row state change, surface to user via the Wireframes v4.2 Screen 3 N2 inline OAuth status row (connection-lost state — see GCAL-S05).
C. Quota
Google Calendar API daily quota: ~1,000,000 requests/day per project, ~600 req/min/user (varies; verify in GCP console). Document in BE runbook. Surface quota-exceeded state through the N2 inline OAuth status row (already specced in Wireframes v4.2 Screen 3 N2 state machine — see S11 §Per-action OAuth status row state machine).
S13 — System Flow + User Stories + ACs
Flow
| Field | Value |
|---|
| Flow Name | Configure Google Calendar action + Agent books an appointment end-to-end |
| Type | User Journey (admin setup) + API Sequence (runtime booking) |
| # | Step |
|---|
| 1 | Admin/Spv opens AI Agent configuration page |
| 2 | Admin/Spv clicks [+ Add Action] → ActionPickerDrawer opens |
| 3 | Admin/Spv scrolls drawer to existing Other integration group → clicks Google Calendar row |
| 4 | Per-action details form (Screen 3, Pixel MCP-only spec) opens with inline OAuth status row in "Not Connected" state |
| 5 | Admin/Spv clicks [Connect Google Calendar] → redirected to Google OAuth consent |
| 6 | Customer's Google admin approves calendar.events scope |
| 7 | Google redirects with auth code; Qontak exchanges for tokens |
| 8 | Inline OAuth status row flips to "Connected" |
| 9 | Admin/Spv fills the per-action details form (calendar picker, duration, lookup window, event title pattern, attendees mapping) → saves |
| 10 | At runtime: customer chats → Agent invokes list_slots |
| 11 | Agent presents slots; customer picks one |
| 12 | Agent invokes create_event → Google sends invite |
| 13 | Agent confirms booking; events fire |
| 14a-e | Failure branches (see story Error scenarios) |
Story Index
| Story ID | Title | Priority | Dependencies | Designer review? |
|---|
| GCAL-S01 🎨 | Connect AI Agent to Google Calendar via OAuth | Must Have | None | Yes — UI |
| GCAL-S02 🎨 | Configure a Calendar template | Must Have | S01 | Yes — UI |
| GCAL-S03 | Agent looks up available slots | Must Have | S02 | No — backend |
| GCAL-S04 | Agent creates calendar event + sends invite | Must Have | S03 | No — backend |
| GCAL-S05 🎨 | Handle token refresh failure / re-authorize | Should Have | S01 | Yes — UI |
| GCAL-S06 🎨 | Cascade delete AI Agent + tokens + templates | Must Have | S02 | Yes — UI (confirmation modal) |
| GCAL-S07 | Auto-refresh OAuth access token | Must Have | S01 | No — backend |
| GCAL-S08 | Backend: node_registries seed + executor + HTTP client + lookup resources | Must Have | None (BE infra) | No — backend |
| GCAL-S09 | Feature flag rollout: FEATURE_GOOGLE_CALENDAR_NODES | Must Have | S08 | No — backend |
🎨 Story GCAL-S01 — Connect AI Agent to Google Calendar via OAuth
🎨 Designer review: Yes — frontend story. Wulan/Rizky review the UI States and Permission Model below; the full MCP Pixel prompt is in the Wireframes page → Screen 1 + Screen 2 + Screen 7.
| Field | Value |
|---|
| Priority | Must Have — entry point for the entire feature |
| Before state | AI Agent has no Google Calendar action. No OAuth tokens. |
| After delta | Google Calendar row appears in the existing Other integration group of the action drawer. Admin clicks it → per-action details form (Screen 3) opens with inline OAuth status row in Not Connected. Admin clicks Connect → Google OAuth → tokens stored encrypted, bound to agent_id. OAuth status row flips to Connected. |
| User story | As an Admin/Supervisor at a Qontak customer with the Native Integrations add-on, I want to connect my AI Agent to a Google Calendar via a one-time OAuth flow, so that my agent can read availability and create bookings on my team's calendar. |
| UI Reference | S11 §Action drawer (Screen 1) + §Per-action OAuth status row state machine (lives inside Screen 3 form). Screens 3–4 are Pixel MCP-only specs (no Figma node). |
Acceptance Criteria (ATDD / Gherkin)
Scenario: AC-1 — Google Calendar row is visible in Other integration group when entitlement is complete
Type: Constraint — feature flag + add-on entitlement
Given the user is Admin or Supervisor at a company where
google_calendar_native_integration_enabled = ON
AND the company has Native Integrations add-on SKU active
When the user opens the Action drawer
Then the existing "Other integration" group renders a "Google Calendar" row
(alongside any pre-existing rows in that group)
And clicking the row opens the per-action details form (Screen 3) with the inline OAuth status row in Not Connected state
And no new top-level group (such as "Native integrations") is rendered
Scenario: AC-2 — Initiate OAuth from Not Connected state
Type: Happy path
Given the user is on the per-action details form (Screen 3) with the inline OAuth status row in Not Connected state
When the user clicks [Connect Google Calendar]
Then Qontak generates a unique OAuth state nonce
And stores it server-side with 10-minute TTL
And redirects the user to Google OAuth consent URL with
scope = "https://www.googleapis.com/auth/calendar.events"
state = <nonce>
redirect_uri = <configured>
And calendar_oauth_initiated event fires with company_id, user_id, agent_id, scope_requested
Scenario: AC-3 — Exchange auth code for tokens
Type: Happy path
Given Google redirects the user back to /oauth/google/callback?code=<code>&state=<nonce>
When Qontak receives the callback
Then it validates the state nonce matches the stored value (CSRF protection)
And POSTs to Google's token endpoint with the auth code
And receives access_token + refresh_token
And encrypts both at rest, binding them to agent_id
And calendar_oauth_completed event fires with latency_ms
And the user is redirected back to agent config with success state
And the inline OAuth status row flips to Connected
Scenario: AC-4 — State mismatch rejected (CSRF protection)
Type: Edge case — security
Given an OAuth callback arrives with state nonce that doesn't match any stored value
When the callback handler runs
Then the request is rejected with HTTP 400
And an error page shows: "Authorization failed (security check). Try connecting again."
And calendar_oauth_failed event fires with failure_reason = "state_mismatch"
Scenario: AC-5 — User denies consent at Google
Type: Edge case — user cancel
Given the user reaches Google's consent screen
When the user clicks Cancel
Then Google redirects to Qontak with ?error=access_denied
And the inline OAuth status row stays in Not Connected state
And a modal shows: "Connection cancelled. You can try again whenever you're ready."
And calendar_oauth_failed event fires with failure_reason = "user_denied"
Scenario: AC-6 — Role gate enforcement
Type: Constraint — role gate (per S4 Read/Write)
Given the user's role is NOT Admin or Supervisor (e.g., Agent, Read-only, Billing-only)
When the user views AI Agent config
Then the Action drawer is not reachable (entry CTA hidden)
And the [Connect Google Calendar] button is not reachable
And no 403 is surfaced to the user
Errors / Unhappy Path
Scenario: ERR-1 — Qontak fails to generate OAuth URL
Given the user clicks [Connect Google Calendar]
When Qontak's call to generate the OAuth URL fails (config error, Redis down)
Then a modal shows: "Could not start authorization. Try again." with a [Retry] button
And calendar_oauth_failed event is logged with failure_reason = "err_oauth_url"
Scenario: ERR-2 — Google returns 400 on token exchange
Given Qontak receives the auth code from Google
When the token exchange POST returns HTTP 400 (invalid code, redirect mismatch, scope error)
Then a modal shows: "Authorization failed. Verify your Google account and try again."
And calendar_oauth_failed fires with failure_reason = "err_token_exchange"
Scenario: ERR-3 — Token storage / encryption fails
Given tokens are received from Google
When the encrypt + store step fails
Then HTTP 500 is returned and an ops alert fires
And the banner stays in transitional state
And the user sees: "Could not save authorization. Ops has been notified. Try again."
And calendar_oauth_failed fires with failure_reason = "err_token_storage"
Backend implementation cue (BE-canonical, per RFC Google Sheets Workflow Nodes; see also S12.1): On handshake success, persist as a row in organization_connections with org_id=<current_org>, category='credential', code='google_calendar', auth_type='oauth2', auth_data=<lockbox-encrypted {access_token, refresh_token, expires_at}>. If a row already exists for (org_id, 'google_calendar'), update it in place (single credential per org per provider in v1). Token endpoint = https://oauth2.googleapis.com/token. Credential is resolved by the HTTP client using org_id + provider code — NOT a user-facing property. Does NOT change Gherkin AC scope; user-facing assertions above stand as-is.
| Aspect | Detail |
|---|
| Permission — CAN | Admin, Supervisor |
| Permission — CANNOT | Agent, Read-only, Billing-only |
| Permission — Unauthorized | Action drawer entry CTA hidden; Google Calendar row in Other integration group not reachable |
| UI — Empty (Not Connected) | "Connect to Google Calendar to enable in-conversation booking." + [Connect Google Calendar] |
| UI — Loading (Connecting) | Spinner + "Redirecting to Google for authorization…" |
| UI — Error | Modal error + Retry |
| UI — Success (Connected) | Green check + "Connected as [admin_email]" + [Re-authorize] / [Disconnect] |
| Field | Type | Required | Source |
|---|
company_id | string (UUID) | Yes | Auth session |
user_id | string | Yes | Auth session |
agent_id | string | Yes | URL param |
state_nonce | string (random) | Yes | Server-generated, 10-min TTL |
auth_code | string | Yes | Google callback param |
access_token | string (sensitive) | Yes | Encrypted at rest |
refresh_token | string (sensitive) | Yes | Encrypted at rest |
connected_google_email | string | Yes | Google's userinfo |
🎨 Story GCAL-S02 — Configure a Calendar template
🎨 Designer review: Yes — frontend story. This is the largest form surface in P1. Wulan/Rizky review the Action details form (Screen 3, extends E7 — Pixel MCP-only spec) including the Calendar dropdown field, all MpFormControl-wrapped inputs, validation states, the slider behavior, and the attached-action row in the populated Actions tab (Screen 1, extends E3). Full MCP Pixel prompt: Wireframes v4.2 → Screen 3.
| Field | Value |
|---|
| Priority | Must Have — defines the booking shape per use case |
| Before state | OAuth Connected (N2 inline OAuth status row = connected inside the Action details form); no booking templates attached yet — Actions tab shows the empty state (Screen 0, extends E2). |
| After delta | Admin selects Google Calendar from the Action drawer (Other integration group) → Action details form (Screen 3, extends E7) opens. Admin configures name, calendar (via Calendar dropdown field inside the form), duration, lookup window, event title pattern, location, description, attendees mapping. On Save, one booking template = one attached action row in the Actions tab populated state (Screen 1, extends E3). Max 10 attached Calendar actions per customer (S4 constraint). calendar_template_configured fires. |
| User story | As an Admin/Supervisor, I want to attach one or more Google Calendar booking actions (e.g., 30-min test drive, 60-min property viewing, 15-min demo call) so that my agent uses the right defaults for each kind of booking in my industry. |
| UI Reference | S11 §Screen 3 — Action details form (extends E7, Pixel MCP-only spec). Calendar dropdown field = MpFormControl label="Calendar" + MpSelect populated from Google Calendar Lists API. N2 inline OAuth status row inside the form's Form configuration block. Attached actions surface as rows in the Actions tab populated state (Screen 1, extends E3) — no separate template-list management surface. |
Acceptance Criteria (ATDD / Gherkin)
Scenario: AC-1 — Open the Action details form for a new Google Calendar action
Type: Happy path
Given OAuth is Connected (N2 inline OAuth status row = connected)
And no Google Calendar action is attached yet
When the user opens the Action drawer and selects `Google Calendar` under `Other integration`
Then the Action details form (Screen 3, extends E7) opens with empty fields
And the Calendar dropdown field is populated by calling Google Calendar Lists API
Scenario: AC-2 — Save a new Calendar action
Type: Happy path
Given all required fields are filled in the Action details form (name, calendar_id, duration, event_title_pattern, attendee_mapping)
When the user clicks [Save]
Then the template is persisted server-side as one attached action
And a new attached action row appears in the Actions tab populated state (Screen 1, extends E3)
And calendar_template_configured event fires with action="created" and template_count
Scenario: AC-3 — Reject unknown placeholders in event title
Type: Edge case — validation
Given the event title pattern contains an unknown placeholder, e.g. {{undefined_var}}
When the user clicks [Save Template]
Then an inline validation error shows under the field:
"Unknown placeholder. Valid options: {{customer_name}}, {{vehicle}}, {{lead_source}}, {{phone}}, {{email}}"
And the [Save Template] button is disabled until the error is fixed
Scenario: AC-4 — Lookup window bounds
Type: Edge case — boundary
Given the user adjusts the lookup window slider
When the slider value would be 0 or > 30 days
Then the slider physically clamps to the 1-30 range
And no error is shown (prevention by control)
Scenario: AC-5 — Attached-action count limit (S4 constraint)
Type: Boundary — quota
Given the customer already has 10 Google Calendar actions attached to the AI Agent
When the user opens the Action drawer and tries to select `Google Calendar` again
Then the row is disabled in the drawer
And a tooltip shows:
"Maximum 10 Google Calendar actions per customer. Delete an unused action to add a new one."
Scenario: AC-6 — Edit an existing Calendar action
Type: Happy path
Given an attached Calendar action exists in the Actions tab populated state (Screen 1, extends E3)
When the admin clicks the action row to open it
Then the Action details form (Screen 3, extends E7) opens pre-filled with the action's values
And on Save, calendar_template_configured fires with action="edited"
Scenario: AC-7 — Delete a Calendar action
Type: Happy path
Given an attached Calendar action exists in the Actions tab populated state
When the user clicks Delete on the action row and confirms in the modal
Then the attached action is removed from the Actions tab
And calendar_template_configured fires with action="deleted"
Errors / Unhappy Path
Scenario: ERR-1 — Calendar dropdown field fails to load Google calendars
Given the user has opened the Action details form (Screen 3, extends E7)
When the Calendar dropdown field fails to load (Google 5xx)
Then the dropdown shows: "Could not load your calendars. Try again." with a [Retry] button
Scenario: ERR-2 — Save fails (Qontak storage error)
Given the user clicks [Save] in the Action details form
When Qontak's storage write fails
Then the form shows: "Could not save action. Try again." with a [Retry] button
And the error is logged
Scenario: ERR-3 — Template deleted mid-booking
Given an active conversation is mid-booking using template_id = X
When template X is deleted in another tab
Then per S3 #5, runtime booking failure is handled by the agent (parent runtime PRD scope)
Backend implementation cue (BE-canonical, per RFC Google Sheets Workflow Nodes; see also S6.6 + S12.1): The Calendar dropdown is sourced via Repositories::NodeResources::GoogleCalendar::LookupResources with resource_key='calendar'. Underlying call: GET https://www.googleapis.com/calendar/v3/users/me/calendarList?fields=items(id,summary,primary). Each item returned as {name: summary, value: id}. Primary calendar marked with a badge / sorted to the top. Lookup is only called when the N2 inline OAuth status row inside Screen 3 is in connected state. The calendar_id field in node_registries.properties declares is_rl=true, resource_types=[{provider: "google_calendar", resource_key: "calendar"}] per the canonical RFC properties shape. Does NOT change Gherkin AC scope.
| Aspect | Detail |
|---|
| Permission — CAN | Admin, Supervisor |
| Permission — CANNOT | Other roles |
| UI — Empty (Actions tab) | Screen 0 (extends E2) — empty state with prompt to attach an action via the Action drawer |
| UI — Loading | 3 skeleton rows in the Actions tab populated state (Screen 1, extends E3) |
| UI — Error | "Could not load actions. Try again." + Retry |
| UI — Success | Attached-action rows in Screen 1 (extends E3); Google Calendar row in the Action drawer disabled at 10 attached |
| Field | Type | Required | Source |
|---|
template_id | string (UUID) | Yes | Server-generated |
name | string | Yes | User input |
calendar_id | string | Yes | Calendar dropdown field (Screen 3 Action details form) |
duration_minutes | integer | Yes | User input |
lookup_window_days | integer (1-30) | Yes | User input |
event_title_pattern | string (with placeholders) | Yes | User input |
location | string | No | User input |
description_pattern | string (with placeholders) | No | User input |
attendee_mapping | object | Yes | User input |
Story GCAL-S03 — Agent looks up available slots in a conversation
No designer review needed — backend behavior; status reflected via N2 inline OAuth status row inside Screen 3 (covered by GCAL-S05 review).
| Field | Value |
|---|
| Priority | Must Have — first runtime touch in the closed loop |
| Before state | OAuth Connected + ≥1 template configured. No runtime action invoked yet. |
| After delta | Agent invokes list_slots(template_id, optional_date_hint). Qontak decrypts token (refreshes if needed per S07), calls Google freeBusy, returns aligned slots. |
| User story | As the AI Agent at runtime, I want to look up available slots in the configured Google Calendar within the configured lookup window, so that I can offer real options to the customer in the conversation without human handoff. |
| UI Reference | N/A — runtime backend behavior |
Acceptance Criteria (ATDD / Gherkin)
Scenario: AC-1 — Happy path slot lookup
Type: Happy path
Given OAuth is Connected, a template exists, and access_token is valid
When the agent invokes list_slots(template_id)
Then Qontak calls Google freeBusy with the token + calendar_id + lookup_window
And returns a slot list aligned to the template's duration_minutes
And calendar_slot_lookup event fires with slot_count_returned, status="success", latency_ms
Scenario: AC-2 — Token refresh inherited from S07
Type: Inherited behavior
Given the access_token's TTL has expired (or is near-expired) between calls
When the agent invokes list_slots
Then auto-refresh handled by GCAL-S07 (proactive at 50min TTL; reactive on 401)
And the refresh is transparent to this story (no additional refresh logic implemented here)
Scenario: AC-3 — Date hint biases slot selection
Type: Edge case — date hint
Given the customer says "next Tuesday" and the agent passes optional_date_hint=2026-05-12
When Qontak builds the slot list
Then slots in the lookup window are returned
But the list is biased toward slots near 2026-05-12
Scenario: AC-4 — Empty lookup window
Type: Edge case — no availability
Given the calendar is fully booked across the lookup window
When the agent invokes list_slots
Then an empty slot list is returned
And calendar_slot_lookup fires with slot_count_returned=0
And the agent's prompt logic (upstream) handles the "no slots available" message
Scenario: AC-5 — Token refresh failure cascade
Type: Constraint enforcement — token validity
Given the refresh_token is revoked or invalid
When token refresh fails
Then calendar_token_refresh_failed fires
And calendar_slot_lookup returns status="failed", failure_reason="token_invalid"
And the banner flips to Connection Lost
And the agent prompt receives a failure signal
Scenario: AC-6 — Large date range performance
Type: Volume — performance constraint
Given lookup_window_days=30 and the calendar has 200+ events in range
When the agent invokes list_slots
Then Google freeBusy handles up to 5 calendars / 90-day max per call
And total latency is ≤ 1.5s p95 (per S4 §Performance)
And slot computation paginates if needed
Errors / Unhappy Path
Scenario: ERR-1 — Google 5xx
Given the agent invokes list_slots
When Google returns a 5xx error
Then calendar_slot_lookup fires with status="failed", failure_reason="api_5xx"
And the agent returns a failure signal to runtime prompt
Scenario: ERR-2 — Quota exceeded
Given a high-volume agent
When Google returns 429 quota error
Then Qontak backs off and retries once
And if still 429, calendar_slot_lookup fires with failure_reason="quota_exceeded"
And the banner flips to Quota Exceeded
Scenario: ERR-3 — Bad template_id
Given the agent invokes list_slots with a template_id that doesn't exist
When the template lookup fails (deleted while conversation in progress)
Then failure_reason="template_not_found" is returned
And the agent runtime handles the error path
| Aspect | Detail |
|---|
| Permission — CAN trigger | AI Agent runtime only (authenticated via agent's app credential) |
| Permission — CANNOT | Any external caller |
| UI — Backend story | No UI of its own |
| Data — Returned | Slot list with start_iso, end_iso per slot |
| Field | Type | Required | Source |
|---|
template_id | string | Yes | Agent prompt |
optional_date_hint | ISO date | No | Customer message NLU |
available_slots[] | array of {start, end} | Yes | Google freeBusy |
latency_ms | integer | Yes | Backend |
status | enum (success/failed) | Yes | Backend |
failure_reason | string | If failed | Backend |
Story GCAL-S04 — Agent creates a calendar event and sends invite
No designer review needed — backend behavior; agent confirms in chat via runtime prompt logic (out of scope per S3 #11).
| Field | Value |
|---|
| Priority | Must Have — this story IS the closed-loop conversion event |
| Before state | Agent invoked list_slots per S03. Customer picked a slot. No event yet on Google's side. |
| After delta | Agent invokes create_event(template_id, slot_start, customer_data). Qontak fills template placeholders, POSTs to Google events with sendUpdates=all, receives event_id. Google sends invite. calendar_booking_created + calendar_invite_sent fire. |
| User story | As the AI Agent at runtime, I want to create a calendar event with the right title, time, attendees, and Google-sent invite, so that the customer's booking is real and immediately visible in their own calendar. |
| UI Reference | N/A — runtime backend behavior |
Acceptance Criteria (ATDD / Gherkin)
Scenario: AC-1 — Happy path create event
Type: Happy path — conversion event
Given the customer picked a slot and the agent has customer_data + template_id
When the agent invokes create_event(template_id, slot_start, customer_data)
Then Qontak fills template placeholders with conversation context
And POSTs to Google Calendar events endpoint with sendUpdates=all
And receives event_id from Google
And Google sends invite emails to all attendees
And calendar_booking_created fires with event_id, attendee_count, time_to_book_sec
And calendar_invite_sent fires
And success + event_id is returned to the agent
Scenario: AC-2 — Token refresh inherited from S07
Type: Inherited behavior
Given the access_token expires between list_slots and create_event
When the agent invokes create_event
Then auto-refresh is handled by GCAL-S07 (proactive + reactive + single-flight)
And the refresh is transparent to this story
Scenario: AC-3 — Slot conflict (concurrent booking)
Type: Edge case — concurrency
Given another agent or conversation booked the same slot between list_slots and create_event
When Google processes the create_event POST
Then Google returns event creation success with a conflict warning
And Phase 1 does NOT prevent double-booking on Google's side
(Google's standard behavior is to allow concurrent events)
And the agent treats this as success
Scenario: AC-4 — Performance constraint
Type: Constraint — performance SLA
Given a high-traffic period
When the agent invokes create_event
Then total latency is ≤ 2s p95 from invocation to event_id returned (per S4 §Performance)
Scenario: AC-5 — Quota exceeded
Type: Volume — quota
Given the customer is at Google's API quota limit
When the agent invokes create_event
Then Google returns 429
And Qontak backs off and retries once
And if still 429, calendar_booking_failed fires with failure_reason="quota_exceeded"
And the banner flips to Quota Exceeded
Errors / Unhappy Path
Scenario: ERR-1 — Google 5xx
Given the agent invokes create_event
When Google returns a 5xx error
Then Qontak retries once with backoff
And if still failing, calendar_booking_failed fires with failure_reason="api_5xx"
And the agent's prompt receives a failure signal
Scenario: ERR-2 — Malformed customer email
Given the customer's email in customer_data is malformed
When Google rejects the POST with HTTP 400
Then calendar_booking_failed fires with failure_reason="invalid_attendee_email"
And the agent re-prompts the customer for a valid email
Scenario: ERR-3 — Token invalidated mid-call
Given the agent invokes create_event
When the access_token refresh fails mid-call (revocation)
Then calendar_token_refresh_failed + calendar_booking_failed both fire
And the banner flips to Connection Lost
And the conversation pauses with: "Couldn't confirm your booking right now — a team member will follow up."
And the agent hands off to a human
Scenario: ERR-4 — Event created but invite send fails partially
Given the event creation POST succeeds
When Google returns success but with an attendeesOmittedFromInvite warning
Then event_id is stored
And the banner stays Connected
And calendar_invite_sent fires with invite_status="partial_failure"
And a downstream alert surfaces: "couldn't email customer; please contact them directly"
| Aspect | Detail |
|---|
| Permission — CAN trigger | AI Agent runtime only |
| Permission — CANNOT | External callers |
| UI — Backend story | No UI of its own; agent confirms in chat via runtime prompt logic |
| Data — Returned | event_id, start, end, attendees[], html_link |
| Field | Type | Required | Source |
|---|
template_id | string | Yes | Agent prompt |
slot_start | ISO datetime | Yes | Customer-selected slot |
customer_data | object (name, email, phone, custom fields) | Yes | Captured by agent |
event_id | string | Returned | Google response |
time_to_book_sec | integer | Yes | now - conversation_start_for_this_booking |
attendee_count | integer | Yes | Template mapping + customer email |
🎨 Story GCAL-S05 — Handle token refresh failure / re-authorization
🎨 Designer review: Yes — frontend story. Wulan/Rizky review the N2 inline OAuth status row state transitions (connected → connection-lost → re-auth → connected) and the Re-authorize UX inside Screen 3 (Action details form). Full MCP Pixel prompt: Wireframes v4.2 → Screen 5.
| Field | Value |
|---|
| Priority | Should Have — Closed Beta scale allows manual intervention; full self-serve re-authorize UX matters more at Open Beta and GA scale. Acceptable to defer to Phase 1.5 if Stage 2 capacity is tight. |
| Before state | OAuth Connected; tokens working. N2 inline OAuth status row inside Screen 3 = connected state. |
| After delta | When token refresh fails (revoked by customer's Google admin, refresh token expired, scope downgraded), N2 row → connection-lost state (MpBanner variant="danger"). [Re-authorize Google Calendar] CTA surfaces inline. Click → re-runs S01 OAuth flow; attached Calendar actions remain intact. |
| User story | As an Admin/Supervisor, I want a clear "Connection Lost — re-authorize" path when my Google tokens expire or get revoked, so that I can restore booking capability without losing my attached Calendar actions or AI Agent configuration. |
| UI Reference | S11 §Screen 3 — Action details form → N2 inline OAuth status row, connection-lost state (with inline [Re-authorize Google Calendar] CTA). Per S11 §Per-action OAuth status row state machine. |
Acceptance Criteria (ATDD / Gherkin)
Scenario: AC-1 — Detect refresh failure
Type: Happy path — failure detection
Given the agent attempts a Calendar API call and token refresh fails
When the backend catches the refresh error
Then calendar_token_refresh_failed fires with failure_reason
And the N2 inline OAuth status row inside Screen 3 flips to connection-lost
And an admin email notification is sent (if configured)
Scenario: AC-2 — Re-authorize from Connection Lost
Type: Happy path — re-authorize
Given the N2 inline OAuth status row inside Screen 3 shows connection-lost
When the admin clicks [Re-authorize Google Calendar]
Then the OAuth flow from GCAL-S01 re-runs
And on success, new tokens are stored
And the N2 inline OAuth status row flips back to connected
And existing attached Calendar actions remain unchanged
Scenario: AC-3 — Re-authorize with different Google account
Type: Edge case — account mismatch
Given the admin re-authorizes but selects a different Google account at Google's account picker
When the new OAuth flow completes
Then attached Calendar actions remain bound to the agent
But the calendars they reference may no longer be accessible
And the N2 inline OAuth status row shows connected
And actions with stale calendar_id will fail at runtime per S03/S04 (graceful failure)
And the admin needs to edit each action's Calendar dropdown field inside Screen 3
Scenario: AC-4 — Failure during active conversation
Type: Edge case — mid-conversation failure
Given an agent invokes list_slots or create_event mid-conversation
And the token refresh fails
When the runtime call returns failure
Then per S03/S04 ERRs:
The agent prompt receives a failure signal
Runtime prompt logic (out of scope) handles UX in chat (human handoff or graceful message)
The N2 inline OAuth status row inside Screen 3 flips to connection-lost in admin UI
Errors / Unhappy Path
Scenario: ERR-1 — Re-authorize flow fails
Given the N2 inline OAuth status row inside Screen 3 shows connection-lost
When the admin clicks [Re-authorize] and the OAuth flow fails (S01 ERR paths)
Then the N2 row stays connection-lost
And the admin sees an error toast
And can retry
Scenario: ERR-2 — Re-authorize to completely different Google Workspace
Given the admin re-authorizes with a different Google Workspace
When the new tokens are stored
Then the calendars referenced by attached actions are no longer accessible
And actions with stale calendar_id fail at runtime per S03/S04
And the admin must update each action's Calendar dropdown field inside Screen 3
Backend implementation cue (BE-canonical, per RFC Google Sheets Workflow Nodes; see also S12.1): The connection-lost state is detected when the refresh-on-401 attempt in GoogleCalendar::CalendarHttpClient fails (invalid_grant, network error, scope revoked, etc. — see GCAL-S07). The Re-authorize flow updates the existing organization_connections row for (org_id, 'google_calendar') in place — does NOT create a new row (single credential per org per provider in v1). N2 row transitions: connected → (refresh fails) → connection-lost → (user clicks Re-authorize) → connected. Does NOT change Gherkin AC scope.
| Aspect | Detail |
|---|
| Permission — CAN | Admin, Supervisor |
| Permission — CANNOT | Agent role, Read-only |
| UI — Connection Lost | N2 inline OAuth status row inside Screen 3 = connection-lost state (MpBanner variant="danger"): "Connection lost — your Google Calendar authorization is no longer valid. Bookings are paused." + inline [Re-authorize Google Calendar] CTA |
| UI — Re-authorizing | Same loading states as S01 |
| UI — Restored (Connected) | N2 row = connected state (success check + user email); attached Calendar actions remain |
| Field | Type | Required | Source |
|---|
agent_id | string | Yes | URL param |
failure_reason | enum (revoked / expired / scope_downgrade / unknown) | Yes | Backend |
last_known_good_at | timestamp | Yes | Backend |
re_authorized_at | timestamp | When success | Backend |
🎨 Story GCAL-S06 — Cascade delete AI Agent + tokens + templates
🎨 Designer review: Yes — extends the parent AI Agent delete confirmation modal (Screen 5, owned by parent AI Agent PRD) via its cascadeWarnings prop with Calendar-specific warning copy. Wulan/Rizky review the modal layout + warning hierarchy. Full MCP Pixel prompt: Wireframes v4.2 → Screen 5 — parent AI Agent delete modal.
| Field | Value |
|---|
| Priority | Must Have — without cascade, deleted agents leave orphaned tokens (security exposure) |
| Before state | AI Agent has Google Calendar action: OAuth tokens + N attached Calendar actions bound. |
| After delta | Agent deletion cascade-purges tokens + attached Calendar actions + emits per-action calendar_template_configured action=deleted. Tokens on Google's side are NOT proactively revoked. |
| User story | As an Admin/Supervisor, I want deleting an AI Agent to also remove its bound Google OAuth tokens and attached Calendar actions, so that I don't leave orphaned secrets in Qontak storage or stale configuration in the system. |
| UI Reference | Parent AI Agent delete confirmation modal (Screen 5, owned by parent AI Agent PRD) with Calendar-specific copy injected via the cascadeWarnings prop. This PRD does NOT own the modal — it owns only the Calendar-specific warning string + cascade behavior. |
Acceptance Criteria (ATDD / Gherkin)
Scenario: AC-1 — Cascade delete in Connected state
Type: Happy path
Given an AI Agent has OAuth Connected + 1+ attached Calendar actions
When the admin deletes the AI Agent via the parent AI Agent delete modal (Screen 5)
Then the AI Agent record is deleted (per parent PRD)
And bound tokens are purged from encrypted storage
And all attached Calendar actions are purged
And calendar_template_configured fires with action="deleted" for each action
And the N2 inline OAuth status row inside Screen 3 is removed alongside the agent
Scenario: AC-2 — Cascade delete in Connection Lost state
Type: Edge case — already-invalid tokens
Given an AI Agent is in connection-lost status (tokens already invalid) + has attached Calendar actions
When the agent is deleted via the parent AI Agent delete modal (Screen 5)
Then attached Calendar actions are purged
And the refresh_token entry is purged (even though already invalid)
And no token_refresh_failed retries are triggered
Scenario: AC-3 — Role gate
Type: Constraint — role gate (per S4 DELETE)
Given the user's role is NOT Admin or Supervisor
When the user attempts to delete an AI Agent
Then the delete action is not available in the UI
And server-side returns HTTP 403 (existing parent PRD behavior)
Scenario: AC-4 — Bulk deletion
Type: Volume — concurrent deletes
Given a customer has 5 AI Agents with Calendar action attached
When the customer deletes them sequentially
Then each deletion runs independently
And each cascade purges its own tokens + attached Calendar actions
And no shared-state corruption occurs between agents
Scenario: AC-5 — Calendar-specific confirmation modal copy
Type: Happy path — confirmation copy
Given the admin clicks Delete on an agent with attached Calendar action(s)
When the parent AI Agent delete modal (Screen 5) opens
Then the `cascadeWarnings` slot renders Calendar-specific text:
"This will also remove the Google Calendar connection and all attached Calendar actions for this agent.
(Bookings already on Google Calendar will NOT be affected.)"
Errors / Unhappy Path
Scenario: ERR-1 — Partial cascade failure
Given the agent is deleted
When the token purge fails (storage error)
Then an alert is logged for ops
And the orphan token entry is purged by the next nightly cleanup cron (per S4.7)
Scenario: ERR-2 — Customer wants to revoke on Google's side too
Given the customer wants to revoke OAuth at Google
When they ask how
Then the customer playbook documents:
"To fully revoke, remove the app from your Google Workspace admin console at
myaccount.google.com/connections"
And this is out of Qontak's scope
| Aspect | Detail |
|---|
| Permission — CAN delete | Admin, Supervisor (per S4) |
| Permission — CANNOT | Other roles |
| UI — Confirmation modal | Parent AI Agent delete modal (Screen 5) with Calendar-specific warning rendered via cascadeWarnings prop |
| UI — Post-delete | Agent removed; toast "AI Agent deleted." |
| Field | Type | Required | Source |
|---|
agent_id | string | Yes | URL param |
had_oauth_connected | boolean | Yes | Derived |
attached_actions_purged_count | integer | Yes | Derived |
Story GCAL-S07 — Auto-refresh OAuth access token (proactive + reactive, concurrent-safe)
No designer review needed — backend behavior; failure cascades to the N2 inline OAuth status row inside Screen 3 (covered by GCAL-S05 review).
| Field | Value |
|---|
| Priority | Must Have — without proactive auto-refresh, OAuth access tokens expire every 60 minutes and cause silent booking failures. Daily-use conversational feature requires baseline auto-refresh. |
| Before state | Phase 1 originally treated token refresh as edge-case ACs. No dedicated mechanism; no proactive refresh; no concurrent-refresh safety. |
| After delta | Dedicated background mechanism. Proactive: refresh at ≤10min remaining on 60min TTL. Reactive: on 401 from Google, refresh + retry once. Concurrent-safe: single-flight per agent_id. Atomic: token replacement is atomic. On failure: cascade to GCAL-S05. |
| User story | As the Qontak Chatbot system, I want to proactively and reactively refresh Google OAuth access tokens with concurrency safety, so that all active customer conversations can invoke calendar actions seamlessly regardless of the 60-minute TTL. |
| UI Reference | N/A — backend behavior; status reflected via the N2 inline OAuth status row inside Screen 3 |
Acceptance Criteria (ATDD / Gherkin)
Scenario: AC-1 — Proactive refresh at 50min TTL
Type: Happy path — proactive
Given an agent's access_token has ≤10 minutes remaining on its 60min TTL
When the background refresh job runs (every 1min cron or equivalent scheduler)
Then Qontak POSTs to Google's token endpoint with the refresh_token
And a new access_token is returned
And the new token is stored atomically
And calendar_token_refreshed event fires with refresh_type="proactive" and latency_ms
And no user-facing booking is disrupted
Scenario: AC-2 — Reactive refresh on 401 mid-call
Type: Happy path — reactive
Given the agent is mid-call to Google Calendar API (Behavior 4 or 5)
When Google returns HTTP 401 (token unexpectedly expired)
Then Qontak triggers a refresh inline
And the new access_token is stored
And the original API call is retried once with the new token
And success is returned to the agent
And calendar_token_refreshed fires with refresh_type="reactive" and latency_ms
And total user-facing latency added is ≤ 500ms p95 (per S4 §Performance)
Scenario: AC-3 — Single-flight concurrency
Type: Concurrency — single-flight
Given 5 active conversations all invoke list_slots or create_event simultaneously
And all detect access_token with <10min TTL
When each invocation attempts to refresh
Then a mutex (or distributed lock) per agent_id ensures exactly ONE refresh fires to Google
And the other 4 conversations wait ≤ 500ms
And the 4 waiting conversations reuse the newly-stored access_token
And only ONE calendar_token_refreshed event fires
Scenario: AC-4 — Atomic token replacement
Type: Atomicity — no torn writes
Given a refresh completes and storage replace is in progress
When a concurrent reader requests the access_token at that exact moment
Then the reader gets either the OLD or the NEW value, never a torn/partial value
And no call ever uses a partial or corrupted token
Scenario: AC-5 — Refresh latency alert
Type: Constraint enforcement — performance
Given a refresh fires (proactive or reactive)
When the refresh roundtrip to Google takes > 500ms p95
Then a latency alert fires (per S6 alerts table)
And ops investigates Google's response time / network
Scenario: AC-6 — Failure cascade to GCAL-S05
Type: Failure cascade
Given a refresh is attempted (revoked, expired, network failure)
When Google returns 400/401/5xx on the refresh call
Then calendar_token_refresh_failed fires with failure_reason
And the N2 inline OAuth status row inside Screen 3 flips connected → connection-lost (per GCAL-S05)
And any in-flight booking fails gracefully with the agent message:
"Couldn't confirm your booking right now — a team member will follow up."
And the admin sees the inline [Re-authorize Google Calendar] CTA in the N2 row
Scenario: AC-7 — Idempotency between proactive and reactive paths
Type: Idempotency
Given a reactive refresh is in progress
When the proactive background job also fires for the same agent_id
Then the single-flight mutex catches both
And only ONE Google call fires
And both code paths reuse the same new token
And no duplicate calendar_token_refreshed events fire
Errors / Unhappy Path
Scenario: ERR-1 — Background job crashes mid-refresh
Given a refresh is in progress
When the background job process crashes
Then the mutex auto-releases after its TTL (e.g., 10s)
And the next call retries
And no zombie locks remain
Scenario: ERR-2 — Storage error on atomic replace
Given a refresh response is received
When the encrypt + store step fails
Then a log alert fires
And the old token remains valid until its natural TTL expiry (no data loss)
And the next refresh attempt retries
And if persistent, cascade to S05
Scenario: ERR-3 — Google token endpoint quota
Given a high-volume customer with many concurrent refreshes
When Google's token endpoint quota is hit (typically per-OAuth-client, high limit)
Then Qontak backs off and retries
And if persistent, an ops alert fires
Backend implementation cue (BE-canonical, per RFC Google Sheets Workflow Nodes; see also S6.6 + S12.1): Refresh is owned by Repositories::NodeResources::GoogleCalendar::CalendarHttpClient, which wraps every Google Calendar API call. The implementation mirrors MekariQontakCrm::CrmHttpClient#request_with_auth (copy-paste with Google-specific token endpoint). Refresh-on-401 behavior:
- On any 401 response from Google, attempt refresh via
POST https://oauth2.googleapis.com/token using the stored refresh_token from organization_connections.auth_data for (org_id, 'google_calendar').
- On refresh success: update
organization_connections.auth_data with the new access_token + new expires_at. Retry the original request once.
- On refresh failure: mark credential as
connection-lost (handled by GCAL-S05). Trigger the N2 inline OAuth status row state change in Screen 3.
The proactive vs reactive vs single-flight vs atomic-replacement ACs above (AC-1 through AC-7) stand as-is — they describe the high-level semantics; this cue declares the canonical file + token endpoint + storage column the implementation uses. Does NOT change Gherkin AC scope.
| Aspect | Detail |
|---|
| Permission — CAN trigger | System (background job) + Agent runtime (reactive); no human trigger |
| Permission — CANNOT | External callers; admin UI does not directly trigger refresh |
| UI — Backend story | No UI of its own; failure cascades to N2 inline OAuth status row inside Screen 3 via S05 |
| Data — refresh metadata | last_refresh_at, next_refresh_at, refresh_count_24h |
| Field | Type | Required | Source |
|---|
agent_id | string | Yes | Internal scheduler / reactive trigger |
refresh_token | string (sensitive) | Yes | Encrypted storage |
access_token (new) | string (sensitive) | Yes (output) | Google response |
refresh_type | enum (proactive / reactive) | Yes | Backend |
latency_ms | integer | Yes | Backend |
next_refresh_at | timestamp | Yes | new_expiry - 10min |
Story GCAL-S08 — Backend: node_registries seed + executor + HTTP client + lookup resources
No designer review needed — backend infrastructure; no UI of its own. Mirrors the canonical mekari_qontak_crm pattern per RFC Google Sheets Workflow Nodes. See also S6.6 (file inventory) and S12.1 (credential storage).
| Field | Value |
|---|
| Priority | Must Have — the FE-facing stories (S01, S02, S03, S04, S05, S07) all depend on these BE files existing. |
| Before state | No google_calendar provider module exists in the codebase. node_type_registry.rb has no google_calendar entry. PROVIDER_MODULES constant in lookup_resources.rb does not include GoogleCalendar. No db/seeds/google_calendar_nodes.rb. |
| After delta | All canonical files exist (executor, HTTP client, lookup resources, seed) and the two registry files are updated. Seed produces exactly one node_registries row for the Calendar booking action and is idempotent (re-running does not duplicate). Specs cover executor happy path, lookup happy path, refresh-on-401, seed idempotency. |
| User story | As the Qontak Chatbot backend, I want a google_calendar provider module that mirrors the proven mekari_qontak_crm pattern, so that the FE-facing OAuth, lookup, and booking stories have a working backend to call into. |
| UI Reference | N/A — backend infrastructure |
Acceptance Criteria (ATDD / Gherkin)
Scenario: AC-1 — Executor file exists and mirrors mekari_qontak_crm
Type: Happy path — file existence
Given the BE team implements GCAL-S08
When the codebase is inspected
Then app/core/repositories/node_executions/nodes/google_calendar/execute.rb exists
And it defines Repositories::NodeExecutions::Nodes::GoogleCalendar::Execute
And the structure mirrors Repositories::NodeExecutions::Nodes::MekariQontakCrm::Execute
And it uses process_path for path interpolation
Scenario: AC-2 — HTTP client implements refresh-on-401 via Google's token endpoint
Type: Happy path — refresh-on-401
Given app/core/repositories/node_resources/google_calendar/calendar_http_client.rb exists
And it defines Repositories::NodeResources::GoogleCalendar::CalendarHttpClient
When the client receives a 401 from a Google Calendar API call
Then it POSTs to https://oauth2.googleapis.com/token with grant_type=refresh_token
and the stored refresh_token from organization_connections.auth_data for (org_id, 'google_calendar')
And on success, updates organization_connections.auth_data with new access_token + expires_at
And retries the original request exactly once
And on refresh failure, marks the credential as connection-lost (cascades to GCAL-S05)
Scenario: AC-3 — Lookup module exposes resource_key='calendar'
Type: Happy path — lookup contract
Given app/core/repositories/node_resources/google_calendar/lookup_resources.rb exists
And it defines Repositories::NodeResources::GoogleCalendar::LookupResources
When the FE invokes lookup with resource_key='calendar'
Then the module calls GET https://www.googleapis.com/calendar/v3/users/me/calendarList?fields=items(id,summary,primary)
And returns each item as {name: summary, value: id}
And marks the primary calendar with a badge / sorts it to the top
Scenario: AC-4 — node_type_registry registers google_calendar executor
Type: Constraint — registry wiring
Given app/core/repositories/node_executions/node_type_registry.rb is updated
When the registry is inspected
Then it contains the mapping 'google_calendar' => 'GoogleCalendar::Execute'
Scenario: AC-5 — PROVIDER_MODULES registers GoogleCalendar
Type: Constraint — registry wiring
Given app/api/frontend_service/v1/node_resources/use_cases/lookup_resources.rb is updated
When the PROVIDER_MODULES constant is inspected
Then it includes GoogleCalendar (alongside MekariQontakCrm and any other native providers)
Scenario: AC-6 — Seed creates exactly one node_registries row and is idempotent
Type: Idempotency — re-runnable seed
Given db/seeds/google_calendar_nodes.rb exists
When `rails db:seed:google_calendar_nodes` (or equivalent) is run on a fresh DB
Then exactly one row is created in node_registries for the Google Calendar booking action
And the row's properties JSON follows the canonical RFC §6 shape, with the 8 fields:
calendar_id (select, is_rl=true, resource_types=[{provider:'google_calendar', resource_key:'calendar'}], required, destination=body)
duration_minutes (input/number, required, validation min=5/max=480, destination=body)
lookup_window_days (slider, required, validation min=1/max=30/step=1, destination=body)
event_title_pattern (input/text, required, destination=body, placeholders documented)
location (input, optional, destination=body)
event_description (textarea, optional, destination=body)
attendees_customer_email_field (select, required, destination=body, options from conversation context)
attendees_always_cc (input, optional, destination=body)
When the seed is re-run
Then no duplicate row is created and the existing row is updated in place (upsert semantics)
Scenario: AC-7 — Spec coverage
Type: Quality — spec coverage
Given the spec/ tree is inspected
Then specs exist covering:
executor happy path (mirrors mekari_qontak_crm executor specs)
lookup happy path (calendarList API → {name, value} mapping)
refresh-on-401 (401 → token refresh → retry → success)
seed idempotency (re-run does not duplicate)
Errors / Unhappy Path
Scenario: ERR-1 — Seed runs against a DB where the row exists but is malformed
Given a node_registries row for google_calendar exists but is missing required properties keys
When the seed re-runs
Then the seed upserts the canonical properties JSON, overwriting the malformed values
And an info log records the upsert (not an error)
Scenario: ERR-2 — Refresh-on-401 fails with invalid_grant
Given the HTTP client receives 401 and the refresh attempt returns invalid_grant
When the failure propagates
Then the credential is marked connection-lost in organization_connections
And the cascade to GCAL-S05 fires (N2 inline OAuth status row → connection-lost state)
| Aspect | Detail |
|---|
| Permission — CAN | BE team (code merge); no runtime user trigger |
| Permission — CANNOT | External callers |
| UI — Backend story | No UI of its own; powers S01, S02, S03, S04, S05, S07 |
| Files — New | app/core/repositories/node_executions/nodes/google_calendar/execute.rb, app/core/repositories/node_resources/google_calendar/calendar_http_client.rb, app/core/repositories/node_resources/google_calendar/lookup_resources.rb, db/seeds/google_calendar_nodes.rb, specs |
| Files — Modified | app/core/repositories/node_executions/node_type_registry.rb, app/api/frontend_service/v1/node_resources/use_cases/lookup_resources.rb |
| Deliverable — follow-up RFC | Exact node_registries.properties JSON for the Calendar booking action — owned by Eko + PM, mirroring RFC §6 Sheets format. |
Story GCAL-S09 — Feature flag rollout: FEATURE_GOOGLE_CALENDAR_NODES
No designer review needed — rollout control; no UI of its own. Mirrors the canonical rollout pattern per RFC Google Sheets Workflow Nodes.
| Field | Value |
|---|
| Priority | Must Have — the entire Phase 1 ships behind this flag; no way to control rollout otherwise. |
| Before state | No FEATURE_GOOGLE_CALENDAR_NODES flag exists. Native integrations row would be visible to all orgs as soon as the seed merges. |
| After delta | Flag exists, default OFF. When OFF: the Google Calendar row is hidden from the Action drawer (Other integration group) and the OAuth handshake endpoint returns 404. When ON per-organization: full Phase 1 flow available for that org only. Closed Beta enabling tracked per ANCHOR S8a (2026-05-06) — Citroen + 2-3 friendly customers. |
| User story | As the Qontak Chatbot rollout owner, I want a per-organization feature flag controlling Google Calendar visibility and reachability, so that Closed Beta can validate with a small cohort before global enable. |
| UI Reference | N/A — backend rollout control |
Acceptance Criteria (ATDD / Gherkin)
Scenario: AC-1 — Flag exists, default OFF
Type: Constraint — flag wiring
Given the BE team implements GCAL-S09
When the feature flag service is inspected
Then FEATURE_GOOGLE_CALENDAR_NODES is registered
And its default value is OFF
And it is scopable per organization
Scenario: AC-2 — When OFF, Native integrations row is hidden
Type: Happy path — flag OFF
Given FEATURE_GOOGLE_CALENDAR_NODES = OFF for org X
When an Admin/Supervisor in org X opens the Action drawer
Then the `Google Calendar` row is NOT rendered in the `Other integration` group
And the OAuth handshake endpoint returns HTTP 404 (not 403, not 401)
And no `Google Calendar`-related UI surfaces are reachable for org X
Scenario: AC-3 — When ON per-organization, full Phase 1 flow is available
Type: Happy path — flag ON
Given FEATURE_GOOGLE_CALENDAR_NODES = ON for org Y
When an Admin/Supervisor in org Y opens the Action drawer
Then the `Google Calendar` row IS rendered in the `Other integration` group (per GCAL-S01 AC-1)
And the OAuth handshake endpoint is reachable (per GCAL-S01)
And the per-action details form (Screen 3) opens normally
And all P1 stories (S01, S02, S03, S04, S05, S07) function end-to-end for org Y
Scenario: AC-4 — Closed Beta enables per-organization for Citroen + 2-3 friendlies
Type: Rollout — Closed Beta
Given Closed Beta begins (per S5 Rollout table — late May / June 2026)
When the rollout owner (PM + CSM) enables FEATURE_GOOGLE_CALENDAR_NODES per org
Then it is enabled for Citroen + 2-3 friendly customers (named in S5 / ANCHOR S8a)
And not enabled for any other org
And monitoring (per S6 Observability + S6.5) tracks latency, refresh failures, quota for the enabled cohort only
Scenario: AC-5 — Global enable gated on one sprint of monitoring
Type: Rollout — Open Beta / GA gate
Given Closed Beta has run for 4 weeks
And the S6c Open Beta success gate is met
And one sprint (2 weeks) of monitoring shows latency, refresh failures, and quota within S6b thresholds
When the rollout owner enables FEATURE_GOOGLE_CALENDAR_NODES globally
Then all eligible orgs (Native Integrations add-on purchasers, per S4) see the `Google Calendar` row
Errors / Unhappy Path
Scenario: ERR-1 — Flag service unavailable mid-request
Given the FE requests the Action drawer
When the feature flag service is unreachable
Then the system fails CLOSED (treats flag as OFF) — the `Google Calendar` row is hidden
And an ops alert fires
And no 5xx is surfaced to the user
Scenario: ERR-2 — Org transitions from ON to OFF mid-session
Given an Admin was using the per-action details form with the flag ON
When the rollout owner toggles the flag OFF for that org
Then any in-flight save attempt returns 404 (consistent with AC-2)
And the Admin sees a non-destructive message: "Google Calendar is not available for your account."
And existing organization_connections.google_calendar rows remain in the DB (no cascade purge from flag toggle)
| Aspect | Detail |
|---|
| Permission — CAN toggle | Rollout owner (PM + Platform Eng), via the feature flag service admin UI |
| Permission — CANNOT | End users (Admin, Supervisor, etc.) — flag is opaque to UI |
| UI — Backend story | No UI of its own; governs visibility of all S01–S08 surfaces |
| Threshold targets | See S6b Success Metrics for monitoring thresholds gating global enable |
Negative Scenarios (mapped to S3 Non-Goals)
Scenario: NEG-1 — No other Google products in P1 (S3 #1)
Given the admin opens the Action drawer
When they search/scroll for Sheets / Drive / Gmail in the `Other integration` group
Then no Sheets / Drive / Gmail rows are present in P1
And the only Phase 1 native row in `Other integration` is `Google Calendar`
Scenario: NEG-2 — No other calendar providers in P1 (S3 #2)
Given the admin searches for Outlook / Apple Calendar in the Action drawer
When they look at the `Other integration` group
Then no Outlook / Apple rows are visible in P1
Scenario: NEG-3 — Single shared calendar only in P1 (S3 #3)
Given the admin tries to assign different reps to different bookings
When they open the Action details form (Screen 3, extends E7)
Then no per-rep / per-team controls are shown
And the admin sees one Calendar dropdown field per attached Calendar action only
Scenario: NEG-4 — No reschedule/cancel by agent (S3 #4)
Given the customer asks the agent to reschedule an existing booking
When the agent processes the request at runtime
Then the agent cannot invoke a reschedule action (it does not exist in P1)
And the prompt logic (out of scope) hands off to a human
Scenario: NEG-5 — No two-way sync (S3 #5)
Given the rep changes an event time directly in Google Calendar
When the change occurs on Google's side
Then Qontak's view of the booking remains stale (no automatic update)
And if the customer asks the agent about the event, the agent has no knowledge of the change
And this is out of P1 scope
Scenario: NEG-6 — Single events only (S3 #6)
Given the customer asks "book me every Tuesday for the next 4 weeks"
When the agent processes the request
Then the agent cannot invoke recurring (can only create one event per create_event call)
And the prompt logic handles "I can only book one at a time"
Scenario: NEG-7 — No group bookings (S3 #7)
Given the customer asks "book a session for me and 3 colleagues"
When the agent processes the request
Then the agent creates an event with a single customer attendee
And if the customer asks to add others, the prompt logic hands off
Scenario: NEG-8 — No calendar widget (S3 #8)
Given the customer wants to see a visual calendar
When the agent presents booking options
Then the agent presents slot options as text/buttons in chat only
And no embedded calendar UI is rendered
Scenario: NEG-9 — No WhatsApp/SMS reminders from Qontak (S3 #9)
Given the customer expects a WhatsApp reminder 1hr before
When the booking is created
Then Google sends standard email reminder (per Google Calendar settings)
And no Qontak-side WhatsApp/SMS reminder fires
Scenario: NEG-10 — Data residency (S3 #10)
Given the customer asks if their data stays in Indonesia
When they raise this in a sales conversation
Then the honest answer is:
Customer's calendar data is in Google's regions, governed by their own Google
Workspace data residency policy. Not a Qontak guarantee.
Scenario: NEG-11 — No LLM scope (S3 #11)
Given an AI coding agent consumes this PRD
When it generates acceptance criteria
Then the agent does NOT generate ACs that validate model behavior
(prompt selection, tool selection, fallback strategy)
And those concerns belong in the parent AI Agent runtime PRD
PRD CHANGELOG
| Version | Date | By | Section | Type | Summary |
|---|
| 1.0 | 2026-05-06 | Claude | All | CREATED | Initial NEW PRD. ANCHOR (Part A) created alongside. |
| 1.1 | 2026-05-07 | Claude | S7, S11, S13 | MODIFIED | Polish: Component Tree diagram fixed; Dependency Graph in S7; GCAL-S05 → Should Have; explicit Flow Name + Type. Score 9.5 → 9.7. |
| 1.2 | 2026-05-07 | Claude | Header, S4, S6, S8, S9, S11, S12, S13 | MODIFIED | Added GCAL-S07 (auto-refresh OAuth) as Must Have; AI SDLC + MCP Pixel pattern. MoSCoW: 6 Must · 1 Should. |
| 1.3 | 2026-05-12 | Claude | S13 | MODIFIED | Expanded all 7 stories with ATDD content (table format). |
| 1.4 | 2026-05-15 | Claude | S13 | REFORMATTED | Rewrote all AC / Errors / Negatives as fenced Gherkin scenarios. |
| 1.5 | 2026-05-15 | Claude | New 🎨 Designer Review Notes section; inline 🎨 markers on UI-bearing stories (S01, S02, S05, S06); explicit "no designer review needed" notes on backend stories (S03, S04, S07); 🎨 marker added to S11 heading and S6c Internal Alpha owner row. | MODIFIED | Added explicit Designer Review Notes section after blockquote header, identifying which sections Wulan + Rizky need to review (S11 + UI-bearing stories) vs which are backend-only. Includes review gate definition and fallback path (manual UI build in Phase 1.5 if MCP Pixel quality fails). |
| 1.6 | 2026-05-15 | Claude | S11, top callout | REALIGNED | Replaced MpTabs + NativeIntegrationsTabPanel with the existing Figma drawer pattern: Google Calendar = single row in a new "Native integrations" product group. Added E1–E8 reuse table + N1–N8 new-components table. Cross-linked Wireframes v3.0. |
| 1.7 | 2026-05-19 | Claude | S11, top callout | REWRITTEN | Major realignment after Gap Analysis of v3.0 implementation drift. IA collapsed: no separate Google Calendar config surface; per-action templates per PM decision (a); calendar fields embedded in existing E7 Action details form schema. Dropped 7 v1.5 components as inventions. Added V1–V7 outstanding verifications. Component count 11 → 8. |
| 1.8 | 2026-05-22 | Claude | Header, S11, S13 (Flow + GCAL-S01 + NEG-1/NEG-2) | REALIGNED | S11 IA correction to Wireframes v4.1 (Figma 2026-05-22). Cancels v1.6/v1.7 invention of a new "Native integrations" sibling group: native rows live under the existing Other integration group of the action drawer (5 groups, fixed order: Mekari Qontak / Talenta / Jurnal / Desty / Other integration). Phase 1 adds exactly one row: Google Calendar. Replaced drawer states list with the 4 canonical P0 states from Figma (Collapse/expand, Scrolled, Search found, Search not found) and moved Loading + Failed-to-load to P1 under new V8 verification. Screen 3 (per-action details form) clarified as Pixel MCP-only spec — no Figma node required (V2 dropped for Screen 3). Added hard brand-asset guard: every brand SVG slot must be marked [BRAND_SVG_PENDING_V4 — DO NOT CODE-GEN WITHOUT REPLACING]; code-gen forbidden until V4 closes; no MpIcon name="calendar" placeholder names. V1 marked ✅ RESOLVED 2026-05-22 (single-column right-side drawer). Added V8 (runtime drawer states). S13 Flow steps 3/4/8/9 and GCAL-S01 (After delta, UI Reference, AC-1/AC-2/AC-3/AC-5/AC-6, Permission Unauthorized row) and NEG-1/NEG-2 retargeted from "Native Integrations tab" + GoogleCalendarConfigSection + OAuthConnectionCard + CalendarStatusBanner to "Google Calendar row in Other integration group" + per-action details form (Screen 3, Pixel MCP-only) + inline OAuth status row. Cross-links updated to Native Integrations ANCHOR v1.2 and Mekari Action ANCHOR v1.3. |
| 1.9 | 2026-05-22 | Claude | Header, S11 V3 verification gate | MODIFIED | MCP replacement. Canonical design-system MCP is now https://ai.mekari.design/mcp (replaces the deprecated pixel-mcp.netlify.app). Updated V3 verification gate in S11. No IA or scope changes. |
| 1.10 | 2026-05-22 | Claude | Top Designer Review Notes, S11 cross-links, GCAL-S02, GCAL-S03, GCAL-S05, GCAL-S06, GCAL-S07, NEG-3 | REALIGNED | Story-body cleanup. Realigned GCAL-S02, S05, S06, S07 + top Designer Review Notes to v4.2 IA: replaced references to dropped components (TemplateEditorModal, TemplateList, CalendarStatusBanner, OAuthConnectionCard, CalendarPicker, GoogleCalendarConfigSection, Calendar-variant DeleteAgentModal) with the corresponding v4.2 surfaces (Screen 3 Action details form / N2 inline OAuth status row / Calendar dropdown field / parent AI Agent delete modal). GCAL-S02 retargeted from "template list/editor" surfaces to "one booking template = one attached action row" model: Action details form (Screen 3, extends E7) is the editor; attached actions surface as rows in the Actions tab populated state (Screen 1, extends E3). GCAL-S05 retargeted CalendarStatusBanner state machine to N2 inline OAuth status row state machine inside Screen 3. GCAL-S06 retargeted Calendar-variant delete modal to parent AI Agent delete modal (Screen 5) with cascadeWarnings prop. GCAL-S07 and S03 backend-cascade notes updated to reference the N2 inline OAuth status row. NEG-3 retargeted from CalendarPicker per-template to Calendar dropdown field per attached action. Applied [BRAND_SVG_PENDING_V4] guard sweep — no Mekari product MpIcon placeholders found inside MCP Pixel prompt code blocks (only inside §Brand asset guard / Hard NOs as explicit DO-NOT-USE examples, preserved). Updated stale Wireframes cross-links from v4.1/rev 5 to v4.2/rev 6 throughout S11 (8 refs). No IA, scope, AC count, or constraint changes — only realigning the prose to the canonical Wireframes v4.2. |
| 1.11 | 2026-05-22 | Claude | Header (PRD Version, RFC Link), S6 (new S6.6 BE Architecture), S12 (new S12.1 Credential Storage & OAuth), S8a (new decision row), S9 (3 new open questions #13/#14/#15), GCAL-S01/S02/S05/S07 (backend implementation cues), GCAL-S08 + GCAL-S09 (new backend stories), Story Index, Changelog | MODIFIED | RFC alignment (full BE). Encoded BE-canonical auth + architecture per RFC Google Sheets Workflow Nodes into S6 (new S6.6 BE Architecture subsection — file inventory mirroring mekari_qontak_crm), S12 (new S12.1 Credential Storage & OAuth subsection — organization_connections table, https://oauth2.googleapis.com/token, refresh-on-401, quota note), S8a (new decision row dated 2026-05-22 — BE infrastructure mirrors mekari_qontak_crm; credential canonical = organization_connections; single credential per org per provider; credential resolved by HTTP client using org_id + provider code, NOT a user-facing property), GCAL-S01 (backend cue — organization_connections row write/upsert), GCAL-S02 (backend cue — LookupResources pattern with resource_key='calendar'), GCAL-S05 (backend cue — connection-lost via refresh-on-401), GCAL-S07 (backend cue — canonical HTTP client GoogleCalendar::CalendarHttpClient + token endpoint + storage column). Added GCAL-S08 (BE infra: executor + HTTP client + lookup resources + node_registries seed; 7 ACs + 2 ERRs + file inventory) and GCAL-S09 (FEATURE_GOOGLE_CALENDAR_NODES rollout: default OFF, per-org enable, Closed Beta → global; 5 ACs + 2 ERRs). Flagged 3 new open questions in S9 (#13 design-system HTML elements, #14 userinfo.email scope, #15 "per org per provider" vs ANCHOR's "per AI Agent" credential framing tension — needs follow-up alignment). No UX label changes; no Gherkin AC scope changes on existing stories (only ADDED backend-cue blockquotes below the existing ACs); MoSCoW priorities unchanged; S11 untouched; Wireframes v4.2 cross-links preserved. Story count 7 → 9 (all new stories are backend Must Have). |