package db import ( "context" "time" ) func (r *PGRepository) ListBookingsByTenantBetween(ctx context.Context, tenantID string, from time.Time, to time.Time) ([]BookingRecord, error) { rows, err := r.pool.Query(ctx, ` SELECT id, tenant_id, service_id, class_session_id, staff_id, location_id, customer_name, customer_email, starts_at, ends_at, status, reference FROM bookings WHERE tenant_id = $1 AND starts_at < $3 AND ends_at > $2 ORDER BY starts_at ASC `, tenantID, from, to) if err != nil { return nil, err } defer rows.Close() var records []BookingRecord for rows.Next() { var record BookingRecord if err := rows.Scan( &record.ID, &record.TenantID, &record.ServiceID, &record.ClassSessionID, &record.StaffID, &record.LocationID, &record.CustomerName, &record.CustomerEmail, &record.StartsAt, &record.EndsAt, &record.Status, &record.Reference, ); err != nil { return nil, err } records = append(records, record) } return records, rows.Err() } func (r *PGRepository) CreateBooking(ctx context.Context, params CreateBookingParams) (CreatedBooking, error) { var created CreatedBooking err := r.pool.QueryRow(ctx, ` INSERT INTO bookings ( tenant_id, service_id, class_session_id, staff_id, location_id, booking_mode, customer_name, customer_email, starts_at, ends_at, status, reference, notes ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13) RETURNING id, reference, status `, params.TenantID, params.ServiceID, params.ClassSessionID, params.StaffID, params.LocationID, params.BookingMode, params.CustomerName, params.CustomerEmail, params.StartsAt, params.EndsAt, params.Status, params.Reference, params.Notes, ).Scan(&created.ID, &created.Reference, &created.Status) return created, err } func (r *PGRepository) AppendWaitlistEntry(ctx context.Context, params WaitlistEntryParams) error { _, err := r.pool.Exec(ctx, ` INSERT INTO waitlist_entries (tenant_id, class_session_id, customer_name, customer_email, position) VALUES ($1,$2,$3,$4,$5) `, params.TenantID, params.ClassSessionID, params.CustomerName, params.CustomerEmail, params.Position) return err } func (r *PGRepository) CreateReminderJob(ctx context.Context, params ReminderJobParams) error { _, err := r.pool.Exec(ctx, ` INSERT INTO reminder_jobs (tenant_id, booking_id, channel, scheduled_for) VALUES ($1, $2, $3, $4) `, params.TenantID, params.BookingID, params.Channel, params.ScheduledFor) return err } func (r *PGRepository) ListDueReminderJobs(ctx context.Context, dueBefore time.Time, limit int) ([]ReminderJobRecord, error) { rows, err := r.pool.Query(ctx, ` SELECT rj.id, rj.tenant_id, t.name, t.locale, t.timezone, rj.booking_id, rj.channel, rj.scheduled_for, b.customer_name, b.customer_email, b.reference, b.starts_at, rj.status FROM reminder_jobs rj INNER JOIN bookings b ON b.id = rj.booking_id INNER JOIN tenants t ON t.id = rj.tenant_id WHERE rj.status = 'pending' AND rj.scheduled_for <= $1 ORDER BY rj.scheduled_for ASC LIMIT $2 `, dueBefore, limit) if err != nil { return nil, err } defer rows.Close() var records []ReminderJobRecord for rows.Next() { var record ReminderJobRecord if err := rows.Scan( &record.ID, &record.TenantID, &record.TenantName, &record.Locale, &record.Timezone, &record.BookingID, &record.Channel, &record.ScheduledFor, &record.CustomerName, &record.CustomerEmail, &record.Reference, &record.StartsAt, &record.Status, ); err != nil { return nil, err } records = append(records, record) } return records, rows.Err() } func (r *PGRepository) MarkReminderJobDispatched(ctx context.Context, reminderJobID string, status string, dispatchedAt time.Time) error { _, err := r.pool.Exec(ctx, ` UPDATE reminder_jobs SET status = $2, dispatched_at = $3 WHERE id = $1 `, reminderJobID, status, dispatchedAt) return err } func (r *PGRepository) CreateNotificationDeliveryLog(ctx context.Context, params NotificationDeliveryLogParams) error { _, err := r.pool.Exec(ctx, ` INSERT INTO notification_delivery_logs ( tenant_id, reminder_job_id, channel, provider, recipient, delivery_status, external_id, error_message ) VALUES ($1, $2, $3, $4, $5, $6, $7, NULLIF($8, '')) `, params.TenantID, params.ReminderJobID, params.Channel, params.Provider, params.Recipient, params.Status, params.ExternalID, params.ErrorMessage) return err } func (r *PGRepository) GetDashboardMetrics(ctx context.Context, tenantID string, startsAt time.Time, endsAt time.Time) (DashboardMetrics, error) { var metrics DashboardMetrics err := r.pool.QueryRow(ctx, ` SELECT COUNT(*) FILTER (WHERE starts_at >= $2 AND starts_at < $3) AS bookings_count, COUNT(*) FILTER (WHERE starts_at >= $2 AND starts_at < $3 AND status = 'cancelled') AS cancellations_count, COALESCE( ROUND( 100.0 * COUNT(*) FILTER (WHERE starts_at >= $2 AND starts_at < $3 AND status = 'confirmed') / NULLIF(COUNT(*) FILTER (WHERE starts_at >= $2 AND starts_at < $3), 0) ), 0 )::integer AS utilization_percent FROM bookings WHERE tenant_id = $1 `, tenantID, startsAt, endsAt).Scan( &metrics.BookingsCount, &metrics.CancellationsCount, &metrics.UtilizationPercent, ) return metrics, err }