Plugin Documentation
Overview
The ICoTA Members plugin is a custom Craft CMS 5.8+ plugin that manages memberships and integrates with Stripe for payment processing. It handles the complete membership lifecycle from purchase to expiration, with support for legacy member imports and chapter assignments.
Plugin Details
- Name: ICoTA Members
- Handle:
_icota-members - Namespace:
mearnsgill\crafticotamembers - Version: 1.0.0
- PHP Version: 8.2+
- Craft CMS: 5.8.0+
Architecture
Core Components
Services
MembershipService (src/services/MembershipService.php)
- Creates memberships from Stripe purchases
- Manages manual and legacy membership creation
- Handles membership lifecycle (expiry, cancellation, extension)
- Implements flexible expiry date calculation with October 1st rule
PurchaseService (src/services/PurchaseService.php)
- Manages Stripe purchase records
- Tracks purchase status changes
Database Records
MembershipRecord (src/records/MembershipRecord.php)
- Table:
icota_members - Stores membership data including user ID, chapter ID, dates, status, and source
- Status enum:
active,expired,cancelled - Source types:
purchase,legacy,manual, or custom
PurchaseRecord (src/records/PurchaseRecord.php)
- Table:
icota_purchases - Stores Stripe payment intent data
- Tracks customer ID, amount, currency, and status
WebhookEventRecord (src/records/WebhookEventRecord.php)
- Table:
icota_webhook_events - Logs all incoming Stripe webhook events for audit trail
Controllers
MembershipController (src/controllers/MembershipController.php)
- Web controller for membership management
CpController (src/controllers/CpController.php)
- Control panel interface
- CSV export functionality
MembershipsController (src/console/controllers/MembershipsController.php)
- Console commands for membership operations
Custom Fields
MemberPurchases (src/fields/MemberPurchases.php)
- Custom field type for displaying purchase history in user profiles
Chapter (src/fields/Chapter.php)
- Custom field for chapter assignment (multi-site selection)
Stripe Integration
The plugin handles the following Stripe webhook events:
- payment_intent.succeeded - Creates purchase record and membership
- payment_intent.payment_failed - Records failed payment
- payment_intent.canceled - Updates purchase status to canceled
- charge.dispute.created - Marks purchase as disputed
- charge.refunded - Updates purchase status to refunded
- customer.created - Automatically creates Craft user accounts
Purchase-to-Membership Flow
- Stripe webhook receives
payment_intent.succeeded - Plugin creates
PurchaseRecordwith Stripe data - Plugin extracts product metadata (duration, strict mode, chapter-id)
- Plugin links purchase to Craft user (by Stripe customer ID or billing email)
- Plugin creates
MembershipRecordwith calculated expiry date - If chapter-id exists in product metadata, assigns chapter to user
- Always assigns "ICoTA Global" chapter to all users
Membership Expiry Logic
Standard Expiry Rules (No Duration)
- Purchase before October 1st: Membership expires December 31st of current year
- Purchase on/after October 1st: Membership expires December 31st of following year
- All expiry dates set to 23:59:59 in Pacific/Auckland timezone (UTC+12/+13)
Custom Duration (Product Metadata)
Products can specify custom duration via Stripe product metadata:
Metadata Keys:
duration- Number of days (e.g., "365")strict- Boolean ("true" or "false")chapter-id- Site UID for chapter assignment
Strict Mode Disabled (default):
- Membership duration = purchase date + duration days
- If purchased on/after October 1st, extends to end of following year (whichever is later)
Strict Mode Enabled:
- Membership duration = exactly purchase date + duration days
- No October 1st extension applied
Examples
Standard (no duration)
- Purchase: March 15, 2025 → Expires: December 31, 2025
- Purchase: October 5, 2025 → Expires: December 31, 2026
Custom Duration (365 days, strict=false)
- Purchase: March 15, 2025 → Expires: March 15, 2026
- Purchase: October 5, 2025 → Expires: December 31, 2026 (extended)
Custom Duration (365 days, strict=true)
- Purchase: March 15, 2025 → Expires: March 15, 2026
- Purchase: October 5, 2025 → Expires: October 5, 2026 (no extension)
Chapter Management
Chapter Configuration
Chapters are defined in plugins/icota-members/chapters.json as UUID-to-name mappings:
{
"e3cae15b-9623-4eb8-95db-8f96f1a691a8": "USA Chapter",
"19cd36d2-fa3e-4a3f-9dd3-490fa27d5df0": "Asia Chapter",
"022de685-6eff-408b-b93b-803577ab7c43": "European Chapter",
"9e668174-c8fa-432e-beba-0a87d7f9e988": "Canadian Chapter",
"eae60906-eb70-43a5-a64a-7b1ab761ab13": "Latin America Chapter",
"aa3b0b2c-6d8e-4b99-bb90-852cbbe629f7": "Middle East & North Africa Chapter",
"bda224ee-134c-4b40-9f78-55b152f80927": "China Chapter",
"eef47251-977c-4035-bb07-9623c8b0b457": "ICoTA Global"
}TIP
ICoTA Global (UID: eef47251-977c-4035-bb07-9623c8b0b457) is automatically assigned to all users.
Chapter Assignment
Chapters can be assigned in three ways:
- Stripe Product Metadata: Set
chapter-idmetadata on Stripe product - User Import: Use
chapterfield in user import CSV - Manual Assignment: Edit user's
chapterfield in control panel
Member Import Process
For detailed instructions on importing legacy members, see Member Import Guide.
Quick Overview:
- Prepare CSV - Use
prep-import-csv.phpto convert legacy data - Import Users - Upload to Craft CP with chapter assignments
- Import Memberships - Use
import-csvconsole command - Backfill (optional) - Sync chapter data between users and memberships
Key Scripts:
plugins/icota-members/scripts/prep-import-csv.php- Prepare CSV datacraft _icota-members/memberships/import-csv- Import membershipsplugins/icota-members/scripts/backfill-user-chapters.php- Sync user chaptersplugins/icota-members/scripts/backfill-legacy-chapter.php- Sync membership chapters
Console Commands
Expire Memberships
Marks memberships as expired when their expiry date has passed.
craft _icota-members/memberships/expireWARNING
Schedule this command to run daily via cron job
Show Statistics
Displays membership statistics by status.
craft _icota-members/memberships/statsOutput:
Membership Statistics
====================
Active memberships: 245
Expired memberships: 102
Canceled memberships: 8
Expiring within 30 days: 15
Total memberships: 355Show Expiring Soon
Lists memberships expiring within specified days.
craft _icota-members/memberships/expiring-soon [days]Default: 7 days
Example:
craft _icota-members/memberships/expiring-soon 30Import Legacy Member (Single)
Imports a single legacy member.
craft _icota-members/memberships/import-legacy <email-or-id> <expiry-date> [--status=active]Examples:
craft _icota-members/memberships/import-legacy user@example.com 2026-12-31
craft _icota-members/memberships/import-legacy 123 2026-12-31 --status=expiredImport CSV (Bulk)
Imports multiple members from CSV file.
craft _icota-members/memberships/import-csv <csv-path> [--skip-errors]See the Member Import Guide for complete details.
Control Panel
Members Section
Navigate to ICoTA Members in the control panel to:
- View all memberships
- Export membership data to CSV
- View membership statistics
Export CSV
Endpoint: /_icota-members/export-csv
Exports all membership data to CSV format.
Twig Extensions
PurchaseExtension (src/twigextensions/PurchaseExtension.php)
Provides custom Twig functions for accessing purchase data in templates.
Development
Code Quality Tools
Check Code Style:
composer check-csFix Code Style:
composer fix-csRun Static Analysis:
composer phpstanDatabase Migrations
Migrations are located in src/migrations/:
Install.php- Initial plugin installationm240912_000000_create_webhook_events_table.phpm240912_000001_create_purchases_table.phpm240912_000002_create_members_table.phpm240915_000001_add_source_column_to_members.phpm240915_000002_make_customer_purchase_ids_nullable.phpm240915_000003_make_status_enum.phpm241016_000001_make_dates_nullable.phpm241016_000002_make_customer_purchase_ids_nullable_v2.phpm241201_000001_add_chapter_and_notes_to_members.php
Stripe Product Configuration
Setting Up Membership Products
- Create product in Stripe Dashboard
- Add metadata to product:
duration- Number of days (e.g., "365")strict- "true" or "false"chapter-id- Site UID fromchapters.json
- Metadata is automatically extracted when payment succeeds
- Membership expiry calculated based on metadata
Webhook Configuration
Configure Stripe webhook to send these events to your Craft site:
payment_intent.succeededpayment_intent.payment_failedpayment_intent.canceledcharge.dispute.createdcharge.refundedcustomer.created
Troubleshooting
Memberships Not Being Created
Debug Steps
- Check Stripe webhook logs in Stripe Dashboard
- Review Craft logs:
storage/logs/web.log - Verify
icota_webhook_eventstable for received events - Check
icota_purchasestable for purchase records - Confirm product metadata is set correctly
Chapter Assignment Issues
Debug Steps
- Verify chapter UUIDs in
chapters.jsonmatch site UIDs - Check user's
chapterfield in control panel - Review membership
chapterIdin database - Ensure ICoTA Global UID is correct
Import Errors
Common Issues
"User not found with email":
- Import users first before importing memberships
"User already has a legacy membership":
- Duplicate import detected - this is expected if re-running import
"Invalid chapter UID":
- Chapter UID doesn't exist as a site in Craft
- Update
chapters.jsonwith correct UIDs
Best Practices
Regular Maintenance
- Daily: Run
craft _icota-members/memberships/expireto expire old memberships - Weekly: Review
craft _icota-members/memberships/expiring-soon 30for upcoming expirations - Monthly: Run
craft _icota-members/memberships/statsto monitor membership health
Data Import
- Always test import scripts with small sample data first
- Use
--skip-errorsflag for bulk imports to continue on errors - Keep original CSV files as backup
- Review import summary for any skipped or failed rows
Stripe Integration
- Test webhook integration in Stripe's test mode before going live
- Monitor webhook logs regularly for failures
- Keep product metadata consistent across all products
- Document custom chapter-id mappings
API Reference
MembershipService Methods
| Method | Description |
|---|---|
createMembershipFromPurchase($purchase, $durationDays, $strictMode, $chapterId) | Create from Stripe purchase |
createManualMembership($user, $startDate, $expiryDate, $source, $chapterId, $notes) | Create manual membership |
createLegacyMembership($user, $expiryDate, $status, $source, $chapterId) | Create legacy membership |
updateMembership($membership, $startDate, $expiryDate, $chapterId, $notes) | Update existing membership |
getMembershipsByUser($user) | Get all memberships for user |
getActiveMembershipsByUser($user) | Get active memberships only |
userHasActiveMembership($user) | Check if user has active membership |
cancelMembership($membership) | Cancel a membership |
expireOldMemberships() | Expire all past-due memberships |
extendMembership($membership, $additionalMonths) | Extend membership expiry |
MembershipRecord Query Methods
| Method | Description |
|---|---|
findByUserId($userId) | Find by user ID |
findByCustomerId($customerId) | Find by Stripe customer ID |
findByPurchaseId($purchaseId) | Find by purchase ID |
findByStatus($status) | Find by status |
findActiveByUserId($userId) | Find active memberships for user |
findExpired() | Find expired memberships |
findExpiringSoon($days) | Find memberships expiring within days |
findByChapterId($chapterId) | Find by chapter ID |
findActiveByUserIdAndChapterId($userId, $chapterId) | Find active for user and chapter |