Skip to content

Appointment Management

Complete guide to managing appointments, bookings, scheduling, payments, and analytics in the Reserva platform.


Overview

The appointment system provides comprehensive booking management with support for:

  • Multi-Service Bookings - Book multiple services in one appointment
  • Smart Staff Assignment - Automatic skill-based staff matching with load balancing
  • Payment Integration - Paper.id payment links and manual payment recording
  • Conflict Prevention - Real-time availability checking and double-booking prevention
  • Status Tracking - Full lifecycle management from booking to completion
  • Analytics Dashboard - Revenue, cancellation, and performance metrics
  • Subscription Limits - Automatic enforcement of plan-based booking limits
  • Automatic Notifications - Booking confirmations, reminders, cancellation, and reschedule notifications

Key Concepts:

  • PENDING = Customer-initiated booking awaiting payment
  • CONFIRMED = Verified booking ready for service
  • COMPLETED = Service delivered and marked done
  • NO_SHOW = Customer didn't arrive
  • CANCELLED = Booking cancelled with reason

Subscription Plan Limits

Appointment creation is subject to subscription plan limits. The system automatically validates against the tenant's current subscription.

Plan-Based Limits

Plan Max Appointments/Month Max Services per Appointment
FREE 100 20
PRO 2,000 Unlimited
ENTERPRISE Unlimited Unlimited

Enforcement:

  • Checked during appointment creation
  • Blocks booking if monthly limit exceeded
  • Returns 403 error with upgrade recommendation
  • Resets monthly on subscription renewal date

Error Response:

{
  "detail": "Monthly appointment limit exceeded (100/100). Please upgrade your plan to continue booking.",
  "current_plan": "FREE",
  "usage": {
    "appointments_this_month": 100,
    "limit": 100
  },
  "upgrade_available": true
}


List Appointments

Retrieve paginated list of appointments with comprehensive filtering options.

Endpoint

GET /api/v1/appointments?page=1&size=20&status=confirmed&date_from=2025-01-15

Example with completion notes search:

GET /api/v1/appointments?status=completed&completion_notes_search=satisfied

Authentication: Required (JWT token)

Query Parameters

Pagination:

  • page (optional) - Page number (default: 1)
  • size (optional) - Items per page (default: 20, max: 100)

Date Filters:

  • date_from (optional) - Filter appointments from date (YYYY-MM-DD)
  • date_to (optional) - Filter appointments until date (YYYY-MM-DD)

Status Filters:

  • status (optional) - Filter by status: pending, confirmed, completed, cancelled, no_show
  • appointment_type (optional) - Filter by type: walk_in, scheduled, online
  • payment_status (optional) - Filter by payment: pending, paid, partially_paid, refunded

Entity Filters:

  • customer_id (optional) - Filter by customer ID
  • staff_id (optional) - Filter by staff member ID
  • outlet_id (optional) - Filter by outlet ID
  • service_id (optional) - Filter by service ID

Text Search Filters:

  • completion_notes_search (optional) - Search in completion notes (case-insensitive partial match, min 2 characters)

Sorting:

  • sort_by (optional) - Sort field (default: appointment_date)
  • sort_direction (optional) - Sort direction: asc or desc (default: desc)

Response

{
  "items": [
    {
      "id": "507f1f77bcf86cd799439011",
      "tenant_id": "507f1f77bcf86cd799439010",
      "customer_id": "507f1f77bcf86cd799439012",
      "customer_name": "John Doe",
      "outlet_id": "507f1f77bcf86cd799439013",
      "appointment_date": "2025-01-15T00:00:00Z",
      "start_time": "14:30",
      "end_time": "15:30",
      "status": "confirmed",
      "payment_status": "paid",
      "services": [
        {
          "service_id": "507f1f77bcf86cd799439014",
          "service_name": "Hair Styling",
          "staff_id": "507f1f77bcf86cd799439015",
          "staff_name": "Jane Smith",
          "price": 75000,
          "duration_minutes": 60,
          "start_time": "14:30",
          "end_time": "15:30"
        }
      ],
      "total_price": 75000,
      "notes": "First time customer",
      "paper_payment_url": "https://paper.id/checkout/abc123xyz",
      "created_at": "2025-01-10T10:30:00Z",
      "confirmed_at": "2025-01-10T10:35:00Z"
    },
    {
      "id": "507f1f77bcf86cd799439020",
      "tenant_id": "507f1f77bcf86cd799439010",
      "customer_id": "507f1f77bcf86cd799439021",
      "customer_name": "Maria Rodriguez",
      "outlet_id": "507f1f77bcf86cd799439013",
      "appointment_date": "2025-01-12T00:00:00Z",
      "start_time": "10:00",
      "end_time": "11:00",
      "status": "completed",
      "payment_status": "paid",
      "services": [
        {
          "service_id": "507f1f77bcf86cd799439022",
          "service_name": "Manicure",
          "staff_id": "507f1f77bcf86cd799439023",
          "staff_name": "Alice Wong",
          "price": 45000,
          "duration_minutes": 60,
          "start_time": "10:00",
          "end_time": "11:00"
        }
      ],
      "total_price": 45000,
      "notes": "Customer requested nail art",
      "completion_notes": "Service completed successfully. Customer very satisfied with nail art design.",
      "created_at": "2025-01-10T14:20:00Z",
      "confirmed_at": "2025-01-10T14:25:00Z",
      "completed_at": "2025-01-12T11:00:00Z",
      "paid_at": "2025-01-12T11:05:00Z"
    }
  ],
  "total": 45,
  "page": 1,
  "size": 20,
  "pages": 3
}

Response Fields:

  • customer_name - Full name of the customer (built from first_name + last_name)
  • paper_payment_url - Payment link URL if invoice was created via Paper.id integration (null if not applicable)

Access Control:

  • OUTLET_MANAGER - Sees only their outlet's appointments
  • TENANT_ADMIN - Sees all tenant appointments
  • SUPER_ADMIN - Sees all tenant appointments

Create Appointment

Create a new appointment booking with automatic validation and conflict detection.

Endpoint

POST /api/v1/appointments

Authentication: Required (JWT token)

Request Body

IMPORTANT SECURITY NOTE: Price and duration are auto-populated from the service catalog by the server. Client-provided values are ignored to prevent price manipulation and ensure data integrity.

{
  "customer_id": "507f1f77bcf86cd799439011",
  "outlet_id": "507f1f77bcf86cd799439012",
  "appointment_date": "2025-01-15",
  "start_time": "14:30",
  "services": [
    {
      "service_id": "507f1f77bcf86cd799439013",
      "staff_id": "507f1f77bcf86cd799439014"
    }
  ],
  "notes": "First time customer"
}

Parameters:

  • customer_id (required) - Customer ID
  • outlet_id (required) - Outlet ID
  • appointment_date (required) - Appointment date (YYYY-MM-DD)
  • start_time (required) - Start time (HH:MM 24-hour format)
  • services (required) - Array of service bookings:
  • service_id (required) - Service ID
  • staff_id (optional) - Staff ID for preferred staff, or omit for auto-assignment with load balancing
  • notes (optional) - Appointment notes (max 1000 characters)

Server-Side Auto-Population:

The following fields are automatically populated by the server from the service catalog and cannot be provided by the client:

  • price - Current service price (prevents client-side price manipulation)
  • duration_minutes - Service duration from catalog
  • service_name - Service name (cached for history)
  • staff_name - Staff name (cached for history, if staff assigned)

Response

{
  "id": "507f1f77bcf86cd799439020",
  "tenant_id": "507f1f77bcf86cd799439010",
  "customer_id": "507f1f77bcf86cd799439011",
  "outlet_id": "507f1f77bcf86cd799439012",
  "appointment_date": "2025-01-15T00:00:00Z",
  "start_time": "14:30",
  "end_time": "15:30",
  "status": "confirmed",
  "payment_status": "pending",
  "services": [
    {
      "service_id": "507f1f77bcf86cd799439013",
      "service_name": "Hair Styling",
      "staff_id": "507f1f77bcf86cd799439014",
      "staff_name": "Jane Smith",
      "price": 75000,
      "duration_minutes": 60,
      "start_time": "14:30",
      "end_time": "15:30"
    }
  ],
  "total_price": 75000,
  "fee_estimation": {
    "base_amount": 75000,
    "platform_fee": 6000,
    "total_with_fee": 81000,
    "fee_rate": 0.08,
    "subscription_plan": "FREE",
    "note": "Estimated payment processing fee (applied when customer pays)"
  },
  "notes": "First time customer",
  "created_at": "2025-01-10T10:30:00Z",
  "created_by_id": "507f1f77bcf86cd799439021"
}

Note: The fee_estimation field is optional and may not be included if fee calculation fails. This is a display-only feature and does not block appointment creation.

Validation Flow (11 Steps)

The appointment creation process includes comprehensive business logic validation:

Flow 0: Auto-Populate Service Details - CRITICAL SECURITY FEATURE

  • Fetch service price from catalog (server-authoritative)
  • Fetch service duration from catalog (auto-populated if not provided)
  • Auto-populate service name for caching
  • Auto-populate staff name for caching (if staff assigned)
  • Client-provided price and duration are IGNORED
  • Prevents price manipulation attacks
  • Ensures pricing consistency with service catalog

Flow 1: Scheduling Constraints - Validates:

  • Appointment within business hours
  • Meets minimum advance booking time
  • Not exceeding maximum advance booking days
  • Duration fits within working hours

Flow 2: Customer Validation - Verifies:

  • Customer exists in tenant
  • Customer account is active
  • Customer ID is valid ObjectId

Flow 3: Outlet Validation - Checks:

  • Outlet belongs to tenant
  • Outlet is active
  • Outlet ID is valid (if specified)

FLow 4: Service Validation - For each service:

  • Service exists and is active
  • Service belongs to tenant
  • Service price matches server calculation
  • Service is available for booking

Flow 5: Staff Assignment - Either:

  • Manual: Validate specified staff with skill matching
  • Auto: Assign best available staff with load balancing

Flow 6: Staff Skills Validation - Ensures:

  • Staff has required skills for service
  • Staff skill level meets service requirements
  • Staff is qualified to perform service

Flow 7: Availability Check - Validates:

  • Staff working hours cover appointment time
  • No breaks or time-off during appointment
  • Staff availability rules satisfied

Flow 8: Conflict Detection - Prevents:

  • Double-booking (overlapping appointments)
  • Staff scheduling conflicts
  • Resource allocation issues

Flow 9: Duplicate Booking Prevention - Blocks:

  • Same customer booking identical appointment (same service + staff + time)
  • Prevents accidental double-clicks or duplicate submissions
  • Validates uniqueness per customer across service, staff, date, and time

Flow 10: Price Calculation - Performs:

  • Server-side total calculation using server-populated service prices
  • Sum of all service prices from catalog
  • Discount application (if applicable)
  • No client price validation - server-calculated price always used

Important Notes:

  • Staff created appointments start with CONFIRMED status
  • Customer bookings (via public API) start with PENDING status
  • Auto-assignment uses load balancing to distribute appointments
  • Server-side pricing prevents price manipulation
  • Platform fees estimated based on subscription tier

Notifications: See Automatic Notifications for details on booking confirmation and reminder notifications triggered by appointment creation.

Business Rules:

  • Cannot book past appointments
  • Must respect outlet operating hours
  • Staff availability checked per-service
  • Server-side pricing only - client-provided prices ignored
  • Duplicate prevention - Same customer cannot book identical service+staff+time
  • Appointment slots locked upon creation
  • Service details cached for history (service_name, staff_name)

