Skip to content

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:

  1. payment_intent.succeeded - Creates purchase record and membership
  2. payment_intent.payment_failed - Records failed payment
  3. payment_intent.canceled - Updates purchase status to canceled
  4. charge.dispute.created - Marks purchase as disputed
  5. charge.refunded - Updates purchase status to refunded
  6. customer.created - Automatically creates Craft user accounts

Purchase-to-Membership Flow

  1. Stripe webhook receives payment_intent.succeeded
  2. Plugin creates PurchaseRecord with Stripe data
  3. Plugin extracts product metadata (duration, strict mode, chapter-id)
  4. Plugin links purchase to Craft user (by Stripe customer ID or billing email)
  5. Plugin creates MembershipRecord with calculated expiry date
  6. If chapter-id exists in product metadata, assigns chapter to user
  7. 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:

json
{
  "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:

  1. Stripe Product Metadata: Set chapter-id metadata on Stripe product
  2. User Import: Use chapter field in user import CSV
  3. Manual Assignment: Edit user's chapter field in control panel

Member Import Process

For detailed instructions on importing legacy members, see Member Import Guide.

Quick Overview:

  1. Prepare CSV - Use prep-import-csv.php to convert legacy data
  2. Import Users - Upload to Craft CP with chapter assignments
  3. Import Memberships - Use import-csv console command
  4. Backfill (optional) - Sync chapter data between users and memberships

Key Scripts:

  • plugins/icota-members/scripts/prep-import-csv.php - Prepare CSV data
  • craft _icota-members/memberships/import-csv - Import memberships
  • plugins/icota-members/scripts/backfill-user-chapters.php - Sync user chapters
  • plugins/icota-members/scripts/backfill-legacy-chapter.php - Sync membership chapters

Console Commands

Expire Memberships

Marks memberships as expired when their expiry date has passed.

bash
craft _icota-members/memberships/expire

WARNING

Schedule this command to run daily via cron job

Show Statistics

Displays membership statistics by status.

bash
craft _icota-members/memberships/stats

Output:

Membership Statistics
====================
Active memberships:     245
Expired memberships:    102
Canceled memberships:   8
Expiring within 30 days: 15
Total memberships:      355

Show Expiring Soon

Lists memberships expiring within specified days.

bash
craft _icota-members/memberships/expiring-soon [days]

Default: 7 days

Example:

bash
craft _icota-members/memberships/expiring-soon 30

Import Legacy Member (Single)

Imports a single legacy member.

bash
craft _icota-members/memberships/import-legacy <email-or-id> <expiry-date> [--status=active]

Examples:

bash
craft _icota-members/memberships/import-legacy user@example.com 2026-12-31
craft _icota-members/memberships/import-legacy 123 2026-12-31 --status=expired

Import CSV (Bulk)

Imports multiple members from CSV file.

bash
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:

bash
composer check-cs

Fix Code Style:

bash
composer fix-cs

Run Static Analysis:

bash
composer phpstan

Database Migrations

Migrations are located in src/migrations/:

  • Install.php - Initial plugin installation
  • m240912_000000_create_webhook_events_table.php
  • m240912_000001_create_purchases_table.php
  • m240912_000002_create_members_table.php
  • m240915_000001_add_source_column_to_members.php
  • m240915_000002_make_customer_purchase_ids_nullable.php
  • m240915_000003_make_status_enum.php
  • m241016_000001_make_dates_nullable.php
  • m241016_000002_make_customer_purchase_ids_nullable_v2.php
  • m241201_000001_add_chapter_and_notes_to_members.php

Stripe Product Configuration

Setting Up Membership Products

  1. Create product in Stripe Dashboard
  2. Add metadata to product:
    • duration - Number of days (e.g., "365")
    • strict - "true" or "false"
    • chapter-id - Site UID from chapters.json
  3. Metadata is automatically extracted when payment succeeds
  4. Membership expiry calculated based on metadata

Webhook Configuration

Configure Stripe webhook to send these events to your Craft site:

  • payment_intent.succeeded
  • payment_intent.payment_failed
  • payment_intent.canceled
  • charge.dispute.created
  • charge.refunded
  • customer.created

Troubleshooting

Memberships Not Being Created

Debug Steps
  1. Check Stripe webhook logs in Stripe Dashboard
  2. Review Craft logs: storage/logs/web.log
  3. Verify icota_webhook_events table for received events
  4. Check icota_purchases table for purchase records
  5. Confirm product metadata is set correctly

Chapter Assignment Issues

Debug Steps
  1. Verify chapter UUIDs in chapters.json match site UIDs
  2. Check user's chapter field in control panel
  3. Review membership chapterId in database
  4. 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.json with correct UIDs

Best Practices

Regular Maintenance

  1. Daily: Run craft _icota-members/memberships/expire to expire old memberships
  2. Weekly: Review craft _icota-members/memberships/expiring-soon 30 for upcoming expirations
  3. Monthly: Run craft _icota-members/memberships/stats to monitor membership health

Data Import

  1. Always test import scripts with small sample data first
  2. Use --skip-errors flag for bulk imports to continue on errors
  3. Keep original CSV files as backup
  4. Review import summary for any skipped or failed rows

Stripe Integration

  1. Test webhook integration in Stripe's test mode before going live
  2. Monitor webhook logs regularly for failures
  3. Keep product metadata consistent across all products
  4. Document custom chapter-id mappings

API Reference

MembershipService Methods

MethodDescription
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

MethodDescription
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

ICoTA Members Plugin Documentation