Skip to content

Instantly share code, notes, and snippets.

@aungkyawminn
Created December 30, 2025 16:04
Show Gist options
  • Select an option

  • Save aungkyawminn/9136ad86637f427786dbccad8d2adc9b to your computer and use it in GitHub Desktop.

Select an option

Save aungkyawminn/9136ad86637f427786dbccad8d2adc9b to your computer and use it in GitHub Desktop.

Settlement Feature - Implementation Guide

Project: Admin-Backend (Event Ticketing System)
Module: SettlementModule
Feature: Organizer Settlement Management
Implementation Date: December 26, 2025
Last Updated: December 27, 2025


1. Overview

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.

Key Principle: Never Recalculate 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.


2. Architecture & Structure

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

3. Database Schema

Tables (Already Migrated)

settlements table:

  • id - Snowflake ID
  • event_id - Foreign key to events
  • event_title - Denormalized event name
  • organizer_id - Foreign key to organizers
  • organizer_name - Denormalized organizer name
  • total_gross_amount - Sum of all ticket prices (what customers paid)
  • total_platform_fee - Sum of platform commissions
  • total_payment_fee - Sum of payment gateway fees
  • total_tax_amount - Sum of taxes collected
  • total_net_amount - Same as total_gross_amount
  • total_payout_amount - Sum of what organizer receives
  • currency - ISO currency code (MMK)
  • settlement_status - Enum: pending, processing, settled, failed
  • settlement_version - Increments on reversal + re-settlement
  • settled_at - Timestamp when settlement completed
  • created_at, updated_at, created_by, updated_by

settlement_purchase_tickets table:

  • id - Snowflake ID
  • settlement_id - Foreign key to settlements
  • purchase_tickets_id - Foreign key to purchase_tickets
  • gross_amount - Ticket price (from purchase_tickets.price)
  • platform_fee - From purchase_tickets.platform_fee
  • payment_fee - From purchase_tickets.payment_fee
  • tax_amount - From purchase_tickets.tax_amount
  • net_amount - From purchase_tickets.net_amount
  • payout_amount - From purchase_tickets.payout_amount
  • currency - From purchase_tickets.currency
  • created_at, created_by

4. Models

Settlement Model

Location: app/Models/Settlement.php

Inherits from BaseModel:

  • OrganizerScopeTrait - Automatic role-based filtering
  • BasicAudit - created_by, updated_by tracking
  • SnowflakeID - 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)

SettlementPurchaseTicket Model

Location: app/Models/SettlementPurchaseTicket.php

Relationships:

  • belongsTo(Settlement::class)
  • belongsTo(PurchaseTicket::class, 'purchase_tickets_id')
  • belongsTo(User::class, 'created_by')

5. Features & Jobs

CreateSettlementFeature

Location: app/Modules/SettlementModule/Features/CreateSettlementFeature.php

Purpose: Handles settlement creation request

Flow:

  1. Receives CreateSettlementRequest with event_id
  2. Delegates to CreateSettlementJob
  3. Returns success/error response

CreateSettlementJob

Location: app/Modules/SettlementModule/Jobs/CreateSettlementJob.php

Business Logic:

  1. Fetch Event: Load event details using Event::findOrFail($eventId)
  2. 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 = ?
  3. Validate: Ensure tickets exist (fail if 0 tickets)
  4. Create Settlement Record: Status = PENDING
  5. Link Tickets: Create settlement_purchase_tickets records for each ticket
  6. Update Event: Set event's settlement_status to PENDING
  7. Transaction Safe: All wrapped in DB transaction with rollback on error

IndexSettlementFeature

Location: app/Modules/SettlementModule/Features/IndexSettlementFeature.php

Purpose: List settlements with filtering and pagination

Query Parameters:

  • current_page - Page number
  • per_page - Items per page
  • search - Search term (searches: id, event_title, organizer_name, settlement_status)
  • order - Sort order (default: created_at DESC)
  • event_id - Filter by specific event
  • status - Filter by settlement status

IndexSettlementJob

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 filtering