Service-Level Time Calculations

For multi-service appointments, each service receives calculated start and end times:

  • First service: Starts at appointment start_time
  • Subsequent services: Start immediately after previous service ends
  • Service timing: service_start = appointment_start + cumulative_minutes
  • Example:
  • Appointment start: 14:30
  • Service 1 (60 min): 14:30 - 15:30
  • Service 2 (30 min): 15:30 - 16:00
  • Appointment end: 16:00

Benefits:

  • Sequential service delivery without gaps
  • Accurate staff scheduling across services
  • Precise conflict detection per service
  • Clear time expectations for customers

Staff Auto-Assignment Algorithm

When staff_id is not provided for a service, the system uses intelligent auto-assignment:

  1. Skill Matching: Find all staff with required skills for the service
  2. Availability Filtering: Remove unavailable staff (time off, working hours)
  3. Conflict Checking: Exclude staff with overlapping appointments
  4. Load Balancing: Select staff with fewest appointments to distribute workload evenly
  5. Assignment: Assign best available staff automatically

Benefits:

  • Even workload distribution across team
  • Automatic skill-based matching
  • Reduces manual assignment effort
  • Optimizes staff utilization
  • Prevents staff burnout from overloading

Implementation Note: Uses StaffAssignmentService.assign_best_available_staff() for intelligent staff selection.


Get Appointment Details

Retrieve detailed information about a specific appointment with comprehensive payment information.

Endpoint

GET /api/v1/appointments/{appointment_id}

Authentication: Required (JWT token)

Response

{
  "id": "507f1f77bcf86cd799439020",
  "tenant_id": "507f1f77bcf86cd799439010",
  "customer_id": "507f1f77bcf86cd799439011",
  "outlet_id": "507f1f77bcf86cd799439012",
  "appointment_date": "2025-01-15T00:00:00Z",
  "start_time": "14:30",
  "end_time": "16:00",
  "status": "confirmed",
  "payment_status": "partially_paid",
  "services": [
    {
      "service_id": "507f1f77bcf86cd799439013",
      "service_name": "Hair Styling",
      "staff_id": "507f1f77bcf86cd799439014",
      "staff_name": "Jane Smith",
      "price": 75000,
      "duration_minutes": 60,
      "start_time": "14:30",
      "end_time": "15:30"
    },
    {
      "service_id": "507f1f77bcf86cd799439015",
      "service_name": "Manicure",
      "staff_id": "507f1f77bcf86cd799439016",
      "staff_name": "Lisa Wong",
      "price": 45000,
      "duration_minutes": 30,
      "start_time": "15:30",
      "end_time": "16:00"
    }
  ],
  "total_price": 120000,
  "payment_details": {
    "total_amount": 120000,
    "paid_amount": 75000,
    "remaining_balance": 45000,
    "payment_count": 2,
    "last_payment_at": "2025-01-15T11:30:00Z",
    "payment_history": [
      {
        "id": "507f1f77bcf86cd799439017",
        "amount": 50000,
        "method": "cash",
        "status": "completed",
        "recorded_by": "Maria Rodriguez",
        "recorded_at": "2025-01-15T10:30:00Z",
        "receipt_number": "RCPT-2025-001",
        "notes": "First payment"
      },
      {
        "id": "507f1f77bcf86cd799439018",
        "amount": 25000,
        "method": "pos_terminal",
        "status": "completed",
        "recorded_by": "Maria Rodriguez",
        "recorded_at": "2025-01-15T11:30:00Z",
        "receipt_number": "RCPT-2025-002",
        "notes": "Second partial payment"
      }
    ]
  },
  "fee_breakdown": {
    "base_amount": 120000,
    "platform_fee": 9600,
    "total_with_fee": 129600,
    "fee_rate": 0.08,
    "fee_percentage": "8.0%",
    "subscription_plan": "FREE",
    "note": "Platform fee breakdown (applied when customer pays)"
  },
  "rescheduled_from": {
    "date": "2025-01-10",
    "start_time": "10:00",
    "end_time": "11:30"
  },
  "rescheduled_to": {
    "date": "2025-01-15",
    "start_time": "14:30",
    "end_time": "16:00"
  },
  "rescheduled_at": "2025-01-08T15:20:00Z",
  "notes": "First time customer, requested specific stylist\n[Rescheduled on 2025-01-08 15:20] Customer requested different time slot",
  "completion_notes": "Service completed successfully. Customer very happy with hair styling. Recommended return visit in 6 weeks.",
  "created_at": "2025-01-10T10:30:00Z",
  "confirmed_at": "2025-01-10T10:35:00Z",
  "completed_at": "2025-01-15T16:00:00Z",
  "created_by_id": "507f1f77bcf86cd799439021"
}

✨ Payment Details Field (NEW)

🎉 Enhanced Feature: The endpoint now includes comprehensive payment information automatically for all appointments with total_price > 0.

payment_details Object (included for all paid appointments):

  • total_amount - Full appointment price
  • paid_amount - Cumulative amount paid across all payment transactions
  • remaining_balance - Amount still owed (automatically calculated)
  • payment_count - Total number of payment transactions made
  • last_payment_at - Timestamp of most recent payment
  • payment_history - Complete payment history array with audit trail

Payment History Details (for each payment):

  • id - Payment record ID
  • amount - Payment amount
  • method - Payment method (cash, pos_terminal, bank_transfer, etc.)
  • status - Payment status (completed, pending, failed)
  • recorded_by - Staff member who recorded the payment
  • recorded_at - Timestamp when payment was recorded
  • receipt_number - Receipt or reference number (if provided)
  • notes - Payment notes (if provided)

Consistent Across All Payment Statuses:

The payment_details field uses a consistent structure regardless of payment status:

Example: Pending (Unpaid)

"payment_details": {
  "total_amount": 120000,
  "paid_amount": 0,
  "remaining_balance": 120000,
  "payment_count": 0,
  "last_payment_at": null,
  "payment_history": []
}

Example: Partially Paid

"payment_details": {
  "total_amount": 120000,
  "paid_amount": 75000,
  "remaining_balance": 45000,
  "payment_count": 2,
  "last_payment_at": "2025-01-15T11:30:00Z",
  "payment_history": [...]
}

Example: Fully Paid

"payment_details": {
  "total_amount": 120000,
  "paid_amount": 120000,
  "remaining_balance": 0,
  "payment_count": 1,
  "last_payment_at": "2025-01-15T10:30:00Z",
  "payment_history": [...]
}

Benefits:

No extra API calls - All payment info in one response

Complete audit trail - See who recorded each payment and when

Consistent structure - Same fields across all payment statuses

Self-contained - No need to call /payment-status endpoint separately

Chronological history - Payments sorted by recorded timestamp

Additional Fields:

  • completion_notes - Notes recorded when appointment was completed (max 1000 characters, only present for completed appointments)

  • rescheduled_from - Original appointment time (embedded object) - set only on first reschedule

  • rescheduled_to - New appointment time after reschedule (embedded object) - updated on every reschedule

  • rescheduled_at - Timestamp when most recent reschedule occurred

Fee Breakdown by Subscription:

Plan Fee Percentage Example (100,000 IDR)
FREE 8% Platform fee: 8,000 IDR
PRO 5% Platform fee: 5,000 IDR
ENTERPRISE 3% Platform fee: 3,000 IDR

Access Control:

  • OUTLET_MANAGER - Can only view appointments in their outlets
  • TENANT_ADMIN - Can view all tenant appointments
  • Returns 403 if accessing appointment outside scope

Update Appointment

Update appointment details with full validation and conflict detection.

Endpoint

PUT /api/v1/appointments/{appointment_id}

Authentication: Required (JWT token)

Request Body

{
  "appointment_date": "2025-01-20",
  "start_time": "15:00",
  "notes": "Rescheduled per customer request"
}

Updatable Fields:

  • appointment_date (optional) - New appointment date
  • start_time (optional) - New start time
  • services (optional) - Update service list
  • notes (optional) - Update notes

Important:

  • Cannot update COMPLETED or CANCELLED appointments
  • Rescheduling triggers full validation (scheduling constraints, staff availability, conflicts)
  • Service changes recalculate pricing
  • Staff skill validation reapplied

Response

Returns updated appointment object with recalculated times and prices.

Validation Applied When Rescheduling:

  1. Scheduling Constraints - New time must meet business rules
  2. Staff Availability - All staff must be available at new time
  3. Conflict Detection - No overlapping appointments (excluding current)
  4. Staff Skills - Skills revalidated for all services

Cancel Appointment

Cancel an appointment with mandatory reason tracking (soft delete).

Endpoint

DELETE /api/v1/appointments/{appointment_id}

Authentication: Required (JWT token)

Request Body

{
  "cancellation_reason": "Customer requested cancellation due to emergency"
}

Parameters:

  • cancellation_reason (required) - Reason for cancellation (1-500 characters)

Response

{
  "message": "Appointment has been cancelled successfully"
}

Business Rules:

  • Cannot cancel already cancelled appointments
  • Reason is mandatory for audit trail
  • Appointment preserved in database for reporting
  • Time slot released for new bookings
  • Payment status preserved for billing records
  • Sets cancelled_at timestamp automatically

Soft Delete:

  • Appointment not removed from database
  • Status changed to CANCELLED
  • Cancellation reason recorded in metadata
  • Historical data preserved for analytics

Notifications: See Automatic Notifications for details on cancellation notifications and scheduled reminder cleanup.


Reschedule Appointment

Reschedule appointment to new date/time with full validation.

Endpoint

POST /api/v1/appointments/{appointment_id}/reschedule

Authentication: Required (JWT token)

Request Body

{
  "new_date": "2025-01-20",
  "new_time": "15:00",
  "reason": "Customer requested different time slot"
}

Parameters:

  • new_date (required) - New appointment date (YYYY-MM-DD)
  • new_time (required) - New start time (HH:MM)
  • reason (optional) - Reason for rescheduling (max 500 characters)

Response

Returns updated appointment with new date/time, preserved service details, and reschedule tracking information.

{
  "id": "507f1f77bcf86cd799439020",
  "appointment_date": "2025-01-20T00:00:00Z",
  "start_time": "15:00",
  "end_time": "16:00",
  "services": [...],
  "rescheduled_from": {
    "date": "2025-01-15",
    "start_time": "14:30",
    "end_time": "15:30"
  },
  "rescheduled_to": {
    "date": "2025-01-20",
    "start_time": "15:00",
    "end_time": "16:00"
  },
  "rescheduled_at": "2025-01-10T12:30:00.000Z",
  "notes": "First time customer\n[Rescheduled on 2025-01-10 12:30] Customer requested different time slot"
}

Reschedule Tracking Feature

NEW FEATURE: The system automatically tracks reschedule history to maintain appointment audit trail.

Tracking Fields:

  • rescheduled_from (object) - Original appointment time (set only on first reschedule):

  • date - Original appointment date (YYYY-MM-DD)

  • start_time - Original start time (HH:MM)
  • end_time - Original end time (HH:MM)

  • rescheduled_to (object) - Current/new appointment time (updated on every reschedule):

  • date - New appointment date (YYYY-MM-DD)

  • start_time - New start time (HH:MM)
  • end_time - New end time (HH:MM)

  • rescheduled_at (datetime) - Timestamp of most recent reschedule

