369 lines
12 KiB
Markdown
369 lines
12 KiB
Markdown
# CLAUDE.md
|
|
|
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
|
|
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.
|
|
|
|
---
|
|
|
|
## This defines the best practices to write backend code in the Frappe Framework
|
|
|
|
* 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.
|
|
|
|
## Backend Development
|
|
|
|
### JSON & Request Handling
|
|
|
|
* Always use built-in functions for parsing JSON:
|
|
|
|
* `frappe.parse_json` (handles dicts, lists, and JSON strings safely)
|
|
|
|
* Never use `json.loads` directly on request data.
|
|
|
|
* For outbound HTTP requests (calling external APIs), use:
|
|
|
|
* `frappe.integration.utils.make_get_request`
|
|
* `frappe.integration.utils.make_post_request`
|
|
* `frappe.integration.utils.make_put_request`
|
|
* `frappe.integration.utils.make_patch_request`
|
|
|
|
### Datatype Conversion & Utilities
|
|
|
|
* For converting datatypes (e.g. str → int, str → float, etc.) use built-in helpers:
|
|
|
|
* `frappe.utils.data.cint`
|
|
* `frappe.utils.data.cstr`
|
|
* `frappe.utils.data.flt`
|
|
* `frappe.utils.data.getdate`
|
|
* `frappe.utils.data.get_datetime`
|
|
|
|
* `frappe.utils.data` contains most conversion and formatting helpers you will ever need:
|
|
|
|
* 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 event_manager/public/dashboard + event_manager/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 event_manager/
|
|
ruff format event_manager/
|
|
|
|
# install app to site
|
|
bench --site [site-name] install-app event_manager
|
|
```
|
|
|
|
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 event_manager.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**: `Pohodex Event Manager Event` DocType drives everything (tickets, sponsors, schedule, payments).
|
|
|
|
**Main modules** (inside `event_manager/`):
|
|
- `events/` - Event, Venue, Category, Talks, Sponsors, Check-ins
|
|
- `ticketing/` - Bookings, Tickets, Add-ons, Cancellations, Coupons
|
|
- `proposals/` - Talk Proposals, Sponsorship Enquiries
|
|
- `event_manager/` - 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 `event_manager/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 Pohodex Event Manager 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**: `event_manager/api.py`, `event_manager/ticketing/doctype/event_booking/`, `dashboard/src/components/BookingForm.vue`
|
|
|
|
**Ticket lifecycle**: `event_manager/ticketing/doctype/event_ticket/`, `dashboard/src/pages/TicketDetails.vue`
|
|
|
|
**Sponsorships**: `event_manager/proposals/doctype/sponsorship_enquiry/`, `dashboard/src/pages/SponsorshipDetails.vue`
|
|
|
|
**Check-in**: `event_manager/api.py` (validate_ticket_for_checkin, checkin_ticket), `dashboard/src/pages/CheckInScanner.vue`
|
|
|
|
**Event config**: `event_manager/events/doctype/buzz_event/`
|
|
|
|
**Reports**: `event_manager/events/report/` and `event_manager/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
|