# Pewpros Groups Feature Guide ## Overview Groups let instructors sell team/organization access to courses. An organization buys a block of seats, gets a group, and can invite members who automatically get access to all linked courses. **Key advantage over LifterLMS:** A single group can grant access to *multiple* courses (LifterLMS limits to one). ## How It Works ### 1. Creating a Group **Admin UI:** Navigate to `/admin/groups` → New Group. Fields: - **Name** / **Description** / **Slug** (auto-generated) - **Total Seats** — max members allowed - **Visibility** — `private` (invite only), `open` (join link), or `closed` (no new members) - **Pricing Model** — `per_seat` (price × quantity) or `fixed_tier` (flat price includes N seats) - **Stripe Price ID** — links to a Stripe Price for checkout **Via API:** `POST /api/v1/groups` with Bearer token authentication. **Via Stripe Purchase:** A customer buys group access at `/courses/:slug/group-purchase`, picks seat count, names the group, and completes Stripe Checkout. The webhook automatically creates the group, makes the purchaser `primary_admin`, and links the course(s). ### 2. Linking Courses to a Group **Admin UI:** On the group show page (`/admin/groups/:id`), scroll to "Add Course" — it lists all courses not yet linked. Click "Add" to link one. **Via Stripe:** The group purchase checkout passes `course_ids` in Stripe metadata. On successful payment, those courses are auto-linked. **Note:** Only *courses* are linkable to groups. Events are not currently supported. ### 3. Roles & Permissions Four roles with hierarchical permissions: | Role | Manage Members | Manage Managers | Manage Info | Manage Seats | View Reports | Delete Group | |------|:-:|:-:|:-:|:-:|:-:|:-:| | **primary_admin** | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | **admin** | ✓ | ✓ | ✓ | ✓ | ✓ | | | **leader** | ✓ | | ✓ | | ✓ | | | **member** | | | | | | | - The group creator (or Stripe purchaser) is always `primary_admin` - `primary_admin` cannot be demoted - Admins can change roles for leaders/members - Leaders can invite/remove members but can't manage other leaders/admins ### 4. Inviting Members Two invitation types: **Email Invitations:** - Admin enters one or more emails (comma/space/newline separated) → batch invitations sent - Each invited person receives an email with an "Accept Invitation" link - The invitation is locked to that email address — only the matching account can accept - Pending email invitations count toward seat usage (reserving the seat) - Default expiry: 7 days **Open Invitations (Join Links):** - Generate a shareable URL at `/groups/join/:token` - Anyone with the link can join (no email matching) - One active open invitation per group at a time - Open invitations do NOT count toward seat usage (seats are consumed on accept) - Default expiry: 1 year - Can be disabled/re-enabled by toggling ### 5. Accepting an Invitation When a user visits `/groups/join/:token`: 1. Token is looked up → shows error if not found, expired, or already used 2. For email invitations: logged-in user's email must match the invitation email 3. Checks if user is already a member → shows "Already a Member" if so 4. Checks seat availability → shows "Group Full" if no seats left 5. If everything checks out → shows group info + "Accept & Join" button 6. On accept: creates an active seat for the user with `member` role, redirects to leader dashboard ### 6. Stripe Group Purchase Flow Route: `/courses/:slug/group-purchase` 1. User selects seat count (or picks a tier) and names the group 2. Creates a Stripe Checkout Session with metadata: - `purchase_type: "group"` - `group_name`, `group_seats`, `course_ids` - Supports both `payment` (one-time) and `subscription` modes 3. On successful payment (webhook `checkout.session.completed`): - Creates the group with specified seats - Adds purchaser as `primary_admin` - Links specified courses - Stores `stripe_subscription_id` if subscription mode ### 7. Group Leader Dashboard Route: `/my/groups` (index) and `/my/groups/:slug` (manage) Available to users with `primary_admin`, `admin`, or `leader` role. **Index page** shows: - All groups where user has leader+ role - Member count / total seats with progress bar - Linked course count, pending invitation count **Show page** has tabbed sections: - **Overview** — group details, visibility, join link management (generate/copy/disable) - **Members** — list with role badges, role change dropdowns, remove buttons - **Invitations** — send batch invitations via textarea, list pending with revoke buttons - **Courses** — linked courses with status badges ### 8. REST API Endpoints All endpoints require Bearer token authentication (`Authorization: Bearer `). | Method | Path | Description | |--------|------|-------------| | `GET` | `/api/v1/groups` | List user's groups | | `POST` | `/api/v1/groups` | Create a group | | `GET` | `/api/v1/groups/:id` | Get group details | | `PATCH` | `/api/v1/groups/:id` | Update group | | `DELETE` | `/api/v1/groups/:id` | Delete group | | `POST` | `/api/v1/groups/:token/accept-invitation` | Accept invitation by token | | `GET` | `/api/v1/groups/:group_id/members` | List members | | `GET` | `/api/v1/groups/:group_id/members/:id` | Get member details | | `PATCH` | `/api/v1/groups/:group_id/members/:id` | Change member role | | `DELETE` | `/api/v1/groups/:group_id/members/:id` | Remove member | | `GET` | `/api/v1/groups/:group_id/invitations` | List invitations | | `POST` | `/api/v1/groups/:group_id/invitations` | Create invitation(s) | | `DELETE` | `/api/v1/groups/:group_id/invitations/:id` | Revoke invitation | | `GET` | `/api/v1/groups/:group_id/seats` | Get seat info (total/used/available) | | `PUT` | `/api/v1/groups/:group_id/seats` | Update total seats | ### 9. Seat Counting Logic - **Used seats** = active seats + pending *email* invitations (open invitations don't reserve seats) - **Available seats** = total_seats - used_seats - A group is "full" when available_seats <= 0 - Changing `total_seats` can be done by users with `manage_seats` permission ### 10. Key Files | Area | Files | |------|-------| | Schema | `lib/pewpros/groups/group.ex`, `group_seat.ex`, `group_invitation.ex` | | Context | `lib/pewpros/groups.ex` | | Email | `lib/pewpros/emails/group_invitation_email.ex` | | Worker | `lib/pewpros/workers/group_invitation_worker.ex` | | Join Page | `lib/pewpros_web/live/group_live/join.ex` + `.html.heex` | | Purchase | `lib/pewpros_web/live/group_live/purchase.ex` + `.html.heex` | | Dashboard | `lib/pewpros_web/live/group_live/dashboard/{index,show}.ex` + `.html.heex` | | API | `lib/pewpros_web/controllers/api/v1/group_{controller,member_controller,invitation_controller,seat_controller}.ex` | | Admin | `lib/pewpros_web/live/admin/group_live/{show,form_component}.ex` | | Auth Plug | `lib/pewpros_web/plugs/group_authorization.ex` | | Billing | `lib/pewpros/billing.ex` (group checkout + webhook handler) |