Tracking Logic:

  1. First Reschedule:

  2. rescheduled_from captures original time (preserved forever)

  3. rescheduled_to captures new time
  4. rescheduled_at records timestamp

  5. Subsequent Reschedules:

  6. rescheduled_from remains unchanged (preserves original)

  7. rescheduled_to updated to latest time
  8. rescheduled_at updated to latest timestamp

  9. Reason Tracking:

  10. Reason appended to notes with format: [Rescheduled on YYYY-MM-DD HH:MM] {reason}

  11. Previous notes preserved with newline separator
  12. Creates complete audit trail in notes field

Use Cases:

  • Track how many times an appointment was rescheduled
  • View original scheduled time for analytics
  • Audit trail for customer service
  • Pattern detection (frequent reschedulers)
  • Compare original vs final appointment time

Validation Process (Detailed):

The reschedule endpoint performs thorough validation:

  1. Appointment Validation

  2. Verify appointment exists

  3. Check belongs to current tenant
  4. Validate appointment ID format

  5. Status Check

  6. Verify appointment status allows rescheduling

  7. Reject if COMPLETED or CANCELLED
  8. Only PENDING, CONFIRMED, IN_PROGRESS can be rescheduled

  9. Time Format Validation

  10. Parse new_time string (HH:MM format)

  11. Return 422 error if format invalid
  12. Example error: "Invalid time format. Use HH:MM format (e.g., 14:30)"

  13. Scheduling Constraints

  14. Validate new time within business hours

  15. Check minimum advance booking time
  16. Check maximum advance booking days
  17. Verify duration fits within working hours

  18. Per-Service Validation (for each service in appointment):

a. Calculate Service Time Window

  - Service start = appointment start + cumulative minutes
  - Service end = service start + service duration

b. Staff Skill Validation

  - Verify staff has required skills for service
  - Check skill level meets requirements
  - Uses `StaffAssignmentService.validate_preferred_staff()`

c. Availability Check

  - Validate staff working hours cover new time
  - Check no time-off or breaks during service
  - Verify staff availability rules satisfied

d. Conflict Detection

  - Check for overlapping appointments
  - **Excludes current appointment from conflict check**
  - Prevents double-booking at new time
  1. Service Time Recalculation

  2. Recalculate start/end times for each service

  3. Maintain sequential service delivery
  4. Update service-level time fields for data consistency

  5. Reschedule History Tracking

  6. Set rescheduled_from if first reschedule

  7. Update rescheduled_to with new time
  8. Record rescheduled_at timestamp

  9. Reason Recording

  10. Append reason to notes with timestamp format

  11. Preserve existing notes
  12. Create audit trail

  13. Database Update

  14. Update appointment with new times

  15. Update all service times
  16. Record updated_by_id for audit

Business Rules:

  • Cannot reschedule COMPLETED or CANCELLED appointments
  • All staff must be available at new time (checked per-service)
  • No conflicts with existing appointments (current appointment excluded from check)
  • Preserves services and staff assignments (no service changes allowed)
  • Original time preserved in rescheduled_from on first reschedule only
  • New time recorded in rescheduled_to on every reschedule
  • Timestamp recorded in rescheduled_at for audit trail
  • Reason is optional but recommended for customer service
  • Reason appended to notes with format: [Rescheduled on YYYY-MM-DD HH:MM] {reason}
  • Service-level start/end times automatically recalculated

Notifications: See Automatic Notifications for details on reschedule notifications and reminder re-scheduling.


Appointment Status Transitions

Confirm Appointment (Manual)

Manually confirm a PENDING appointment (typically for webhook failure recovery).

Endpoint

POST /api/v1/appointments/{appointment_id}/confirm

Authentication: Required (JWT token)

Use Cases

  • Webhook failure recovery - Payment webhook failed to auto-confirm
  • Manual payment verification - Customer paid via offline/alternative method
  • Staff approval override - Special cases requiring manual confirmation
  • Payment gateway issues - Recovery from Paper.id processing errors

Context - Normal Flow:

  1. Customer books → status: PENDING
  2. Customer pays via Paper.id
  3. Webhook auto-confirms → status: CONFIRMED

This endpoint serves as manual backup for exceptional cases.

Note: Staff-created appointments start directly at CONFIRMED status and don't need this endpoint.

Response

Returns appointment with CONFIRMED status and confirmed_at timestamp.

Business Rules:

  • Only PENDING appointments can be confirmed
  • Status changes to CONFIRMED immediately
  • Sets confirmed_at timestamp automatically
  • Cannot confirm COMPLETED, CANCELLED, or NO_SHOW appointments

Notifications: See Automatic Notifications for details on booking confirmation and reminder notifications triggered by manual confirmation.


Complete Appointment

Mark appointment as completed with optional completion notes.

Endpoint

POST /api/v1/appointments/{appointment_id}/complete

Authentication: Required (JWT token)

Request Body

{
  "completion_notes": "Service completed successfully, customer satisfied"
}

Parameters:

  • completion_notes (optional) - Completion notes (max 1000 characters)

Response

{
  "id": "507f1f77bcf86cd799439011",
  "tenant_id": "507f1f77bcf86cd799439010",
  "outlet_id": "507f1f77bcf86cd799439012",
  "customer_id": "507f1f77bcf86cd799439013",
  "appointment_date": "2025-01-20",
  "start_time": "14:00",
  "end_time": "15:30",
  "services": [
    {
      "service_id": "507f1f77bcf86cd799439014",
      "service_name": "Hair Cut & Style",
      "staff_id": "507f1f77bcf86cd799439015",
      "staff_name": "Jane Smith",
      "price": 75.00,
      "duration_minutes": 60,
      "start_time": "14:00",
      "end_time": "15:00"
    },
    {
      "service_id": "507f1f77bcf86cd799439016",
      "service_name": "Hair Treatment",
      "staff_id": "507f1f77bcf86cd799439015",
      "staff_name": "Jane Smith",
      "price": 50.00,
      "duration_minutes": 30,
      "start_time": "15:00",
      "end_time": "15:30"
    }
  ],
  "appointment_type": "online",
  "status": "completed",
  "payment_status": "paid",
  "total_price": 125.00,
  "total_duration_minutes": 90,
  "notes": "Customer prefers organic products",
  "completion_notes": "Service completed successfully, customer satisfied",
  "confirmed_at": "2025-01-15T10:30:00Z",
  "completed_at": "2025-01-20T15:30:00Z",
  "paid_at": "2025-01-15T14:45:00Z",
  "created_at": "2025-01-15T10:30:00Z",
  "updated_at": "2025-01-20T15:30:00Z"
}

Returns appointment with COMPLETED status, completed_at timestamp, completion_notes, and updated payment status (if payment verified).

Business Rules:

  • Only CONFIRMED or IN_PROGRESS appointments can be completed
  • Payment verification required for paid appointments:
  • Checks for completed payment record in database
  • Rejects completion if no payment found (for appointments with total_price > 0)
  • Updates payment_status to PAID only if payment record exists
  • Cannot complete unpaid appointments without payment record
  • Sets completed_at timestamp automatically
  • Triggers customer review workflows

Payment Verification:

# Appointment with payment required
if appointment.total_price > 0:
    # Must have payment record
    if not has_payment_record:
        return 400 Error: "Cannot complete without verified payment"

# Free appointments (total_price = 0) skip payment check

Error Response:

{
  "detail": "Cannot complete appointment without verified payment. Please record payment first using:\n- POST /appointments/{id}/record-payment (for manual payments)\n- POST /appointments/{id}/create-payment-link (for online payments)"
}

Mark No-Show

Mark appointment as no-show when customer doesn't arrive.

Endpoint

POST /api/v1/appointments/{appointment_id}/no-show

Authentication: Required (JWT token)

Request Body

{
  "reason": "Customer did not arrive, no prior notice"
}

Parameters:

  • reason (optional) - No-show reason (max 500 characters)

Response

Returns appointment with NO_SHOW status and reason recorded in notes.

Business Rules:

  • Only CONFIRMED appointments can be marked as no-show
  • Optional reason appended to notes with [No-Show] prefix
  • Time slot released for new bookings
  • Sets no_show_at timestamp automatically

Future Features (TODO):

  • Customer no-show count tracking
  • No-show fee application based on tenant configuration
  • Customer account restrictions after repeated no-shows
  • No-show analytics and pattern detection

Automatic Notifications

The appointment system includes automatic notification integration to keep customers informed about their bookings. Notifications are sent via multiple channels (push, email, WhatsApp) and are non-blocking - notification failures do not prevent appointment operations.

Related Notification Documentation

This section covers automatic notifications triggered by appointment operations. For complete notification system documentation:

  • Notification Management - Send manual notifications, schedule future notifications, view usage statistics, and access notification history
  • Notification Settings - Customize reminder timings, create custom message templates, and configure notification preferences per tenant

Notification Types

Notification Type Trigger Channels Description
Booking Confirmation Appointment created or confirmed Push, Email, WhatsApp Confirms booking details to customer
Booking Reminder Scheduled before appointment Based on tenant settings Reminds customer of upcoming appointment
Booking Cancelled Appointment cancelled Push, Email, WhatsApp Notifies customer of cancellation
Booking Rescheduled Appointment rescheduled Push, Email, WhatsApp Notifies customer of new date/time

Notification Flow by Endpoint

Create Appointment (POST /appointments)

When a staff member creates a new appointment:

  1. Booking Confirmation - Sent immediately after successful creation
  2. Booking Reminders - Scheduled based on tenant's reminder timing configuration
┌─────────────────────────────────────────────────────────────┐
│  Appointment Created Successfully                           │
├─────────────────────────────────────────────────────────────┤
│  ✅ Confirmation notification sent                          │
│     └─ Push, Email, WhatsApp                                │
│                                                             │
│  ⏰ Reminders scheduled:                                    │
│     └─ 24 hours before (push + email + whatsapp)            │
│     └─ 2 hours before (push + whatsapp)                     │
│     └─ 30 minutes before (push)                             │
└─────────────────────────────────────────────────────────────┘

Confirm Appointment (POST /appointments/{id}/confirm)

When manually confirming a PENDING appointment (e.g., after payment verification):

  1. Booking Confirmation - Sent immediately
  2. Booking Reminders - Scheduled based on tenant's reminder timing configuration

Cancel Appointment (DELETE /appointments/{id})

When cancelling an appointment:

  1. Cancel Scheduled Reminders - All pending reminders for this appointment are cancelled
  2. Booking Cancelled - Cancellation notification sent to customer
┌─────────────────────────────────────────────────────────────┐
│  Appointment Cancelled                                      │
├─────────────────────────────────────────────────────────────┤
│  🗑️ Scheduled reminders cancelled: 3                        │
│     └─ Cloud tasks deleted: 2                               │
│     └─ MongoDB records updated: 1                           │
│                                                             │
│  📧 Cancellation notification sent                          │
│     └─ Includes cancellation reason                         │
└─────────────────────────────────────────────────────────────┘

Reschedule Appointment (POST /appointments/{id}/reschedule)

When rescheduling an appointment to a new date/time:

  1. Cancel Old Reminders - Existing scheduled reminders are cancelled
  2. Booking Rescheduled - Reschedule notification sent with old and new times
  3. Schedule New Reminders - Fresh reminders scheduled for new appointment time
┌─────────────────────────────────────────────────────────────┐
│  Appointment Rescheduled                                    │
├─────────────────────────────────────────────────────────────┤
│  🗑️ Old reminders cancelled                                 │
│                                                             │
│  📧 Reschedule notification sent                            │
│     └─ Old: Jan 15, 2025 at 10:30                           │
│     └─ New: Jan 20, 2025 at 14:00                           │
│                                                             │
│  ⏰ New reminders scheduled for Jan 20                       │
└─────────────────────────────────────────────────────────────┘