Filtering Logic:

  • Global Admin: Sees all settlements (no filter applied)
  • Organizer Admin: Sees only their organizer's settlements (filtered by organizer_id)

ReadSettlementFeature & ReadSettlementJob

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)

6. API Endpoints

Base Path: /api/v1/settlements

All routes require:

  • auth:api middleware (JWT authentication)
  • user.check middleware (user validation)

1. List Settlements (Index)

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
}

2. Create Settlement

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": [ ... ]
  }
}

3. Read Settlement (Detail)

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"
    }
  }
}

7. Authorization & Access Control

Roles

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

Required Abilities

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.


8. Key Implementation Details

8.1 Immutable Fee Storage

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(...);

8.2 OrganizerScopeTrait Usage

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

8.3 Transaction Safety

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;
}

8.4 Settlement Status Flow

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.


9. Data Flow

┌─────────────────────────────────────────────────────┐
│ 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                        │
└─────────────────────────────────────────────────────┘

10. Testing Guide

Prerequisites

  1. Run migrations:

    php artisan migrate
  2. Seed abilities for Settlement:

    INSERT INTO abilities (action, subject) VALUES 
    ('index', 'Settlement'),
    ('create', 'Settlement'),
    ('read', 'Settlement');
  3. Assign to Global Admin role:

    INSERT INTO role_ability (role_id, ability_id) VALUES 
    ({global_admin_role_id}, {ability_id});

Test Scenarios

Test 1: Create Settlement (Global Admin)

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

Test 2: List Settlements (Global Admin)

Request:

GET /api/v1/settlements?per_page=10&current_page=1
Authorization: Bearer {global_admin_jwt}

Expected: 200 OK, all settlements across all organizers

Test 3: List Settlements (Organizer Admin)

Request:

GET /api/v1/settlements
Authorization: Bearer {organizer_admin_jwt}

Expected: 200 OK, only settlements for organizer's events (filtered automatically)

Test 4: Read Settlement Detail

Request:

GET /api/v1/settlements/{settlement_id}
Authorization: Bearer {jwt}

Expected: 200 OK, settlement with relationships loaded

Test 5: Create Settlement (No Tickets)

Request:

POST /api/v1/settlements
{
  "event_id": "{event_with_no_tickets}"
}

Expected: 400/500 Error, "No tickets found for this event to settle"

Test 6: Unauthorized Access (Organizer tries to create)

Request:

POST /api/v1/settlements
Authorization: Bearer {organizer_admin_jwt}
{
  "event_id": "..."
}

Expected: 403 Forbidden (only Global Admin can create)


11. Future Enhancements

Phase 2: Complete Settlement Workflow

  1. Update Settlement Status Endpoints:

    • PUT /api/v1/settlements/{id}/process - Mark as PROCESSING
    • PUT /api/v1/settlements/{id}/complete - Mark as SETTLED
  2. Payment Integration:

    • Integrate with payment gateway for bank transfers
    • Store settlement reference/transaction ID
    • Handle payment failures
  3. Settlement Reversal:

    • Support for reversing settlements
    • Increment settlement_version
    • Create reversal records
  4. Reporting:

    • Generate settlement PDF reports
    • Export settlement data to Excel
    • Email settlement statements to organizers
  5. Bulk Operations:

    • Batch settle multiple events
    • Periodic automatic settlements (e.g., monthly)

Phase 3: Advanced Features

  1. Partial Settlements:

    • Settle specific date ranges
    • Multiple settlements per event
  2. Settlement Holds:

    • Withhold settlements for dispute resolution
    • Release holds after verification
  3. Settlement Forecasting:

    • Preview settlement amounts before creating
    • Projected payout calculations

12. Related Documentation

  • docs/SETTLEMENT_DEV.md - Settlement calculation logic from customer-backend perspective
  • docs/TICKET_PRICE_CALCULATION.md - Fee calculation reverse pricing model
  • docs/PLATFORM_FEE_RULES.md - Platform fee configuration
  • docs/SETTINGS_RULES.md - Payment fee and tax rules

13. Summary

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment