Project: Admin-Backend (Event Ticketing System)
Module: SettlementModule
Feature: Organizer Settlement Management
Implementation Date: December 26, 2025
Last Updated: December 27, 2025
The Settlement feature enables Global Admins to create and manage settlements for organizers based on ticket sales. It aggregates purchase data from the purchase_tickets table (populated by customer-backend during ticket purchases) and creates settlement records without recalculating any fees.
All fee data (payout_amount, platform_fee, payment_fee, tax_amount) is stored immutably at purchase time by the customer-backend. The settlement feature only aggregates these stored values.
Following Mojura Framework architecture pattern (same as Category module):
app/
├── Models/
│ ├── Settlement.php
│ └── SettlementPurchaseTicket.php
├── Modules/
│ └── SettlementModule/
│ ├── Features/
│ │ ├── CreateSettlementFeature.php
│ │ ├── IndexSettlementFeature.php
│ │ └── ReadSettlementFeature.php
│ ├── Jobs/
│ │ ├── CreateSettlementJob.php
│ │ ├── IndexSettlementJob.php
│ │ └── ReadSettlementJob.php
│ └── Http/
│ ├── Controllers/
│ │ └── SettlementController.php
│ └── Requests/
│ └── CreateSettlementRequest.php
└── Enums/
└── SettlementStatusEnum.php
routes/
└── api/
└── v1/
└── settlements.php
settlements table:
id- Snowflake IDevent_id- Foreign key to eventsevent_title- Denormalized event nameorganizer_id- Foreign key to organizersorganizer_name- Denormalized organizer nametotal_gross_amount- Sum of all ticket prices (what customers paid)total_platform_fee- Sum of platform commissionstotal_payment_fee- Sum of payment gateway feestotal_tax_amount- Sum of taxes collectedtotal_net_amount- Same as total_gross_amounttotal_payout_amount- Sum of what organizer receivescurrency- ISO currency code (MMK)settlement_status- Enum:pending,processing,settled,failedsettlement_version- Increments on reversal + re-settlementsettled_at- Timestamp when settlement completedcreated_at,updated_at,created_by,updated_by
settlement_purchase_tickets table:
id- Snowflake IDsettlement_id- Foreign key to settlementspurchase_tickets_id- Foreign key to purchase_ticketsgross_amount- Ticket price (from purchase_tickets.price)platform_fee- From purchase_tickets.platform_feepayment_fee- From purchase_tickets.payment_feetax_amount- From purchase_tickets.tax_amountnet_amount- From purchase_tickets.net_amountpayout_amount- From purchase_tickets.payout_amountcurrency- From purchase_tickets.currencycreated_at,created_by
Location: app/Models/Settlement.php
Inherits from BaseModel:
OrganizerScopeTrait- Automatic role-based filteringBasicAudit- created_by, updated_by trackingSnowflakeID- Distributed ID generation
Relationships:
belongsTo(Event::class)belongsTo(Organizer::class)hasMany(SettlementPurchaseTicket::class)belongsTo(User::class, 'created_by')belongsTo(User::class, 'updated_by')
Query Scopes:
scopeForEvent($eventId)scopeForOrganizer($organizerId)scopeByStatus($status)
Location: app/Models/SettlementPurchaseTicket.php
Relationships:
belongsTo(Settlement::class)belongsTo(PurchaseTicket::class, 'purchase_tickets_id')belongsTo(User::class, 'created_by')
Location: app/Modules/SettlementModule/Features/CreateSettlementFeature.php
Purpose: Handles settlement creation request
Flow:
- Receives
CreateSettlementRequestwithevent_id - Delegates to
CreateSettlementJob - Returns success/error response
Location: app/Modules/SettlementModule/Jobs/CreateSettlementJob.php
Business Logic:
- Fetch Event: Load event details using
Event::findOrFail($eventId) - Aggregate Purchase Data:
SELECT COUNT(*) as tickets_sold, SUM(price) as total_gross_amount, SUM(platform_fee) as total_platform_fee, SUM(payment_fee) as total_payment_fee, SUM(tax_amount) as total_tax_amount, SUM(net_amount) as total_net_amount, SUM(payout_amount) as total_payout_amount, currency FROM purchase_tickets WHERE event_id = ?
- Validate: Ensure tickets exist (fail if 0 tickets)
- Create Settlement Record: Status =
PENDING - Link Tickets: Create
settlement_purchase_ticketsrecords for each ticket - Update Event: Set event's
settlement_statustoPENDING - Transaction Safe: All wrapped in DB transaction with rollback on error
Location: app/Modules/SettlementModule/Features/IndexSettlementFeature.php
Purpose: List settlements with filtering and pagination
Query Parameters:
current_page- Page numberper_page- Items per pagesearch- Search term (searches: id, event_title, organizer_name, settlement_status)order- Sort order (default: created_at DESC)event_id- Filter by specific eventstatus- Filter by settlement status
Location: app/Modules/SettlementModule/Jobs/IndexSettlementJob.php
Key Implementation:
$query = Settlement::query()
->with(['event:id,title', 'organizer:id,name'])
->filterByRoleScopeOneOrganizer(auth()->user()); // Automatic role-based filteringFiltering Logic:
- Global Admin: Sees all settlements (no filter applied)
- Organizer Admin: Sees only their organizer's settlements (filtered by
organizer_id)
Location: app/Modules/SettlementModule/Features/ReadSettlementFeature.php
Purpose: Retrieve single settlement with full details
Eager Loads:
- Event details (id, title, start_date, end_date)
- Organizer details (id, name)
- Settlement purchase tickets (all linked tickets)
- Created by user (id, name)
- Updated by user (id, name)
All routes require:
auth:apimiddleware (JWT authentication)user.checkmiddleware (user validation)
Endpoint: GET /api/v1/settlements
Authorization: can:index,Settlement
Query Parameters:
current_page: integer (optional)
per_page: integer (optional)
search: string (optional)
order: array (optional) - e.g., [{"column": "created_at", "order": "desc"}]
event_id: string (optional) - Filter by event
status: string (optional) - Filter by status (pending, processing, settled, failed)
Response:
{
"success": true,
"message": "Settlements have been successfully retrieved",
"data": [
{
"id": "518017911886059740",
"event_id": "518017911886059741",
"event_title": "Summer Music Festival",
"organizer_id": "518017911886059742",
"organizer_name": "ABC Events",
"total_gross_amount": "2837850.00",
"total_platform_fee": "141892.50",
"total_payment_fee": "70946.25",
"total_tax_amount": "141892.50",
"total_net_amount": "2837850.00",
"total_payout_amount": "2483118.75",
"currency": "MMK",
"settlement_status": "pending",
"settlement_version": 1,
"settled_at": null,
"created_at": "2025-12-26T12:00:00Z"
}
],
"current_page": 1,
"per_page": 20,
"total": 15
}Endpoint: POST /api/v1/settlements
Authorization: can:create,Settlement (Global Admin only)
Request Body:
{
"event_id": "518017911886059741"
}Validation Rules:
event_id: required, must exist in events table
Response:
{
"success": true,
"message": "Settlement has been successfully created",
"data": {
"id": "518017911886059740",
"event_id": "518017911886059741",
"event_title": "Summer Music Festival",
"organizer_id": "518017911886059742",
"organizer_name": "ABC Events",
"total_gross_amount": "2837850.00",
"total_payout_amount": "2483118.75",
"currency": "MMK",
"settlement_status": "pending",
"settlement_version": 1,
"created_at": "2025-12-26T12:00:00Z",
"event": { ... },
"organizer": { ... },
"settlement_purchase_tickets": [ ... ]
}
}Endpoint: GET /api/v1/settlements/{id}
Authorization: can:read,Settlement
Response:
{
"success": true,
"message": "Settlement has been successfully retrieved",
"data": {
"id": "518017911886059740",
"event_id": "518017911886059741",
"event": {
"id": "518017911886059741",
"title": "Summer Music Festival",
"start_date": "2025-08-15",
"end_date": "2025-08-16"
},
"organizer": {
"id": "518017911886059742",
"name": "ABC Events"
},
"total_payout_amount": "2483118.75",
"settlement_status": "pending",
"settlement_purchase_tickets": [
{
"id": "...",
"purchase_tickets_id": "...",
"gross_amount": "56757.00",
"payout_amount": "50000.00",
"platform_fee": "2500.00",
"payment_fee": "1419.00",
"tax_amount": "2838.00",
"currency": "MMK"
}
],
"created_by": {
"id": "...",
"name": "Admin User"
}
}
}Global Admin (scope: global):
- Can create settlements:
POST /api/v1/master/settlements - Can view all settlements:
GET /api/v1/master/settlements - Can view any settlement detail:
GET /api/v1/master/settlements/{id}
Organizer Admin (scope: organizer):
- Cannot create settlements (Global Admin only)
- Can view only their organizer's settlements (automatic filtering)
- Can view only their organizer's settlement details
Must be seeded in role_ability table:
Settlement:
- index
- create
- read
Assign to Global Admin role only for create/index/read.
Note: Organizer Admin gets read-only access automatically through OrganizerScopeTrait filtering.
Critical: Settlement never recalculates fees. All values come from purchase_tickets table.
// ✅ CORRECT: Aggregate stored values
$aggregates = PurchaseTicket::where('event_id', $eventId)
->selectRaw('SUM(payout_amount) as total_payout_amount')
->first();
// ❌ WRONG: Never do this
$payout = FeeCalculatorService::calculatePayout(...);Inherited from BaseModel, automatically applies role-based filtering:
// In IndexSettlementJob
$query = Settlement::query()
->filterByRoleScopeOneOrganizer(auth()->user());How it works:
- Checks authenticated user's role scope
- If scope = 'organizer': Filters
WHERE organizer_id IN (user's organizer IDs) - If scope = 'global': No filter applied
All database operations in CreateSettlementJob wrapped in transaction:
DB::beginTransaction();
try {
// Create settlement
// Link tickets
// Update event
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}Event Created → Tickets Sold (customer-backend)
↓
Admin Creates Settlement
↓
Status: PENDING
↓
[Future: Admin marks as PROCESSING]
↓
[Future: Payment transferred to organizer]
↓
[Future: Status: SETTLED, settled_at timestamp set]
Current Implementation: Only handles creation with PENDING status.
Future Enhancement: Add complete/process endpoints to update status to SETTLED.
┌─────────────────────────────────────────────────────┐
│ Customer-Backend (Ticket Purchase) │
├─────────────────────────────────────────────────────┤
│ 1. Customer buys ticket │
│ 2. FeeCalculatorService calculates fees │
│ 3. Store in purchase_tickets table: │
│ - payout_amount (organizer's share) │
│ - platform_fee │
│ - payment_fee │
│ - tax_amount │
│ - price (total customer paid) │
└─────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────┐
│ Admin-Backend (Settlement Feature) │
├─────────────────────────────────────────────────────┤
│ 1. Global Admin triggers settlement creation │
│ 2. CreateSettlementJob: │
│ - Aggregate SUM(payout_amount) from tickets │
│ - Create settlement record │
│ - Link tickets via settlement_purchase_tickets │
│ 3. Settlement record created with status: PENDING │
└─────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────┐
│ Future: Payment Transfer │
├─────────────────────────────────────────────────────┤
│ 1. Admin marks settlement as PROCESSING │
│ 2. Transfer total_payout_amount to organizer │
│ 3. Update status to SETTLED │
│ 4. Set settled_at timestamp │
└─────────────────────────────────────────────────────┘
-
Run migrations:
php artisan migrate
-
Seed abilities for Settlement:
INSERT INTO abilities (action, subject) VALUES ('index', 'Settlement'), ('create', 'Settlement'), ('read', 'Settlement');
-
Assign to Global Admin role:
INSERT INTO role_ability (role_id, ability_id) VALUES ({global_admin_role_id}, {ability_id});
Request:
POST /api/v1/settlements
Authorization: Bearer {global_admin_jwt}
Content-Type: application/json
{
"event_id": "518017911886059741"
}Expected: 201 Created, settlement record with aggregated totals
Request:
GET /api/v1/settlements?per_page=10¤t_page=1
Authorization: Bearer {global_admin_jwt}Expected: 200 OK, all settlements across all organizers
Request:
GET /api/v1/settlements
Authorization: Bearer {organizer_admin_jwt}Expected: 200 OK, only settlements for organizer's events (filtered automatically)
Request:
GET /api/v1/settlements/{settlement_id}
Authorization: Bearer {jwt}Expected: 200 OK, settlement with relationships loaded
Request:
POST /api/v1/settlements
{
"event_id": "{event_with_no_tickets}"
}Expected: 400/500 Error, "No tickets found for this event to settle"
Request:
POST /api/v1/settlements
Authorization: Bearer {organizer_admin_jwt}
{
"event_id": "..."
}Expected: 403 Forbidden (only Global Admin can create)
-
Update Settlement Status Endpoints:
PUT /api/v1/settlements/{id}/process- Mark as PROCESSINGPUT /api/v1/settlements/{id}/complete- Mark as SETTLED
-
Payment Integration:
- Integrate with payment gateway for bank transfers
- Store settlement reference/transaction ID
- Handle payment failures
-
Settlement Reversal:
- Support for reversing settlements
- Increment
settlement_version - Create reversal records
-
Reporting:
- Generate settlement PDF reports
- Export settlement data to Excel
- Email settlement statements to organizers
-
Bulk Operations:
- Batch settle multiple events
- Periodic automatic settlements (e.g., monthly)
-
Partial Settlements:
- Settle specific date ranges
- Multiple settlements per event
-
Settlement Holds:
- Withhold settlements for dispute resolution
- Release holds after verification
-
Settlement Forecasting:
- Preview settlement amounts before creating
- Projected payout calculations
docs/SETTLEMENT_DEV.md- Settlement calculation logic from customer-backend perspectivedocs/TICKET_PRICE_CALCULATION.md- Fee calculation reverse pricing modeldocs/PLATFORM_FEE_RULES.md- Platform fee configurationdocs/SETTINGS_RULES.md- Payment fee and tax rules
The Settlement feature enables systematic payout management for event organizers by:
✅ Aggregating ticket sale data from purchase_tickets
✅ Never recalculating fees (uses immutable stored values)
✅ Supporting role-based access (Global Admin creates, Organizer Admin views)
✅ Following Mojura architecture (Features → Jobs pattern)
✅ Ensuring data integrity through transactions
✅ Providing clear audit trail with created_by/updated_by
Status: Phase 1 Complete - Creation and viewing functionality implemented.
Implementation Completed: December 26, 2025
Developer: Cascade AI Assistant
Framework: Laravel + Mojura Architecture