Tenant Reminder Configuration

Tenants can customize reminder timing through the Notification Settings API. Each reminder timing specifies:

  • Value - Numeric amount (e.g., 24, 2, 30)
  • Unit - Time unit: minutes, hours, or days
  • Channels - Which channels to use for this reminder

Example Configuration:

{
  "reminder_timings": [
    { "value": 24, "unit": "hours", "channels": ["push", "email", "whatsapp"] },
    { "value": 2, "unit": "hours", "channels": ["push", "whatsapp"] },
    { "value": 30, "unit": "minutes", "channels": ["push"] }
  ]
}

Default: If no custom configuration exists, reminders default to 24 hours before appointment.

Configure via API: Use PUT /api/v1/notifications/settings/reminders to customize reminder timings. See Update Booking Reminders for details.

Notification Template Data

All booking notifications include the following template variables:

Variable Description Example
customer_name Customer's full name "John Doe"
tenant_name Business name "Beauty Salon"
service_names Comma-separated service list "Hair Cut, Hair Wash"
appointment_date Formatted date "15 January 2025"
appointment_time Formatted time (24h) "14:30"
outlet_name Outlet/branch name "Downtown Branch"
outlet_address Outlet address "123 Main Street"
staff_name Primary staff member "Jane Smith"
total_amount Formatted price "Rp 150,000"
booking_reference Short reference ID "BK-A1B2C3D4"

Additional variables for specific notifications:

  • Cancellation: cancellation_reason
  • Reschedule: old_date, old_time, new_date, new_time

Custom Templates: Tenants can customize message content using these variables with Jinja2 syntax. See Update Template Override for customization options.

Error Handling

Notification operations are non-blocking - errors are logged but do not prevent the main appointment operation:

# Notification errors are caught internally
try:
    await _send_booking_confirmation(...)
except Exception as e:
    logger.error(f"Failed to send booking confirmation: {e}")
    # Appointment creation still succeeds

Benefits:

  • Appointment operations always complete successfully
  • Notification failures are logged for debugging
  • Customer experience not impacted by notification service issues
  • Retry mechanisms can be implemented independently

Frontend UI Suggestions

Notification Status Indicator

Show notification status in appointment details:

┌─────────────────────────────────────────────────────────────┐
│  📅 Appointment Details                                     │
├─────────────────────────────────────────────────────────────┤
│  Customer: John Doe                                         │
│  Service:  Hair Cut & Style                                 │
│  Date:     Jan 20, 2025 at 14:30                            │
│  Status:   ● Confirmed                                      │
├─────────────────────────────────────────────────────────────┤
│  🔔 Notifications                                           │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  ✅ Confirmation sent                               │    │
│  │     Jan 15, 2025 at 10:30 AM                        │    │
│  │                                                     │    │
│  │  ⏰ Reminders scheduled:                            │    │
│  │     • 24h before - Jan 19 at 14:30                  │    │
│  │     • 2h before - Jan 20 at 12:30                   │    │
│  │     • 30min before - Jan 20 at 14:00                │    │
│  └─────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────┘

Cancellation with Notification Preview

Show notification preview when cancelling:

┌─────────────────────────────────────────────────────────────┐
│  ⚠️ Cancel Appointment                                      │
├─────────────────────────────────────────────────────────────┤
│  You are about to cancel this appointment:                  │
│                                                             │
│  Customer: John Doe                                         │
│  Service:  Hair Cut & Style                                 │
│  Date:     Jan 20, 2025 at 14:30                            │
├─────────────────────────────────────────────────────────────┤
│  🔔 Notification Actions                                    │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  The following will happen automatically:           │    │
│  │                                                     │    │
│  │  🗑️ 3 scheduled reminders will be cancelled         │    │
│  │  📧 Cancellation notification will be sent          │    │
│  │     → Push notification                             │    │
│  │     → Email to john@example.com                     │    │
│  │     → WhatsApp to +62812345678                      │    │
│  └─────────────────────────────────────────────────────┘    │
├─────────────────────────────────────────────────────────────┤
│  Cancellation Reason: [Required field                    ]  │
│                                                             │
│                    [Keep Appointment]  [Cancel Appointment] │
└─────────────────────────────────────────────────────────────┘

Reschedule with Notification Preview

Show notification preview when rescheduling:

┌─────────────────────────────────────────────────────────────┐
│  📅 Reschedule Appointment                                  │
├─────────────────────────────────────────────────────────────┤
│  Current: Jan 15, 2025 at 10:30                             │
│  New:     Jan 20, 2025 at 14:00                             │
├─────────────────────────────────────────────────────────────┤
│  🔔 Notification Actions                                    │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  The following will happen automatically:           │    │
│  │                                                     │    │
│  │  🗑️ Old reminders (Jan 15) will be cancelled        │    │
│  │  📧 Reschedule notification will be sent            │    │
│  │     → Shows old and new date/time                   │    │
│  │  ⏰ New reminders will be scheduled for Jan 20       │    │
│  └─────────────────────────────────────────────────────┘    │
├─────────────────────────────────────────────────────────────┤
│  Reason (optional): [Customer requested                  ]  │
│                                                             │
│                          [Cancel]  [Confirm Reschedule]     │
└─────────────────────────────────────────────────────────────┘

Implementation Example

// Notification status component for appointment details
const NotificationStatus = ({ appointment }) => {
  const { notifications } = useAppointmentNotifications(appointment.id);

  return (
    <Card>
      <CardHeader>
        <BellIcon /> Notifications
      </CardHeader>
      <CardContent>
        {/* Confirmation Status */}
        {notifications.confirmation && (
          <StatusItem status="success">
            <CheckIcon /> Confirmation sent
            <Timestamp>{notifications.confirmation.sent_at}</Timestamp>
          </StatusItem>
        )}

        {/* Scheduled Reminders */}
        {notifications.reminders?.length > 0 && (
          <ReminderList>
            <Label>Reminders scheduled:</Label>
            {notifications.reminders.map((reminder) => (
              <ReminderItem key={reminder.id}>
                <ClockIcon />
                {reminder.timing_description} - {formatDate(reminder.scheduled_at)}
                <ChannelBadges channels={reminder.channels} />
              </ReminderItem>
            ))}
          </ReminderList>
        )}
      </CardContent>
    </Card>
  );
};

// Cancel confirmation dialog with notification preview
const CancelAppointmentDialog = ({ appointment, onConfirm }) => {
  return (
    <Dialog>
      <DialogHeader>Cancel Appointment</DialogHeader>
      <DialogContent>
        {/* Appointment details */}
        <AppointmentSummary appointment={appointment} />

        {/* Notification preview */}
        <NotificationPreview>
          <PreviewHeader>
            <BellIcon /> Notification Actions
          </PreviewHeader>
          <PreviewContent>
            <PreviewItem icon={<TrashIcon />}>
              Scheduled reminders will be cancelled
            </PreviewItem>
            <PreviewItem icon={<MailIcon />}>
              Cancellation notification will be sent via:
              <ChannelList>
                <Badge>Push</Badge>
                <Badge>Email</Badge>
                <Badge>WhatsApp</Badge>
              </ChannelList>
            </PreviewItem>
          </PreviewContent>
        </NotificationPreview>

        {/* Reason input */}
        <TextArea
          label="Cancellation Reason"
          required
          placeholder="Enter reason for cancellation..."
        />
      </DialogContent>
      <DialogActions>
        <Button variant="secondary">Keep Appointment</Button>
        <Button variant="danger" onClick={onConfirm}>
          Cancel Appointment
        </Button>
      </DialogActions>
    </Dialog>
  );
};

Best Practices

DO:

  • Display notification status in appointment details
  • Show notification preview before destructive actions (cancel/reschedule)
  • Indicate which channels will receive notifications
  • Handle notification API errors gracefully in UI
  • Allow users to understand what notifications will be sent

DON'T:

  • Block appointment operations on notification failures
  • Expose internal notification IDs to end users unnecessarily
  • Assume notifications are delivered instantly (async processing)
  • Skip notification preview for bulk operations

Payment Management

Get Payment Status

Retrieve complete payment status and history for an appointment.

Endpoint

GET /api/v1/appointments/{appointment_id}/payment-status

Authentication: Required (Staff only)

Response

{
  "appointment_id": "507f1f77bcf86cd799439011",
  "payment_status": "paid",
  "total_amount": 150000,
  "paid_amount": 150000,
  "remaining_balance": 0,
  "platform_fee": 12000,
  "platform_fee_percentage": 8.0,
  "payment_history": [
    {
      "id": "507f1f77bcf86cd799439012",
      "amount": 150000,
      "method": "cash",
      "provider": "manual",
      "status": "completed",
      "recorded_by": "John Doe",
      "recorded_at": "2025-01-15T10:30:00Z",
      "notes": "Paid at checkout",
      "receipt_number": "RCPT-2025-001"
    }
  ],
  "pending_invoice": null,
  "can_complete": true
}

Information Provided:

  • Current payment status
  • Total and paid amounts
  • Remaining balance for partial payments
  • Platform fee breakdown
  • Complete payment history with audit trail
  • Pending payment link details (if exists)
  • Completion eligibility check

Use Cases:

  • Check if appointment can be completed
  • View payment history and audit trail
  • Find pending payment links
  • Calculate remaining balance for partial payments

Record Manual Payment

Record manual offline payment (cash, POS terminal, bank transfer) with support for multiple partial payments.

Endpoint

POST /api/v1/appointments/{appointment_id}/record-payment

Authentication: Required (Staff only)

Request Body

{
  "amount": 75000,
  "payment_method": "cash",
  "notes": "Paid in cash at checkout",
  "receipt_number": "RCPT-2025-001"
}

Parameters:

  • amount (required) - Payment amount received (must be > 0)
  • payment_method (required) - Payment method: cash, pos_terminal, bank_transfer
  • notes (optional) - Payment notes (max 500 characters)
  • receipt_number (optional) - Receipt or reference number (max 100 characters)

Response Examples

Example 1: Full Payment (Single Transaction)

{
  "status": "success",
  "message": "Payment recorded successfully - appointment fully paid",
  "payment": {
    "id": "507f1f77bcf86cd799439012",
    "amount": 150000,
    "method": "cash",
    "status": "completed",
    "recorded_by": "John Doe",
    "recorded_at": "2025-01-15T10:30:00Z",
    "receipt_number": "RCPT-2025-001"
  },
  "appointment": {
    "payment_status": "paid",
    "total_amount": 150000,
    "paid_amount": 150000,
    "remaining_balance": 0,
    "payment_count": 1
  }
}

Example 2: First Partial Payment (50%)

{
  "status": "success",
  "message": "Partial payment recorded - 1 of multiple payments",
  "payment": {
    "id": "507f1f77bcf86cd799439013",
    "amount": 75000,
    "method": "cash",
    "status": "completed",
    "recorded_by": "Maria Rodriguez",
    "recorded_at": "2025-01-15T10:30:00Z",
    "receipt_number": "RCPT-2025-001"
  },
  "appointment": {
    "payment_status": "partially_paid",
    "total_amount": 150000,
    "paid_amount": 75000,
    "remaining_balance": 75000,
    "payment_count": 1
  }
}

Example 3: Second Partial Payment (Completing)

