package storage import ( "context" "errors" "time" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "github.com/alchemistkay/guestguard/internal/domain" ) type GuestRepo struct { pool *pgxpool.Pool } func NewGuestRepo(db *DB) *GuestRepo { return &GuestRepo{pool: db.Pool} } type CreateGuestParams struct { EventID uuid.UUID Name string Email *string Phone *string PlusOnes int DietaryNotes *string TableNumber *int } func (r *GuestRepo) Create(ctx context.Context, p CreateGuestParams) (*domain.Guest, error) { const q = ` INSERT INTO guests (event_id, name, email, phone, plus_ones, dietary_notes, table_number) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id, event_id, name, email, phone, plus_ones, dietary_notes, table_number, created_at ` row := r.pool.QueryRow(ctx, q, p.EventID, p.Name, p.Email, p.Phone, p.PlusOnes, p.DietaryNotes, p.TableNumber, ) return scanGuest(row) } func (r *GuestRepo) Get(ctx context.Context, id uuid.UUID) (*domain.Guest, error) { const q = ` SELECT id, event_id, name, email, phone, plus_ones, dietary_notes, table_number, created_at FROM guests WHERE id = $1 ` g, err := scanGuest(r.pool.QueryRow(ctx, q, id)) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return nil, domain.ErrGuestNotFound } return nil, err } return g, nil } func (r *GuestRepo) ListByEvent(ctx context.Context, eventID uuid.UUID, limit, offset int) ([]*domain.Guest, error) { if limit <= 0 || limit > 500 { limit = 100 } if offset < 0 { offset = 0 } const q = ` SELECT id, event_id, name, email, phone, plus_ones, dietary_notes, table_number, created_at FROM guests WHERE event_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3 ` rows, err := r.pool.Query(ctx, q, eventID, limit, offset) if err != nil { return nil, err } defer rows.Close() var out []*domain.Guest for rows.Next() { g, err := scanGuest(rows) if err != nil { return nil, err } out = append(out, g) } return out, rows.Err() } func scanGuest(s rowScanner) (*domain.Guest, error) { var g domain.Guest err := s.Scan( &g.ID, &g.EventID, &g.Name, &g.Email, &g.Phone, &g.PlusOnes, &g.DietaryNotes, &g.TableNumber, &g.CreatedAt, ) if err != nil { return nil, err } return &g, nil } // GuestWithRSVP is the dashboard view: a guest plus the RSVP submitted // against their token, if any. RSVP fields are nil when no response yet. type GuestWithRSVP struct { *domain.Guest RSVPResponse *string `json:"rsvp_response,omitempty"` RSVPPlusOnes *int `json:"rsvp_plus_ones,omitempty"` RSVPRiskScore *int `json:"rsvp_risk_score,omitempty"` RSVPSubmittedAt *time.Time `json:"rsvp_submitted_at,omitempty"` HasToken bool `json:"has_token"` } func (r *GuestRepo) ListByEventWithRSVP(ctx context.Context, eventID uuid.UUID, limit, offset int) ([]*GuestWithRSVP, error) { if limit <= 0 || limit > 500 { limit = 100 } if offset < 0 { offset = 0 } const q = ` SELECT g.id, g.event_id, g.name, g.email, g.phone, g.plus_ones, g.dietary_notes, g.table_number, g.created_at, r.response, r.plus_ones, r.risk_score, r.submitted_at, t.id IS NOT NULL AS has_token FROM guests g LEFT JOIN rsvps r ON r.guest_id = g.id LEFT JOIN tokens t ON t.guest_id = g.id WHERE g.event_id = $1 ORDER BY g.created_at DESC LIMIT $2 OFFSET $3 ` rows, err := r.pool.Query(ctx, q, eventID, limit, offset) if err != nil { return nil, err } defer rows.Close() var out []*GuestWithRSVP for rows.Next() { var ( g domain.Guest response *string rsvpPlusOnes *int riskScore *int submittedAt *time.Time hasToken bool ) if err := rows.Scan( &g.ID, &g.EventID, &g.Name, &g.Email, &g.Phone, &g.PlusOnes, &g.DietaryNotes, &g.TableNumber, &g.CreatedAt, &response, &rsvpPlusOnes, &riskScore, &submittedAt, &hasToken, ); err != nil { return nil, err } out = append(out, &GuestWithRSVP{ Guest: &g, RSVPResponse: response, RSVPPlusOnes: rsvpPlusOnes, RSVPRiskScore: riskScore, RSVPSubmittedAt: submittedAt, HasToken: hasToken, }) } return out, rows.Err() }