package storage import ( "context" "encoding/json" "errors" "fmt" "time" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgconn" "github.com/jackc/pgx/v5/pgxpool" "github.com/alchemistkay/guestguard/internal/domain" ) type RSVPRepo struct { pool *pgxpool.Pool } func NewRSVPRepo(db *DB) *RSVPRepo { return &RSVPRepo{pool: db.Pool} } type CreateRSVPParams struct { GuestID uuid.UUID Response domain.RSVPResponse PlusOnes int DietaryNotes *string DeviceFingerprint map[string]any IPAddress string RiskScore *int } func (r *RSVPRepo) Create(ctx context.Context, p CreateRSVPParams) (*domain.RSVP, error) { var fpJSON []byte if p.DeviceFingerprint != nil { b, err := json.Marshal(p.DeviceFingerprint) if err != nil { return nil, fmt.Errorf("marshal fingerprint: %w", err) } fpJSON = b } var ip *string if p.IPAddress != "" { ip = &p.IPAddress } const q = ` INSERT INTO rsvps (guest_id, response, plus_ones, dietary_notes, device_fingerprint, ip_address, risk_score) VALUES ($1, $2, $3, $4, $5, $6::inet, $7) RETURNING id, guest_id, response, plus_ones, dietary_notes, submitted_at, device_fingerprint, ip_address::text, risk_score ` row := r.pool.QueryRow(ctx, q, p.GuestID, p.Response, p.PlusOnes, p.DietaryNotes, fpJSON, ip, p.RiskScore, ) rs, err := scanRSVP(row) if err != nil { var pgErr *pgconn.PgError if errors.As(err, &pgErr) && pgErr.Code == "23505" { return nil, domain.ErrRSVPAlreadySubmitted } return nil, err } return rs, nil } // RSVPActivity is a denormalised RSVP entry for the activity feed — // includes the guest's name so the API can hand it to the frontend // without a separate lookup. type RSVPActivity struct { GuestID uuid.UUID GuestName string Response string PlusOnes int SubmittedAt time.Time } // ListRecentByEvent returns the most recent RSVPs for an event, newest first. func (r *RSVPRepo) ListRecentByEvent(ctx context.Context, eventID uuid.UUID, limit int) ([]RSVPActivity, error) { if limit <= 0 || limit > 200 { limit = 50 } const q = ` SELECT r.guest_id, g.name, r.response, r.plus_ones, r.submitted_at FROM rsvps r JOIN guests g ON g.id = r.guest_id WHERE g.event_id = $1 ORDER BY r.submitted_at DESC LIMIT $2 ` rows, err := r.pool.Query(ctx, q, eventID, limit) if err != nil { return nil, err } defer rows.Close() var out []RSVPActivity for rows.Next() { var a RSVPActivity if err := rows.Scan(&a.GuestID, &a.GuestName, &a.Response, &a.PlusOnes, &a.SubmittedAt); err != nil { return nil, err } out = append(out, a) } return out, rows.Err() } func scanRSVP(s rowScanner) (*domain.RSVP, error) { var ( rs domain.RSVP fpJSON []byte ip *string ) err := s.Scan( &rs.ID, &rs.GuestID, &rs.Response, &rs.PlusOnes, &rs.DietaryNotes, &rs.SubmittedAt, &fpJSON, &ip, &rs.RiskScore, ) if err != nil { return nil, err } if len(fpJSON) > 0 { _ = json.Unmarshal(fpJSON, &rs.DeviceFingerprint) } if ip != nil { rs.IPAddress = ip } return &rs, nil }