{
  "status": "success",
  "message": "Payment recorded successfully - appointment fully paid",
  "payment": {
    "id": "507f1f77bcf86cd799439014",
    "amount": 75000,
    "method": "pos_terminal",
    "status": "completed",
    "recorded_by": "Maria Rodriguez",
    "recorded_at": "2025-01-15T14:45:00Z",
    "receipt_number": "RCPT-2025-002"
  },
  "appointment": {
    "payment_status": "paid",
    "total_amount": 150000,
    "paid_amount": 150000,
    "remaining_balance": 0,
    "payment_count": 2
  }
}

Supported Payment Methods:

  • cash - Cash payment at location
  • pos_terminal - Credit/debit card via POS terminal
  • bank_transfer - Direct bank transfer

Business Rules:

  • Payment method must be offline type only
  • ✨ NEW: Supports multiple partial payments until appointment is fully paid
  • Each payment cannot exceed remaining balance
  • Overpayment prevention: Total payments cannot exceed appointment total
  • Already-paid prevention: Cannot add payment to fully paid appointments
  • Immediate status: Payment marked as COMPLETED (no pending state)
  • Audit trail: Records staff user who recorded each payment

Partial Payment Flow:

  1. First Payment: Records amount → status becomes PARTIALLY_PAID
  2. Additional Payments: Adds to cumulative total → remains PARTIALLY_PAID
  3. Final Payment: Completes remaining balance → status becomes PAID

Example Scenario:

  • Appointment total: IDR 150,000
  • Payment 1: IDR 75,000 (cash) → Status: PARTIALLY_PAID, Remaining: 75,000
  • Payment 2: IDR 75,000 (POS) → Status: PAID, Remaining: 0

Audit Trail (for each payment):

  • Staff user ID and name in metadata
  • Timestamp of when payment was recorded
  • Optional receipt/reference number
  • Payment notes for additional context
  • Payment method for each transaction

Updates Appointment (cumulative):

  • payment_status: PARTIALLY_PAIDPAID (when fully paid)
  • paid_amount: Cumulative total of all payments
  • remaining_balance: Automatically calculated
  • payment_count: Total number of payment transactions
  • total_amount: Original appointment total (unchanged)

Validation:

  • ✅ Rejects payments that exceed remaining balance
  • ✅ Prevents additional payments to fully paid appointments
  • ✅ Validates payment amount is greater than zero
  • ✅ Only allows offline payment methods
  • ✅ Calculates cumulative paid amount from all successful payments

Generate Paper.id invoice and payment link for appointment.

Endpoint

POST /api/v1/appointments/{appointment_id}/create-payment-link

Authentication: Required (Staff only)

Request Body

{
  "send_email": true,
  "send_whatsapp": false,
  "send_sms": false,
  "notes": "Online payment for hair styling appointment",
  "due_date": "2025-01-20"
}

Parameters:

  • send_email (optional) - Send invoice via email (default: true, auto-disabled if no email)
  • send_whatsapp (optional) - Send invoice via WhatsApp (default: false)
  • send_sms (optional) - Send invoice via SMS (default: false)
  • notes (optional) - Invoice notes (max 500 characters)
  • due_date (optional) - Payment due date (default: appointment date or 3 days)

Flexible Contact Validation:

The endpoint automatically validates customer contact information and delivery methods:

Scenario 1: Customer has email

// Request (all delivery methods available)
{
  "send_email": true,
  "send_whatsapp": false,
  "send_sms": false
}
// ✅ Valid - Email will be sent

Scenario 2: Customer has NO email, but has phone

// Request (automatic email disable, requires WhatsApp/SMS)
{
  "send_email": true,  // Automatically disabled by server
  "send_whatsapp": true,
  "send_sms": false
}
// ✅ Valid - WhatsApp will be sent, email automatically disabled

Scenario 3: Customer has NO email, WhatsApp/SMS not enabled

// Request
{
  "send_email": false,
  "send_whatsapp": false,
  "send_sms": false
}
// ❌ Error: "Customer has no email address. Please enable WhatsApp or SMS delivery."

Scenario 4: Customer has NEITHER email NOR phone

// Any request
// ❌ Error: "Customer must have either email or phone number to receive invoice"

Validation Rules:

  1. At least one contact method required: Customer must have email OR phone
  2. At least one delivery method enabled: Must send via email, WhatsApp, or SMS
  3. Automatic email disable: If customer has no email, send_email automatically set to false
  4. Alternative delivery required: If no email, WhatsApp or SMS must be enabled
  5. Smart fallbacks: System uses phone for WhatsApp/SMS when email unavailable

Response

{
  "status": "payment_link_created",
  "message": "Payment link sent to customer via email",
  "invoice": {
    "id": "507f1f77bcf86cd799439015",
    "invoice_number": "INV-APT-2025-001",
    "amount": 81000,
    "subtotal": 75000,
    "platform_fee": 6000,
    "platform_fee_percentage": 8.0,
    "currency": "IDR",
    "due_date": "2025-01-18",
    "paper_invoice_id": "uuid-from-paper-id",
    "paper_payment_url": "https://stg-v2.paper.id/xyz123",
    "paper_pdf_url": "https://stg-v2.paper.id/pdf/abc456"
  },
  "payment_link": {
    "url": "https://stg-v2.paper.id/xyz123",
    "short_url": "https://payper.id/abc",
    "expires_at": "2025-01-18T23:59:59Z",
    "sent_via": ["email"]
  },
  "payment": {
    "id": "507f1f77bcf86cd799439016",
    "status": "pending",
    "awaiting_customer_payment": true
  },
  "next_steps": [
    "1. Customer will receive invoice via selected channels",
    "2. Customer opens payment link and selects payment method",
    "3. Upon payment completion, webhook will automatically confirm",
    "4. Appointment will be marked as PAID automatically"
  ]
}

Complete Flow (9 Steps)

  1. Validate Appointment - Check exists, not paid, has customer
  2. Validate Customer Contact - Flexible email/phone validation with automatic delivery method adjustment
  3. Validate Tenant - Get tenant and Paper.id configuration
  4. Create Paper.id Client - Initialize with tenant's credentials (decrypted)
  5. Calculate Platform Fee - Based on subscription tier
  6. Create Invoice - In local database with line items + platform fee (supports optional email)
  7. Ensure Customer Partner - Lazy creation/reuse of Paper.id partner record (supports phone-only customers)
  8. Generate Paper.id Invoice - Using tenant's Paper.id account with flexible contact fields
  9. Link Records - Connect local invoice to Paper.id invoice
  10. Create Payment Record - Mark as pending, await webhook confirmation

Enhanced Partner Management:

The partner creation/lookup process now supports customers without email:

  • Search by email (if customer has email) - Most accurate method
  • Search by partner number (if no email) - Uses unique partner identifier
  • Idempotent partner reuse - Prevents duplicate partner creation
  • Flexible contact fields - Partners created with phone-only if email unavailable
  • Empty email handling - Passes empty string to Paper.id when no email exists

Platform Fees:

Subscription Fee Rate Example (100,000 IDR)
FREE 8% Fee: 8,000 IDR, Total: 108,000 IDR
PRO 5% Fee: 5,000 IDR, Total: 105,000 IDR
ENTERPRISE 3% Fee: 3,000 IDR, Total: 103,000 IDR

Invoice Includes:

  • Appointment services breakdown
  • Platform fee as separate line item
  • Customer details with Paper.id partner ID
  • Professional PDF invoice
  • Due date (appointment date or 3 days default)

After Payment:

  • Webhook receives confirmation at /api/v1/webhooks/paper-invoice/tenant/{tenant_id}
  • Appointment marked as PAID automatically
  • Customer receives confirmation email
  • Invoice marked as PAID
  • Payment record updated to COMPLETED

Security:

  • Uses tenant's Paper.id account (revenue goes to tenant)
  • Customer.id mapped to Paper.id partner_id
  • Webhook URL includes tenant_id for proper routing
  • Client secret encrypted, never exposed to frontend

Customer Contact Requirements (Flexible):

  • Customer must have either email OR phone number
  • If customer has email: Can send via email, WhatsApp, or SMS
  • If customer has NO email: Must enable WhatsApp or SMS delivery (automatic)
  • At least ONE delivery method must be enabled

Prerequisites:

  • Tenant must have Paper.id configured (paper_id_config.enabled = true)
  • Customer must have either email OR phone number (flexible)
  • Appointment must not already be paid
  • No existing completed payment record

Statistics & Analytics

Get Appointment Statistics

Retrieve comprehensive appointment statistics and performance metrics.

Endpoint

GET /api/v1/appointments/stats/summary?period=month&outlet_id=507f1f77bcf86cd799439012

Authentication: Required (TENANT_ADMIN+ only)

Query Parameters

  • period (required) - Statistics period: day, week, month, quarter, year
  • outlet_id (optional) - Filter by specific outlet

Response

{
  "period": "month",
  "date_range": {
    "start": "2025-09-14T17:34:05.063607",
    "end": "2025-10-14T17:34:05.063607"
  },
  "total_appointments": 156,
  "by_status": {
    "completed": 120,
    "confirmed": 25,
    "cancelled": 8,
    "no_show": 3
  },
  "total_revenue": 18750000,
  "cancellation_rate": 5.13,
  "no_show_rate": 1.92
}

Metrics Provided:

  • Total Appointments - Count for the period
  • Status Breakdown - Appointments by status
  • Total Revenue - Sum of completed appointments
  • Cancellation Rate - Percentage of cancelled appointments
  • No-Show Rate - Percentage of no-show appointments

Time Periods:

  • day - Last 24 hours
  • week - Last 7 days
  • month - Last 30 days (default)
  • quarter - Last 90 days
  • year - Last 365 days

Access Control:

  • Only TENANT_ADMIN and SUPER_ADMIN can access
  • Automatically filtered by tenant
  • Optional outlet filter for location-specific analysis

Use Cases:

  • Revenue tracking and forecasting
  • Staff performance analysis
  • Cancellation pattern detection
  • Business trend analysis
  • Outlet comparison metrics

Package Credit Redemption

Use prepaid package credits to pay for appointments instantly.

Overview

Package credit redemption allows customers to use their prepaid service credits when booking appointments. This feature:

  • Zero-Cost Appointments - Credits from purchased packages cover service costs
  • Automatic Payment - Payment status set to PAID instantly when using credits
  • FIFO Ordering - Oldest credits (closest to expiry) used first
  • Auto-Detection - Just set credit_redeemed=true, backend auto-detects package
  • Atomic Operations - Credit redemption and appointment creation are transactional

Related Documentation:

How Credit Redemption Works

graph TD
    A[Create Appointment Request] --> B{credit_redeemed = true?}
    B -->|No| C[Standard Flow - Payment Pending]
    B -->|Yes| D[Check Credit Availability]
    D -->|No Credits| E[400 Error - No Available Credits]
    D -->|Has Credits| F{customer_package_id provided?}
    F -->|No| G[Auto-detect from FIFO credit]
    F -->|Yes| H[Use provided package]
    G --> I[Create Appointment]
    H --> I
    I --> J[Redeem Credit - FIFO]
    J -->|Success| K[Set Payment Status = PAID]
    K --> L[Return Appointment with credit_id + customer_package_id]
    J -->|Failure| M[Rollback - Delete Appointment]
    M --> N[400 Error - Credit Redemption Failed]

Request Format

To create an appointment with credit redemption, simply set credit_redeemed to true:

Simple Request (Recommended):

{
  "customer_id": "507f1f77bcf86cd799439011",
  "outlet_id": "507f1f77bcf86cd799439012",
  "appointment_date": "2025-01-15",
  "start_time": "14:30",
  "services": [
    {
      "service_id": "507f1f77bcf86cd799439013",
      "staff_id": "507f1f77bcf86cd799439014"
    }
  ],
  "credit_redeemed": true,
  "notes": "Using package credit"
}

