diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md deleted file mode 100644 index e8b1539b..00000000 --- a/ARCHITECTURE.md +++ /dev/null @@ -1,249 +0,0 @@ -# Buzz App Architecture Notes - -## Overview -- Buzz is a Frappe app for event management with a Vue 3 (FrappeUI) dashboard and Frappe Builder/public pages. -- Backend is organized into Frappe modules: `Buzz`, `Events`, `Ticketing`, `Proposals`. -- Frontend dashboard lives in `dashboard/`, built with Vite and published into `buzz/public/dashboard` and `buzz/www/dashboard.html`. - -## Backend Structure -- App bootstrap and hooks: `buzz/hooks.py` - - Requires `frappe/payments`. - - Scheduler: `buzz.tasks.unpublish_ticket_types_after_last_date` (daily). - - Doc events: assigns `Buzz User` role on user creation; syncs Speaker Profile display name on User update. - - App icon entry in Desk apps screen. -- Core API: `buzz/api.py` - - Booking, tickets, add-ons, sponsorship, check-in, languages, translations. -- Payments integration: `buzz/payments.py` - - Creates `Event Payment` records and generates payment links via the Payments app. -- Utilities: `buzz/utils.py` - - App-install guard decorator, custom field helpers, role assignment. -- Install/fixtures: - - `buzz/install.py` creates default Event Categories and zoom integration fields if installed. - - `buzz/fixtures` and `buzz/patches` for setup and migrations. - -## Key DocTypes (Data Model) - -### Events Module -- `Buzz Event` (`buzz/events/doctype/buzz_event`) - - Primary entity; holds schedule, category, venue, ticketing/payment settings, sponsorship settings. - - Creates default `Sponsorship Tier` and `Event Ticket Type` after insert. - - Optional Zoom webinar creation/updates when `zoom_integration` is installed. -- `Event Category`, `Event Venue`, `Event Host` - - Category slug creation; venue location geojson and map embed cleanup. -- `Event Talk`, `Talk Speaker`, `Speaker Profile`, `Event Track` - - Talks can be generated from proposals; speaker profiles tied to User records. -- `Event Sponsor`, `Sponsorship Tier`, `Sponsorship Deck Item` - - Sponsor uniqueness enforced per enquiry. -- `Event Check In` - - Used for check-in records and attendance reporting. -- `Event Payment Gateway` - - Child table for multiple payment gateways per event. -- `Additional Event Page` - - Event-specific extra pages with route validation. - -### Ticketing Module -- `Event Booking` - - Holds attendees, pricing, tax, UTM parameters, custom fields. - - On submit: generates `Event Ticket` documents and applies add-ons/custom fields. -- `Event Ticket` - - Generates QR code, sends ticket email + print format attachment. - - Creates Zoom webinar registration (if enabled). - - Supports transfer/cancellation flows. -- `Event Ticket Type` - - Inventory logic; tracks max tickets and remaining count. -- `Ticket Add-on` + `Ticket Add-on Value` + `Attendee Ticket Add-on` - - Add-on definitions and per-ticket selections. -- `Bulk Ticket Coupon` - - Auto-generates code; limits usage via claimed count. -- `Ticket Cancellation Request` + `Ticket Cancellation Item` - - Cancel booking or specific tickets on acceptance. -- `Additional Field` - - Generic key/value for ticket/booking custom fields. - -### Proposals Module -- `Talk Proposal` - - Maps into `Event Talk` and auto-creates users/speaker profiles if needed. -- `Sponsorship Enquiry` - - Handles approval/payment flow; creates `Event Sponsor` on payment completion. - -### Buzz Module -- `Buzz Custom Field` - - Event-specific custom fields applied to bookings or tickets. -- `Buzz Settings` - - Central configuration for transfer/add-on/cancellation windows. - -## Backend Flows - -### Booking + Payment -1. Dashboard calls `buzz.api.get_event_booking_data` to load ticket types, add-ons, custom fields, and payment gateways. -2. `buzz.api.process_booking` creates an `Event Booking` with attendees, add-ons, custom fields, and UTM parameters. -3. If total is 0, booking is auto-submitted; otherwise `Event Payment` is created and a payment URL is returned. -4. On payment authorization, `Event Booking.on_payment_authorized` marks the payment received and submits the booking. -5. Booking submission creates `Event Ticket` records and triggers QR + email flow. - -### Ticket Lifecycle -- Ticket creation generates QR code file and email (with print format attachment). -- Transfers are handled via `buzz.api.transfer_ticket` with window checks from `Buzz Settings`. -- Add-on preference changes use `buzz.api.change_add_on_preference` with window checks. -- Cancellation requests are created via `buzz.api.create_cancellation_request` and are accepted/rejected in Desk. - -### Sponsorships -- Web form submits `Sponsorship Enquiry`. -- Approval moves status to `Payment Pending` and triggers email notification. -- Payment link is generated via `buzz.api.create_sponsorship_payment_link`. -- Payment authorization creates `Event Sponsor` and marks enquiry as paid. - -### Check-in -- Dashboard scanner validates tickets with `buzz.api.validate_ticket_for_checkin`. -- Successful check-in creates `Event Check In` and returns event/ticket context. - -## API Surface (Whitelisted) -- Booking: `get_event_booking_data`, `process_booking`, `get_booking_details`, `create_cancellation_request`. -- Ticket actions: `get_ticket_details`, `transfer_ticket`, `change_add_on_preference`. -- Sponsorships: `get_sponsorship_details`, `get_user_sponsorship_inquiries`, `create_sponsorship_payment_link`, `withdraw_sponsorship_enquiry`. -- Check-in: `validate_ticket_for_checkin`, `checkin_ticket`. -- Payments: `get_event_payment_gateways` (plus payment helpers in `buzz/payments.py`). -- User + i18n: `get_user_info`, `get_enabled_languages`, `update_user_language`, `get_translations`. - -## Reports -- Events: - - `Event Overview` (tickets sold, add-ons sold, sales) in `buzz/events/report/event_overview`. - - `Event Attendance Summary` with dynamic per-day check-ins and chart in `buzz/events/report/event_attendance_summary`. -- Ticketing: - - `Event Add-Ons Overview` in `buzz/ticketing/report/event_add_ons_overview`. - - `Detailed Event Registrations` with dynamic custom fields, add-ons, UTM params in `buzz/ticketing/report/detailed_event_registrations`. - -## Public Pages + Web Forms -- Dashboard is served via `buzz/www/dashboard.html` (built from `dashboard/` output). -- Web forms: - - `Apply for Sponsorship` and `Propose a Talk` under `buzz/buzz/web_form/`. -- Frappe Builder assets are under `buzz/builder_files` and `buzz/public/builder_files` (currently empty stubs/asset containers). - -## Frontend (Vue Dashboard) - -### Entry + Build -- Entry: `dashboard/src/main.js` mounts `App.vue` with router, resources, translation plugin, and socket. -- Build: `dashboard/vite.config.js` outputs to `buzz/public/dashboard` and updates `buzz/www/dashboard.html`. -- Base URL: `/dashboard` (router history uses `createWebHistory("/dashboard")`). - -### Routing -- Public-like flows: booking (`/book-tickets/:eventRoute`) and check-in (`/check-in`). -- Account area under `/account`: - - bookings list/details, tickets list/details, sponsorships list/details. -- Guard: `router.beforeEach` checks `buzz.api.get_user_info` and redirects to `/login` if unauthenticated. - -### Data Access Pattern -- Uses `frappe-ui` `createResource` and `createListResource` for API calls. -- API endpoints map directly to backend whitelisted methods (see API Surface above). -- Session state in `dashboard/src/data/session.js` handles login/logout and cookie-based session user. - -### Key UI Components -- Booking flow: `BookingForm.vue`, `PaymentGatewayDialog.vue`, `BookingSummary.vue`. -- Ticket management: `TicketDetails.vue`, `TicketTransferDialog.vue`, `AddOnPreferenceDialog.vue`, `CancellationRequestDialog.vue`. -- Sponsorship management: `SponsorshipDetails.vue`, `SponsorshipPaymentDialog.vue`, `SponsorLogoUploader.vue`. -- Check-in: `CheckInScanner.vue`, `EventSelector.vue`, `QRScanner.vue`. -- Shared: `Navbar.vue`, `LanguageSwitcher.vue`, `SuccessMessage.vue`. - -### Notable Composables + Utils -- `useTicketValidation` handles check-in validation flow and audio feedback. -- `usePaymentSuccess` handles success banners + confetti and URL cleanup. -- `useBookingFormStorage` keeps draft booking data in localStorage per event. -- `useLanguage` changes user language and reloads translations. - -## Integrations -- Payments app required (gateway link generation + payment receipt recording). -- Zoom integration optional; `Buzz Event` can create/update webinars and register attendees. - -## Permissions + Roles -- Roles: `Buzz User`, `Frontdesk Manager` (fixtures in hooks). -- Check-in APIs restricted to `Frontdesk Manager`. -- Sponsorship details restricted to enquiry owner or users with permission. - -## Key Paths for Feature Work -- Booking logic: `buzz/api.py`, `buzz/ticketing/doctype/event_booking`, `dashboard/src/components/BookingForm.vue`. -- Ticket lifecycle: `buzz/ticketing/doctype/event_ticket`, `dashboard/src/pages/TicketDetails.vue`. -- Sponsorship lifecycle: `buzz/proposals/doctype/sponsorship_enquiry`, `dashboard/src/pages/SponsorshipDetails.vue`. -- Check-in flow: `buzz/api.py`, `dashboard/src/pages/CheckInScanner.vue`. -- Event configuration: `buzz/events/doctype/buzz_event` and related event doctypes. - -## Notes / Gotchas -- Payment gateway selection is event-scoped via `Event Payment Gateway` child rows. -- Event routes are auto-generated when publishing (`Buzz Event`, `Additional Event Page`). -- Custom fields are event-scoped (`Buzz Custom Field`) and can apply to booking or ticket. -- Ticket type availability is enforced at booking time, plus daily auto-unpublish by scheduler. - -## Data Model Diagram (DocTypes + Relationships) -```mermaid -erDiagram - BUZZ_EVENT ||--o{ EVENT_TICKET_TYPE : has - BUZZ_EVENT ||--o{ TICKET_ADD_ON : has - BUZZ_EVENT ||--o{ EVENT_BOOKING : has - BUZZ_EVENT ||--o{ EVENT_SPONSOR : has - BUZZ_EVENT ||--o{ SPONSORSHIP_TIER : has - BUZZ_EVENT ||--o{ BUZZ_CUSTOM_FIELD : has - BUZZ_EVENT ||--o{ EVENT_CHECK_IN : has - BUZZ_EVENT ||--o{ ADDITIONAL_EVENT_PAGE : has - EVENT_BOOKING ||--o{ EVENT_BOOKING_ATTENDEE : has - EVENT_BOOKING ||--o{ ADDITIONAL_FIELD : has - EVENT_BOOKING ||--o{ UTM_PARAMETER : has - EVENT_BOOKING ||--o{ EVENT_TICKET : generates - EVENT_TICKET ||--o{ TICKET_ADD_ON_VALUE : has - EVENT_TICKET ||--o{ ADDITIONAL_FIELD : has - EVENT_TICKET_TYPE ||--o{ EVENT_TICKET : sold_as - ATTENDEE_TICKET_ADD_ON ||--o{ TICKET_ADD_ON_VALUE : has - TICKET_CANCELLATION_REQUEST ||--o{ TICKET_CANCELLATION_ITEM : includes - TICKET_CANCELLATION_REQUEST }o--|| EVENT_BOOKING : cancels - EVENT_CHECK_IN }o--|| EVENT_TICKET : for - SPONSORSHIP_ENQUIRY }o--|| BUZZ_EVENT : for - SPONSORSHIP_ENQUIRY ||--o{ EVENT_SPONSOR : creates - TALK_PROPOSAL }o--|| BUZZ_EVENT : for - TALK_PROPOSAL ||--o{ EVENT_TALK : maps_to -``` - -## Frontend Page -> API Map -- `dashboard/src/pages/BookTickets.vue` - - `buzz.api.get_event_booking_data` to load event booking config. - - `buzz.api.process_booking` via `dashboard/src/components/BookingForm.vue`. -- `dashboard/src/pages/BookingDetails.vue` - - `buzz.api.get_booking_details` for booking + ticket data. - - `buzz.api.create_cancellation_request` via `dashboard/src/components/CancellationRequestDialog.vue`. - - `buzz.api.transfer_ticket` via `dashboard/src/components/TicketsSection.vue`. -- `dashboard/src/pages/TicketDetails.vue` - - `buzz.api.get_ticket_details`. - - `buzz.api.change_add_on_preference` via `dashboard/src/components/AddOnPreferenceDialog.vue`. - - `buzz.api.transfer_ticket` via `dashboard/src/components/TicketTransferDialog.vue`. -- `dashboard/src/pages/SponsorshipsList.vue` - - `buzz.api.get_user_sponsorship_inquiries`. -- `dashboard/src/pages/SponsorshipDetails.vue` - - `buzz.api.get_sponsorship_details`. - - `buzz.api.withdraw_sponsorship_enquiry`. - - `buzz.api.get_event_payment_gateways` and `buzz.api.create_sponsorship_payment_link` via `dashboard/src/components/SponsorshipPaymentDialog.vue`. -- `dashboard/src/pages/CheckInScanner.vue` - - `buzz.api.validate_ticket_for_checkin` and `buzz.api.checkin_ticket` via `dashboard/src/composables/useTicketValidation.js`. -- `dashboard/src/data/user.js` - - `buzz.api.get_user_info` for session user details and roles. -- `dashboard/src/composables/useLanguage.js` - - `buzz.api.get_enabled_languages`, `buzz.api.update_user_language`. -- `dashboard/src/translation.js` - - `buzz.api.get_translations`. - -## Feature Development Checklist (Common Changes) -- Tickets / booking changes - - Update `buzz/api.py` for API shape changes and `Event Booking`/`Event Ticket` doctypes for business logic. - - Keep `BookingForm.vue` payload mapping in sync with backend expectations. - - Update reports if new fields should be exported (`Detailed Event Registrations`). -- Add-on or custom field changes - - Check `Buzz Custom Field` logic and `Additional Field` usage in booking/tickets. - - Update frontend rendering in `CustomFieldInput.vue` and `BookingForm.vue`. -- Payment flow changes - - Update `buzz/payments.py` and any event-scoped gateway selection logic. - - Ensure payment redirects still land on `/dashboard/...?...success=true`. -- Sponsorship flow changes - - Update `Sponsorship Enquiry` for status transitions and `SponsorshipDetails.vue` for UI state. - - Verify sponsor creation in `on_payment_authorized`. -- Check-in changes - - Update `buzz.api.validate_ticket_for_checkin` and `CheckInScanner.vue`/`useTicketValidation.js`. - - Confirm role gating for `Frontdesk Manager` remains consistent. -- Event publishing or public pages - - Review `Buzz Event` route generation and `Additional Event Page` route uniqueness. - - If using Builder, update assets under `buzz/builder_files` and publish to `buzz/public/builder_files`. diff --git a/CLAUDE.md b/CLAUDE.md index dce9c33d..15808650 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,368 +1,50 @@ -# CLAUDE.md +## Implementation -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +Guidelines for writing good code for a developer: -At the start of every session, read `.claude/memory/memory.md` to load project context. -After completing significant work (new patterns, architectural decisions, solved problems), -update `.claude/memory/memory.md`. Keep it under 300 lines — summarize when it grows. +1. Choose clean code over clever code. +2. Write object oriented code as much as possible. +3. Keep function sizes small, ideally 10 lines. +4. Try and keep files between 100 and 300 lines. +5. Don't keep too many files in a folder or module. Try and keep it under 15. +6. Avoid abbreviations. +7. Use standard API as much as possible. +8. Reuse. Write as little code as possible. +9. Use Frappe UI, espresso design system for UI styling. +10. Always write tests, and make sure they work. +11. Build the minimum working app, then iterate towards your goals. +12. Keep the verbosity less in new changes (inline comments, docstrings erc). + Explain only what's absolutely needed in inline comments. + Actual changes explanation can be part of commit message. ---- +## DocType -## This defines the best practices to write backend code in the Frappe Framework +* Use column breaks and tab breaks to create user friendly doctype forms -* Frappe Framework is a full-stack web application framework that contains all the necessary components for building modern web applications. -* It provides background workers using Redis, real-time updates using sockets, and a database layer using MariaDB. -* Bench is the official command-line tool for managing Frappe applications. +## Skills -## Backend Development +Always load frappe-app-dev and frappe-ui skills before any implementation -### JSON & Request Handling +## Planning -* Always use built-in functions for parsing JSON: +For creating specs use tracer bullet approach. - * `frappe.parse_json` (handles dicts, lists, and JSON strings safely) +> Tracer bullets comes from the Pragmatic Programmer. When building systems, you want to write code that gets you feedback as quickly as possible. Tracer bullets are small slices of functionality that go through all layers of the system, allowing you to test and validate your approach early. This helps in identifying potential issues and ensures that the overall architecture is sound before investing significant time in development. -* Never use `json.loads` directly on request data. +Create specs in `specs/`. Maintain a `PROGRESS.md` file to track progress of implementation phases. -* For outbound HTTP requests (calling external APIs), use: +## Commit / PR - * `frappe.integration.utils.make_get_request` - * `frappe.integration.utils.make_post_request` - * `frappe.integration.utils.make_put_request` - * `frappe.integration.utils.make_patch_request` +* Always use conventional commits +* Commit the spec before you begin +* Reconcile the specs after implementation -### Datatype Conversion & Utilities +## Testing -* For converting datatypes (e.g. str → int, str → float, etc.) use built-in helpers: +Use agent-browser for quick manual e2e checks. - * `frappe.utils.data.cint` - * `frappe.utils.data.cstr` - * `frappe.utils.data.flt` - * `frappe.utils.data.getdate` - * `frappe.utils.data.get_datetime` +Automated e2e uses Playwright (root `package.json`, specs in `e2e/`). -* `frappe.utils.data` contains most conversion and formatting helpers you will ever need: +## Credentials - * date / datetime parsing - * currency formatting - * number formatting - -* Do NOT create custom utility functions for these conversions. - -* If unsure, ask before implementing. - -### DocType Access Patterns - -* When fetching an existing DocType, prefer: - - * `frappe.get_cached_doc` - -* Use `frappe.get_doc` when: - - * creating a new document - - * To create a new doc go to bench console via bench --site sitename console and use frappe.new_doc("DocType") and then create the doc, don't create the doc via json as the validations doesn't run - - -### Optimization - -* Don't use get_doc or get_cached_doc inside for loop it creates n+1 db problem use frappe.get_all with all the params required and then loop over that list - -### Database Access - -* Prefer ORM methods: - - * `frappe.get_all` - * `frappe.get_list` - * `frappe.db.get_value` - -* Avoid raw SQL absolutely. - - -### Permissions & Security - -* Always respect user permissions. -* Use `ignore_permissions=True` only when absolutely required and justified. - -### Background Jobs & Performance - -* For long-running or heavy operations, always use: - - * `frappe.enqueue` - -* Never block request-response cycles with heavy business logic. - -### Error Handling & Logging - -* Use `frappe.throw` or specific exceptions like `frappe.ValidationError` for user-facing errors. -* Use `frappe.log_error` for unexpected or system-level exceptions. -* Avoid bare `except:` blocks. - -### General Guidelines - -* Prefer framework conventions over custom implementations. -* Keep business logic out of controllers where possible. -* Write readable, predictable, and maintainable code. - - - -## Frontend Development - -1. Always use async/await; avoid callback-based patterns and nested promises. - -2. Use Frappe-provided APIs for server calls: `frappe.call` with `async: true`. Prefer Promise-based usage over callbacks. - -3. Use Frappe's global JS helpers instead of native JS equivalents: - * `cstr()` instead of `String()` - * `cint()` instead of `parseInt()` - * `flt()` instead of `parseFloat()` - * `is_null()` instead of manual null/undefined/empty checks - * `format_currency()` for currency formatting - - -## Crawling - -Always use gemini as much as possible for getting the context, to get the help use gemini --help - -For checking if the site works you can use the agent-browser use agent-browser --help to get the context for it - - -## Commands - -### Frontend (Dashboard) -```bash -# dev server -yarn dev # or: cd dashboard && yarn dev - -# build for production -yarn build # outputs to buzz/public/dashboard + buzz/www/dashboard.html - -# lint/format frontend -cd dashboard && yarn lint -``` - -### Backend (Python) - -Always run bench migrate after doctype schema changes. - -```bash -# linting/formatting (via pre-commit) -pre-commit run --all-files - -# run ruff directly -ruff check buzz/ -ruff format buzz/ - -# install app to site -bench --site [site-name] install-app buzz -``` - -Use bench --help to see how to work with frappe bench, e.g. bench execute, bench console, etc. are very useful - -### Testing - -There are unit tests, run using bench run-tests. Site name is buzz.localhost, but if not found, ask user for it. The credentials are Administrator/admin. - -* To test in UI, use agent-browser. -* For frontend changes use :8080 since yarn dev server is running. -* Use in headed mode unless specified - -## Architecture - -**Three-tier stack:** -1. **Backend**: Frappe Framework (Python) - DocTypes, API, permissions, scheduler -2. **Dashboard**: Vue 3 + FrappeUI + Vite - attendee/sponsor/checkin UI - -**Core entity**: `Buzz Event` DocType drives everything (tickets, sponsors, schedule, payments). - -**Main modules** (inside `buzz/`): -- `events/` - Event, Venue, Category, Talks, Sponsors, Check-ins -- `ticketing/` - Bookings, Tickets, Add-ons, Cancellations, Coupons -- `proposals/` - Talk Proposals, Sponsorship Enquiries -- `buzz/` - Settings, Custom Fields -- `api.py` - whitelisted API methods for dashboard -- `payments.py` - integration with frappe/payments app - -**Frontend structure** (inside `dashboard/`): -- `src/pages/` - route components (BookTickets, TicketDetails, CheckInScanner, etc) -- `src/components/` - BookingForm, dialogs, shared UI -- `src/composables/` - reusable logic (useTicketValidation, usePaymentSuccess, etc) -- `src/data/` - frappe-ui resources for API calls -- Vite builds to `buzz/public/dashboard/`, router base is `/dashboard` - -**Key flows:** -- Booking: load event data → fill form → create booking → generate payment link → on payment auth → submit booking → generate tickets + QR + email -- Ticket actions: transfer, cancel, change add-on (window checks from Buzz Settings) -- Sponsorship: enquiry → approval → payment link → payment auth → create sponsor record -- Check-in: scan QR → validate → create check-in record (requires Frontdesk Manager role) - -**Integrations:** -- `frappe/payments` required for payment gateways -- `buildwithhussain/zoom_integration` optional for webinar creation/registration - -## Key Paths for Common Tasks - -**Booking changes**: `buzz/api.py`, `buzz/ticketing/doctype/event_booking/`, `dashboard/src/components/BookingForm.vue` - -**Ticket lifecycle**: `buzz/ticketing/doctype/event_ticket/`, `dashboard/src/pages/TicketDetails.vue` - -**Sponsorships**: `buzz/proposals/doctype/sponsorship_enquiry/`, `dashboard/src/pages/SponsorshipDetails.vue` - -**Check-in**: `buzz/api.py` (validate_ticket_for_checkin, checkin_ticket), `dashboard/src/pages/CheckInScanner.vue` - -**Event config**: `buzz/events/doctype/buzz_event/` - -**Reports**: `buzz/events/report/` and `buzz/ticketing/report/` - - -## Joining or creating report - "Never write `frappe.db.sql` again" -=========================================================== - -1. **Ban `frappe.db.sql` in new code** - * Add a pre-commit rule or CI step that greps for `\.db\.sql` and fails the build. - * Legacy code => wrap in `frappe.db.sql("...", as_dict=1)` and add a `# TODO-QB` comment so the next refactor is trackable. - -2. **Use the typed entry point** - ```python - from frappe.query_builder import DocType, Field - from frappe.query_builder.functions import Count, Sum, Coalesce, Date - ``` - Never `import pypika` directly; the `frappe.qb` namespace already returns the correct `MariaDB/PostgreSQL` dialect. - -3. **Parameterise, never interpolate** - ```python - # Bad - frappe.db.sql(f"... {user_input}") # injection bomb - # Good - frappe.qb.from_(...).where(table.field == user_input) # auto-escaped - ``` - -4. **Prefer joins over N+1** - ```python - so = DocType("Sales Order") - si = DocType("Sales Invoice") - query = ( - frappe.qb.from_(so) - .left_join(si) - .on(so.name == si.sales_order) - .select(so.name, si.name) - .where(so.customer == customer) - ) - ``` - One round-trip, no loops. - -5. **Sub-queries > raw SQL strings** - Need *"latest row per group"*? - ```python - latest = ( - frappe.qb.from_(si) - .select(si.name) - .where(si.sales_order == so.name) - .orderby(si.creation, order=Order.desc) - .limit(1) - ) - query = frappe.qb.from_(so).where(so.name == latest) - ``` - Keeps everything composable and dialect-agnostic. - -6. **Use `case` for conditional aggregates** - ```python - from frappe.query_builder.functions import Case - paid_amt = Sum( - Case() - .when(si.status == "Paid", si.grand_total) - .else_(0) - ) - ``` - -7. **Respect Frappe field casing** - * SQL column: `grand_total` - * Frappe field: `grand_total` - * No back-ticks needed; QB adds the correct quotes per DB. - -8. **Use `as_dict=True` or ORM objects** - ```python - rows = query.run(as_dict=True) # list[dict] - docs = query.run(as_dict=False) # list[tuple] - obj = frappe.get_doc("Doctype", pk) # when you need the full DocType hooks - ``` - -9. **Pagination with `limit_page_length` and `limit_start`** - ```python - query = query.limit(limit_page_length).offset(limit_start) - ``` - Same pattern the REST API uses. - -10. **Index-friendly WHERE order** - Put indexed columns first (`company`, `customer`, `status`) so MariaDB/PostgreSQL can use composite indexes. - -11. **Avoid `SELECT *` in reports** - Explicit list of fields keeps wire-size small and prevents breaking changes when new fields are added. - -13. **Cache heavy aggregations** - ```python - @frappe.whitelist() - @redis_cache(ttl=300) - def get_dashboard_stats(company): - inv = DocType("Sales Invoice") - total = frappe.qb.from_(inv).select(Sum(inv.grand_total)).where(inv.company == company).run() - return total[0][0] or 0 - ``` - - -Quick migration template ----------------------- - -Legacy: -```python -rows = frappe.db.sql(""" - select name, grand_total - from `tabSales Invoice` - where customer = %s - and docstatus = 1 -""", customer, as_dict=1) -``` - -QB equivalent: -```python -si = DocType("Sales Invoice") -rows = ( - frappe.qb.from_(si) - .select(si.name, si.grand_total) - .where((si.customer == customer) & (si.docstatus == 1)) - .run(as_dict=True) -) -``` - - -### Report Patterns -- Entry: `def execute(filters): return get_columns(), get_data(filters)` -- QB imports: `from frappe.query_builder import DocType` + `functions.Sum, Case, Count` -- Build lookup maps first, then loop + merge (avoid N+1) -- Caching: `@redis_cache(ttl=seconds)` for conversion factors - -### Token-Saving Workflow -- Default to `/model haiku` for routine edits, `/model sonnet` for moderate tasks -- Use `/model opus` ONLY for architecture, debugging complex issues -- Use `/compact` after completing each subtask -- Use `gemini -p "prompt"` via stdin to read/summarize files without burning Claude tokens -- Scope tasks narrowly: one feature/fix per session -- Dump progress to `.claude/memory/scratch.md` before session ends -- At session START: read `.claude/memory/scratch.md` — if it has content, resume from there -- At session END (or when user says "dump progress"): fill in scratch.md with current task state -- After resuming from scratch.md, clear it once the task is complete - - -### Variable and Function naming convention - -- always use full names for variables don't use abbreviations for ex: use -"for row in rows" instead of "for r in rows" -proxy_sku = DocType("Proxy SKU") instead of ps = DocType("Proxy SKU") -- avoid starting python functions with underscore "_" unless it's a private version behind a whitelisted function (e.g. `get_dial_codes` (whitelisted) -> `_get_dial_codes` (cached logic)) -- use camelCase in JS and follow the surrounding code style in the project -- always put imports at the top of the file, never inside functions - -## Notes - -- Read `ARCHITECTURE.md` for comprehensive details on data model, API surface, flows +The site is buzz.localhost:8000 (Administrator/admin) \ No newline at end of file diff --git a/entities.png b/entities.png deleted file mode 100644 index 95dbcb16..00000000 Binary files a/entities.png and /dev/null differ diff --git a/specs/v2/00-teams/00-plan.md b/specs/v2/00-teams/00-plan.md new file mode 100644 index 00000000..2c364f1c --- /dev/null +++ b/specs/v2/00-teams/00-plan.md @@ -0,0 +1,64 @@ +# 00-teams — Plan + +Multi-tenancy foundation. Everything in `01-dashboard/` assumes these steps have landed. + +## Steps + +| # | Step | Delivers | +|---|---|---| +| [01](01-team-doctypes-and-core-api.md) | Team doctypes & core API | `Buzz Team`, `Buzz Team Membership`, create/list/switch APIs, role fixtures, default team on install | +| [02](02-team-field-on-events-and-backfill.md) | Tenant field & backfill | `team` on `Buzz Event` + cascade to descendants, backfill patch | +| [03](03-permission-hooks.md) | Permission hooks | `buzz/permissions.py`, row-level isolation, guest-flow regression tests | +| [04](04-team-invitations.md) | Team invitations | User Invitation `after_accept` → membership creation | +| [05](05-team-settings.md) | Buzz Team Settings | per-team settings doctype split from site-wide `Buzz Settings`, seeded on create | + +Order is strict: 01 → 02 → 03 → 04 → 05. Steps 01–03 unblock the dashboard tracer bullet +(`01-dashboard/01`); steps 04–05 unblock the members/invites/settings UI (`01-dashboard/13`). + +## Design decisions (from earlier PRD research) + +- **Standalone junction doctype** (`Buzz Team Membership`), not a child table à la Gameplan's + `GP Member` — a user belongs to many teams and we query memberships by user constantly. +- **`team_role` is a Select enum** (`Owner/Admin/Manager/Frontdesk/Viewer`), not per-team Frappe + Role records. Frappe global roles stay as *capability* gates (`Event Manager`, `Frontdesk + Manager`, `Buzz User`); `team_role` gates *which team's data* and *what within it*. +- **Permission model = Gameplan pattern**: central `buzz/permissions.py` with + `permission_query_conditions` (row-level list scoping via membership subquery) + + deny-only `has_permission` hooks. Verified against + `frappe/model/db_query.py:get_permission_query_conditions` and + `frappe/permissions.py:has_controller_permissions` calling conventions. +- **Invitations reuse Frappe's `User Invitation`** (`frappe/core/doctype/user_invitation/`) via the + `user_invitation` hook's `extra_invite_params` + `after_accept` — already partially declared in + `buzz/hooks.py`. + +## Tenant doctype inventory + +**Team-direct** (user picks/creates under a team): `Buzz Event`, `Event Venue`, `Event Host`, +`Event Template`, `Buzz Campaign`. + +**Derived-from-event** (`team` stamped from linked `event`): `Event Ticket Type`, `Ticket Add-on`, +`Buzz Coupon Code`, `Sponsorship Tier`, `Event Sponsor`, `Event Booking`, `Event Ticket`, +`Event Payment`, `Event Check In`, `Sponsorship Enquiry`, `Talk Proposal`, `Event Talk`, +`Event Track`, `Event Feedback`, `Additional Event Page`, `Offline Payment Method`, +`Ticket Cancellation Request`, `Buzz Custom Field`. + +**Team-scoped, own `team` column**: `Buzz Team Settings` (one row per team, see +[05](05-team-settings.md); write restricted to Owner/Admin). + +**Not team-scoped**: `Event Category` (global taxonomy), `Speaker Profile` (attendee-owned; +remodel pending — `../02-speaker-remodel.md`), `Event User Preferences` (per-user), +`Buzz Settings` (site-wide Single, global admin config only), `Event Proposal` (inbound, pre-team). + +## Team role capability matrix + +| Capability | Owner | Admin | Manager | Frontdesk | Viewer | +|---|---|---|---|---|---| +| Read team data | ✅ | ✅ | ✅ | check-in scope | ✅ | +| Create/edit events & children | ✅ | ✅ | ✅ | ❌ | ❌ | +| Delete records | ✅ | ✅ | ❌ | ❌ | ❌ | +| Check-in operations | ✅ | ✅ | ✅ | ✅ | ❌ | +| Manage members & invites | ✅ | ✅ | ❌ | ❌ | ❌ | +| Edit team profile / delete team | ✅ | ❌ | ❌ | ❌ | ❌ | + +Mapping to Frappe global roles (granted automatically on membership save): +`Owner/Admin/Manager` → `Event Manager`; `Frontdesk` → `Frontdesk Manager`; all → `Buzz User`. diff --git a/specs/v2/00-teams/01-team-doctypes-and-core-api.md b/specs/v2/00-teams/01-team-doctypes-and-core-api.md new file mode 100644 index 00000000..e2d81430 --- /dev/null +++ b/specs/v2/00-teams/01-team-doctypes-and-core-api.md @@ -0,0 +1,141 @@ +# 01 — Team doctypes & core API + +| Phase | Depends on | Status | +|---|---|---| +| Foundation | — | Draft | + +## Goal + +Introduce the tenancy primitives — `Buzz Team` and `Buzz Team Membership` — plus the minimal API +the dashboard shell will consume (my teams, active team, create team). Includes the missing role +fixtures and default-team creation for existing installs. + +## Demo criteria (definition of done) + +Two fresh users each create a team via the API; `get_my_teams` returns only their own team with +`team_role: Owner`; `set_active_team` persists across requests; saving a membership auto-grants the +mapped Frappe role. + +## Scope + +### In +- `Buzz Team`, `Buzz Team Membership` doctypes + controllers +- `buzz/teams.py` domain module, `buzz/api/teams.py` whitelisted API +- Role fixtures for `Event Manager` and `Frontdesk Manager` (currently referenced in 22 doctype + perm blocks but never exported — `fixtures/role.json` only has `Buzz User` + `Frontdesk Manager`) +- `active_team` field on the existing `Event User Preferences` doctype +- Install/migrate hook: default team for existing Event Manager users + +### Out (deferred to) +- `team` field on any event doctype → `02-team-field-on-events-and-backfill.md` +- Permission enforcement → `03-permission-hooks.md` +- Invitations → `04-team-invitations.md` + +## Backend changes + +### New doctype: `Buzz Team` (module: Events) + +| fieldname | type | options | notes | +|---|---|---|---| +| `team_name` | Data | reqd, in list view | display name | +| `slug` | Data | unique | autoslug from team_name in `before_insert`; reserved for public host pages later | +| `logo` | Attach Image | | | +| `owner_user` | Link | User, reqd, read-only | creator; set in `before_insert` | + +Naming: `autoname: format:TEAM-{####}` (slug stays a mutable display handle). +Controller `after_insert`: create `Buzz Team Membership` (owner_user, `team_role=Owner`, +`status=Active`). + +### New doctype: `Buzz Team Membership` (module: Events) + +| fieldname | type | options | notes | +|---|---|---|---| +| `team` | Link | Buzz Team, reqd | | +| `user` | Link | User, reqd | | +| `team_role` | Select | Owner\nAdmin\nManager\nFrontdesk\nViewer | reqd, default Manager | +| `status` | Select | Active\nDisabled | default Active | + +Constraints & controller: +- `validate`: uniqueness of `(team, user)` (`frappe.db.exists` check + unique index via + `unique` constraint in migration or `autoname: format:{team}-{user}` — pick unique index). +- `on_update` + `after_insert`: sync Frappe roles per the matrix in `00-plan.md` + (`Owner/Admin/Manager` → add `Event Manager`; `Frontdesk` → add `Frontdesk Manager`). Never + remove roles automatically (user may hold them via another team) — recompute across all Active + memberships of that user in a helper `buzz.teams.sync_frappe_roles(user)`. +- `validate`: a team must always retain at least one Active `Owner` (block demote/disable of the + last owner). + +### `buzz/teams.py` (new domain module) + +- `create_team(team_name: str) -> Buzz Team` — creates team (+ owner membership via controller). +- `get_teams_for_user(user: str) -> list[dict]` — Active memberships joined to team + (name, team_name, slug, logo, team_role) via `frappe.get_all` on membership + team fields. +- `get_membership(team: str, user: str) -> dict | None` — Active membership row or None. +- `sync_frappe_roles(user: str)` — see above. +- `create_default_team_for(user: str)` — idempotent; used by install hook. + +### `buzz/api/teams.py` (new API module — organizer APIs get their own files, do not grow `buzz/api/__init__.py`) + +All `@frappe.whitelist()`, no guest access. + +### `Event User Preferences` change + +Add field `active_team` (Link → Buzz Team). Doctype currently has a single `user` field and is +already per-user. `set_active_team` validates the caller has an Active membership in that team. + +### Install & fixtures + +- `fixtures/role.json`: add `Event Manager` (desk_access 0 in v2 posture; keep existing entries). +- `buzz/install.py` `after_install` + a patch `buzz/patches/create_default_teams.py` + (registered in `patches.txt`): for each enabled user holding `Event Manager`, call + `create_default_team_for(user)` with team name `{first_name}'s Team`. Idempotent. + +## Frontend changes + +None. (Consumed by `01-dashboard/01`.) + +## API contract + +| Endpoint | Method | Params | Returns | Auth | +|---|---|---|---|---| +| `buzz.api.teams.create_team` | POST | `team_name` | `{name, team_name, slug}` | any logged-in user | +| `buzz.api.teams.get_my_teams` | GET | — | `[{name, team_name, slug, logo, team_role}]` | logged-in | +| `buzz.api.teams.get_active_team` | GET | — | `{name, team_name, slug, logo, team_role}` or null; falls back to first membership | logged-in | +| `buzz.api.teams.set_active_team` | POST | `team` | `{ok: true}` | member of `team` | + +## Permissions notes + +- Doctype perms: `Buzz Team` + `Buzz Team Membership` readable by `Buzz User` role (row-level + scoping arrives in step 03); write via API only (`System Manager` full access in Desk). +- `create_team` is open to any logged-in user — this is the self-serve entry point. + +## Demo script + +1. `bench --site buzz.localhost console`: create users `alice@x.com`, `bob@x.com`. +2. As alice (API): `create_team("Alice Events")` → team + Owner membership exist. +3. As alice: `get_my_teams()` → exactly one team, `team_role == "Owner"`; alice now holds + `Event Manager` role. +4. As bob: `get_my_teams()` → `[]`; `set_active_team()` → throws. +5. `bench migrate` on a site with an existing Event Manager user → default team created once; + re-running the patch is a no-op. + +## Acceptance criteria (merge gate) + +### Automated tests — `buzz/tests/test_teams.py` (new) +`bench --site buzz.localhost run-tests --module buzz.tests.test_teams` +- `create_team` creates the team + Owner membership + grants `Event Manager` to creator. +- Duplicate `(team, user)` membership insert raises. +- Role sync: adding Frontdesk membership grants `Frontdesk Manager`; demoting the only Owner / + disabling the last Active Owner membership raises. +- `get_my_teams` returns only the caller's Active memberships (two-user isolation case). +- `set_active_team` for a non-member raises; for a member persists to `Event User Preferences`. +- `create_default_team_for` is idempotent (call twice → one team). +- Patch `create_default_teams` run twice → no duplicates. + +### Browser checks (agent-browser) +None — no UI in this step (covered by `01-dashboard/01` checks). + +## Dependencies & risks + +- `Event Manager` fixture must not clobber existing site roles — fixture export merges by name. +- Role sync is additive-only by design; revocation handled in `sync_frappe_roles` recompute. diff --git a/specs/v2/00-teams/02-team-field-on-events-and-backfill.md b/specs/v2/00-teams/02-team-field-on-events-and-backfill.md new file mode 100644 index 00000000..6bcbe75a --- /dev/null +++ b/specs/v2/00-teams/02-team-field-on-events-and-backfill.md @@ -0,0 +1,113 @@ +# 02 — Tenant field on events & backfill + +| Phase | Depends on | Status | +|---|---|---| +| Foundation | 00-teams/01 | Draft | + +## Goal + +Every `Buzz Event` belongs to a team, and `team` is denormalized onto all event descendants so the +permission layer (step 03) is a single-column filter on every doctype. + +## Demo criteria (definition of done) + +`bench migrate` on a site with existing data leaves every `Buzz Event` and descendant row with +`team` set; creating an `Event Ticket Type` in Desk auto-stamps `team` from its event; creating a +`Buzz Event` without `team` fails validation. + +## Scope + +### In +- `team` field on all tenant doctypes (inventory in `00-plan.md`) +- Cascade helper + `doc_events` wiring +- Backfill patch + +### Out (deferred to) +- Enforcement (hooks) → `03-permission-hooks.md` +- Per-team settings split of `Buzz Settings` → future (site-wide Single stays global for now) + +## Backend changes + +### Field additions + +- **`Buzz Event`**: `team` Link → Buzz Team, **reqd**, `in_standard_filter`, indexed + (`search_index: 1`). Placed in the first section next to `title`. +- **Team-direct doctypes** (`Event Venue`, `Event Host`, `Event Template`, `Buzz Campaign`): + `team` Link → Buzz Team, reqd, indexed. +- **Derived-from-event doctypes** (all 18 in `00-plan.md` inventory): `team` Link → Buzz Team, + read-only, hidden, indexed. Stamped, never user-entered. + +### Cascade helper — `buzz/teams.py` + +```python +def inherit_team_from_event(doc, method=None): + """doc_events handler: stamp doc.team from its linked event.""" +``` + +- Resolves the event link field (`event` on most doctypes) → `frappe.db.get_value("Buzz Event", + event, "team")` and sets `doc.team`. Throws if the event has no team. +- Wired in `hooks.py` `doc_events` `before_insert` for every derived doctype. `validate` also + re-stamps when the `event` link changed (ticket transfers between events don't exist, but coupon + `event` is editable). +- Child tables (`Schedule Item`, `Buzz Event Form`, etc.) need **no** team field — they are + reachable only through their parent and inherit its permissions. + +### `Buzz Event` controller + +`validate`: `team` required (schema-level reqd covers Desk; keep an explicit check for +`ignore_mandatory` code paths). New-event creation from the dashboard always passes the active team. + +### Backfill patch — `buzz/patches/assign_default_team.py` (register in `patches.txt` after `create_default_teams`) + +1. Site-default team: reuse the default team of the site's first System Manager/Event Manager + (created by step 01's patch); create one (`"Default Team"`) if none exists. +2. `UPDATE tabBuzz Event SET team = WHERE IFNULL(team, '') = ''` via + `frappe.db.set_value`-style bulk (`frappe.qb.update` — no raw SQL). +3. Cascade to each derived doctype with a query-builder update joining on its `event` column. +4. Idempotent; safe to re-run. + +> Decision recorded: **one site-default team** for backfill (not per-`Event Host` teams). Existing +> installs are single-tenant in practice; owners can reassign later from Desk. + +## Frontend changes + +None. + +## API contract + +No new endpoints. + +## Permissions notes + +None enforced yet — `team` is inert data until step 03. Ship 02 and 03 in quick succession; a site +sitting on 02 alone behaves exactly as today. + +## Demo script + +1. `bench --site buzz.localhost migrate`. +2. In Desk, open any pre-existing `Buzz Event` → `team` populated with the default team. +3. `frappe.db.count("Event Booking", {"team": ("is", "not set")})` → 0 (repeat per derived doctype). +4. Create a new `Event Ticket Type` for that event in Desk → `team` auto-stamped, read-only. +5. Try inserting a `Buzz Event` without `team` in console → `frappe.MandatoryError`. + +## Acceptance criteria (merge gate) + +### Automated tests — `buzz/tests/test_team_cascade.py` (new) +`bench --site buzz.localhost run-tests --module buzz.tests.test_team_cascade` +- Inserting `Event Ticket Type` / `Event Booking` / `Event Sponsor` for an event stamps `team` + from the event (one parametrized case per derived doctype in the inventory). +- Changing a coupon's `event` link re-stamps `team` on validate. +- Inserting `Buzz Event` without `team` raises `MandatoryError` (both Desk path and + `ignore_mandatory` guard). +- Backfill patch: seed legacy rows with NULL team → run patch → zero NULL-team rows across every + doctype in the inventory; run patch again → no change (idempotent). + +### Browser checks (agent-browser) +None — no UI in this step. + +## Dependencies & risks + +- Patch ordering: must run **after** doctype sync and step 01's `create_default_teams` + (patches.txt order), and **before** step 03's hooks are exercised — NULL `team` under active + hooks hides rows from everyone but System Manager. +- `bench migrate` required after schema change (per project convention). diff --git a/specs/v2/00-teams/03-permission-hooks.md b/specs/v2/00-teams/03-permission-hooks.md new file mode 100644 index 00000000..6d039d5d --- /dev/null +++ b/specs/v2/00-teams/03-permission-hooks.md @@ -0,0 +1,166 @@ +# 03 — Permission hooks (row-level isolation) + +| Phase | Depends on | Status | +|---|---|---| +| Foundation | 00-teams/02 | Draft | + +## Goal + +Team isolation actually enforced: a member of Team A can never list or open Team B's records — +in Desk, `frappe.client`, REST, and frappe-ui `useList` alike. Replaces the commented-out stubs at +`buzz/hooks.py:161-167`. + +## Demo criteria (definition of done) + +Two managers in two teams: Desk/API list views show only their own team's events and bookings; +direct URL to the other team's doc → 403. Simultaneously, a Guest completes a booking on a +published event and an attendee still sees their own booking at `/dashboard/account/bookings` — +zero regression. + +## Scope + +### In +- `buzz/permissions.py` with query-condition + has_permission implementations +- `hooks.py` wiring for every tenant doctype + `Buzz Team`/`Buzz Team Membership` +- Guest/attendee endpoint audit + regression test suite + +### Out (deferred to) +- UI 403 page → `01-dashboard/01` +- Frontdesk check-in scoping niceties → `01-dashboard/08` + +## Backend changes + +### `buzz/permissions.py` (new, Gameplan pattern — one central module) + +```python +TEAM_SCOPED_WRITE_ROLES = {"Owner", "Admin", "Manager"} +TEAM_SCOPED_DELETE_ROLES = {"Owner", "Admin"} + +def team_query_conditions(user=None, doctype=None, **kwargs) -> str | None: + """permission_query_conditions hook, shared by all tenant doctypes. + + Called by db_query as fn(user, doctype=doctype). Returns namespaced SQL: + `tab{doctype}`.`team` in (select `team` from `tabBuzz Team Membership` + where `user` = %(user)s and `status` = 'Active') + Built with frappe.qb / pypika criterion (framework renders + escapes it). + System Manager / Administrator -> None (unrestricted). + """ + +def membership_query_conditions(user=None, **kwargs) -> str | None: + """For Buzz Team Membership itself: rows of teams the user belongs to.""" + +def team_doc_query_conditions(user=None, **kwargs) -> str | None: + """For Buzz Team: teams the user is an Active member of.""" + +def team_has_permission(doc, ptype="read", user=None, **kwargs) -> bool: + """has_permission hook — DENY-ONLY (composes over role perms). + + read/select/print/email/export/report -> any Active membership on doc.team + write/create/submit/cancel/amend -> team_role in TEAM_SCOPED_WRITE_ROLES + delete -> team_role in TEAM_SCOPED_DELETE_ROLES + System Manager -> True. doc.team unset -> True (pre-backfill tolerance). + """ +``` + +Notes: +- Signatures follow framework calling conventions verified in + `frappe/model/db_query.py:get_permission_query_conditions` (`fn(user, doctype=...)`) and + `frappe/permissions.py:has_controller_permissions` (`fn(doc=, ptype=, user=, debug=)`); + `**kwargs` absorbs extras. +- One generic implementation, not per-doctype copies. Membership subquery built once via + `frappe.qb` and rendered with `with_namespace=True`. +- **Attendee carve-out**: for `Event Booking`, `Event Ticket`, `Event Payment`, + `Ticket Cancellation Request` the query condition is + `( OR owner = user)` — attendees list *their own* docs today via + framework `owner`/`if_owner` perms and existing API checks; team scoping must widen, not narrow, + that access. + +### `hooks.py` wiring + +```python +permission_query_conditions = { + "Buzz Team": "buzz.permissions.team_doc_query_conditions", + "Buzz Team Membership": "buzz.permissions.membership_query_conditions", + "Buzz Event": "buzz.permissions.team_query_conditions", + # ... every doctype in the 00-plan.md tenant inventory +} +has_permission = { + "Buzz Team": "buzz.permissions.team_doc_has_permission", + "Buzz Event": "buzz.permissions.team_has_permission", + # ... same inventory +} +``` + +### Guest & attendee flow audit (hard requirement of this step) + +Every whitelisted endpoint in `buzz/api/__init__.py` is enumerated in the PR description with its +verdict. The critical ones: + +| Endpoint | Path today | Verdict | +|---|---|---| +| `get_event_booking_data`, `validate_coupon` | guest-allowed reads of published event config | reads use `frappe.get_all` with explicit filters — must switch to `ignore_permissions=True` reads of *published* events only (guest has no membership) | +| `process_booking` | creates booking/attendees as guest or user | doc inserts already run with explicit flows; inserts get `ignore_permissions=True` where the actor is a non-member by design | +| `get_booking_details`, `get_ticket_details` | owner-checked | keep explicit owner checks; owner carve-out in query conditions keeps list views working | +| `transfer_ticket`, `change_add_on_preference`, `create_cancellation_request` | owner + window checks | unchanged; inserts stamped with team via cascade | +| `validate_ticket_for_checkin`, `checkin_ticket` | `frappe.only_for("Frontdesk Manager")` | additionally verify frontdesk user has Active membership (any role with check-in capability) in the ticket's team | + +### Tests — `buzz/tests/test_team_permissions.py` (new) + +1. Cross-team isolation: `frappe.get_list("Buzz Event")` as member A excludes team B's events; + `frappe.get_doc(...).check_permission("read")` raises for B's event. +2. Role matrix: Viewer can read, cannot write; Manager can write, cannot delete; Admin can delete. +3. Guest booking end-to-end on a published event (existing test helpers) still passes. +4. Attendee reads own booking/tickets without membership. +5. Frontdesk of team A cannot check in team B's ticket. + +## Frontend changes + +None. + +## API contract + +No new endpoints. + +## Permissions notes + +This step **is** the permission layer — matrix in `00-teams/00-plan.md`. `has_permission` hooks can +only deny, never grant; base doctype role perms (`Event Manager` etc.) still gate capability. + +## Demo script + +1. Create teams A (alice) and B (bob), one event each. +2. As alice in Desk: Buzz Event list shows only A's event; open B's event URL → 403. +3. As alice via REST: `/api/resource/Buzz Event` → only A's. +4. Incognito guest: book a free ticket on B's published event → success; ticket email received. +5. Attendee logs in → `/dashboard/account/bookings` shows the booking. +6. `bench --site buzz.localhost run-tests --module buzz.tests.test_team_permissions` → green. + +## Acceptance criteria (merge gate) + +### Automated tests — `buzz/tests/test_team_permissions.py` (the suite defined above, expanded) +`bench --site buzz.localhost run-tests --module buzz.tests.test_team_permissions` +- Cross-team isolation: `frappe.get_list` excludes other team's rows for **every** doctype in the + tenant inventory (parametrized); `check_permission("read")` raises cross-team. +- Role matrix: Viewer read-only; Manager write-not-delete; Admin delete; System Manager + unrestricted. +- Owner carve-out: attendee (non-member) lists own `Event Booking`/`Event Ticket` rows. +- Frontdesk of team A cannot `checkin_ticket` for team B's ticket. +- NULL-team tolerance: doc with unset team readable by System Manager only; no exception raised. +- Guest API surface: `get_event_booking_data`, `validate_coupon`, `process_booking` (free ticket) + succeed as Guest on a published event with hooks active. + +### Browser checks (agent-browser) +1. Fresh session (no login) → open `/dashboard/book-tickets/` → complete a + free-ticket guest booking (OTP flow) → success screen; ticket email queued. +2. Log in as that attendee → `/dashboard/account/bookings` → booking visible; open ticket detail. +3. Log in as team-A manager → open Desk `/app/buzz-event` → assert list shows only team A's + events; navigate to team B's event URL → 403/permission error page. + +## Dependencies & risks + +- **Highest-risk step in v2.** The attendee/guest carve-outs are the sharp edge — the test suite is + a merge blocker, not optional. +- NULL `team` tolerance (`doc.team unset -> True` in has_permission; query condition still filters + NULL out for non-System-Managers) keeps a half-migrated site from hard-breaking. +- Performance: membership subquery per list query; Frappe's role check stays Redis-cached. If it + shows up in profiles, cache team IDs per user in `frappe.cache` keyed on membership modification. diff --git a/specs/v2/00-teams/04-team-invitations.md b/specs/v2/00-teams/04-team-invitations.md new file mode 100644 index 00000000..3f9ac78b --- /dev/null +++ b/specs/v2/00-teams/04-team-invitations.md @@ -0,0 +1,112 @@ +# 04 — Team invitations + +| Phase | Depends on | Status | +|---|---|---| +| Foundation | 00-teams/03 | Draft | + +## Goal + +Owners/Admins invite members by email with a team role, reusing Frappe's built-in +`User Invitation` (`frappe/core/doctype/user_invitation/`) — no bespoke invite doctype. + +## Demo criteria (definition of done) + +API-invite a fresh email with `team_role=Manager`; recipient accepts the emailed link as a brand-new +user → Active `Buzz Team Membership` created, `Event Manager` role granted, `get_my_teams` includes +the team on their first login. + +## Scope + +### In +- `user_invitation` hook extension (`extra_invite_params`, `after_accept`) +- `invite_member` wrapper API + pending-invite listing + revoke +- Inviter authorization (Owner/Admin only) + +### Out (deferred to) +- Members/invites UI → `01-dashboard/13` +- Attendee-facing invitations (marketing) → future marketing pillar + +## Backend changes + +### `hooks.py` + +```python +user_invitation = { + "allowed_roles": {"Event Manager": ["Buzz User"], "Buzz User": ["Buzz User"]}, + "extra_invite_params": ["team", "team_role"], + "after_accept": ["buzz.teams.on_invitation_accepted"], +} +``` + +### `buzz/teams.py` + +- `on_invitation_accepted(invitation, user, user_inserted)` — reads `team` + `team_role` from the + invitation's extra params; creates Active `Buzz Team Membership` (idempotent: upsert on + `(team, user)`, re-activate if previously Disabled). Role sync happens via the membership + controller from step 01. +- Validation at *invite time*, not accept time: the wrapper API checks the inviter. + +### `buzz/api/teams.py` additions + +- `invite_member(team, email, team_role)` — validates caller has `team_role in {Owner, Admin}` for + `team`; `team_role` must not be `Owner` (ownership transfer is a separate explicit action); + delegates to `frappe.core.api.user_invitation.invite_by_email` with + `extra_params={"team": team, "team_role": team_role}` and `redirect_to_path="/dashboard"`. +- `get_pending_invitations(team)` — Owner/Admin only; pending `User Invitation` rows for the team. +- `revoke_invitation(team, invitation)` — Owner/Admin only. + +## Frontend changes + +None (UI in `01-dashboard/13`). + +## API contract + +| Endpoint | Method | Params | Returns | Auth | +|---|---|---|---|---| +| `buzz.api.teams.invite_member` | POST | `team, email, team_role` | `{invitation}` | Owner/Admin of team | +| `buzz.api.teams.get_pending_invitations` | GET | `team` | `[{name, email, team_role, status, creation}]` | Owner/Admin | +| `buzz.api.teams.revoke_invitation` | POST | `team, invitation` | `{ok}` | Owner/Admin | + +## Permissions notes + +- Inviting grants at most `Admin` — never `Owner`. +- An invited email that already has an account: `after_accept` fires with `user_inserted=False`; + membership upsert covers both paths. +- Invited existing member: `invite_member` short-circuits with a friendly `frappe.throw`. + +## Demo script + +1. As alice (Owner of team A): `invite_member(A, "carol@x.com", "Manager")` → invitation email + (or console link in dev). +2. `get_pending_invitations(A)` → 1 row. +3. Open the accept link in incognito → account created → redirected to `/dashboard`. +4. As carol: `get_my_teams()` → team A with `team_role: Manager`; carol holds `Event Manager`. +5. As bob (non-member): `invite_member(A, ...)` → throws. + +## Acceptance criteria (merge gate) + +### Automated tests — `buzz/tests/test_team_invitations.py` (new) +`bench --site buzz.localhost run-tests --module buzz.tests.test_team_invitations` +- `invite_member` as Owner creates a pending `User Invitation` with extra params + `{team, team_role}`. +- `invite_member` as Manager/non-member raises; `team_role="Owner"` raises. +- Accept for a brand-new email: User created, Active membership with invited role, + `Event Manager`/`Frontdesk Manager` role granted per mapping. +- Accept for an existing user (`user_inserted=False`): membership upserted; previously Disabled + membership re-activated, not duplicated. +- Inviting an existing Active member throws the friendly error. +- `revoke_invitation` as Admin works; as Manager raises. + +### Browser checks (agent-browser) +1. As Owner (console/API) create an invitation; grab the accept URL (dev mode prints it). +2. Fresh browser session → open the accept URL → account/password flow → assert final redirect + lands on `/dashboard`. +3. Log in as the new user → assert `get_my_teams` (via any team-aware page once + `01-dashboard/01` exists; until then assert via API response in the browser session) includes + the team with the invited role. + +## Dependencies & risks + +- `User Invitation` app-key: invitations are namespaced per app via the hook config — verify the + accept flow passes `app_name=buzz` end to end on this frappe version. +- Email delivery in dev: dev mode prints the accept URL; demo uses that. diff --git a/specs/v2/00-teams/05-team-settings.md b/specs/v2/00-teams/05-team-settings.md new file mode 100644 index 00000000..0862608c --- /dev/null +++ b/specs/v2/00-teams/05-team-settings.md @@ -0,0 +1,152 @@ +# 05 — Buzz Team Settings + +| Phase | Depends on | Status | +|---|---|---| +| Foundation | 00-teams/01–03 | Draft | + +## Goal + +Split tenant-specific configuration out of the site-wide `Buzz Settings` Single into a per-team +**`Buzz Team Settings`** doctype (regular doctype, one row per team — not a Single, since it is +per tenant). `Buzz Settings` remains for truly global, admin-level, backend-only config. + +## Demo criteria (definition of done) + +Two teams set different ticket-transfer windows; each team's events enforce their own window in +the attendee flow (`can_transfer_ticket`). `bench migrate` seeds every existing team's settings +from current global values — attendee-visible behavior unchanged on upgrade day. + +## Scope + +### In +- `Buzz Team Settings` doctype + auto-create on team creation + seed patch +- Resolution helpers + rewiring existing read sites (audit table below) +- API for the frontend settings dialog +### Out (deferred to) +- Settings UI → `01-dashboard/13` (Team Settings dialog gets a "Defaults" section) +- Per-event overrides of these values (event-level fields already exist for some, e.g. + `ticket_email_template` on Buzz Event — that hierarchy stays: event field → team settings) + +## Field split decision + +### Moves to `Buzz Team Settings` (per team) + +| fieldname | type | today read at | +|---|---|---| +| `allow_transfer_ticket_before_event_start_days` | Int | `buzz/api/__init__.py:151` (`can_transfer_ticket`) | +| `allow_add_ons_change_before_event_start_days` | Int | `buzz/api/__init__.py:166` (`can_change_add_ons`) | +| `allow_ticket_cancellation_request_before_event_start_days` | Int | `buzz/api/__init__.py:191` (`can_request_cancellation`) | +| `support_email` | Data | ticket emails / public pages | +| `default_ticket_email_template` | Link Email Template | `event_ticket.py:106` | +| `auto_send_pitch_deck` | Check | `sponsorship_enquiry.py:82` | +| `default_sponsor_deck_email_template` | Link Email Template | `sponsorship_enquiry.py:82` | +| `default_sponsor_deck_reply_to` | Data | same | +| `default_sponsor_deck_cc` | Small Text | same | +| `default_webinar_template` | Link | `buzz_event.py:198` (verify field exists in Buzz Settings JSON — read in code, missing from schema dump; reconcile while implementing) | + +### Stays in `Buzz Settings` (site-wide, Desk/System Manager only) + +| fieldname | why global | +|---|---| +| `login_banner` | site login page (`auth.py:13`) | +| `accept_event_proposals`, `allow_guest_event_proposals`, `event_proposal_banner_title`, `event_proposal_success_title`, `event_proposal_success_message` | inbound event-proposal funnel is site-level (pre-team) — `forms.py:367` | +| `custom_fields_go_after_this` | form-layout plumbing | + +## Backend changes + +### New doctype: `Buzz Team Settings` (module: Events) + +- `team` Link → Buzz Team, reqd, **unique** — one settings row per team. +- All "moves" fields above. +- `autoname: field:team` (row addressable as the team name). +- Not a child table, not a Single — plain doctype so permission hooks from `00-teams/03` apply + (add it to the tenant inventory: query condition = its own `team` column; write requires + Owner/Admin — settings are team administration, stricter than Manager). + +### Lifecycle + +- `Buzz Team.after_insert` → `buzz.teams.create_team_settings(team)` seeding field values from + current `Buzz Settings` globals (copy-on-create; **no runtime fallback chain** — one read, + predictable; changing a global later never silently mutates existing teams). +- Patch `buzz/patches/create_team_settings_for_existing_teams.py`: idempotent seed for teams + created before this step. + +### Resolution helpers — `buzz/teams.py` + +```python +def get_team_settings(team: str) -> Document: + """frappe.get_cached_doc('Buzz Team Settings', team); creates+seeds if missing (self-heal).""" + +def get_event_team_settings(event: str) -> Document: + """Resolve event -> team -> settings. The standard entry point for attendee-flow code.""" +``` + +### Rewire call sites (audit — the heart of this step) + +| Site | Change | +|---|---| +| `buzz/api/__init__.py:151,166,191` | `frappe.get_single("Buzz Settings")` → `get_event_team_settings(event)` (event in scope at each site) | +| `buzz/ticketing/doctype/event_ticket/event_ticket.py:106` | event-level `ticket_email_template` stays first; fallback → team settings default | +| `buzz/proposals/doctype/sponsorship_enquiry/sponsorship_enquiry.py:82` | sponsor-deck defaults → team settings (enquiry → event → team) | +| `buzz/events/doctype/buzz_event/buzz_event.py:198` | webinar template default → team settings | +| `buzz/api/auth.py:13`, `buzz/api/forms.py:367` | unchanged (global) | + +Add regression tests: per-team window enforcement + upgrade parity (seeded values equal old +globals). + +## Frontend changes + +None here. `01-dashboard/13` Team Settings dialog gains a **Defaults** section (windows, support +email, email-template Links, sponsor-deck defaults) bound to `useDoc("Buzz Team Settings", team)`. +Owner/Admin editable, others read-only. + +## API contract + +No bespoke endpoints — `useDoc` CRUD under permission hooks suffices (row addressable by team +name via `autoname: field:team`). + +## Permissions notes + +- Read: any team member (attendee-flow reads happen server-side, unaffected). +- Write: Owner/Admin only (stricter than the Manager write default — add a doctype-specific + branch in `buzz/permissions.py:team_has_permission` or a dedicated `has_permission` mapping). +- `Buzz Settings` keeps System-Manager-only Desk access; never surfaced in the dashboard. + +## Demo script + +1. `bench migrate` → every team has a seeded `Buzz Team Settings` row; values match old globals. +2. Team A sets transfer window 7 days; Team B leaves 2. +3. Attendee with ticket to A's event (5 days out) → transfer blocked; B's event (5 days out) → + allowed. +4. New team created via onboarding → settings row exists immediately. +5. As Manager-role member: `useDoc` save on team settings → server denies. + +## Acceptance criteria (merge gate) + +### Automated tests — `buzz/tests/test_team_settings.py` (new) +`bench --site buzz.localhost run-tests --module buzz.tests.test_team_settings` +- Creating a `Buzz Team` auto-creates its `Buzz Team Settings` row seeded from `Buzz Settings` + values (assert field-by-field for the moved set). +- `get_team_settings` self-heals a missing row. +- Window enforcement: team A window 7 days, team B 2 days → `can_transfer_ticket` / + `can_change_add_ons` / `can_request_cancellation` return opposite verdicts for tickets on + events 5 days out. +- Ticket email template resolution order: event field → team settings default (both branches). +- Sponsor-deck defaults resolved from the enquiry's event's team. +- Write perms: Manager-role member save raises; Owner/Admin save succeeds; cross-team read + excluded (list isolation). +- Seed patch idempotent; seeded values equal pre-migration globals (upgrade-parity case). + +### Browser checks (agent-browser) +1. Attendee with a ticket to team A's event 5 days out (window 7) → ticket detail page: transfer + action absent/disabled; same-aged ticket on team B's event (window 2) → transfer action + available. + +## Dependencies & risks + +- **Decision recorded: copy-on-create, no fallback chain.** Trade-off: platform admin changing a + global default doesn't propagate to existing teams. Revisit only if that becomes a real need. +- `default_webinar_template` schema/code mismatch (read in `buzz_event.py:198`, absent from the + current Buzz Settings field dump) — reconcile during implementation. +- Update `00-teams/00-plan.md` tenant inventory + `01-dashboard/13` scope when this lands (done in + this spec's PR). diff --git a/specs/v2/01-dashboard/00-plan.md b/specs/v2/01-dashboard/00-plan.md new file mode 100644 index 00000000..7427a3b9 --- /dev/null +++ b/specs/v2/01-dashboard/00-plan.md @@ -0,0 +1,93 @@ +# 01-dashboard — Plan + +Unified dashboard: one SPA serving attendees (existing `/account/*` + public booking routes, +untouched) **and** event managers (new `/manage/*` area). Goal of phases A–D: **Desk parity** — +an organizer never opens `/app`. + +## Step 0 — prerequisite gate (frappe-ui v1 beta) + +All specs below are written against **frappe-ui v1 beta** APIs. `dashboard/package.json` currently +pins `frappe-ui ^0.1.257` — **Phase A must not start until the migration is merged.** Verification +checklist: + +- [ ] `package.json` pins the v1 beta release; `yarn build` green +- [ ] `import { DesktopShell } from 'frappe-ui'` resolves and renders in a scratch page +- [ ] `frappe-ui/list` subpath (`List`, `ListRow`) importable +- [ ] `useCall` / `useList` / `useDoc` available +- [ ] Semantic tokens compile (`bg-surface-base`, `text-ink-gray-9`, `border-outline-gray-2`) +- [ ] `Dialog v-model:open`, imperative `toast` / `dialog` helpers work +- [ ] `telemetryPlugin` exported from `frappe-ui/frappe` (needed by step 14) +- [ ] Existing attendee pages still function (booking flow smoke test on :8080) + +## Conventions (binding for all steps) + +**Routing** — manager area under `/manage/*` in `dashboard/src/router.ts`, lazy-loaded chunk +(`() => import(...)`), guarded by `meta: { requiresTeam: true }`; the router guard resolves +`get_my_teams` once (cached) — no teams → onboarding screen; not logged in → existing +`LoginRequired` handling. No team slug in URLs: team identity = active-team preference. Deep link +into another team's event → auto-switch active team if member, else 403 page. + +**Data layer** — all new manager code uses `useList` / `useDoc` / `useCall` composables, organized +per domain in `dashboard/src/data/` (`teams.ts`, `events.ts`, `tickets.ts`, `bookings.ts`, …). +**No new inline `createResource`.** The 68 existing inline attendee resources are explicitly out of +scope — do not refactor them in these steps. + +**UI language** — frappe-ui components only (`Button`, `Dialog`, `FormControl`, `List`, `Badge`, +`Dropdown`, `Tabs`, `FileUploader`, `Breadcrumbs`); semantic tokens only; icons via +`lucide-*` classes; espresso design per the frappe-ui skill `DESIGN.md` (shell anatomy, data-table +archetype, settings-dialog archetype). + +**Backend** — plain doctype CRUD goes through `frappe.client` via `useList`/`useDoc` under the +team permission hooks (`00-teams/03`). Bespoke endpoints only for aggregation/validation, in +per-domain modules `buzz/api/.py`. + +## Phases + +| Phase | Steps | Outcome | +|---|---|---| +| **A — tracer bullet** | [01](01-app-shell-and-events-list.md), [02](02-event-workspace-and-ticket-types.md) | Shell + team-scoped events list + ticket type CRUD, end to end | +| **B — core workspace** | [03](03-event-overview-and-editing.md), [04](04-coupons.md), [05](05-ticket-add-ons.md), [06](06-sponsorship-tiers-and-sponsors.md) | Event fully configurable without Desk | +| **C — operations** | [07](07-bookings-and-attendees.md), [08](08-checkin-desk.md) | Money + people + door | +| **D — parity long tail** | [09](09-venues-hosts-library.md), [10](10-schedule-tracks-and-talks.md), [11](11-event-templates.md), [12](12-event-settings-payments-and-forms.md), [13](13-team-settings-members-and-invites.md) | Full Desk parity + team admin | +| **cross-cutting** | [14](14-pulse-analytics.md) | Pulse product analytics (init early, instrument as steps land) | + +## Event workspace tab map + +`/manage/events/:eventId` — tabs registered up front (step 02), enabled as steps land: + +| Tab | Step | Manages | +|---|---|---| +| Overview | 03 | KPIs, core `Buzz Event` fields, publish | +| Tickets | 02 | `Event Ticket Type` | +| Add-ons | 05 | `Ticket Add-on` | +| Coupons | 04 | `Buzz Coupon Code` | +| Sponsors | 06 | `Sponsorship Tier`, `Event Sponsor`, enquiries (read) | +| Bookings | 07 | `Event Booking`, `Event Payment` | +| Attendees | 07 | `Event Booking Attendee` / `Event Ticket` | +| Check-in | 08 | `Event Check In` + scanner | +| Schedule | 10 | `Schedule Item`, `Event Track`, `Event Talk`, proposals inbox | +| Settings | 12 | gateways, tax, registration windows, custom fields/forms, extra pages | + +## Desk-parity matrix + +| Doctype | Home in dashboard | Step | +|---|---|---| +| Buzz Event | Events list + workspace | 01–03 | +| Event Ticket Type | Tickets tab | 02 | +| Buzz Coupon Code | Coupons tab | 04 | +| Ticket Add-on | Add-ons tab | 05 | +| Sponsorship Tier / Event Sponsor / Sponsorship Enquiry | Sponsors tab | 06 | +| Event Booking / Event Payment | Bookings tab | 07 | +| Event Ticket / Event Booking Attendee | Attendees tab | 07 | +| Event Check In | Check-in tab | 08 | +| Event Venue / Event Host | Library pages | 09 | +| Schedule Item / Event Track / Event Talk / Talk Proposal | Schedule tab | 10 | +| Event Template (+ children) | Templates page | 11 | +| Event Payment Gateway / Offline Payment Method / Buzz Custom Field / Buzz Event Form / Additional Event Page | Settings tab | 12 | +| Buzz Team / Buzz Team Membership / User Invitation / Buzz Team Settings | Team settings | 13 | + +**Phase E candidates (not covered, stay Desk-only for now — parity claims carve these out):** +`Buzz Settings` (site-wide Single — global admin config only; tenant-facing fields move to +`Buzz Team Settings` per `00-teams/05`), `Event Featured Speaker`, +`Speaker Profile`, `Buzz Campaign` + `UTM Parameter`, `Event Feedback` (read-only report later), +`Event Proposal` moderation, `Ticket Cancellation Request` approval queue. diff --git a/specs/v2/01-dashboard/01-app-shell-and-events-list.md b/specs/v2/01-dashboard/01-app-shell-and-events-list.md new file mode 100644 index 00000000..dd71309e --- /dev/null +++ b/specs/v2/01-dashboard/01-app-shell-and-events-list.md @@ -0,0 +1,116 @@ +# 01 — App shell & events list + +| Phase | Depends on | Status | +|---|---|---| +| A | 00-teams/01–03, Step-0 gate | Draft | + +## Goal + +First pixels of the manager app: frappe-ui app shell with sidebar + team switcher, and a +team-scoped events list with event creation. Proves teams, permissions, routing, and the new +frappe-ui stack in one slice. + +## Demo criteria (definition of done) + +A manager logs in, lands on `/manage/events`, sees only their active team's events, switches team +from the sidebar and the list swaps, creates an event from a dialog and it appears in Desk with the +correct team. A user with no team gets the "Create your team" onboarding. Attendee routes +unaffected. + +## Scope + +### In +- `ManagerLayout` shell (desktop + mobile), sidebar nav skeleton, team switcher +- Events list page + New Event dialog +- No-team onboarding screen; 403 page for non-member deep links +- `data/teams.ts`, `data/events.ts` + +### Out (deferred to) +- Event workspace → step 02 +- Sidebar items Venues/Hosts/Templates/Team Settings render disabled → steps 09/11/13 + +## Backend changes + +None. Consumes `buzz.api.teams.*` (00-teams/01); list scoping comes free from permission hooks. + +## Frontend changes + +### Routes (`dashboard/src/router.ts`) + +| Path | Name | Meta | +|---|---|---| +| `/manage` | redirect → `/manage/events` | `requiresTeam` | +| `/manage/events` | `ManageEventsList` | `requiresTeam` | +| `/manage/onboarding` | `TeamOnboarding` | logged-in only | +| `/manage/403` | `ManageForbidden` | | + +All under a parent route rendering `ManagerLayout` as a lazy chunk. Router guard: resolve +`data/teams.ts` `myTeams` once → empty ⇒ redirect onboarding. + +### Components + +- `src/layouts/ManagerLayout.vue` — `DesktopShell` / `MobileShell` split per DESIGN.md shell + anatomy. `Sidebar` (~14rem): header = **team switcher** `Dropdown` (team logo `Avatar`, + `team_name`, chevron; items from `myTeams`, active check, "Create team" action) → + `set_active_team` then invalidate all manager lists; nav `SidebarItem`s: Events + (`lucide-calendar`), Venues, Hosts, Templates, Team Settings (disabled until their steps); + footer: "Attendee view" link → `/account/bookings`, user `Dropdown` (logout). +- `src/pages/manage/EventsList.vue` — data-table archetype: `PageHeader` (title "Events", primary + `Button variant="solid"` "New Event"), `List` from `frappe-ui/list` with columns: title, + `start_date` (formatted), venue, status `Badge` (`is_published` → green "Live" / gray "Draft"), + tickets sold. Row click → step 02 workspace (until then, no-op). `#empty` slot: illustration + + "Create your first event" CTA. +- `NewEventDialog.vue` — `Dialog v-model:open`; `FormControl`s: title (reqd), start_date / + end_date (`DatePicker`), category (Link to `Event Category`), medium Select. Insert via + `events.ts` with `team: activeTeam` stamped; `toast.success` + route to the new event. +- `src/pages/manage/TeamOnboarding.vue` — centered card: team name input → `create_team` → + redirect `/manage/events`. + +### Data layer + +- `src/data/teams.ts` — `useCall` wrappers: `myTeams`, `activeTeam` (+ `setActiveTeam(team)` that + awaits the POST then invalidates manager caches). +- `src/data/events.ts` — `useList({ doctype: "Buzz Event", fields: [...], filters: { team } })` + + `insertEvent`. + +## API contract + +Consumes `buzz.api.teams.get_my_teams`, `get_active_team`, `set_active_team`, `create_team` only. + +## Permissions notes + +List scoping enforced server-side by `00-teams/03` — the `team` filter in `useList` is UX +(active-team narrowing), not security. Non-member deep link → server 403 → render `ManageForbidden`. + +## Demo script + +1. `yarn dev` (:8080), log in as alice (Owner, team A with 2 events) → `/manage/events` lists 2. +2. Sidebar switcher → team X (alice also member) → list swaps without reload. +3. "New Event" → fill dialog → toast, event visible; Desk shows it with `team = X`. +4. Log in as dave (no teams) → `/manage` → onboarding → create team → empty events list. +5. `/account/bookings` and public `/book-tickets/:route` unchanged. + +## Acceptance criteria (merge gate) + +### Automated tests +None new (no backend). Teams suites (`test_teams`, `test_team_permissions`) must still pass. + +### Browser checks (agent-browser, dev server :8080) +1. Log in as team-A manager → `http://localhost:8080/dashboard/manage/events` → assert sidebar + renders (team switcher shows team A name; nav items Events enabled, Venues/Hosts/Templates/ + Team Settings disabled) and list shows exactly team A's seeded events. +2. Team switcher → select team X → assert events list swaps without full reload (URL unchanged). +3. "New Event" → fill title + start date → submit → toast appears, row appears; assert via Desk + API that the doc's `team` == active team. +4. Log in as user with no teams → `/dashboard/manage` → assert redirect to onboarding screen; + create team → lands on empty events list with CTA. +5. Log in as team-B manager → paste team-A workspace URL → assert 403 page (not blank/error). +6. Regression: `/dashboard/account/bookings` renders; public `/dashboard/book-tickets/` + booking form loads. +7. Mobile viewport (390px): shell switches to `MobileShell`; nav reachable; list usable. + +## Dependencies & risks + +- Step-0 gate (frappe-ui v1 beta) is a hard blocker. +- Team switcher cache invalidation: central `invalidateManagerData()` helper in `teams.ts` from + day 1, or stale cross-team data will leak into the UI (cosmetic, not security). diff --git a/specs/v2/01-dashboard/02-event-workspace-and-ticket-types.md b/specs/v2/01-dashboard/02-event-workspace-and-ticket-types.md new file mode 100644 index 00000000..af9e4ebf --- /dev/null +++ b/specs/v2/01-dashboard/02-event-workspace-and-ticket-types.md @@ -0,0 +1,102 @@ +# 02 — Event workspace & ticket types + +| Phase | Depends on | Status | +|---|---|---| +| A | 01-dashboard/01 | Draft | + +## Goal + +Complete the tracer bullet: open an event into a tabbed workspace and manage `Event Ticket Type` +CRUD without Desk — the first full manage-to-public loop. + +## Demo criteria (definition of done) + +Manager opens their event → Tickets tab → creates "Early Bird ₹499, 100 available" → opens the +public `/dashboard/book-tickets/:route` page → the new ticket type is purchasable. A manager from +another team opening this event's URL gets 403. + +## Scope + +### In +- Event workspace frame with full tab registry (only Overview-stub + Tickets enabled) +- Ticket types table + create/edit dialog + publish toggle + delete +### Out (deferred to) +- Real Overview content → step 03; every other tab → its step + +## Backend changes + +**None.** Plain `frappe.client` CRUD through `useList`/`useDoc`, protected by role perms + team +hooks. The spec asserts this deliberately: if a bespoke endpoint proves necessary for basic CRUD, +that's a smell in the permission layer — fix `00-teams/03` instead. + +## Frontend changes + +### Routes + +| Path | Name | Meta | +|---|---|---| +| `/manage/events/:eventId` | `EventWorkspace` (redirects to `overview`) | `requiresTeam` | +| `/manage/events/:eventId/:tab` | child views | `requiresTeam` | + +### Components + +- `src/pages/manage/EventWorkspace.vue` — header: `Breadcrumbs` (Events / {title}), status `Badge`, + "View page" `Button variant="ghost"` (→ public route, new tab). `Tabs` bound to the route param; + full registry from `00-plan.md` tab map, tabs whose step hasn't landed render disabled with a + "soon" tooltip. Loads the event once via `useDoc("Buzz Event", eventId)` and provides it to tabs. +- `src/pages/manage/event/OverviewTab.vue` — stub: event title/date summary card only. +- `src/pages/manage/event/TicketTypesTab.vue` — `List`: title, price (`tabular-nums`, formatted + via `format_currency`), sold/available (`tickets_sold` / `max_tickets_available`), remaining, + published `Badge`; row actions `Dropdown`: Edit, Publish/Unpublish, Delete (imperative + `dialog.confirm` → `toast`). Header `Button` "New Ticket Type". +- `TicketTypeDialog.vue` — `Dialog v-model:open`; `FormControl`s mapped to real fields: + `title` (reqd), `price` + `currency` (Link Currency), `max_tickets_available` (Int), + `auto_unpublish_after` (DatePicker), `is_published` (Switch). Create + edit modes share it. + +### Data layer + +- `src/data/tickets.ts` — `useList({ doctype: "Event Ticket Type", filters: { event } })` with + insert/update/delete helpers. + +## API contract + +No new endpoints. + +## Permissions notes + +- Manager/Admin/Owner can CRUD; Viewer sees the tab read-only (hide mutating buttons by + `team_role` from `activeTeam`); server denies regardless. +- `tickets_sold` / `remaining_tickets` are system-maintained — read-only in the dialog. + +## Demo script + +1. `/manage/events` → click event → workspace opens on Overview stub, Tickets tab enabled. +2. New Ticket Type: "Early Bird", 499 INR, 100 available, published → row appears, toast. +3. Public booking page for the event → Early Bird listed and bookable (complete a test booking). +4. Unpublish it → gone from public page; sold counters unaffected. +5. As bob (team B): open the same workspace URL → 403 page. + +## Acceptance criteria (merge gate) + +### Automated tests +None new (step asserts zero bespoke endpoints). `test_team_permissions` parametrized case for +`Event Ticket Type` covers the CRUD authorization; extend it if the tracer bullet exposes gaps. + +### Browser checks (agent-browser, dev server :8080) +1. Manager → open event → assert workspace header (breadcrumbs, status badge) + tab strip with + Tickets enabled and roadmap tabs disabled. +2. Create ticket type "Early Bird", 499 INR, 100 available, published → row appears with correct + formatted price and 0/100 sold. +3. Public page `/dashboard/book-tickets/` (fresh session) → "Early Bird" listed; complete + a booking → Tickets tab shows sold count 1. +4. Unpublish via row action → public page no longer offers it. +5. Edit dialog: `tickets_sold`/`remaining_tickets` rendered read-only. +6. Viewer-role member → Tickets tab visible, no New/Edit/Delete controls; direct mutation via + console `useList.insert` equivalent (REST call) → server 403. +7. Team-B manager → workspace URL → 403 page. + +## Dependencies & risks + +- Delete of a ticket type with sold tickets: framework link validation will throw — + surface the server message via `toast.error`, don't pre-empt client-side. +- **Tracer bullet exit criteria:** after this step, architecture review before Phase B fan-out. diff --git a/specs/v2/01-dashboard/03-event-overview-and-editing.md b/specs/v2/01-dashboard/03-event-overview-and-editing.md new file mode 100644 index 00000000..cb25d3f6 --- /dev/null +++ b/specs/v2/01-dashboard/03-event-overview-and-editing.md @@ -0,0 +1,96 @@ +# 03 — Event overview & editing + +| Phase | Depends on | Status | +|---|---|---| +| B | 01-dashboard/02 | Draft | + +## Goal + +The Overview tab becomes the event's home: KPI strip + editable core fields + publish control — +day-to-day event setup no longer needs Desk. + +## Demo criteria (definition of done) + +Manager edits title, dates, venue, and banner from Overview; the public event page reflects it. +KPI numbers match Desk counts. Publish/unpublish from the header works. + +## Scope + +### In +- KPI strip (bookings, tickets sold, gross revenue, check-ins) +- Editable core `Buzz Event` fields + banner upload + publish/unpublish + danger zone +### Out (deferred to) +- Sponsor/email/tax/gateway settings → step 12; schedule → step 10; featured speakers → Phase E + +## Backend changes + +`buzz/api/events.py` (new module): + +```python +@frappe.whitelist() +def get_event_stats(event: str) -> dict: + """{bookings, tickets_sold, gross_revenue, currency, checked_in} + Aggregates via frappe.qb (Count/Sum) over Event Booking (docstatus 1), + Event Ticket, Event Payment (payment_received=1), Event Check In. + Permission: frappe.has_permission("Buzz Event", "read", event) explicit check. + """ +``` + +## Frontend changes + +- `OverviewTab.vue` (replaces stub): + - KPI strip — dashboard archetype: 4 stats in a `divide-x divide-outline-gray-2` row, values + `text-2xl font-semibold tabular-nums`, labels `text-sm text-ink-gray-5`; `useCall` to + `get_event_stats`. + - Details form — `useDoc("Buzz Event", eventId)` bound `FormControl`s: `title`, + `short_description`, `about` (`Editor` from `frappe-ui/editor`), `start_date`/`end_date` + + `start_time`/`end_time`, `time_zone` (Combobox), `medium` Select, `venue` (Link → Event Venue, + team-scoped by hooks), `category`, `host` (Link → Event Host), `route` (slug input with + prefix hint), `banner_image` + `card_image` + `meta_image` via `FileUploader`. + Save via `doc.save.submit()` on a sticky footer `Button` (dirty-state aware). + - Header publish control: `is_published` toggle `Button` (solid green "Publish" / subtle + "Unpublish") with `dialog.confirm` when unpublishing an event with sold tickets. + - Danger zone card: cancel event (writes status; framework delete stays Desk-only). +- `data/events.ts`: add `useEventDoc(eventId)` + `useEventStats(eventId)`. + +## API contract + +| Endpoint | Method | Params | Returns | Auth | +|---|---|---|---|---| +| `buzz.api.events.get_event_stats` | GET | `event` | `{bookings, tickets_sold, gross_revenue, currency, checked_in}` | read perm on event (team member) | + +## Permissions notes + +Viewer: form read-only (disable inputs by `team_role`), stats visible. Write path enforced +server-side by team hooks. + +## Demo script + +1. Overview shows KPIs; cross-check numbers in Desk report views. +2. Change title + upload banner → save → public event page shows both. +3. Unpublish → public page 404s/hides; publish again. +4. Viewer-role member sees stats + disabled form. + +## Acceptance criteria (merge gate) + +### Automated tests — `buzz/tests/test_event_stats.py` (new) +`bench --site buzz.localhost run-tests --module buzz.tests.test_event_stats` +- Fixture: event with 2 bookings (one paid, one pending), 3 tickets, 1 check-in → + `get_event_stats` returns exact counts; gross revenue sums only `payment_received=1`. +- Zero-activity event → all zeros, no division/None errors. +- Cross-team caller → `PermissionError`. + +### Browser checks (agent-browser, dev server :8080) +1. Overview tab → KPI strip values match the fixture counts. +2. Edit title + upload banner → save → public event page shows new title + banner image request + 200. +3. Unpublish (event with sold tickets) → confirm dialog appears → public page hides/404s the + event; publish again → restored. +4. Viewer-role member → form inputs disabled, KPIs visible, save button absent. +5. Dirty-state: edit a field, navigate tabs → unsaved-changes guard (or explicit sticky save + still shows dirty state). + +## Dependencies & risks + +- Revenue definition: sum of `Event Payment.amount` where `payment_received=1` — document the + formula in the endpoint docstring; taxes make "gross" ambiguous (note for spec 12). diff --git a/specs/v2/01-dashboard/04-coupons.md b/specs/v2/01-dashboard/04-coupons.md new file mode 100644 index 00000000..f42ce6c5 --- /dev/null +++ b/specs/v2/01-dashboard/04-coupons.md @@ -0,0 +1,78 @@ +# 04 — Coupons + +| Phase | Depends on | Status | +|---|---|---| +| B | 01-dashboard/02 | Draft | + +## Goal + +`Buzz Coupon Code` management in the event workspace — create, edit, activate/deactivate, monitor +usage. + +## Demo criteria (definition of done) + +Manager creates a 10%-off coupon; attendee applies it at public checkout (existing +`validate_coupon`) and sees the discounted total; the Coupons tab shows `times_used` tick up. + +## Scope + +### In +- Coupons tab: table + create/edit dialog covering the discount path +- Free-tickets coupon type (`coupon_type`, `number_of_free_tickets`) +### Out (deferred to) +- `free_add_ons` child rows in the dialog → step 05 (needs add-ons UI patterns); until then that + child table stays Desk-editable +- Cross-event coupons (`applies_to`/`event_category` scope) → render read-only note if set + +## Backend changes + +None — plain CRUD under team hooks (coupon has `event` link; team stamped via cascade). + +## Frontend changes + +- `src/pages/manage/event/CouponsTab.vue` — `List`: `code` (mono), type (discount/free-tickets + `Badge`), value (`discount_value` + `discount_type` %/flat, or `number_of_free_tickets`), + usage `times_used`/`max_usage_count`, validity `valid_from`–`valid_till`, `is_active` Switch + inline. Header "New Coupon". +- `CouponDialog.vue` — `FormControl`s on real fields: `code` (reqd, uppercase), `coupon_type` + Select, then conditional: discount → `discount_type`, `discount_value`, + `maximum_discount_amount`, `minimum_order_value`; free tickets → `number_of_free_tickets`, + `ticket_type` (Link, filtered to this event). Common: `valid_from`/`valid_till`, + `max_usage_count`, `max_usage_per_user`, `is_active`. +- `data/coupons.ts` — `useList({ doctype: "Buzz Coupon Code", filters: { event } })`. + +## API contract + +No new endpoints (checkout reuses existing `buzz.api.validate_coupon`). + +## Permissions notes + +Manager+ CRUD; Viewer read-only. Code uniqueness errors surfaced from server via `toast.error`. + +## Demo script + +1. Create `LAUNCH10`, 10% off, max 100 uses, active. +2. Public checkout → apply `LAUNCH10` → total drops 10%. +3. Complete booking → tab shows `times_used = 1`. +4. Toggle inactive → checkout rejects the code. + +## Acceptance criteria (merge gate) + +### Automated tests +Extend existing coupon tests (or add `buzz/tests/test_coupons.py`) only for gaps exposed: +- `validate_coupon` respects `is_active=0`, `valid_till` past, `max_usage_count` reached, + `minimum_order_value` unmet (skip cases already covered by existing suite — audit first). +- Team cascade: coupon created via dashboard carries the event's team (covered by + `test_team_cascade`; assert once here if not parametrized). + +### Browser checks (agent-browser, dev server :8080) +1. Create coupon `LAUNCH10` (10%, active) → row appears with usage 0/100. +2. Fresh session → public checkout → apply `LAUNCH10` → total reduced 10%; complete booking. +3. Coupons tab → `times_used` shows 1. +4. Toggle `is_active` off inline → checkout rejects the code with visible error message. +5. Create dialog: switching `coupon_type` swaps discount vs free-tickets field groups. +6. Duplicate code creation → server error surfaced as toast, dialog stays open. + +## Dependencies & risks + +- Coupon validation logic stays server-side in `validate_coupon` — the dialog never duplicates it. diff --git a/specs/v2/01-dashboard/05-ticket-add-ons.md b/specs/v2/01-dashboard/05-ticket-add-ons.md new file mode 100644 index 00000000..00f8aa38 --- /dev/null +++ b/specs/v2/01-dashboard/05-ticket-add-ons.md @@ -0,0 +1,72 @@ +# 05 — Ticket add-ons + +| Phase | Depends on | Status | +|---|---|---| +| B | 01-dashboard/02 | Draft | + +## Goal + +`Ticket Add-on` CRUD in the workspace, including option lists (e.g. T-shirt sizes). + +## Demo criteria (definition of done) + +Manager adds "T-shirt (S/M/L), ₹299"; the public booking flow offers it with the size selector; +an attendee's selection lands in `Attendee Ticket Add-on`. + +## Scope + +### In +- Add-ons tab: table + create/edit dialog with options editor, enable/disable +- Coupons dialog follow-up: `free_add_ons` child rows editor (deferred from step 04) +### Out (deferred to) +- Attendee-side add-on change flow (already exists) — untouched + +## Backend changes + +None. + +## Frontend changes + +- `src/pages/manage/event/AddOnsTab.vue` — `List`: `title`, price (`format_currency`), options + summary (chips), `enabled` Switch inline. +- `AddOnDialog.vue` — real fields: `title` (reqd), `price` + `currency`, `description`, + `user_selects_option` (Switch) revealing an **options editor**: tag-style input writing + newline-separated values into the `options` Small Text field (matches current backend parsing), + `enabled`. +- Coupons: extend `CouponDialog.vue` with `free_add_ons` child-table editor (add-on Link + + option) now that add-on selectors exist. +- `data/addons.ts`. + +## API contract + +No new endpoints. + +## Permissions notes + +Manager+ CRUD; Viewer read-only. + +## Demo script + +1. Create "T-shirt", ₹299, options S/M/L, user_selects_option on, enabled. +2. Public booking → add-on appears with size Select; book with "M". +3. Desk: `Attendee Ticket Add-on` row records the selection. +4. Disable the add-on → hidden from new bookings. + +## Acceptance criteria (merge gate) + +### Automated tests — `buzz/tests/test_add_ons.py` (new or extend existing) +- Options round-trip: save "S\nM\nL" via the editor payload → stored `options` string is exactly + newline-delimited with no trailing newline; booking-side parser yields `["S","M","L"]`. +- Disabled add-on excluded from `get_event_booking_data` payload. + +### Browser checks (agent-browser, dev server :8080) +1. Create "T-shirt" ₹299, options S/M/L via tag editor, enabled → row shows option chips. +2. Public booking flow → add-on card with size `Select` (3 options) → book with "M". +3. Assert `Attendee Ticket Add-on` records "M" (Desk/REST check). +4. Toggle `enabled` off → new booking session no longer shows the add-on. +5. Coupon dialog now offers `free_add_ons` rows referencing the add-on. + +## Dependencies & risks + +- `options` is newline-delimited Small Text — the editor must round-trip exactly (no trailing + newline drift) or attendee selects break. diff --git a/specs/v2/01-dashboard/06-sponsorship-tiers-and-sponsors.md b/specs/v2/01-dashboard/06-sponsorship-tiers-and-sponsors.md new file mode 100644 index 00000000..ccf8136e --- /dev/null +++ b/specs/v2/01-dashboard/06-sponsorship-tiers-and-sponsors.md @@ -0,0 +1,77 @@ +# 06 — Sponsorship tiers & sponsors + +| Phase | Depends on | Status | +|---|---|---| +| B | 01-dashboard/02 | Draft | + +## Goal + +Sponsors tab: manage `Sponsorship Tier` and `Event Sponsor`, and see incoming +`Sponsorship Enquiry` records — the sponsorship pipeline visible where the event lives. + +## Demo criteria (definition of done) + +Manager creates a "Gold ₹50k" tier and adds a sponsor with logo; both render on the public event +page's sponsors section. Incoming enquiries appear read-only with status. + +## Scope + +### In +- Tiers CRUD (`title`, `price`, `currency`) +- Sponsors CRUD grouped by tier (`company_name`, `tier`, `company_logo`, `website`, `country`) +- Enquiries list (read-only) with status badges +### Out (deferred to) +- Enquiry approval/payment-link workflow (exists via attendee flow + Desk) — surfacing actions + here is a follow-up once payments v2 lands (future payments pillar) +- Pitch-deck email settings → step 12 + +## Backend changes + +None. + +## Frontend changes + +- `src/pages/manage/event/SponsorsTab.vue` — three sections: + 1. **Tiers** — compact `List` (title, price) + `TierDialog` (3 fields). + 2. **Sponsors** — grouped by tier; each sponsor: `Avatar`-style logo, company_name, website + link; `SponsorDialog`: `company_name` (reqd), `tier` (Select from this event's tiers), + `company_logo` `FileUploader`, `website`, `country` (Link). + 3. **Enquiries** — read-only `List` of `Sponsorship Enquiry` for the event: company, tier, + status `Badge`, created; row → slide-over detail (`Dialog` right placement) showing the + enquiry; no mutations. +- `data/sponsors.ts` — three `useList`s keyed by event. + +## API contract + +No new endpoints. + +## Permissions notes + +Manager+ CRUD tiers/sponsors; enquiries read-only for all team roles except Frontdesk. +Existing attendee-side enquiry endpoints untouched. + +## Demo script + +1. Create tier "Gold" ₹50,000. +2. Add sponsor "Acme" with logo under Gold → public event page sponsors section shows logo. +3. Submit a sponsorship enquiry from the public flow → appears in Enquiries with "Pending Approval". + +## Acceptance criteria (merge gate) + +### Automated tests +None new — CRUD under hooks; `test_team_cascade`/`test_team_permissions` parametrized cases cover +`Sponsorship Tier`/`Event Sponsor`/`Sponsorship Enquiry`. Existing enquiry-dedup test +(fix `8eba8cb`) must stay green. + +### Browser checks (agent-browser, dev server :8080) +1. Create tier "Gold" ₹50,000 → appears in Tiers section. +2. Add sponsor "Acme" (logo upload) under Gold → sponsors section groups it under Gold with logo. +3. Public event page → sponsors section renders Acme logo (image request 200). +4. Fresh session → submit sponsorship enquiry via public flow → Enquiries section shows it with + "Pending Approval" badge; slide-over opens read-only. +5. Frontdesk-role member → Sponsors tab hidden/inaccessible. + +## Dependencies & risks + +- `Event Sponsor.enquiry` dedup validation (recent fix `8eba8cb`) lives server-side — dialog just + surfaces errors. diff --git a/specs/v2/01-dashboard/07-bookings-and-attendees.md b/specs/v2/01-dashboard/07-bookings-and-attendees.md new file mode 100644 index 00000000..3a08c6de --- /dev/null +++ b/specs/v2/01-dashboard/07-bookings-and-attendees.md @@ -0,0 +1,100 @@ +# 07 — Bookings & attendees + +| Phase | Depends on | Status | +|---|---|---| +| C | 01-dashboard/03 | Draft | + +## Goal + +See the money and the people: Bookings tab (payments, offline confirmation) and Attendees tab +(search, filter, export). + +## Demo criteria (definition of done) + +A test booking completed publicly appears in the Bookings tab; manager confirms an offline +payment; finds an attendee by name in Attendees and exports CSV. + +## Scope + +### In +- Bookings tab: list + detail panel + confirm-offline-payment action +- Attendees tab: merged ticket/attendee list, search, filters, CSV export +### Out (deferred to) +- Refunds (needs future payments pillar); cancellation-request approval queue → Phase E +- Manual check-in from attendee rows → step 08 + +## Backend changes + +`buzz/api/bookings.py` (new): + +```python +@frappe.whitelist() +def confirm_offline_payment(booking: str) -> dict: + """Marks the booking's Event Payment as received + submits booking + (mirrors current Desk flow on Event Payment / Event Booking). + Guard: write perm on the booking (team Manager+).""" + +@frappe.whitelist() +def export_attendees(event: str) -> str: + """CSV string of attendees (name, email, ticket type, add-ons, checked-in, booking id). + Built with frappe.get_all joins — one query, no N+1. Read perm on event.""" +``` + +## Frontend changes + +- `src/pages/manage/event/BookingsTab.vue` — `List`: booking id, booker (name/email), amount + (`tabular-nums`), payment status `Badge` (Paid green / Pending orange / Failed red), gateway or + "Offline", created (`relative time`). Filter `Select` by payment status. Row → detail panel + (right slide-over `Dialog`): attendees, `Event Payment` info (gateway, id, amount, tax), + applied coupon, action `Button` "Mark payment received" (offline, unpaid only; + `dialog.confirm`). +- `src/pages/manage/event/AttendeesTab.vue` — `List` over `Event Ticket` joined attendee fields: + name, email, ticket type, add-ons summary, checked-in `Badge`. Debounced search `FormControl` + (name/email), filters: ticket type `Select`, checked-in `Select`. Header: count summary + + "Export CSV" `Button` → `export_attendees` → client download. +- `data/bookings.ts`, `data/attendees.ts`. + +## API contract + +| Endpoint | Method | Params | Returns | Auth | +|---|---|---|---|---| +| `buzz.api.bookings.confirm_offline_payment` | POST | `booking` | `{ok, payment_status}` | team Manager+ | +| `buzz.api.bookings.export_attendees` | GET | `event` | CSV text | team member (read) | + +## Permissions notes + +Bookings/tickets query conditions include the owner carve-out (00-teams/03) — manager lists here +are filtered by `event`, so only team data shows. Viewer read-only (no confirm button). + +## Demo script + +1. Complete a public test booking (offline method) → Bookings tab shows Pending. +2. "Mark payment received" → confirm → Badge flips Paid; attendee gets ticket email. +3. Attendees tab → search attendee name → row found; filter by ticket type. +4. Export CSV → opens with correct columns/rows. + +## Acceptance criteria (merge gate) + +### Automated tests — `buzz/tests/test_bookings_api.py` (new) +`bench --site buzz.localhost run-tests --module buzz.tests.test_bookings_api` +- `confirm_offline_payment`: pending offline booking → payment marked received, booking + submitted, ticket emails queued; already-paid booking raises; gateway (non-offline) booking + raises; Viewer-role caller raises `PermissionError`. +- `export_attendees`: CSV header + row count match fixture; add-ons column aggregates values; + cross-team caller raises; query count bounded (no N+1 — assert with `frappe.db` query counter + or `assertNumQueries`-style helper). + +### Browser checks (agent-browser, dev server :8080) +1. Fresh session → offline-method booking on the event → Bookings tab (manager session) shows + Pending badge. +2. Row → detail panel → "Mark payment received" → confirm → badge flips Paid without reload. +3. Attendees tab → search by attendee name → row filtered; filter by ticket type works. +4. "Export CSV" → file downloads; parse: header columns as specced, row for the new attendee. +5. Viewer-role member → no "Mark payment received" button; tabs render read-only. + +## Dependencies & risks + +- `confirm_offline_payment` must reuse the existing `mark_payment_as_received`/submit logic from + `buzz/payments.py` + booking controller — no duplicate state machine. +- Realtime: `useList` auto-refresh via socket (`refetch_resource` pattern) is a nice-to-have; + manual refresh button is the fallback. diff --git a/specs/v2/01-dashboard/08-checkin-desk.md b/specs/v2/01-dashboard/08-checkin-desk.md new file mode 100644 index 00000000..56d402b4 --- /dev/null +++ b/specs/v2/01-dashboard/08-checkin-desk.md @@ -0,0 +1,100 @@ +# 08 — Check-in desk + +| Phase | Depends on | Status | +|---|---|---| +| C | 01-dashboard/07 | Draft | + +## Goal + +Check-in surfaced in the event workspace with live stats and manual check-in; Frontdesk-role +members get a focused, check-in-only experience. + +## Demo criteria (definition of done) + +Scan (or paste) a ticket → checked in, stats tick up, recent-check-ins feed updates. Manual +check-in from an attendee row works. A `Frontdesk`-role member logs in and sees only the check-in +surface. + +## Scope + +### In +- Check-in tab: stats, embedded scanner, recent check-ins feed, manual check-in +- Frontdesk-role navigation trimming +### Out (deferred to) +- Ticket-tear animation / confetti delight pass → future "check-in redesign" pillar +- Multi-day/date-wise check-in config UI → Phase E + +## Backend changes + +`buzz/api/events.py` addition: + +```python +@frappe.whitelist() +def get_checkin_stats(event: str) -> dict: + """{total_tickets, checked_in, by_ticket_type: [{ticket_type, total, checked_in}]}""" +``` + +`checkin_ticket` / `validate_ticket_for_checkin` (existing) gain the team-membership guard from +`00-teams/03` — no signature change. + +## Frontend changes + +- `src/pages/manage/event/CheckInTab.vue`: + - Stats strip: checked-in/total overall + per ticket type (progress bars, + `bg-surface-gray-2` track / `bg-surface-gray-7` fill). + - Embedded scanner: reuse `CheckInScanner.vue` internals (html5-qrcode + existing + `validate_ticket_for_checkin` → `checkin_ticket` flow) refactored into a + `components/checkin/ScannerPanel.vue` shared by both surfaces. + - Recent check-ins feed: last 20 `Event Check In` rows (attendee, ticket type, time), socket or + poll refresh. + - Manual check-in: search attendee (reuses Attendees data) → `Button` "Check in" per row. +- Attendees tab (step 07): row action "Check in" appears for roles with check-in capability. +- Frontdesk navigation: `ManagerLayout` reads `team_role`; `Frontdesk` sees sidebar with Events → + event list rows deep-link straight to `/manage/events/:id/check-in`; other tabs hidden. +- Existing standalone `/check-in/:eventName` route kept (redirects to the workspace tab when the + user has team access). + +## API contract + +| Endpoint | Method | Params | Returns | Auth | +|---|---|---|---|---| +| `buzz.api.events.get_checkin_stats` | GET | `event` | stats dict | team member w/ check-in capability | + +## Permissions notes + +Check-in capability: Owner/Admin/Manager/Frontdesk (matrix in `00-teams/00-plan.md`). Global +`Frontdesk Manager` Frappe role still required by the existing endpoints — granted automatically +by membership role sync. + +## Demo script + +1. Open Check-in tab → stats show 0/N. +2. Paste a valid ticket id in scanner input → success state, stats 1/N, feed shows the entry. +3. Duplicate scan → "already checked in" warning state. +4. Manual check-in from attendee search → works. +5. Log in as frontdesk-role user → sidebar trimmed; can check in; cannot open Tickets tab. + +## Acceptance criteria (merge gate) + +### Automated tests — `buzz/tests/test_checkin.py` (new or extend existing check-in tests) +`bench --site buzz.localhost run-tests --module buzz.tests.test_checkin` +- `get_checkin_stats`: fixture with 3 tickets / 1 checked-in → totals + per-ticket-type breakdown + exact; cross-team caller raises. +- `checkin_ticket` team guard: frontdesk member of team A checking team B's ticket raises; + same-team succeeds; duplicate check-in returns the already-checked-in response (existing + behavior preserved). + +### Browser checks (agent-browser, dev server :8080) +1. Check-in tab → stats strip shows 0/N; paste valid ticket ID in manual input → success state, + stats tick to 1/N, feed shows the entry. +2. Same ticket again → "already checked in" warning state (no double record). +3. Attendees tab → row "Check in" action → works, stats update. +4. Frontdesk-role member logs in → sidebar shows only Events; event row deep-links to the + check-in tab; Tickets tab route → blocked/hidden. +5. Standalone `/dashboard/check-in/` still functions (regression for mid-event usage). + +## Dependencies & risks + +- Camera permissions on mobile — scanner panel must degrade to manual input (already does today). +- Refactor of `CheckInScanner.vue` must not regress the standalone route mid-event; feature-flag + the redirect. diff --git a/specs/v2/01-dashboard/09-venues-hosts-library.md b/specs/v2/01-dashboard/09-venues-hosts-library.md new file mode 100644 index 00000000..d61f1431 --- /dev/null +++ b/specs/v2/01-dashboard/09-venues-hosts-library.md @@ -0,0 +1,78 @@ +# 09 — Venues & hosts library + +| Phase | Depends on | Status | +|---|---|---| +| D | 01-dashboard/01 | Draft | + +## Goal + +Team-level reusable resources get a sidebar home: `/manage/venues` and `/manage/hosts` — the +"Library" section. Unblocks fully-Desk-free event creation (Overview's venue/host Links need +somewhere to create entries). + +## Demo criteria (definition of done) + +Manager creates a venue and a host from the library; both are selectable in the event Overview +form; host logo renders on the public page. + +## Scope + +### In +- Venues page: CRUD on `Event Venue` (team-scoped) +- Hosts page: CRUD on `Event Host` incl. `social_media_links` child rows +### Out (deferred to) +- `Event Category` management — global taxonomy, stays Desk/System Manager +- Public host profile pages → future "public event frontend" pillar + +## Backend changes + +None (team field + hooks from 00-teams cover both doctypes). + +## Frontend changes + +- Sidebar: enable "Venues" (`lucide-map-pin`) and "Hosts" (`lucide-users`) under a "Library" + `SidebarGroup`. +- `src/pages/manage/VenuesList.vue` — `List`: name, `type` Badge (per Select options), `address` + excerpt. `VenueDialog`: name (doc name/title), `type` Select, `address` Textarea, + `google_maps_embed_code` Code textarea (collapsed "Advanced" section), `latitude`/`longitude`. +- `src/pages/manage/HostsList.vue` — card grid (logo `Avatar`, name, `by_line`). `HostDialog`: + name, `logo` FileUploader, `by_line`, `about` (`Editor`), `country`, `address`, + `social_media_links` child editor (platform Select + URL rows). +- `data/venues.ts`, `data/hosts.ts`. + +## API contract + +No new endpoints. + +## Permissions notes + +Manager+ CRUD; Viewer read-only. Link fields in Overview (step 03) automatically list only the +team's venues/hosts thanks to query conditions applying to link searches. + +## Demo script + +1. `/manage/venues` → create "Community Hall", type + address → appears. +2. `/manage/hosts` → create host with logo + 2 social links. +3. Event Overview → venue/host selectable; save → public page shows host block. +4. Other team's manager sees neither entry in their library or link searches. + +## Acceptance criteria (merge gate) + +### Automated tests +None new — team-direct doctypes covered by `test_team_permissions` parametrized cases +(`Event Venue`, `Event Host` list isolation + role matrix). + +### Browser checks (agent-browser, dev server :8080) +1. `/dashboard/manage/venues` → create "Community Hall" (type, address) → row appears. +2. `/dashboard/manage/hosts` → create host with logo + 2 social links → card renders logo. +3. Event Overview → venue Link search returns "Community Hall"; host Link returns the new host; + save → public page shows host block. +4. Team-B manager session → venues/hosts lists empty of team A's entries; Overview Link search + does not surface them (link-query scoping check). +5. Viewer-role member → library pages read-only (no New button). + +## Dependencies & risks + +- `Event Venue` uses the document name as its title (no separate name field) — dialog maps the + name input accordingly (rename on edit uses `frappe.rename_doc` semantics; keep name immutable + in UI, edit only other fields, to avoid rename complexity). diff --git a/specs/v2/01-dashboard/10-schedule-tracks-and-talks.md b/specs/v2/01-dashboard/10-schedule-tracks-and-talks.md new file mode 100644 index 00000000..534d0135 --- /dev/null +++ b/specs/v2/01-dashboard/10-schedule-tracks-and-talks.md @@ -0,0 +1,101 @@ +# 10 — Schedule, tracks & talks + +| Phase | Depends on | Status | +|---|---|---| +| D | 01-dashboard/03 | Draft | + +## Goal + +Desk parity for the agenda: Schedule tab managing `Event Track`, `Schedule Item` (child table on +Buzz Event), `Event Talk`, plus a read-and-accept inbox for `Talk Proposal` submissions. +(CRUD parity only — the drag-drop visual builder remains a future pillar.) + +## Demo criteria (definition of done) + +Manager builds a 2-track half-day schedule; the public schedule rendering shows it. Accepting a +talk proposal creates an `Event Talk` that can be slotted. + +## Scope + +### In +- Tracks chips CRUD; schedule items list grouped by day → time; talk CRUD with speakers +- Proposals inbox: list + detail + Accept/Reject +### Out (deferred to) +- Gantt/timeline drag-drop builder → future "schedule builder" pillar +- Speaker Profile management (attendee-owned) → Phase E + +## Backend changes + +`buzz/api/talks.py` (new): + +```python +@frappe.whitelist() +def accept_talk_proposal(proposal: str) -> dict: + """Sets Talk Proposal status Approved; creates Event Talk (+ Talk Speaker child rows + from Proposal Speaker). Returns the new talk name. Manager+ on the event's team. + Reuses existing controller logic if present — audit talk_proposal.py first.""" +``` + +(If the accept transition already exists on the doctype controller, the endpoint is a thin wrapper.) + +## Frontend changes + +- `src/pages/manage/event/ScheduleTab.vue` — three stacked sections: + 1. **Tracks** — chip row (`Badge`-styled) + add/rename/delete (`dialog.prompt`). + 2. **Schedule** — `Schedule Item` rows grouped by date, sorted by start time: time range + (`tabular-nums`), title, track chip, linked talk. Row edit dialog: date, start/end time, + title, track Select, talk Link. Saved through the parent `Buzz Event` doc + (`useDoc` child-table update). + 3. **Talks** — `List` of `Event Talk`: title, speakers (Avatars), track, status. `TalkDialog`: + title, description (`Editor`), track, speakers child editor (Link → Speaker Profile). +- **Proposals inbox** — collapsible section: pending `Talk Proposal` rows (title, speaker, + submitted); detail slide-over with full submission; `Button` Accept (→ `accept_talk_proposal`, + toast + talk appears) / Reject (`dialog.confirm`, sets status). +- `data/schedule.ts`, `data/talks.ts`. + +## API contract + +| Endpoint | Method | Params | Returns | Auth | +|---|---|---|---|---| +| `buzz.api.talks.accept_talk_proposal` | POST | `proposal` | `{talk}` | team Manager+ | + +## Permissions notes + +`Talk Proposal` is attendee-submitted (owner = speaker) — team members read via team scoping +(cascade stamps team from event); speakers keep editing their own (existing +`allow_editing_talks_after_acceptance` behavior untouched). + +## Demo script + +1. Add tracks "Main Hall", "Workshop". +2. Create 4 schedule items across two dates → grouped rendering correct. +3. Submit a talk proposal via the public flow → appears in inbox. +4. Accept → `Event Talk` created with speakers; slot it into a schedule item. +5. Public schedule (where rendered) shows the agenda. + +## Acceptance criteria (merge gate) + +### Automated tests — `buzz/tests/test_talks.py` (new) +`bench --site buzz.localhost run-tests --module buzz.tests.test_talks` +- `accept_talk_proposal`: proposal with 2 `Proposal Speaker` rows → status Approved, `Event Talk` + created with matching speakers; second call raises (already accepted); Viewer/cross-team caller + raises. +- Reject path sets status without creating a talk. +- Speaker-side edit-after-acceptance behavior unchanged + (`allow_editing_talks_after_acceptance` on/off). + +### Browser checks (agent-browser, dev server :8080) +1. Schedule tab → add tracks "Main Hall", "Workshop" → chips render. +2. Create 4 schedule items across 2 dates → grouped by date, sorted by time + (assert order in DOM). +3. Fresh session → submit talk proposal via public flow → inbox shows it pending. +4. Accept from slide-over → toast, talk appears in Talks section with speakers; slot it into a + schedule item. +5. Public schedule rendering shows both tracks and the slotted talk. + +## Dependencies & risks + +- `Schedule Item` is a child table — mutations go through the parent event doc save; concurrent + edits overwrite (single-editor assumption fine for now, note it). +- Audit existing `talk_proposal.py` controller before writing the accept endpoint — logic may + already exist Desk-side. diff --git a/specs/v2/01-dashboard/11-event-templates.md b/specs/v2/01-dashboard/11-event-templates.md new file mode 100644 index 00000000..a11c7b7a --- /dev/null +++ b/specs/v2/01-dashboard/11-event-templates.md @@ -0,0 +1,92 @@ +# 11 — Event templates + +| Phase | Depends on | Status | +|---|---|---| +| D | 01-dashboard/02 | Draft | + +## Goal + +`Event Template` manageable from the dashboard and "New event from template" — repeat organizers +spin up configured events in one step. + +## Demo criteria (definition of done) + +Manager creates a template with 2 ticket types and 1 add-on; "New Event" dialog with that template +produces an event with those ticket types/add-ons pre-created. + +## Scope + +### In +- `/manage/templates` list + template editor (children: `template_ticket_types`, + `template_add_ons`, `template_custom_fields`) +- Template picker in the New Event dialog + instantiation endpoint +### Out (deferred to) +- Template sharing across teams — templates are team-scoped like everything else + +## Backend changes + +`buzz/api/templates.py` (new): + +```python +@frappe.whitelist() +def create_event_from_template(template: str, title: str, start_date: str, end_date: str | None = None) -> dict: + """Creates Buzz Event copying template fields (category, medium, banner_image, host, venue, + guest-booking config, tax config, email/deck config) + Event Ticket Type and Ticket Add-on + records from the template child tables. Team from active team; Manager+. + Audit event_template.py first — reuse existing instantiation logic if present.""" +``` + +## Frontend changes + +- Sidebar: enable "Templates" (`lucide-layout-template`). +- `src/pages/manage/TemplatesList.vue` — `List`: `template_name`, category, medium, counts of + child rows. +- `src/pages/manage/TemplateEditor.vue` (route `/manage/templates/:id`) — sectioned form mirroring + the doctype: basics (`template_name`, `category`, `medium`, `banner_image`, `host`, `venue`), + booking config (`allow_guest_booking`, `guest_verification_method`), content + (`short_description`, `about` Editor), tax (`apply_tax`, `tax_label`, `tax_percentage`), + emails (`send_ticket_email`, `ticket_email_template`, sponsor deck fields), children as + sectioned lists: ticket types (title/price/qty rows), add-ons, custom fields. +- `NewEventDialog` (step 01): optional "Start from template" `Select`; when set → calls + `create_event_from_template` instead of plain insert. +- `data/templates.ts`. + +## API contract + +| Endpoint | Method | Params | Returns | Auth | +|---|---|---|---|---| +| `buzz.api.templates.create_event_from_template` | POST | `template, title, start_date, end_date?` | `{event}` | team Manager+ | + +## Permissions notes + +Templates team-scoped (00-teams/02 team-direct doctype). Manager+ CRUD. + +## Demo script + +1. Create template "Monthly Meetup" with 2 ticket types + 1 add-on. +2. New Event → pick template, title "July Meetup", date → created. +3. Workspace Tickets tab shows both ticket types; Add-ons shows the add-on. +4. Public page bookable immediately after publish. + +## Acceptance criteria (merge gate) + +### Automated tests — `buzz/tests/test_event_templates.py` (new) +`bench --site buzz.localhost run-tests --module buzz.tests.test_event_templates` +- `create_event_from_template`: template with 2 ticket types + 1 add-on + 1 custom field → + event created copying scalar config (category, medium, tax, email settings) and children as + real `Event Ticket Type`/`Ticket Add-on`/`Buzz Custom Field` rows with `event` + `team` stamped. +- Cross-team template use raises; Viewer caller raises. +- Template with empty children → event still created cleanly. + +### Browser checks (agent-browser, dev server :8080) +1. `/dashboard/manage/templates` → create "Monthly Meetup" with 2 ticket types + 1 add-on in the + editor → counts show in list. +2. Events list → "New Event" → pick the template, title "July Meetup", date → created; workspace + Tickets tab shows both ticket types, Add-ons shows the add-on. +3. Publish → public page bookable with the template's ticket types. +4. Team-B manager → templates list does not contain team A's template. + +## Dependencies & risks + +- Instantiation copies must stamp `event` + let the team cascade stamp `team` — verify no + `ignore_permissions` needed (creator is Manager+). diff --git a/specs/v2/01-dashboard/12-event-settings-payments-and-forms.md b/specs/v2/01-dashboard/12-event-settings-payments-and-forms.md new file mode 100644 index 00000000..f6add761 --- /dev/null +++ b/specs/v2/01-dashboard/12-event-settings-payments-and-forms.md @@ -0,0 +1,99 @@ +# 12 — Event settings: payments, registration & forms + +| Phase | Depends on | Status | +|---|---|---| +| D | 01-dashboard/03 | Draft | + +## Goal + +The event Settings tab — everything else on the `Buzz Event` form: payment gateways, tax, +registration/guest-booking config, ticket email config, custom fields, custom forms, extra pages. +Completes single-event Desk parity. + +## Demo criteria (definition of done) + +Manager adds a payment gateway row and it appears at public checkout; sets +`registrations_close_at` in the past and public booking closes; adds a custom field and it shows +in the booking form. + +## Scope + +### In (grouped settings sections, all on `useDoc("Buzz Event")` unless noted) +- **Payments**: `payment_gateways` child rows (`Event Payment Gateway`: gateway Link + currency); + `Offline Payment Method` list for the event (separate doctype CRUD); `free_webinar` toggle +- **Tax**: `apply_tax`, `tax_label`, `tax_percentage`, `tax_inclusive` +- **Registration**: `registrations_close_at`, `allow_guest_booking`, + `guest_verification_method`, `external_registration_page` + `registration_url`, + `default_ticket_type`, `attach_calendar_invite` +- **Ticket email**: `send_ticket_email`, `ticket_email_template` (Link → Email Template), + `attach_email_ticket`, `ticket_print_format` +- **Sponsor deck**: `auto_send_pitch_deck`, `sponsor_deck_email_template`, + `sponsor_deck_reply_to`, `sponsor_deck_cc`, `sponsor_deck_attachments`, + `show_sponsorship_section` +- **Custom fields**: `Buzz Custom Field` CRUD for this event (label, type, options, required) +- **Forms & pages**: `custom_forms` child rows (`Buzz Event Form`) linking custom forms; + `Additional Event Page` CRUD + +### Out (deferred to) +- Team-level defaults (transfer/cancel/add-on windows, default email templates, sponsor-deck + defaults) — live in `Buzz Team Settings` (`00-teams/05`), surfaced in the Team Settings dialog + (step 13), not here. `Buzz Settings` (site-wide Single) stays Desk/System Manager for global + admin config (login banner, proposal funnel) +- Email Template authoring UI — Link picker only; authoring stays Desk (Phase E) + +## Backend changes + +None expected — all doc/child CRUD under hooks. If offline-method or custom-field creation needs +validation beyond doctype controllers, add to existing controllers, not new endpoints. + +## Frontend changes + +- `src/pages/manage/event/SettingsTab.vue` — settings-page archetype: left anchor nav + (sections above), right content cards; each card = `FormControl` group bound to `useDoc`, + sticky save. Child-table editors reuse the row-editor pattern from steps 05/11. +- `data/eventSettings.ts` (offline methods, custom fields `useList`s). + +## API contract + +No new endpoints. + +## Permissions notes + +Manager+ writes; Viewer read-only. Gateway credentials themselves live in the gateway apps' +settings (System Manager) — this tab only *selects* gateways, never edits credentials. + +## Demo script + +1. Add Razorpay gateway row (INR) → public checkout offers it. +2. Enable tax 18% "GST" → checkout total shows tax line. +3. Set `registrations_close_at` past → public page shows "registrations closed"; clear it → open. +4. Add custom field "Company" (required) → booking form renders it; value lands in + `Additional Field` rows. +5. Toggle `allow_guest_booking` off → guest checkout demands login. + +## Acceptance criteria (merge gate) + +### Automated tests — extend booking-flow tests (`buzz/tests/test_booking_flow.py` or existing module) +- `registrations_close_at` in the past → `get_event_booking_data`/`process_booking` reject with + the closed-registrations error; cleared → accepted. +- `allow_guest_booking=0` → guest `process_booking` raises; logged-in booking succeeds. +- Tax config: `apply_tax` 18% → booking totals include tax line (inclusive + exclusive branches). +- Required `Buzz Custom Field` missing in submission → validation error; provided → stored in + `Additional Field` rows. + +### Browser checks (agent-browser, dev server :8080) +1. Settings tab → add Razorpay gateway row (INR) → fresh session checkout offers Razorpay. +2. Enable tax 18% "GST" → checkout shows tax line, math correct on screen. +3. Set `registrations_close_at` past → public page shows registrations-closed state; clear → + bookable again. +4. Add required custom field "Company" → booking form renders it; submitting without it blocks + with field error; with it → booking completes. +5. Toggle `allow_guest_booking` off → guest checkout demands login. +6. Anchor nav: each settings section scrolls/activates correctly; sticky save reflects dirty + state per section. + +## Dependencies & risks + +- This tab touches many existing public-flow behaviors — regression pass over the booking flow is + part of the demo, not optional. +- `sponsor_deck_attachments` child (files) needs `FileUploader` multi-attach handling. diff --git a/specs/v2/01-dashboard/13-team-settings-members-and-invites.md b/specs/v2/01-dashboard/13-team-settings-members-and-invites.md new file mode 100644 index 00000000..a1ccfa52 --- /dev/null +++ b/specs/v2/01-dashboard/13-team-settings-members-and-invites.md @@ -0,0 +1,117 @@ +# 13 — Team settings, members & invites + +| Phase | Depends on | Status | +|---|---|---| +| D | 00-teams/04, 00-teams/05, 01-dashboard/01 | Draft | + +## Goal + +Team administration UI: edit team profile, manage members and roles, invite by email, see/revoke +pending invitations. + +## Demo criteria (definition of done) + +Owner invites a colleague as Frontdesk from the UI; colleague accepts, appears in Members, logs in +and sees only the check-in surface. Owner changes a member's role and removes another. + +## Scope + +### In +- Team settings dialog (settings-dialog archetype): General, Members, Invites sections +- Role change / remove member / leave team +### Out (deferred to) +- Ownership transfer flow — explicit future step (Owner role not assignable via invite/role change) +- Billing/plans — future pillar + +## Backend changes + +`buzz/api/teams.py` additions: + +```python +@frappe.whitelist() +def get_team_members(team) -> list # Active memberships + user full_name, avatar; member-visible + +@frappe.whitelist() +def update_member_role(team, user, team_role) # Owner/Admin; cannot touch Owner rows; not to Owner + +@frappe.whitelist() +def remove_member(team, user) # Owner/Admin; sets membership Disabled; cannot remove last Owner + +@frappe.whitelist() +def update_team(team, team_name=None, logo=None) # Owner only + +@frappe.whitelist() +def leave_team(team) # any member except last Owner +``` + +Role revocation on remove/demote goes through `buzz.teams.sync_frappe_roles(user)` (recompute +across remaining memberships). + +## Frontend changes + +- Sidebar: enable "Team Settings" (`lucide-settings`), opens `TeamSettingsDialog.vue` + (settings-dialog archetype: left nav list, right content pane, `Dialog` size xl): + - **General** — team name `FormControl`, logo `FileUploader`, slug (read-only display), + danger zone: Leave team (`dialog.confirm`). + - **Defaults** — `Buzz Team Settings` (00-teams/05) bound via + `useDoc("Buzz Team Settings", team)`: transfer/add-on/cancellation window Ints, support + email, `default_ticket_email_template` + sponsor-deck defaults (Email Template Links, + reply-to, cc, `auto_send_pitch_deck` Switch). Owner/Admin editable; others read-only. + - **Members** — `List`: `Avatar`, full name, email, `team_role` — inline `Select` editable by + Owner/Admin (disabled on Owner rows), remove action (`dialog.confirm`). + - **Invites** — "Invite member" row: email `FormControl` + role `Select` + (Admin/Manager/Frontdesk/Viewer) + `Button` "Send invite" → `invite_member`; pending list + (email, role, sent time) with Revoke. +- `data/teams.ts` additions: members/invites `useList`-style calls + mutations. + +## API contract + +| Endpoint | Method | Params | Returns | Auth | +|---|---|---|---|---| +| `buzz.api.teams.get_team_members` | GET | `team` | `[{user, full_name, user_image, team_role, status}]` | team member | +| `buzz.api.teams.update_member_role` | POST | `team, user, team_role` | `{ok}` | Owner/Admin | +| `buzz.api.teams.remove_member` | POST | `team, user` | `{ok}` | Owner/Admin | +| `buzz.api.teams.update_team` | POST | `team, team_name?, logo?` | `{ok}` | Owner | +| `buzz.api.teams.leave_team` | POST | `team` | `{ok}` | member | +| (from 00-teams/04) `invite_member`, `get_pending_invitations`, `revoke_invitation` | | | | Owner/Admin | + +## Permissions notes + +All mutations double-guarded: UI hides controls by `team_role`, server validates via membership +lookups. Last-Owner protection lives in the membership controller (00-teams/01). + +## Demo script + +1. As Owner: Team Settings → rename team + upload logo → sidebar switcher updates. +2. Invites → invite `frank@x.com` as Frontdesk → pending row appears. +3. Frank accepts → Members shows Frank (Frontdesk); Frank's session: check-in-only nav (step 08). +4. Change Frank → Viewer → his next load shows read-only workspace. +5. Remove Frank → memberships Disabled; Frank's `/manage` → onboarding screen. +6. As Manager-role member: settings dialog shows Members read-only, no invite form. + +## Acceptance criteria (merge gate) + +### Automated tests — extend `buzz/tests/test_teams.py` +- `update_member_role`: Owner/Admin succeeds; Manager raises; targeting an Owner row raises; + promoting to Owner raises. +- `remove_member`: sets Disabled; last-Owner removal raises; role recompute — user in two teams + keeps `Event Manager` when removed from one, loses it when removed from both. +- `leave_team`: member succeeds; last Owner raises. +- `update_team`: Owner succeeds; Admin raises. + +### Browser checks (agent-browser, dev server :8080) +1. Owner → Team Settings dialog → General: rename + logo upload → sidebar switcher label/avatar + update. +2. Defaults section → change transfer window to 7 → saved; attendee-facing effect covered by + `00-teams/05` check 1 (re-run it here). +3. Invites → invite `frank@x.com` as Frontdesk → pending row appears; accept in fresh session → + Members shows Frank (Frontdesk). +4. Frank's session → sidebar trimmed to check-in surface (cross-check with step 08 criteria). +5. Change Frank → Viewer → his next page load shows read-only workspace; remove Frank → his + `/dashboard/manage` shows onboarding. +6. Manager-role member → Members list read-only, no invite form, no Defaults editing. + +## Dependencies & risks + +- Role sync recompute on remove/demote is the fiddly bit — test a user in two teams keeping + `Event Manager` when removed from one. diff --git a/specs/v2/01-dashboard/14-pulse-analytics.md b/specs/v2/01-dashboard/14-pulse-analytics.md new file mode 100644 index 00000000..9a3fa2b4 --- /dev/null +++ b/specs/v2/01-dashboard/14-pulse-analytics.md @@ -0,0 +1,115 @@ +# 14 — Product analytics (Pulse) + +| Phase | Depends on | Status | +|---|---|---| +| Cross-cutting (init after 01, instrument as steps land) | 01-dashboard/01 | Draft | + +## Goal + +Product analytics via **Frappe Pulse**, exactly the way Frappe CRM does it on current develop: +the `telemetryPlugin` that ships inside frappe-ui v1 beta + capture call sites. Near-zero backend +code — boot config, client loading, and ingest all live in frappe / frappe-ui / the Pulse service. + +## How it works (verified against frappe/crm develop + frappe-ui main) + +- frontend init: `app.use(telemetryPlugin, { app_name: 'buzz' })` — plugin from + `frappe-ui/frappe` (`frappe/telemetry/index.ts`). +- The plugin fetches boot config from the whitelisted framework method + `frappe.utils.telemetry.pulse.client.boot_config`, which returns enabled/host/key/site only when + site config has `pulse_api_key` **and** System Settings `enable_telemetry` is on + (or `pulse_force_enabled`). Without config it returns `{enabled: false}` → the plugin no-ops. +- The Pulse client JS is dynamically imported at runtime from the pulse host + (default `https://pulse.m.frappe.cloud/assets/pulse/js/pulse_client.js`); origin-validated. +- Components: `const { capture } = useTelemetry()` then `capture('event_created', {...})` — + **bare snake_case event names**, no app prefix (`app_name` is passed separately on every event). +- Server-side captures: `from frappe.utils.telemetry import capture; capture("active_site", "buzz")`. + +## Demo criteria (definition of done) + +On a staging site with `pulse_api_key` configured: creating an event produces an `event_created` +event visible in the Pulse dashboard attributed to app `buzz`. On an unconfigured site: zero +console errors, zero network noise beyond the one boot_config call. + +## Scope + +### In +- Plugin init + `useTelemetry` capture call sites across manager + attendee flows +- Optional daily server-side `active_site` heartbeat +### Out (deferred to) +- Publisher-facing event analytics (page views/funnels) — separate future pillar +- Self-hosted Pulse — supported via `pulse_host` site config, no code change; not set up here + +## Backend changes + +Optional, tiny: `buzz/tasks.py` daily job → `capture("active_site", "buzz")` (mirrors CRM's +`crm/www/crm.py`). Nothing else — no doctypes, no endpoints. + +## Frontend changes + +- `dashboard/src/main.ts`: `import { telemetryPlugin } from 'frappe-ui/frappe'` + + `app.use(telemetryPlugin, { app_name: 'buzz' })` (pass `router` for pageview tracking on new + sites, mirroring CRM). +- Capture call sites — the **event registry** (single source of truth; add rows as steps land): + +| Event name | Where (step) | Properties | +|---|---|---| +| `team_created` | onboarding + switcher create (01) | — | +| `event_created` | NewEventDialog (01) | `{from_template: bool}` | +| `event_published` / `event_unpublished` | Overview (03) | — | +| `ticket_type_created` | TicketTypeDialog (02) | — | +| `coupon_created` | CouponDialog (04) | `{coupon_type}` | +| `add_on_created` | AddOnDialog (05) | — | +| `sponsor_added` / `sponsorship_tier_created` | Sponsors tab (06) | — | +| `offline_payment_confirmed` | Bookings (07) | — | +| `attendees_exported` | Attendees (07) | — | +| `attendee_checked_in` | Check-in (08) | `{manual: bool}` | +| `member_invited` | Invites (13) | `{team_role}` | +| `member_role_changed` / `member_removed` | Members (13) | — | +| `template_created` / `event_created_from_template` | Templates (11) | — | +| `booking_initiated` / `booking_completed` | attendee BookingForm flow | `{is_guest, has_coupon}` | +| `ticket_transferred` / `cancellation_requested` | attendee TicketDetails | — | + +- Convention: capture **after** the mutation succeeds (in the `await`ed success path), never on + click. No PII in properties (no emails/names); team/user identity comes from Pulse's own + anonymized boot identity. + +## API contract + +No new endpoints (uses framework `boot_config`). + +## Permissions notes + +Telemetry is opt-in at the site level (`enable_telemetry` + `pulse_api_key`). Nothing captured for +sites that don't opt in. Anonymous mode handled by the SDK (cookieless default). + +## Demo script + +1. Unconfigured dev site: boot → one `boot_config` call returning `{enabled: false}`; no errors. +2. Staging with `pulse_api_key`: create team + event + ticket type → three events visible in the + Pulse dashboard under app `buzz`. +3. Grep check: every `capture(` call site uses a name from the registry table above. + +## Acceptance criteria (merge gate) + +### Automated tests +- Registry lint (CI grep or small pytest): every `capture('...')` call site in `dashboard/src` + uses an event name present in this file's registry table; fails on unknown names. +- Backend heartbeat (if shipped): scheduler job calls `frappe.utils.telemetry.capture` — smoke + test that it no-ops without config (no exception). + +### Browser checks (agent-browser, dev server :8080) +1. Unconfigured dev site: load `/dashboard/manage/events` → read console messages → zero + telemetry errors; network log shows exactly one `boot_config` call returning + `{enabled: false}` and no pulse-host requests. +2. Configured staging site (`pulse_api_key` + `enable_telemetry`): create team → event → ticket + type in the UI → network log shows capture requests to the pulse host for `team_created`, + `event_created`, `ticket_type_created`; events visible in the Pulse dashboard under app + `buzz`. +3. Capture timing: cancel the New Event dialog → no `event_created` fired (success-path-only + convention holds). + +## Dependencies & risks + +- Requires frappe version with `frappe.utils.telemetry.pulse` (present on this bench's frappe + develop) and frappe-ui v1 beta (Step-0 gate). +- Registry drift: PR review rule — new capture call sites must update the table in this file. diff --git a/specs/v2/02-speaker-remodel.md b/specs/v2/02-speaker-remodel.md new file mode 100644 index 00000000..43b6739d --- /dev/null +++ b/specs/v2/02-speaker-remodel.md @@ -0,0 +1,116 @@ +# 02 — Speaker model remodel (exploration) + +> **STATUS: OPEN — NO DECISION TAKEN.** This spec captures the problem, the options, and a leaning. +> The model must be finalized before `01-dashboard/10-schedule-tracks-and-talks.md` is implemented +> (it CRUDs talks/speakers) and before the teams permission inventory treats speaker doctypes as +> settled. + +## 1. Current model + +| Doctype | Shape | Notes | +|---|---|---| +| `Speaker Profile` | standalone, autoincrement | `user` (Link User), `display_name` (synced from User via `hooks.py:38` `on_update`), `designation`, `company`, `display_image`, `social_media_links` child | +| `Talk Speaker` | child of `Event Talk` | `speaker` (Link Speaker Profile) **+ its own `social_media_links` child** | +| `Proposal Speaker` | child of `Talk Proposal` | raw `first_name`, `last_name`, `email` — **no link to Speaker Profile** | +| `Event Featured Speaker` | child of `Buzz Event` | `speaker` (Link Speaker Profile) | + +## 2. Problems + +1. **Two disjoint speaker representations.** CFP submissions carry raw contact rows + (`Proposal Speaker`); accepted talks carry profile links (`Talk Speaker`). The accept flow has + to reconcile/dedupe them, and today doesn't — profiles and proposal speakers never meet. +2. **Profile requires a User.** Co-speakers without accounts have no clean home; `Proposal Speaker` + only has an email. +3. **Global profile vs multi-tenant v2.** One global record: Team A edits a bio → Team B's public + event page changes. Ownership is unanswered (`00-teams/00-plan.md` parks Speaker Profile as + "attendee-owned, not team-scoped" — a dodge, not a decision). +4. **Per-event customization already leaking in.** `Talk Speaker.social_media_links` duplicates the + profile's socials — an ad-hoc per-talk override. Real speakers tailor bio/headshot per event. +5. **`Event Featured Speaker` is a separate child table** for what is essentially a flag + ordering + on the event's speakers. + +## 3. Prior art (researched 2026-07) + +Both major CFP/speaker platforms converged on **global person identity + per-event speaker +profile**: + +- **pretalx** — `SpeakerProfile` exists per (user, event): per-event biography, per-event profile + picture; bios from past events offered as suggestions on new submissions. + ([data models](https://docs.pretalx.org/developer/interfaces/models/), + [concepts](https://docs.pretalx.org/developer/architecture/concepts/)) +- **Sessionize** — default profile auto-copied into each event submission, then edited *per event*; + editing the default never touches existing events. + ([speaker dashboard](https://sessionize.com/playbook/speaker-dashboard), + [submit a session](https://sessionize.com/playbook/submit-your-session-for-an-event)) + +## 4. Options + +### Option A — Per-event speaker record (pretalx/Sessionize model) + +New **`Event Speaker`** doctype (standalone, team via cascade): + +| fieldname | type | notes | +|---|---|---| +| `event` | Link Buzz Event, reqd | | +| `team` | Link Buzz Team, read-only | stamped via `inherit_team_from_event` | +| `full_name` | Data, reqd | | +| `email` | Data | identity key for find-or-create | +| `user` | Link User | optional — enables self-serve editing (CFP flow, `allow_editing_talks_after_acceptance`) | +| `designation`, `company` | Data | | +| `bio` | Text Editor | per-event | +| `photo` | Attach Image | per-event | +| `social_media_links` | Table Social Media Link | | +| `is_featured` | Check | replaces `Event Featured Speaker` | +| `featured_order` | Int | | + +Consequences: +- `Talk Speaker` child → Link `Event Speaker`; drop its `social_media_links` child. +- `Event Featured Speaker` child table **deleted** (flag + order above). +- Proposal accept → find-or-create `Event Speaker` by `(event, email)` per `Proposal Speaker` row. +- "Copy from previous event" prefill action (the reuse UX pretalx/Sessionize ship). +- Global `Speaker Profile`: either **dropped**, or shrunk to a thin identity record used only for + prefill suggestions. +- Migration patch: one `Event Speaker` per distinct (event, `Talk Speaker.speaker`) + featured + rows; repoint children. + +**Pros:** per-event bio/photo (industry standard); account-less speakers fine; clean team-scoped +permissions; dashboard Schedule tab CRUD gets simpler. +**Cons:** migration; mild duplication of a person across events (the accepted trade-off everywhere). + +### Option B — Global profile + per-event override junction + +Keep `Speaker Profile` canonical; junction holds override fields (bio, photo, socials). More +normalized, but every renderer resolves two levels, and non-overridden fields still leak across +teams. More moving parts than A for less benefit. + +### Option C — Minimal patch, no remodel + +Make `user` optional; key identity on email; auto-create profile from `Proposal Speaker` on accept; +drop `Talk Speaker` socials duplication. Cheap; solves neither multi-tenant ownership nor per-event +bios. Only defensible if speakers are out of v2 scope. + +### Option D — Team-scoped speaker directory (library model) + +`Speaker` belongs to a team (like Venues/Hosts in `01-dashboard/09`), reused across that team's +events. Great for repeat meetups; simple permissions. No per-event bio; a person speaking for two +teams = two records (Luma/Eventbrite behave this way). + +**Hybrid D+A** (team directory record + per-event rows referencing it with bio override) is +possible but reintroduces B's two-level resolution complexity. + +## 5. Current leaning (not a decision) + +**Option A**, keeping the optional `user` link so CFP self-editing still works, and treating +"repeat speaker" reuse as a copy-from-previous-event action rather than a shared mutable record. +Revisit D if usage turns out meetup-heavy (same roster every month). + +## 6. Open questions to finalize + +1. Drop `Speaker Profile` entirely, or keep as thin prefill identity? +2. Does the speaker get a public self-view/edit page (pretalx-style) in v2, or is editing + manager-only + CFP-flow-only? +3. `Proposal Speaker` — replace with early `Event Speaker` creation at submission time, or keep raw + rows until accept? (Early creation pollutes the speaker list with rejected proposals.) +4. Featured speakers ordering UX — flag on speaker vs drag-ordered list on the event. +5. Where does speaker management live in the dashboard — inside the Schedule tab (step 10) or its + own workspace tab? diff --git a/specs/v2/README.md b/specs/v2/README.md new file mode 100644 index 00000000..5263a247 --- /dev/null +++ b/specs/v2/README.md @@ -0,0 +1,100 @@ +# Buzz v2 Specs + +Buzz v2 turns Buzz from a single-tenant, Desk-administered event app into a **multi-tenant, +self-serve event platform**. Two pillars are specced here as tracer-bullet step sequences: + +| Folder | Pillar | +|---|---| +| [`00-teams/`](00-teams/00-plan.md) | Teams & multi-tenancy — the foundation. `Buzz Team` + `Buzz Team Membership`, tenant field cascade, permission hooks, invitations. | +| [`01-dashboard/`](01-dashboard/00-plan.md) | Unified manager + attendee dashboard — Frappe UI app shell, event workspace, Desk parity for organizers, Pulse product analytics. | +| [`02-speaker-remodel.md`](02-speaker-remodel.md) | **OPEN — decision pending.** Speaker model exploration: per-event speaker vs global profile. Must be finalized before `01-dashboard/10`. | + +## Conventions + +- **One step file = one PR-sized, independently demoable vertical slice.** Filename number = merge order within its folder. +- Each folder has a `00-plan.md` index (step order, dependencies, scope map). +- Every step file starts with a front-matter table: `Phase | Depends on | Status`. +- Status values: `Draft` → `Ready` → `In Progress` → `Done`. + +## Cross-folder build sequence + +``` +00-teams/01 ─► 00-teams/02 ─► 00-teams/03 ─┐ + ├─► 01-dashboard/01 ─► 01-dashboard/02 (tracer bullet demo) + │ +00-teams/04 (invitations) ─────────────────┴─► 01-dashboard/03 … 14 (parity phases B–D) +``` + +Teams steps 01–03 land first (doctypes → tenant field → permissions), then the dashboard tracer +bullet (shell + events list + ticket type CRUD) proves the whole architecture end to end. Team +invitations (00-teams/04) are deliberately deferred until after the tracer bullet — not needed for +the first demo. Dashboard phases B–D then march to Desk parity. + +## Step file template + +```markdown +# NN — + +| Phase | Depends on | Status | +|---|---|---| +| A/B/C/D | | Draft | + +## Goal +1–2 sentences: the thin vertical slice and why it's next. + +## Demo criteria (definition of done) +What a reviewer sees working end to end, phrased as the demo. + +## Scope +### In +### Out (deferred to) + +## Backend changes +Doctypes (field tables), hooks.py keys, API signatures, patches, fixtures. + +## Frontend changes +Routes added to router.ts, pages/components (frappe-ui component names), src/data/*.ts modules. + +## API contract +| Endpoint (dotted path) | Method | Params | Returns | Auth | + +## Permissions notes +Who can do what; guest/attendee flows that must not regress. + +## Demo script +Numbered manual QA steps (exact URLs, expected outcomes). + +## Acceptance criteria (merge gate) +### Automated tests +`bench --site buzz.localhost run-tests --module ...` targets + the cases they must cover. +### Browser checks (agent-browser) +Scripted agent-browser flows against the local site (dev server :8080 for frontend work). + +## Dependencies & risks +``` + +### Acceptance-criteria conventions + +- **Every step is merge-gated by its Acceptance criteria section** — Demo script is the human + walkthrough; acceptance criteria are the strict, repeatable checks. +- **Unit/integration tests**: live in `buzz/tests/` (or doctype `test_*.py`), run via + `bench --site buzz.localhost run-tests --module `. Backend steps must ship tests in the + same PR; a step whose tests are listed here but missing from the PR is incomplete. +- **Browser checks**: executed with **agent-browser** (`agent-browser --help`) against the local + site — frontend steps target the vite dev server on `:8080`, public/attendee flows target the + site directly. Each check is written as a concrete flow (URL → actions → asserted outcome) so it + can be replayed verbatim. Run headed locally; headless is fine for CI. +- **Cross-tenant checks always use two seeded teams** (team A / team B with one event each) — + seed via a shared test fixture/factory so browser and unit checks agree on data. + +## Global assumptions + +1. **frappe-ui v1 beta migration is a prerequisite** (`DesktopShell`, `frappe-ui/list`, + `useCall`/`useList`/`useDoc`, semantic tokens, `telemetryPlugin`). `dashboard/package.json` + currently pins `frappe-ui ^0.1.257` — the Step-0 checklist in `01-dashboard/00-plan.md` gates + Phase A on the upgrade being merged. +2. **Single SPA.** Manager area grafts onto the existing dashboard app under `/manage/*`; + attendee (`/account/*`) and public booking routes stay intact. +3. **Teams are the tenant boundary.** Every organizer-facing record belongs to a `Buzz Team`; + permissions derive from `Buzz Team Membership`, not global roles alone. +4. **No Desk for organizers** at the end of Phase D; Desk remains for System Managers. diff --git a/worktrees/custom-forms b/worktrees/custom-forms deleted file mode 160000 index 334469be..00000000 --- a/worktrees/custom-forms +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 334469be82bbad8970bdfb546531d5720739d30f