Secret Santa Groups
Secret Santa Groups
Section titled “Secret Santa Groups”Summary
Section titled “Summary”The Secret Santa Groups feature enables users to organize gift exchange events where participants are randomly matched to give gifts to each other anonymously.
User Story
Section titled “User Story”As a group organizer,
I want to create a gift exchange group, invite participants, and automatically assign matches,
So that everyone knows who to buy a gift for without revealing the pairings to others.
Acceptance Criteria
Section titled “Acceptance Criteria”- ✅ User can create a group with name, budget, date, and rules
- ✅ User can upload a group image
- ✅ User can invite participants by phone number or shareable link
- ✅ User can generate a Secret Santa raffle (random matching)
- ✅ Group admin can view all members and pending invitations
- ✅ Non-admin members can only see their own match
- ✅ User can view their match’s wishlist
- ✅ User can leave a group they didn’t create
- ✅ Admin can delete members or the entire group
Inputs and Outputs
Section titled “Inputs and Outputs”Inputs:
- Group name (string, required)
- Budget amount (number, required)
- Event date (date, required)
- Rules/description (string, optional)
- Group image (file, optional)
- Participant phone numbers or emails
Outputs:
- Created group with unique ID
- Shareable invitation link
- Raffle results (matches assigned to each member)
- Match’s wishlist and profile information
1. Create Group
Section titled “1. Create Group”User clicks "Create Group" │ ▼Fill form: - Name: "Office Secret Santa" - Budget: $50 - Date: Dec 25, 2025 - Rules: "Max $50, no gag gifts" │ ▼Upload group image (optional) │ ▼Submit form │ ▼POST /groups │ ├─ Success ────────────────────┐ │ │ │ ▼ │ Store group in Redux │ │ │ ▼ │ Navigate to /group/:id │ │ │ ▼ │ Show "Invite Participants" prompt │ └─ Error ──────────────────────┐ │ ▼ Show error toast │ ▼ User retries2. Invite Participants
Section titled “2. Invite Participants”Admin clicks "Invite Participants" │ ├─ Option 1: Manual entry ─────┐ │ │ │ ▼ │ Enter phone numbers │ (one per line or comma-separated) │ │ │ ▼ │ POST /groups/:id/invite │ │ │ ▼ │ Backend sends SMS/email invites │ │ │ ▼ │ Show "X invitations sent" toast │ └─ Option 2: Shareable link ───┐ │ ▼ Generate link: /invite/:sharedId │ ▼ Copy link to clipboard │ ▼ Share via WhatsApp, email, etc.3. Accept Invitation
Section titled “3. Accept Invitation”Invitee receives SMS/email or clicks link │ ▼Opens /invite/:sharedId │ ├─ User logged in ─────────────┐ │ │ │ ▼ │ Show group preview │ (name, image, admin, date) │ │ │ ▼ │ Click "Join Group" │ │ │ ▼ │ POST /groups/shared/:sharedId/join │ │ │ ▼ │ Add user to group members │ │ │ ▼ │ Navigate to /group/:id │ └─ User not logged in ─────────┐ │ ▼ Redirect to /auth/login │ ▼ After login, return to /invite/:sharedId4. Generate Raffle
Section titled “4. Generate Raffle”Admin clicks "Generate Raffle" │ ▼Show confirmation modal: "Are you sure? This cannot be undone." │ ▼User confirms │ ▼POST /groups/:id/raffle │ ├─ Backend algorithm ──────────┐ │ │ │ ▼ │ Shuffle members randomly │ │ │ ▼ │ Assign each member a unique match │ (no self-matches, no duplicates) │ │ │ ▼ │ Store matches in database │ │ │ ▼ │ Send notifications to all members │ ├─ Success ────────────────────┐ │ │ │ ▼ │ Update group.isSorted = 1 │ │ │ ▼ │ Refresh Redux state │ │ │ ▼ │ Show "Raffle complete!" toast │ │ │ ▼ │ Reveal "View Match" button │ └─ Error (e.g., < 2 members) ─┐ │ ▼ Show error toast "Need at least 2 members"5. View Match
Section titled “5. View Match”User clicks "View Match" │ ▼Show match profile: - Name (e.g., "Your match: John Doe") - Profile photo - Wishlist link │ ▼User clicks "View Wishlist" │ ▼Navigate to /group/:id/wishlists/:wishlistId │ ▼Display match's wishlist items │ ▼User browses items and purchase linksEdge Cases
Section titled “Edge Cases”- Admin leaves group: Group ownership transfers to oldest member
- Last member leaves: Group is automatically deleted
- Raffle with 1 member: Show error “Need at least 2 members”
- User tries to view match before raffle: Show “Raffle not generated yet”
- User declines invitation: Invitation removed from pending list
- Duplicate phone number invitation: Backend prevents duplicate; shows error
API/Contracts
Section titled “API/Contracts”Create Group
Section titled “Create Group”Request:
POST /groupsAuthorization: Bearer {token}Content-Type: application/json
{ "name": "Office Secret Santa", "budget": 50, "date": "2025-12-25", "rules": "Max $50, no gag gifts"}Response:
{ "success": true, "group": { "id": 123, "name": "Office Secret Santa", "budget": 50, "date": "2025-12-25", "rules": "Max $50, no gag gifts", "adminId": 1, "admin": { "id": 1, "name": "Jane Doe", "profilePhoto": "https://cdn.rnb.la/..." }, "image": null, "imageBlurhash": null, "isSorted": 0, "match": null, "members": [], "invitations": [], "sharedId": "abc123xyz", "createdAt": "2025-10-29T12:00:00Z" }}Invite Participants
Section titled “Invite Participants”Request:
POST /groups/123/inviteAuthorization: Bearer {token}Content-Type: application/json
[ { "name": "John Smith", "phone": "+1234567890" }, { "name": "Alice Brown", "phone": "+0987654321" }]Response:
{ "success": true, "invitations": [ { "id": 1, "userName": "John Smith", "userPhone": "+1234567890", "userProfilePhoto": null }, { "id": 2, "userName": "Alice Brown", "userPhone": "+0987654321", "userProfilePhoto": null } ]}Generate Raffle
Section titled “Generate Raffle”Request:
POST /groups/123/raffleAuthorization: Bearer {token}Response:
{ "success": true, "message": "Raffle generated successfully"}After raffle, GET /groups/123 returns group with isSorted: 1 and match populated.
Get Group (With Match)
Section titled “Get Group (With Match)”Response after raffle:
{ "success": true, "group": { "id": 123, "isSorted": 1, "match": { "id": 456, "userId": 5, "userName": "John Smith", "userProfilePhoto": "https://cdn.rnb.la/..." }, ... }}Error Handling and Retries
Section titled “Error Handling and Retries”Client-side Errors
Section titled “Client-side Errors”- Validation: Form validates before submission (required fields, date in future)
- Network timeout: Show toast “Request timeout. Please try again.”
- Duplicate group name: Backend allows; no uniqueness constraint
Server-side Errors
Section titled “Server-side Errors”- 400 Bad Request: “Invalid data. Check your inputs.”
- 403 Forbidden: “You don’t have permission to perform this action.”
- 404 Not Found: “Group not found.”
- 409 Conflict: “Raffle already generated.”
- 500 Server Error: “Something went wrong. Please try again later.”
Retry Strategy
Section titled “Retry Strategy”- Create group: Manual retry (user clicks submit again)
- Generate raffle: One-time action; no automatic retry
- Fetch group: Automatic retry with exponential backoff (axios-retry)
Telemetry
Section titled “Telemetry”Metrics
Section titled “Metrics”group.created: Incremented when group is createdgroup.raffle_generated: Incremented when raffle is performedgroup.member_joined: Incremented when user joins groupgroup.member_left: Incremented when user leaves group
console.log('Group created:', groupId);console.log('Raffle generated for group:', groupId);Sentry.addBreadcrumb({ category: 'group', message: 'User viewed match', data: { groupId, matchUserId },});Traces
Section titled “Traces”Sentry tracks transaction duration for:
POST /groups(group creation)POST /groups/:id/raffle(raffle generation)GET /groups/:id(fetch group with members)
Rollout
Section titled “Rollout”Feature Flags
Section titled “Feature Flags”Assumption: No feature flags currently implemented. Consider adding for:
- Beta testing raffle algorithm improvements
- A/B testing invitation flows (SMS vs. link)
Migrations
Section titled “Migrations”- Database: Groups table must have
sharedIdcolumn (added in migration v2.3) - Data: Existing groups without
sharedIdget generated on first access
Example
Section titled “Example”'use client';
import { useForm } from 'react-hook-form';import { yupResolver } from '@hookform/resolvers/yup';import * as yup from 'yup';import { groupsHandlers } from 'src/services/handlers/groups/groupsHandlers';import { useRouter } from 'next/navigation';import { paths } from 'src/routes/paths';import { toast } from 'react-toastify';
const schema = yup.object({ name: yup.string().required('Name is required'), budget: yup.number().positive().required('Budget is required'), date: yup.date().min(new Date(), 'Date must be in the future').required(), rules: yup.string(),});
export default function CreateGroupView() { const router = useRouter(); const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm({ resolver: yupResolver(schema), });
const onSubmit = async (data) => { try { const result = await groupsHandlers.createNewGroup(data); toast.success('Group created successfully!'); router.push(paths.home.group.main(result.group.id)); } catch (error) { toast.error('Failed to create group'); console.error(error); } };
return ( <form onSubmit={handleSubmit(onSubmit)}> <input {...register('name')} placeholder="Group name" /> {errors.name && <span>{errors.name.message}</span>}
<input {...register('budget')} type="number" placeholder="Budget" /> {errors.budget && <span>{errors.budget.message}</span>}
<input {...register('date')} type="date" /> {errors.date && <span>{errors.date.message}</span>}
<textarea {...register('rules')} placeholder="Rules (optional)" />
<button type="submit" disabled={isSubmitting}> {isSubmitting ? 'Creating...' : 'Create Group'} </button> </form> );}Further Reading
Section titled “Further Reading”- Group Invitations - Detailed invitation flow
- Wishlist Management - How matches view wishlists
- State Management - Groups Redux slice
- API Layer - Groups API handlers
- Real-time Chat - Messaging with matches