The system automatically detects the appropriate customer_package_id using FIFO ordering (oldest credits used first).

Explicit Request (Optional):

If you need to specify a particular package, you can still include customer_package_id:

{
  "customer_id": "507f1f77bcf86cd799439011",
  "outlet_id": "507f1f77bcf86cd799439012",
  "appointment_date": "2025-01-15",
  "start_time": "14:30",
  "services": [
    {
      "service_id": "507f1f77bcf86cd799439013",
      "staff_id": "507f1f77bcf86cd799439014"
    }
  ],
  "credit_redeemed": true,
  "customer_package_id": "507f1f77bcf86cd799439020",
  "notes": "Using specific package credit"
}

Parameters:

Field Type Required Description
credit_redeemed boolean Yes* Set to true to use package credits
customer_package_id string No Customer's purchased package ID (auto-detected if omitted)

*Required only when using credit redemption

Auto-Detection Behavior:

When customer_package_id is omitted:

  1. System calls check_credit_availability() to find available credits
  2. Credits are sorted by expiry date (FIFO - oldest first)
  3. First available credit's package is automatically selected
  4. Prevents credit waste from expiration

Response Format

Successful credit redemption returns appointment with credit information:

{
  "id": "507f1f77bcf86cd799439030",
  "tenant_id": "507f1f77bcf86cd799439010",
  "customer_id": "507f1f77bcf86cd799439011",
  "outlet_id": "507f1f77bcf86cd799439012",
  "appointment_date": "2025-01-15T00:00:00Z",
  "start_time": "14:30",
  "end_time": "15:30",
  "status": "confirmed",
  "payment_status": "paid",
  "services": [...],
  "total_price": 75000,
  "credit_redeemed": true,
  "credit_id": "507f1f77bcf86cd799439025",
  "customer_package_id": "507f1f77bcf86cd799439020",
  "created_at": "2025-01-10T10:30:00Z"
}

Credit-Specific Response Fields:

Field Type Description
credit_redeemed boolean true if package credit was used
credit_id string ID of the redeemed credit record
customer_package_id string ID of the package from which credit was redeemed
payment_status string Always paid when credit is used

Credit Validation Rules

The system validates credits before allowing redemption:

  1. Customer Ownership - Credit must belong to the booking customer
  2. Service Match - Credit must be for the booked service
  3. Available Balance - Credit must have remaining_credits > 0
  4. Not Expired - Credit expires_at must be in the future (or null)
  5. Payment Confirmed - Parent package must have payment_status = PAID
  6. Tenant Isolation - Credit must belong to the same tenant

FIFO Credit Selection

Credits are automatically selected using First In, First Out ordering:

  1. System finds all available credits for the customer + service
  2. Credits ordered by expires_at ascending (closest to expiry first)
  3. First credit in list is selected for redemption
  4. Prevents credit waste from expiration

Example:

Customer has 3 credits for "Hair Cut" service:
- Credit A: expires 2025-02-01 (selected first - closest to expiry)
- Credit B: expires 2025-03-15
- Credit C: expires 2025-06-30

Atomic Transaction & Rollback

Credit redemption is atomic with appointment creation:

# Simplified flow from AppointmentService
1. Create appointment record
2. Try to redeem credit
   - If SUCCESS: Update appointment with credit_id, set payment_status=PAID
   - If FAILURE: Delete appointment (hard delete), raise error
3. Return appointment

Rollback Scenarios:

  • Credit already fully used
  • Credit expired during process
  • Database write failure
  • Concurrent redemption conflict

Error Response (Rollback):

{
  "detail": "Credit redemption failed: Credit has no remaining balance. Appointment has been cancelled."
}

Credit Redemption vs Manual Payment

Aspect Credit Redemption Manual Payment
Timing At appointment creation After appointment creation
Payment Status Instant PAID PENDINGPAID
Cost to Customer Zero (prepaid) Full service price
Endpoint POST /appointments POST /appointments/{id}/record-payment
Audit Trail credit_id, customer_package_id payment_id, staff who recorded

Cancellation with Credit Refund

When a credit-paid appointment is cancelled, the credit is automatically refunded:

# AppointmentService.cancel_appointment_with_credit_refund()
1. Get appointment
2. If credit_redeemed and credit_id:
   a. Get credit record
   b. Increment remaining_credits
   c. Decrement used_credits
   d. Update customer_package remaining_credits
   e. If package was DEPLETED, set back to ACTIVE
3. Set appointment status = CANCELLED

Refund Rules:

  • Credit returned to original credit record
  • Customer package balance updated
  • DEPLETED packages become ACTIVE again
  • Refund is immediate (no pending state)

Viewing Credit Information

In Appointment List

Credit redemption info included in list response:

{
  "items": [
    {
      "id": "507f1f77bcf86cd799439030",
      "customer_name": "Maria Rodriguez",
      "payment_status": "paid",
      "credit_redeemed": true,
      "credit_id": "507f1f77bcf86cd799439025",
      "customer_package_id": "507f1f77bcf86cd799439020",
      ...
    }
  ]
}

In Appointment Details

Detailed appointment response includes credit tracking:

{
  "id": "507f1f77bcf86cd799439030",
  "credit_redeemed": true,
  "credit_id": "507f1f77bcf86cd799439025",
  "customer_package_id": "507f1f77bcf86cd799439020",
  "payment_status": "paid",
  "payment_details": null,
  "fee_breakdown": null
}

Note: Credit-paid appointments have:

  • payment_details = null (no payment transactions)
  • fee_breakdown = null (fees applied at package purchase, not redemption)

Checking Customer Credit Availability

Before booking, check if customer has available credits:

Via Staff Portal:

GET /api/v1/staff/customer-packages/{customer_id}/credits?service_id={service_id}

Via Customer Portal:

GET /api/v1/customer/packages/my-packages

See Staff Customer Package Management for details.

Error Handling

No Available Credits:

{
  "detail": "No available credits for service 'Hair Styling'. Customer has no valid, unexpired credits for this service."
}

Invalid Package ID:

{
  "detail": "Customer package not found or does not belong to customer"
}

Credit Already Redeemed:

{
  "detail": "Credit redemption failed: Credit has no remaining balance. Appointment has been cancelled."
}

Expired Credit:

{
  "detail": "Credit redemption failed: Credit has expired. Appointment has been cancelled."
}

Frontend UI Suggestions

Use Case 1: Staff Booking with Credit Option

Scenario: Staff member creates appointment for customer who has package credits.

Recommended UI Flow:

┌─────────────────────────────────────────────────────────────┐
│  📅 New Appointment                                         │
├─────────────────────────────────────────────────────────────┤
│  Customer: [John Doe ▼]                                     │
│  Service:  [Hair Cut & Style ▼]                             │
│  Staff:    [Jane Smith ▼]                                   │
│  Date:     [2025-01-20]  Time: [14:30]                      │
├─────────────────────────────────────────────────────────────┤
│  💳 Payment Method                                          │
│  ┌─────────────────────────────────────────────────────┐    │
│  │ ✨ Use Package Credit                               │    │
│  │    Hair Care Premium Package                        │    │
│  │    3 credits remaining • Expires Feb 15, 2025       │    │
│  │    ⚠️ Expiring soon - use within 25 days           │    │
│  └─────────────────────────────────────────────────────┘    │
│  ○ Pay at checkout (IDR 75,000)                             │
│  ○ Send payment link                                        │
├─────────────────────────────────────────────────────────────┤
│                              [Cancel]  [Book Appointment]   │
└─────────────────────────────────────────────────────────────┘

Implementation Steps:

  1. On Customer Selection - Fetch available credits:

    // When customer is selected
    const credits = await fetch(
      `/api/v1/staff/customer-packages/${customerId}/credits?service_id=${serviceId}`
    );
    

  2. Display Credit Option - Show only if credits available:

    if (credits.length > 0) {
      // Show "Use Package Credit" option
      // Display package name, remaining credits, expiry
      // Highlight if is_expiring_soon = true
    }
    

  3. On Submit - Include credit field (simplified):

    const appointmentData = {
      customer_id: customerId,
      outlet_id: outletId,
      appointment_date: date,
      start_time: time,
      services: [{ service_id, staff_id }],
      // Credit redemption - just set credit_redeemed: true
      // customer_package_id is auto-detected by backend using FIFO
      credit_redeemed: useCredit,
      notes: "Using package credit"
    };
    

  4. Handle Response - Show success with credit info:

    // Success response includes credit_id
    toast.success(`Appointment booked! Credit redeemed from ${packageName}`);
    // Optionally show remaining credits after booking
    


Use Case 2: Customer Self-Service Booking

Scenario: Customer books appointment on customer portal with their package credits.

Recommended UI Flow:

┌─────────────────────────────────────────────────────────────┐
│  🗓️ Book Appointment                                        │
├─────────────────────────────────────────────────────────────┤
│  Service: Hair Cut & Style                                  │
│  Duration: 60 minutes                                       │
│  Price: IDR 75,000                                          │
├─────────────────────────────────────────────────────────────┤
│  ✨ YOU HAVE CREDITS FOR THIS SERVICE!                      │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  📦 Hair Care Premium Package                       │    │
│  │  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 3/5 used       │    │
│  │  2 credits remaining                                │    │
│  │  Expires: Feb 15, 2025 (25 days)                    │    │
│  │                                                     │    │
│  │  [Use 1 Credit - FREE] ← Primary CTA                │    │
│  └─────────────────────────────────────────────────────┘    │
│                                                             │
│  Or pay IDR 75,000 →                                        │
├─────────────────────────────────────────────────────────────┤
│  Select Date & Time:                                        │
│  [Calendar Component]                                       │
├─────────────────────────────────────────────────────────────┤
│                                      [Confirm Booking]      │
└─────────────────────────────────────────────────────────────┘

Implementation Steps:

  1. Fetch Credits on Service Selection:

    // Customer portal - fetch own credits
    const myPackages = await fetch('/api/v1/customer/packages/my-packages');
    const creditsForService = myPackages.filter(
      pkg => pkg.credits.some(c => c.service_id === serviceId && c.remaining > 0)
    );
    

  2. Prominent Credit Display:

    {hasCredits && (
      <CreditBanner variant="success">
        <PackageIcon />
        <div>
          <h4>You have credits for this service!</h4>
          <p>{remainingCredits} credits from {packageName}</p>
          {isExpiringSoon && <WarningBadge>Expires soon</WarningBadge>}
        </div>
        <Button primary onClick={() => setUseCredit(true)}>
          Use Credit - FREE
        </Button>
      </CreditBanner>
    )}
    

  3. Confirmation Screen:

    ┌─────────────────────────────────────────────────────┐
    │  ✅ Booking Confirmed!                              │
    │                                                     │
    │  Hair Cut & Style with Jane Smith                   │
    │  Jan 20, 2025 at 2:30 PM                            │
    │                                                     │
    │  Payment: ✨ Package Credit Used                    │
    │  Status: PAID                                       │
    │                                                     │
    │  📦 Hair Care Premium Package                       │
    │     1 credit remaining after this booking           │
    └─────────────────────────────────────────────────────┘
    


Use Case 3: Appointment List with Credit Indicators

Scenario: Staff views appointment list showing which appointments used credits.

Recommended UI:

┌──────────────────────────────────────────────────────────────────────┐
│  📋 Today's Appointments                              [+ New]        │
├──────────────────────────────────────────────────────────────────────┤
│  ┌────────────────────────────────────────────────────────────────┐  │
│  │ 10:00 AM  John Doe           Hair Cut        💳 IDR 75,000    │  │
│  │           Jane Smith         Confirmed       [View] [Check-in] │  │
│  └────────────────────────────────────────────────────────────────┘  │
│  ┌────────────────────────────────────────────────────────────────┐  │
│  │ 11:30 AM  Maria Rodriguez    Manicure        ✨ Credit Used    │  │
│  │           Alice Wong         Confirmed       [View] [Check-in] │  │
│  │           📦 Spa Bundle Package                                │  │
│  └────────────────────────────────────────────────────────────────┘  │
│  ┌────────────────────────────────────────────────────────────────┐  │
│  │ 2:00 PM   Sarah Chen         Facial          ⏳ Payment Pending│  │
│  │           Lisa Park          Pending         [View] [Send Link]│  │
│  └────────────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────────────┘

Implementation:

// Appointment list item component
const AppointmentCard = ({ appointment }) => {
  const paymentBadge = () => {
    if (appointment.credit_redeemed) {
      return (
        <Badge variant="purple">
          <SparklesIcon /> Credit Used
        </Badge>
      );
    }
    if (appointment.payment_status === 'paid') {
      return <Badge variant="green">💳 {formatPrice(appointment.total_price)}</Badge>;
    }
    return <Badge variant="yellow"> Payment Pending</Badge>;
  };

  return (
    <Card>
      <TimeSlot>{appointment.start_time}</TimeSlot>
      <CustomerName>{appointment.customer_name}</CustomerName>
      <ServiceName>{appointment.services[0].service_name}</ServiceName>
      {paymentBadge()}
      {appointment.credit_redeemed && (
        <PackageInfo>📦 {appointment.package_name}</PackageInfo>
      )}
    </Card>
  );
};

Use Case 4: Appointment Detail with Credit Information

Scenario: Staff views appointment detail showing credit redemption info.

Recommended UI:

┌─────────────────────────────────────────────────────────────┐
│  📅 Appointment Details                          [Edit] [×] │
├─────────────────────────────────────────────────────────────┤
│  Customer: Maria Rodriguez                                  │
│  Service:  Manicure (60 min)                                │
│  Staff:    Alice Wong                                       │
│  Date:     Jan 20, 2025 at 11:30 AM                         │
│  Status:   ● Confirmed                                      │
├─────────────────────────────────────────────────────────────┤
│  💳 Payment Information                                     │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  Status: ✅ PAID                                    │    │
│  │  Method: ✨ Package Credit                          │    │
│  │                                                     │    │
│  │  📦 Spa Relaxation Bundle                           │    │
│  │  ├─ Credit ID: 507f1f77bcf86cd799439025             │    │
│  │  ├─ Package ID: 507f1f77bcf86cd799439020            │    │
│  │  └─ Purchased: Jan 5, 2025                          │    │
│  │                                                     │    │
│  │  ℹ️ No payment transaction - prepaid via package    │    │
│  └─────────────────────────────────────────────────────┘    │
├─────────────────────────────────────────────────────────────┤
│  [Cancel Appointment]  [Mark Complete]  [Mark No-Show]      │
│                                                             │
│  ⚠️ Cancelling will refund the credit to customer's package │
└─────────────────────────────────────────────────────────────┘

Implementation:

const PaymentSection = ({ appointment }) => {
  if (appointment.credit_redeemed) {
    return (
      <PaymentCard>
        <StatusBadge status="paid"> PAID</StatusBadge>
        <MethodBadge> Package Credit</MethodBadge>

        <PackageDetails>
          <PackageIcon />
          <div>
            <h4>{appointment.package_name || 'Package'}</h4>
            <DetailRow>
              <Label>Credit ID:</Label>
              <Code>{appointment.credit_id}</Code>
            </DetailRow>
            <DetailRow>
              <Label>Package ID:</Label>
              <Code>{appointment.customer_package_id}</Code>
            </DetailRow>
          </div>
        </PackageDetails>

        <InfoNote>
          ℹ️ No payment transaction - prepaid via package
        </InfoNote>
      </PaymentCard>
    );
  }

  // Regular payment display...
  return <RegularPaymentSection appointment={appointment} />;
};

Use Case 5: Cancel Credit-Paid Appointment

Scenario: Staff cancels appointment and credit is refunded.

Recommended UI Flow:

┌─────────────────────────────────────────────────────────────┐
│  ⚠️ Cancel Appointment                                      │
├─────────────────────────────────────────────────────────────┤
│  You are about to cancel this appointment:                  │
│                                                             │
│  Customer: Maria Rodriguez                                  │
│  Service:  Manicure                                         │
│  Date:     Jan 20, 2025 at 11:30 AM                         │
├─────────────────────────────────────────────────────────────┤
│  💳 Credit Refund Notice                                    │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  This appointment was paid with a package credit.   │    │
│  │                                                     │    │
│  │  ✅ Credit will be automatically refunded to:       │    │
│  │     📦 Spa Relaxation Bundle                        │    │
│  │     Current: 2 credits → After: 3 credits           │    │
│  └─────────────────────────────────────────────────────┘    │
├─────────────────────────────────────────────────────────────┤
│  Cancellation Reason: [Required field                    ]  │
│                                                             │
│                    [Keep Appointment]  [Cancel & Refund]    │
└─────────────────────────────────────────────────────────────┘

After Cancellation:

┌─────────────────────────────────────────────────────────────┐
│  ✅ Appointment Cancelled                                   │
├─────────────────────────────────────────────────────────────┤
│  The appointment has been cancelled successfully.           │
│                                                             │
│  💳 Credit Refund Complete                                  │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  ✅ 1 credit refunded to Spa Relaxation Bundle      │    │
│  │  Maria Rodriguez now has 3 credits remaining        │    │
│  └─────────────────────────────────────────────────────┘    │
│                                                             │
│                                           [Back to List]    │
└─────────────────────────────────────────────────────────────┘

UI Component Library Suggestions

Credit Badge Component:

// Reusable component for showing credit payment status
const CreditBadge = ({ appointment }) => {
  if (!appointment.credit_redeemed) return null;

  return (
    <Badge
      variant="purple"
      icon={<SparklesIcon />}
      tooltip={`Credit from ${appointment.package_name}`}
    >
      Credit Used
    </Badge>
  );
};

Credit Availability Indicator:

// Show in service selection when customer has credits
const CreditAvailabilityBanner = ({ credits, serviceName }) => {
  if (!credits?.length) return null;

  const firstCredit = credits[0]; // FIFO - will be used first

  return (
    <Banner variant={firstCredit.is_expiring_soon ? 'warning' : 'success'}>
      <SparklesIcon />
      <div>
        <strong>You have {firstCredit.remaining_credits} credits for {serviceName}</strong>
        <p>From: {firstCredit.package_name}</p>
        {firstCredit.is_expiring_soon && (
          <WarningText>⚠️ Expires {formatDate(firstCredit.expires_at)}</WarningText>
        )}
      </div>
      <Button onClick={onUseCredit}>Use Credit</Button>
    </Banner>
  );
};

Payment Method Selector:

// Radio group for payment method selection
const PaymentMethodSelector = ({ credits, price, onChange }) => (
  <RadioGroup onChange={onChange}>
    {credits?.length > 0 && (
      <RadioOption
        value="credit"
        label="Use Package Credit"
        description={`${credits[0].remaining_credits} credits from ${credits[0].package_name}`}
        badge={<Badge variant="green">FREE</Badge>}
        highlighted
      />
    )}
    <RadioOption
      value="pay_later"
      label="Pay at checkout"
      description={formatPrice(price)}
    />
    <RadioOption
      value="payment_link"
      label="Send payment link"
      description="Customer pays online"
    />
  </RadioGroup>
);

State Management Recommendations

// Zustand/Redux store shape for appointment booking with credits
interface BookingState {
  // Customer & Service Selection
  customerId: string | null;
  serviceId: string | null;
  staffId: string | null;

  // Credit Information (fetched when customer+service selected)
  availableCredits: Credit[];
  selectedCredit: Credit | null;

  // Payment Method
  paymentMethod: 'credit' | 'pay_later' | 'payment_link';

  // Actions
  fetchCredits: (customerId: string, serviceId: string) => Promise<void>;
  selectCredit: (credit: Credit) => void;
  clearCredits: () => void;
}

// Fetch credits when both customer and service are selected
useEffect(() => {
  if (customerId && serviceId) {
    bookingStore.fetchCredits(customerId, serviceId);
  } else {
    bookingStore.clearCredits();
  }
}, [customerId, serviceId]);

Error Handling UI

// Handle credit redemption errors gracefully
const handleBookingError = (error) => {
  if (error.detail?.includes('No available credits')) {
    toast.error('Credit Unavailable', {
      description: 'The credit may have been used or expired. Please select a different payment method.',
      action: {
        label: 'Refresh Credits',
        onClick: () => refetchCredits()
      }
    });
  } else if (error.detail?.includes('Credit redemption failed')) {
    toast.error('Booking Failed', {
      description: 'Credit redemption failed. The booking was not created. Please try again.',
    });
  } else {
    toast.error('Booking Error', { description: error.detail });
  }
};

Best Practices

DO:

  • Just set credit_redeemed=true - let backend auto-detect package (simplest approach)
  • Check credit availability before showing redemption option to customer
  • Display expiring credits prominently (use is_expiring_soon flag)
  • Inform customers which credit will be used (FIFO order)
  • Handle rollback errors gracefully in UI
  • Show credit balance after successful booking

DON'T:

  • Manually lookup customer_package_id when not needed (backend auto-detects)
  • Allow credit redemption for services not in the package
  • Skip validation of customer package ownership
  • Assume credit is available without checking
  • Ignore expired credits in availability check
  • Mix credit redemption with manual payment for same appointment

Testing Checklist

  • [ ] Create appointment with credit_redeemed=true only (auto-detect package)
  • [ ] Create appointment with explicit customer_package_id
  • [ ] Verify response includes auto-detected customer_package_id
  • [ ] Reject redemption when no credits available
  • [ ] Reject redemption for wrong service
  • [ ] Reject redemption with expired credits
  • [ ] Verify FIFO ordering selects oldest credit
  • [ ] Verify atomic rollback on failure
  • [ ] Verify payment_status = PAID after credit use
  • [ ] Cancel credit-paid appointment and verify refund
  • [ ] Verify DEPLETED package becomes ACTIVE after refund
  • [ ] List appointments shows credit_redeemed flag
  • [ ] Appointment details shows credit_id

Business Rules Summary

Scheduling Constraints

  1. Business Hours - Appointments must be within outlet operating hours
  2. Advance Booking - Configurable minimum/maximum advance booking days
  3. Minimum Notice - Minimum time before appointment (e.g., 2 hours)
  4. Duration Validation - Appointment duration must fit within working hours
  5. Duplicate Prevention - Same customer cannot book identical service+staff+time combination

Staff Assignment

  1. Skill Matching - Staff must have required skills for service
  2. Load Balancing - Auto-assignment distributes bookings evenly
  3. Availability Check - Staff working hours and time-off respected
  4. Conflict Prevention - No double-booking allowed
  5. Duplicate Detection - Customer cannot book same service+staff+time twice

Payment Rules

  1. Payment Verification - Required before marking appointment as completed
  2. Platform Fees - Applied based on subscription tier (3-8%)
  3. ✨ Partial Payments - Supports multiple partial payments until fully paid (manual payments)
  4. Cumulative Tracking - paid_amount tracks cumulative total across all payments
  5. Overpayment Prevention - Each payment validated against remaining balance
  6. Offline Methods - Manual payment limited to cash, POS, bank transfer
  7. Online Methods - Paper.id payment link for online payments
  8. ✨ Package Credits - Prepaid credits can be used at appointment creation (instant PAID status)
  9. Credit Refunds - Cancelled credit-paid appointments automatically refund credits

Status Transitions

PENDING → CONFIRMED → IN_PROGRESS → COMPLETED
            ↓              ↓
        CANCELLED      NO_SHOW

Valid Transitions:

  • PENDINGCONFIRMED (manual or webhook)
  • CONFIRMEDCOMPLETED (with payment verification)
  • CONFIRMEDNO_SHOW (customer didn't arrive)
  • CONFIRMEDCANCELLED (with reason)
  • IN_PROGRESSCOMPLETED (with payment verification)

Invalid Transitions:

  • Cannot update COMPLETED appointments
  • Cannot update CANCELLED appointments
  • Cannot confirm NO_SHOW appointments
  • Cannot complete without payment (for paid appointments)

API Reference Summary

Endpoint Method Purpose Access
/appointments GET List appointments with filters All Staff
/appointments POST Create new appointment All Staff
/appointments/{id} GET Get appointment details All Staff
/appointments/{id} PUT Update appointment All Staff
/appointments/{id} DELETE Cancel appointment All Staff
/appointments/{id}/reschedule POST Reschedule appointment All Staff
/appointments/{id}/confirm POST Manually confirm (PENDING → CONFIRMED) All Staff
/appointments/{id}/complete POST Mark as completed All Staff
/appointments/{id}/no-show POST Mark as no-show All Staff
/appointments/{id}/payment-status GET Get payment status and history Staff Only
/appointments/{id}/record-payment POST Record manual payment Staff Only
/appointments/{id}/create-payment-link POST Generate Paper.id payment link Staff Only
/appointments/stats/summary GET Get statistics and analytics TENANT_ADMIN+

Integration with Other Systems

Subscription Management

  • Plan Limits - See Subscription Management for plan details
  • Usage Tracking - Appointments counted against monthly limits
  • Platform Fees - Fee rates based on subscription tier

Payment Gateway (Paper.id)

  • Invoice Generation - Professional sales invoices with line items
  • Payment Links - Sharable payment URLs with multiple delivery methods
  • Webhook Integration - Automatic payment confirmation
  • Tenant Credentials - Each tenant uses their own Paper.id account

Notification System

The appointment system integrates with the notification service to keep customers informed:

  • Booking Confirmation - Sent immediately when appointment is created or confirmed
  • Booking Reminders - Scheduled based on tenant's reminder timing configuration
  • Cancellation Notification - Sent when appointment is cancelled (includes reason)
  • Reschedule Notification - Sent when appointment is rescheduled (includes old and new times)

Integration Points:

Endpoint Notifications Triggered
POST /appointments Confirmation + Schedule reminders
POST /appointments/{id}/confirm Confirmation + Schedule reminders
DELETE /appointments/{id} Cancel reminders + Cancellation notification
POST /appointments/{id}/reschedule Cancel old reminders + Reschedule notification + Schedule new reminders

Channels: Push, Email, WhatsApp (based on customer contact info and tenant configuration)

Non-Blocking: Notification failures are logged but do not prevent appointment operations.

See Automatic Notifications for detailed documentation.

Customer Management

  • Customer Portal - Customers can view/manage their appointments
  • Multi-Channel Notifications - Booking confirmations, reminders, and updates via Push, Email, WhatsApp
  • Payment History - Customer payment records linked to appointments

Staff Management

  • Skill-Based Assignment - Automatic matching based on service requirements
  • Availability Management - Working hours and time-off integration
  • Load Balancing - Even distribution of appointments across staff
  • Parallel Booking - See Parallel Booking for group services and capacity management

Package & Credit System

The appointment system integrates with the package management system for prepaid service credits:

  • Package Credit Redemption - Use prepaid credits when booking appointments
  • Automatic Payment - Credit-paid appointments are instantly marked as PAID
  • FIFO Credit Selection - Oldest credits (closest to expiry) used first
  • Cancellation Refunds - Credits automatically refunded when appointments cancelled

Related Package Documentation:

Document Purpose Key Features
Package Management Admin package CRUD Create packages, set pricing, manage items
Customer Package Management Customer self-service Browse packages, purchase, view credits
Staff Customer Package Management Staff operations Manual purchase, credit inquiry, redemption
Customer Package Payments Payment processing Record payments, create payment links

Integration Points:

  1. Appointment Creation (POST /appointments)
  2. Accept credit_redeemed and customer_package_id parameters
  3. Validate credit availability via AppointmentService.check_credit_availability()
  4. Atomic credit redemption via AppointmentService.create_appointment_with_credit()

  5. Credit Availability Check

  6. Staff: GET /staff/customer-packages/{customer_id}/credits?service_id={service_id}
  7. Customer: GET /customer/packages/my-packages

  8. Credit Redemption (Manual, outside appointment)

  9. POST /staff/customer-packages/credits/redeem

  10. Appointment Cancellation (with credit refund)

  11. AppointmentService.cancel_appointment_with_credit_refund()

See Package Credit Redemption section for detailed usage.


Error Handling

Common Errors

400 Bad Request:

{
  "detail": "Scheduling constraint violations: Appointment outside business hours, Minimum 2-hour advance booking required"
}

403 Forbidden (Plan Limit):

{
  "detail": "Monthly appointment limit exceeded (100/100). Please upgrade your plan.",
  "current_plan": "FREE",
  "upgrade_available": true
}

409 Conflict - Staff Double-Booking:

{
  "detail": "Booking conflict: Staff has overlapping appointment"
}

409 Conflict - Duplicate Booking:

{
  "detail": "Duplicate booking: Customer already has this exact appointment booked"
}

422 Unprocessable Entity:

{
  "detail": "Invalid time format. Use HH:MM format (e.g., 14:30)"
}

502 Bad Gateway (Paper.id):

{
  "detail": "Failed to create invoice in Paper.id: Connection timeout"
}


Best Practices

For Creating Appointments

DO:

  • Validate customer details before booking
  • Use auto-assignment when staff preference not specified
  • Record appointment notes for context
  • Calculate pricing server-side (never trust client)
  • Check subscription limits before creating

DON'T:

  • Skip availability validation
  • Trust client-provided prices
  • Create appointments in the past
  • Ignore scheduling constraints
  • Override staff skill requirements

For Payment Handling

DO:

  • Always verify payment before completing appointment
  • Use Paper.id payment links for online payments
  • Record manual payments with receipt numbers
  • ✨ Support partial payments for customer convenience
  • Include audit trail (staff name, timestamp) for each payment
  • Display platform fees transparently
  • Check remaining_balance before accepting additional payments
  • Track payment_count to know how many payments were made
  • ✨ NEW: Support customers with phone-only (no email required)
  • Enable WhatsApp/SMS delivery for customers without email
  • Validate at least one delivery method is selected

DON'T:

  • Complete appointments without payment verification
  • Mix offline methods with create-payment-link endpoint
  • Accept payments exceeding remaining balance
  • Skip cumulative payment calculation
  • Expose tenant Paper.id credentials to frontend
  • Create payment links for already paid appointments
  • Record payments on fully paid appointments
  • NEW: Require email when phone is available (now flexible)
  • Create payment links without any delivery method enabled

For Status Management

DO:

  • Follow valid status transition rules
  • Record reasons for cancellations and no-shows
  • Use appropriate status for each scenario
  • Preserve audit trail with timestamps
  • Notify customers of status changes

DON'T:

  • Modify completed/cancelled appointments
  • Skip payment verification when completing
  • Mark no-show without customer contact attempt
  • Change status without proper validation
  • Delete appointments (use cancellation instead)

Testing Checklist

Appointment Creation

  • [ ] Create appointment with valid data
  • [ ] Reject appointment outside business hours
  • [ ] Reject appointment with invalid staff skills
  • [ ] Reject appointment with scheduling conflict
  • [ ] Reject duplicate booking (same customer+service+staff+time)
  • [ ] Auto-assign staff when not specified
  • [ ] Calculate correct pricing server-side
  • [ ] Enforce subscription plan limits
  • [ ] Validate customer and outlet exist
  • [ ] Calculate platform fees correctly

Appointment Updates

  • [ ] Update appointment date/time successfully
  • [ ] Reject updates to completed appointments
  • [ ] Validate new time slot availability
  • [ ] Detect conflicts when rescheduling
  • [ ] Recalculate service times correctly
  • [ ] Preserve service assignments
  • [ ] Record update audit trail

Payment Processing

  • [ ] Record manual cash payment
  • [ ] ✨ Record first partial payment (status: PARTIALLY_PAID)
  • [ ] ✨ Record second partial payment (cumulative tracking)
  • [ ] ✨ Complete payment with final partial (status: PAID)
  • [ ] ✨ Prevent overpayment (exceed remaining balance)
  • [ ] ✨ Prevent additional payment to fully paid appointment
  • [ ] Verify cumulative paid_amount is correct
  • [ ] Verify remaining_balance calculation
  • [ ] Verify payment_count increments correctly
  • [ ] Create Paper.id payment link (customer with email)
  • [ ] ✨ NEW: Create payment link for customer without email (WhatsApp/SMS)
  • [ ] ✨ NEW: Validate flexible contact requirements (email OR phone)
  • [ ] ✨ NEW: Auto-disable email delivery when customer has no email
  • [ ] ✨ NEW: Require WhatsApp/SMS when no email available
  • [ ] ✨ NEW: Prevent duplicate partner creation for phone-only customers
  • [ ] ✨ NEW: Search existing partners by phone when email unavailable
  • [ ] Handle webhook payment confirmation
  • [ ] Verify payment before completion
  • [ ] Calculate platform fees by plan
  • [ ] Update appointment payment status

Status Transitions

  • [ ] Confirm pending appointment manually
  • [ ] Complete appointment with payment
  • [ ] Reject completion without payment
  • [ ] Mark no-show for confirmed appointment
  • [ ] Cancel appointment with reason
  • [ ] Validate status transition rules

Notification System

Document Description
Notification Management Send manual notifications, schedule future notifications, view usage
Notification Settings Customize reminder timings and message templates

Core Documentation

Package System (Credit Redemption)

Document Description
Package Management Create and configure service packages with credits
Customer Package Management Customer self-service: browse, purchase, view credits
Staff Customer Package Management Staff operations: manual purchase, credit inquiry, redemption
Customer Package Payments Record payments, create payment links for packages

Quick Reference

Task Endpoint Documentation
Create appointment with credit POST /appointments Package Credit Redemption
Check customer credits GET /staff/customer-packages/{id}/credits Staff Customer Package Management
Manual credit redemption POST /staff/customer-packages/credits/redeem Staff Customer Package Management
Purchase package for customer POST /staff/customer-packages Staff Customer Package Management
Customer browse packages GET /customer/packages Customer Package Management

Next Steps:

  1. Review Subscription Management for plan limits
  2. Configure Paper.id credentials in tenant settings
  3. Test appointment creation flow end-to-end
  4. Set up webhook endpoint for payment confirmation
  5. Monitor appointment statistics for business insights
  6. NEW: Set up packages for credit-based appointments - See Package Management
  7. NEW: Test credit redemption flow - See Package Credit Redemption

For webhook integration details, refer to the Webhook Integration documentation.

For package and credit management, refer to the Package Management documentation.