My Mosque Demo
/ Developer Guide
Sign In

Developer Guide

My Mosque — Implementation Reference

Download README

My Mosque — Developer Implementation Guide

Connect. Pray. Manage.

Laravel 12 · Livewire 3 (Volt) · Spatie Permission · MySQL 8 · Flutter 3
Version 1.0 · June 2026 · Confidential


Field Detail
Platform My Mosque — mymosque.in
Backend Laravel 12 + Livewire 3 (Volt) + Alpine.js
Auth Single User model · Spatie Laravel Permission (roles + gates)
Database MySQL 8.x — Multi-tenant, mosque_id scoped
Mobile App Flutter 3 — Android-first, iOS Phase 4
Queue Laravel Horizon + Redis
Real-time Laravel Echo + Pusher / Reverb
PDF Laravel Snappy (wkhtmltopdf)
Push Notifications Firebase Cloud Messaging
Payments Razorpay Route (marketplace) — platform collects, routes to mosque linked accounts
WhatsApp WhatsApp Business Cloud API
Storage AWS S3 / Cloudinary
Caching Redis (session, queue, rate-limit)

Table of Contents

  1. Project Architecture & Setup
  2. Role-Based Authentication
  3. Database Schema
  4. Donation Management
  5. Expense Tracking
  6. Staff Payroll Management
  7. Member Due Tracker
  8. My Mosque Feed
  9. Prayer Times & Azan Alarm
  10. Zakat Calculator
  11. Imam Meal Tracker
  12. Madrasa / Maktab Module
  13. Event & Project Planner
  14. WhatsApp Integration
  15. Payment Platform — Razorpay Route
  16. Subscription Billing
  17. Security & Audit Trail
  18. Web Dashboard — Layout & Screens
  19. Flutter App — Architecture & Screen Map
  20. Multilingual Implementation
  21. Complete API Reference
  22. Scheduler Tasks & Queue Jobs
  23. Deployment & DevOps
  24. Environment Configuration
  25. Developer Quick Reference

1. Project Architecture & Setup

1.1 Folder Structure — Laravel 12

The application is built on a Laravel 12 + Livewire 3 (Volt) starter that ships with Spatie Permission pre-integrated. The existing User model uses HasRoles — every user type (super admin, mosque admin, staff, congregation member) is a row in the users table, distinguished by their Spatie role. A Committee is not a role — it is a named group of member-role users with specific positions (president, secretary, etc.), managed entirely by the mosque admin.

mymosque/
├── app/
│   ├── Http/
│   │   ├── Controllers/
│   │   │   └── Api/V1/              # REST API controllers (Flutter app)
│   │   └── Middleware/
│   │       ├── ResolveMosqueScope.php
│   │       └── CheckMosquePlan.php
│   ├── Livewire/                    # Classic Livewire components (non-Volt)
│   ├── Models/
│   │   ├── User.php                 # Single auth model — HasRoles (Spatie)
│   │   ├── Mosque.php
│   │   ├── Donation.php
│   │   ├── Expense.php
│   │   ├── Staff.php
│   │   ├── Salary.php
│   │   ├── MemberDue.php
│   │   ├── FeedPost.php
│   │   ├── MadrasaStudent.php
│   │   ├── Event.php
│   │   ├── MealRotation.php
│   │   ├── PrayerTime.php
│   │   ├── AuditLog.php
│   │   └── Subscription.php
│   ├── Services/
│   │   ├── DonationService.php
│   │   ├── PdfService.php
│   │   ├── WhatsAppService.php
│   │   ├── ZakatService.php
│   │   └── MealRotationService.php
│   └── Jobs/
│       ├── SendWhatsAppMessage.php
│       ├── GenerateDonationReceipt.php
│       ├── ProcessDueReminders.php
│       └── ProcessMealReminder.php
├── resources/
│   └── views/
│       ├── livewire/
│       │   ├── layout/navigation.blade.php
│       │   └── pages/               # Volt single-file components
│       │       ├── dashboard.blade.php
│       │       ├── users/index.blade.php
│       │       ├── roles/index.blade.php
│       │       ├── donations/index.blade.php
│       │       ├── expenses/index.blade.php
│       │       ├── staff/index.blade.php
│       │       ├── members/index.blade.php
│       │       └── ...
│       └── layouts/
│           ├── app.blade.php        # Authenticated shell (sidebar + topbar)
│           └── guest.blade.php      # Login / register
├── routes/
│   ├── web.php
│   ├── auth.php
│   └── console.php
└── database/
    ├── migrations/
    └── seeders/
        ├── RolesAndPermissionsSeeder.php
        └── DatabaseSeeder.php

1.2 Multi-Tenancy: mosque_id Global Scope

Every authenticated user belongs to one mosque. A BelongsToMosque trait appended as a Global Eloquent Scope keeps all queries automatically scoped to auth()->user()->mosque_id.

// app/Http/Middleware/ResolveMosqueScope.php
public function handle(Request $request, Closure $next): Response
{
    if ($user = auth()->user()) {
        app()->instance('current_mosque_id', $user->mosque_id);
    }
    return $next($request);
}

// app/Models/Traits/BelongsToMosque.php  (applied to all tenant models)
protected static function booted(): void
{
    static::addGlobalScope('mosque', function (Builder $builder) {
        if ($id = app('current_mosque_id')) {
            $builder->where('mosque_id', $id);
        }
    });
    static::creating(function ($model) {
        $model->mosque_id ??= app('current_mosque_id');
    });
}

1.3 Plan Gate Middleware

// app/Http/Middleware/CheckMosquePlan.php
// Usage:  ->middleware('plan:standard')  |  ->middleware('plan:pro')
public function handle(Request $request, Closure $next, string $required): Response
{
    $mosque = auth()->user()->mosque;
    $order  = ['free' => 0, 'standard' => 1, 'pro' => 2];
    if (($order[$mosque->plan] ?? -1) < ($order[$required] ?? 99)) {
        return back()->with('error', 'This feature requires the '.ucfirst($required).' plan.');
    }
    return $next($request);
}

2. Role-Based Authentication

2.1 The Single-User-Model Approach

There is one users table and one User model. All people who interact with the system — platform super-admins, mosque admins, staff, and congregation members — are rows in users differentiated by their Spatie role. There are no separate admins or members tables.

A Committee is not a Spatie role. It is a named group of member-role users with free-form positions (e.g. "President", "Joint Secretary" — no fixed list). The mosque admin can form a committee and adjust its membership at any time through the Committee Management screen. Committee membership is stored in the committees and committee_members tables (see §3.12).

// app/Models/User.php
class User extends Authenticatable
{
    use HasFactory, Notifiable, HasRoles;

    protected $guarded = ['id'];
    protected $hidden  = ['password', 'remember_token'];

    protected function casts(): array
    {
        return [
            'email_verified_at' => 'datetime',
            'password'          => 'hashed',
        ];
    }

    public function mosque(): BelongsTo
    {
        return $this->belongsTo(Mosque::class);
    }
}

2.2 Roles & Permissions Seeder

// database/seeders/RolesAndPermissionsSeeder.php
public function run(): void
{
    app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();

    // --- Permissions ---
    $permissions = [
        // Users & roles
        'view-users', 'create-users', 'edit-users', 'delete-users',
        'view-roles',  'create-roles', 'edit-roles', 'delete-roles',

        // Mosque data
        'view-donations',   'create-donations',   'edit-donations',   'delete-donations',
        'view-expenses',    'create-expenses',    'edit-expenses',    'delete-expenses',
        'view-members',     'create-members',     'edit-members',     'delete-members',
        'view-staff',       'create-staff',       'edit-staff',       'delete-staff',
        'view-payroll',     'process-payroll',
        'view-dues',        'record-due-payments', 'send-due-reminders',
        'view-feed',        'post-feed',          'moderate-feed',
        'view-prayer-times','edit-prayer-times',
        'view-madrasa',     'edit-madrasa',
        'view-events',      'edit-events',
        'view-meal-tracker','edit-meal-tracker',
        'view-reports',
        'send-whatsapp',
        'manage-billing',
        'view-audit-logs',
        'manage-mosque-settings',
    ];

    foreach ($permissions as $perm) {
        Permission::firstOrCreate(['name' => $perm]);
    }

    // --- Roles ---
    Role::firstOrCreate(['name' => 'super-admin'])
        ->givePermissionTo(Permission::all());

    Role::firstOrCreate(['name' => 'mosque-admin'])
        ->givePermissionTo(Permission::where('name', '!=', 'view-roles')
            ->where('name', 'not like', '%-roles')
            ->where('name', '!=', 'delete-users')
            ->get());

    // Committee is a data model, not a Spatie role — see committees / committee_members tables.

    Role::firstOrCreate(['name' => 'staff'])
        ->givePermissionTo([
            'view-donations', 'create-donations',
            'view-members',
            'view-feed', 'post-feed',
            'view-prayer-times',
        ]);

    Role::firstOrCreate(['name' => 'member'])
        ->givePermissionTo([
            'view-feed', 'post-feed',
            'view-prayer-times',
        ]);
}

2.3 Route Protection

All routes use standard auth middleware. Role checks are layered with the Spatie role: middleware.

// routes/web.php
Route::view('/', 'welcome')->name('welcome');

Route::middleware(['auth'])->group(function () {
    Volt::route('dashboard',      'pages.dashboard')->name('dashboard');
    Volt::route('notifications',  'pages.notifications.index')->name('notifications.index');
    Route::view('profile', 'profile')->name('profile');

    Route::post('/logout', function () {
        Auth::logout();
        request()->session()->invalidate();
        request()->session()->regenerateToken();
        return redirect('/');
    })->name('logout');
});

// Admin area (super-admin or mosque-admin)
Route::middleware(['auth', 'role:super-admin|mosque-admin'])->group(function () {
    Volt::route('admin/users',      'pages.users.index')->name('users.index');
    Volt::route('admin/roles',      'pages.roles.index')->name('roles.index');
    Volt::route('admin/members',    'pages.members.index')->name('members.index');
    Volt::route('admin/donations',  'pages.donations.index')->name('donations.index');
    Volt::route('admin/expenses',   'pages.expenses.index')->name('expenses.index');
    Volt::route('admin/staff',      'pages.staff.index')->name('staff.index');
    Volt::route('admin/payroll',    'pages.staff.payroll')->name('payroll.index');
    Volt::route('admin/dues',       'pages.dues.index')->name('dues.index');
    Volt::route('admin/feed',       'pages.feed.index')->name('feed.index');
    Volt::route('admin/feed/pending','pages.feed.moderation')->name('feed.moderation');
    Volt::route('admin/settings/prayer-times','pages.settings.prayer-times')->name('prayer-times.index');
    Volt::route('admin/reports',    'pages.reports.index')->name('reports.index');
    Volt::route('admin/audit',      'pages.settings.audit')->name('audit.index');
});

// Pro features
Route::middleware(['auth', 'role:super-admin|mosque-admin', 'plan:pro'])->group(function () {
    Volt::route('admin/madrasa',      'pages.madrasa.index')->name('madrasa.index');
    Volt::route('admin/events',       'pages.events.index')->name('events.index');
    Volt::route('admin/meal-tracker', 'pages.meal-tracker.index')->name('meal-tracker.index');
});

// Super-admin only
Route::middleware(['auth', 'role:super-admin'])->prefix('super-admin')->group(function () {
    Volt::route('/', 'pages.super-admin.dashboard')->name('super-admin.dashboard');
    Volt::route('mosques', 'pages.super-admin.mosques')->name('super-admin.mosques');
});

2.4 Login Flow

Users log in with email + password at /login — the standard Laravel Breeze / Volt auth screen. After authentication, the dashboard renders conditionally based on their role.

// resources/views/livewire/pages/auth/login.blade.php
// (Generated by the starter — standard Volt email/password form)

// After login, redirect to dashboard. The dashboard component reads the role:
new #[Layout('layouts.app')] class extends Component {
    public function with(): array
    {
        $user = auth()->user();
        return [
            'userCount'       => $user->hasRole('super-admin') ? User::count() : User::where('mosque_id', $user->mosque_id)->count(),
            'roleCount'        => Role::count(),
            'permissionCount' => Permission::count(),
            'recentUsers'     => User::with('roles')->where('mosque_id', $user->mosque_id)->latest()->take(5)->get(),
        ];
    }
};

2.5 Blade Permission Gates

Use Spatie's @can / @role directives in all views — never hard-code role names in display logic.

{{-- Show "New Donation" button only to users with permission --}}
@can('create-donations')
<button wire:click="openCreate" class="...">New Donation</button>
@endcan

{{-- Sidebar: only show Management section to admin roles --}}
@if(auth()->user()->hasAnyRole(['super-admin', 'mosque-admin']))
<div class="px-4 mt-4 mb-1"> ... Management links ... </div>
@endif

{{-- Plan-gate blade component --}}
{{-- Usage: <x-plan-gate :required="'standard'"> ... </x-plan-gate> --}}
@php
  $mosque  = auth()->user()->mosque;
  $allowed = match($required) {
    'standard' => in_array($mosque->plan, ['standard','pro']),
    'pro'      => $mosque->plan === 'pro',
    default    => true,
  };
@endphp
@if($allowed)
  {{ $slot }}
@else
  <div class="rounded-xl border border-amber-300/50 bg-amber-50 dark:bg-amber-900/10 p-4 text-center">
    <p class="font-medium text-amber-800 dark:text-amber-400">
      This feature requires the <strong>{{ ucfirst($required) }}</strong> plan.
    </p>
    <a href="{{ route('billing.plans') }}" class="mt-2 inline-block ...">Upgrade Now →</a>
  </div>
@endif

2.6 Creating Users (Admin Panel)

New mosque users are created from /admin/users by a mosque-admin or super-admin. The role dropdown is populated from Spatie roles. The pattern follows the existing pages/users/index.blade.php component exactly.

// Volt action inside pages/users/index.blade.php (already implemented in base app)
public function save(): void
{
    $this->validate([
        'name'         => 'required|string|max:255',
        'email'        => 'required|email|unique:users,email',
        'password'     => 'required|min:8',
        'selectedRole' => 'required|exists:roles,name',
    ]);

    $user = User::create([
        'name'      => $this->name,
        'email'     => $this->email,
        'password'  => $this->password,
        'mosque_id' => auth()->user()->mosque_id,   // scoped to current mosque
    ]);

    $user->syncRoles([$this->selectedRole]);
    $this->showModal = false;
    session()->flash('success', 'User created.');
}

3. Database Schema (MySQL 8)

All tenant tables include mosque_id (FK → mosques.id). Soft deletes (deleted_at) on all transactional tables. The users table is the single auth table — no separate admins table.

3.1 mosques — Tenant Root

Schema::create('mosques', function (Blueprint $table) {
    $table->id();
    $table->string('name', 120);
    $table->string('slug', 80)->unique();
    $table->string('city', 80);
    $table->string('state', 80);
    $table->string('pincode', 20);
    $table->string('country', 60)->default('IN');
    $table->string('logo_url')->nullable();
    $table->string('language', 10)->default('en');
    $table->string('currency', 10)->default('INR');
    $table->string('timezone', 50)->default('Asia/Kolkata');
    $table->string('waqf_number', 60)->nullable();
    $table->enum('plan', ['free','standard','pro'])->default('free');
    $table->timestamp('plan_expiry')->nullable();
    $table->timestamp('registration_date')->useCurrent();
    $table->enum('status', ['active','suspended','deleted'])->default('active');

    // Razorpay Route — linked account for mosque payouts
    $table->string('rp_account_id')->nullable()->unique();          // acc_XXXXXXXXXX
    $table->string('rp_stakeholder_id')->nullable();                // sth_XXXXXXXXXX
    $table->enum('rp_account_status', [
        'not_created','pending','under_review','activated','suspended'
    ])->default('not_created');
    $table->decimal('platform_fee_percent', 5, 2)->default(2.00);  // platform cut per payment
    $table->boolean('rp_settlements_enabled')->default(false);      // true once KYC cleared
    $table->json('rp_kyc_details')->nullable();                     // PAN, business type, bank
    $table->timestamps();
});

3.2 users — Single Auth Table (add mosque_id column)

The base starter creates the standard users table. Add mosque_id via migration:

// Extend the existing users table migration or add a new migration:
Schema::table('users', function (Blueprint $table) {
    $table->foreignId('mosque_id')->nullable()->after('id')->constrained()->nullOnDelete();
});
// Super-admin users have mosque_id = null (they manage all mosques)

Spatie's roles, permissions, model_has_roles, model_has_permissions, and role_has_permissions tables are auto-created by the package migration. No further changes needed.

3.3 payment_orders — Razorpay Order Tracking

Created before the checkout is shown. Links the Razorpay order to the mosque and the purpose (donation, due payment, etc.).

Schema::create('payment_orders', function (Blueprint $table) {
    $table->id();
    $table->foreignId('mosque_id')->constrained();
    $table->string('rp_order_id', 60)->unique();               // order_XXXXXXXXXX
    $table->string('rp_payment_id', 60)->nullable()->unique(); // pay_XXXXXXXXXX — set after capture
    $table->string('rp_signature', 120)->nullable();           // HMAC — verified on callback
    $table->decimal('amount', 12, 2);                         // total paid by donor (paise → stored in INR)
    $table->decimal('platform_fee', 10, 2)->default(0);       // platform commission deducted
    $table->decimal('transfer_amount', 12, 2)->default(0);    // amount routed to mosque
    $table->string('currency', 10)->default('INR');
    $table->enum('purpose', ['donation','due','madrasa_fee','event'])->default('donation');
    $table->unsignedBigInteger('purpose_id')->nullable();      // FK to donations.id / member_dues.id etc.
    $table->enum('status', ['created','paid','failed','refunded'])->default('created');
    $table->foreignId('created_by')->constrained('users');
    $table->timestamps();
    $table->index(['mosque_id','status']);
    $table->index('rp_order_id');
});

3.4 mosque_transfers — Razorpay Route Transfer Log

One transfer record per successfully captured payment. Tracks the fund flow from platform to mosque.

Schema::create('mosque_transfers', function (Blueprint $table) {
    $table->id();
    $table->foreignId('mosque_id')->constrained();
    $table->foreignId('payment_order_id')->constrained('payment_orders');
    $table->string('rp_transfer_id', 60)->unique();           // trf_XXXXXXXXXX
    $table->string('rp_linked_account_id', 40);              // acc_XXXXXXXXXX (mosque)
    $table->decimal('amount', 12, 2);                        // transfer amount (after platform fee)
    $table->decimal('platform_fee', 10, 2);                  // platform commission kept
    $table->string('currency', 10)->default('INR');
    $table->enum('status', ['pending','processed','failed','reversed'])->default('pending');
    $table->string('settlement_id', 60)->nullable();          // setl_XXXXXXXXXX (Razorpay settlement)
    $table->timestamp('settled_at')->nullable();
    $table->json('rp_response')->nullable();                  // full Razorpay transfer API response
    $table->timestamps();
    $table->index(['mosque_id','status']);
});

3.5 donations

Schema::create('donations', function (Blueprint $table) {
    $table->id();
    $table->foreignId('mosque_id')->constrained();
    $table->string('donor_name', 120);
    $table->string('donor_mobile', 20)->nullable();
    $table->string('donor_email', 120)->nullable();
    $table->foreignId('member_id')->nullable()->constrained('users');
    $table->decimal('amount', 12, 2);                         // total paid by donor
    $table->decimal('platform_fee', 10, 2)->default(0);       // platform commission
    $table->decimal('net_amount', 12, 2)->default(0);         // amount credited to mosque
    $table->enum('type', ['jummah','sadaqah','zakat','eid','special','other'])->default('sadaqah');
    $table->enum('payment_mode', ['online','cash','bank_transfer','cheque'])->default('online');
    $table->foreignId('payment_order_id')->nullable()->constrained('payment_orders'); // null for cash
    $table->date('date');
    $table->string('reference_note')->nullable();
    $table->string('receipt_number', 30)->unique();
    $table->string('receipt_url')->nullable();
    $table->foreignId('created_by')->constrained('users');
    $table->timestamps();
    $table->softDeletes();
    $table->index(['mosque_id','date']);
    $table->index(['mosque_id','type']);
});

3.6 expenses

Schema::create('expenses', function (Blueprint $table) {
    $table->id();
    $table->foreignId('mosque_id')->constrained();
    $table->string('title', 160);
    $table->enum('category', ['maintenance','utilities','staff','events','renovation','other']);
    $table->decimal('amount', 12, 2);
    $table->date('date');
    $table->string('paid_to', 120)->nullable();
    $table->enum('payment_mode', ['cash','upi','bank_transfer','cheque'])->default('cash');
    $table->string('bill_url')->nullable();
    $table->foreignId('approved_by')->nullable()->constrained('users');
    $table->foreignId('event_id')->nullable()->constrained('events');
    $table->text('notes')->nullable();
    $table->foreignId('created_by')->constrained('users');
    $table->timestamps();
    $table->softDeletes();
});

3.7 staff & salaries

Schema::create('staff', function (Blueprint $table) {
    $table->id();
    $table->foreignId('mosque_id')->constrained();
    $table->foreignId('user_id')->nullable()->constrained('users'); // if staff has a login
    $table->string('name', 120);
    $table->enum('role', ['imam','muazzin','caretaker','cleaner','teacher','other']);
    $table->string('mobile', 20)->nullable();
    $table->decimal('monthly_salary', 10, 2);
    $table->date('join_date');
    $table->string('bank_account')->nullable();
    $table->string('id_number')->nullable();
    $table->boolean('is_active')->default(true);
    $table->timestamps();
    $table->softDeletes();
});

Schema::create('salaries', function (Blueprint $table) {
    $table->id();
    $table->foreignId('mosque_id')->constrained();
    $table->foreignId('staff_id')->constrained();
    $table->tinyInteger('month');
    $table->year('year');
    $table->decimal('base_salary', 10, 2);
    $table->decimal('advance_deduction', 10, 2)->default(0);
    $table->decimal('net_paid', 10, 2);
    $table->date('paid_date')->nullable();
    $table->enum('status', ['pending','paid'])->default('pending');
    $table->string('payslip_url')->nullable();
    $table->timestamps();
    $table->unique(['staff_id','month','year']);
});

3.8 member_dues

Schema::create('member_dues', function (Blueprint $table) {
    $table->id();
    $table->foreignId('mosque_id')->constrained();
    $table->foreignId('member_id')->constrained('users');
    $table->decimal('amount', 10, 2);
    $table->date('due_date');
    $table->enum('status', ['pending','paid','partial','waived'])->default('pending');
    $table->decimal('paid_amount', 10, 2)->default(0);
    $table->date('paid_date')->nullable();
    $table->enum('payment_mode', ['cash','upi','other'])->nullable();
    $table->decimal('pending_balance', 10, 2)->storedAs('amount - paid_amount');
    $table->timestamps();
    $table->index(['mosque_id','status']);
    $table->index(['mosque_id','due_date']);
});

3.9 feed_posts, feed_reactions, feed_comments

Schema::create('feed_posts', function (Blueprint $table) {
    $table->id();
    $table->foreignId('mosque_id')->constrained();
    $table->foreignId('author_id')->constrained('users');
    $table->text('content');
    $table->enum('category', ['dua','hadith','announcement','event','reminder','condolence','general']);
    $table->string('media_url')->nullable();
    $table->enum('status', ['pending','approved','rejected'])->default('pending');
    $table->boolean('pinned')->default(false);
    $table->timestamp('scheduled_at')->nullable();
    $table->foreignId('approved_by')->nullable()->constrained('users');
    $table->timestamp('approved_at')->nullable();
    $table->timestamps();
    $table->softDeletes();
    $table->index(['mosque_id','status']);
});

Schema::create('feed_reactions', function (Blueprint $table) {
    $table->id();
    $table->foreignId('post_id')->constrained('feed_posts')->cascadeOnDelete();
    $table->foreignId('user_id')->constrained('users');
    $table->enum('type', ['ameen','mashallah','jazakallah']);
    $table->timestamps();
    $table->unique(['post_id','user_id']);
});

3.10 prayer_times

Schema::create('prayer_times', function (Blueprint $table) {
    $table->id();
    $table->foreignId('mosque_id')->constrained();
    $table->time('fajr_azan');    $table->time('fajr_iqamah');
    $table->time('dhuhr_azan');   $table->time('dhuhr_iqamah');
    $table->time('asr_azan');     $table->time('asr_iqamah');
    $table->time('maghrib_azan'); $table->time('maghrib_iqamah');
    $table->time('isha_azan');    $table->time('isha_iqamah');
    $table->time('jummah_azan');
    $table->date('effective_from');
    $table->date('effective_until')->nullable();
    $table->foreignId('set_by')->constrained('users');
    $table->timestamps();
    $table->index(['mosque_id','effective_from']);
});

3.11 madrasa_students, events, audit_logs, subscriptions

Schema::create('madrasa_students', function (Blueprint $table) {
    $table->id();
    $table->foreignId('mosque_id')->constrained();
    $table->string('name', 120);
    $table->date('dob');
    $table->string('father_name', 120);
    $table->string('mobile', 20)->nullable();
    $table->string('class', 60);
    $table->date('enrolment_date');
    $table->decimal('monthly_fee', 8, 2)->default(0);
    $table->boolean('is_active')->default(true);
    $table->timestamps();
    $table->softDeletes();
});

Schema::create('events', function (Blueprint $table) {
    $table->id();
    $table->foreignId('mosque_id')->constrained();
    $table->string('title', 160);
    $table->enum('type', ['eid','idgah','ramadan','fundraiser','renovation','meeting','other']);
    $table->date('start_date'); $table->date('end_date')->nullable();
    $table->decimal('budget', 12, 2)->default(0);
    $table->enum('status', ['planning','active','completed','cancelled'])->default('planning');
    $table->text('description')->nullable();
    $table->json('assigned_to')->nullable();
    $table->timestamps();
    $table->softDeletes();
});

Schema::create('audit_logs', function (Blueprint $table) {
    $table->id();
    $table->unsignedBigInteger('mosque_id')->index();
    $table->unsignedBigInteger('user_id')->nullable();    // FK → users.id
    $table->string('action', 20);
    $table->string('entity', 60);
    $table->unsignedBigInteger('entity_id');
    $table->json('old_values')->nullable();
    $table->json('new_values')->nullable();
    $table->string('ip_address', 45)->nullable();
    $table->timestamp('created_at')->useCurrent();
    $table->index(['mosque_id','entity','created_at']);
});

Schema::create('subscriptions', function (Blueprint $table) {
    $table->id();
    $table->foreignId('mosque_id')->constrained();
    $table->enum('plan', ['standard','pro']);
    $table->decimal('amount', 10, 2);
    $table->string('gateway', 20);
    $table->string('gateway_subscription_id')->nullable();
    $table->string('gateway_payment_id')->nullable();
    $table->enum('billing_cycle', ['monthly','annual']);
    $table->enum('status', ['active','cancelled','expired'])->default('active');
    $table->timestamp('starts_at'); $table->timestamp('expires_at');
    $table->timestamps();
});

3.12 committees & committee_members

A committee is a named group of member-role users with assigned positions. The mosque admin creates/modifies committees at any time — no role change is needed.

Schema::create('committees', function (Blueprint $table) {
    $table->id();
    $table->foreignId('mosque_id')->constrained()->cascadeOnDelete();
    $table->string('name', 120);                   // e.g. "Executive Committee 2025"
    $table->text('description')->nullable();
    $table->date('established_date')->nullable();
    $table->boolean('is_active')->default(true);
    $table->timestamps();
});

Schema::create('committee_members', function (Blueprint $table) {
    $table->id();
    $table->foreignId('committee_id')->constrained()->cascadeOnDelete();
    $table->foreignId('user_id')->constrained()->cascadeOnDelete();
    $table->string('position', 100)->nullable();   // free-form, e.g. "President", "Joint Secretary"
    $table->date('joined_date')->nullable();
    $table->boolean('is_active')->default(true);
    $table->timestamps();

    $table->unique(['committee_id', 'user_id']);   // one position per user per committee
});

User model helper methods:

// app/Models/User.php

public function committees(): BelongsToMany
{
    return $this->belongsToMany(Committee::class, 'committee_members')
                ->withPivot('position', 'joined_date', 'is_active')
                ->withTimestamps();
}

public function isCommitteeMember(): bool
{
    return $this->committees()->wherePivot('is_active', true)->exists();
}

public function committeePosition(int $committeeId): ?string
{
    return $this->committees()
                ->wherePivot('committee_id', $committeeId)
                ->wherePivot('is_active', true)
                ->value('position');
}

Access control note: Committee members retain the member Spatie role. If a screen should be visible to all active committee members across all committees of the mosque, gate it with $user->isCommitteeMember() in a policy or middleware — do not create a committee Spatie role.


4. Donation Management (Free Forever)

All payments flow through the platform's Razorpay account first. After capture the platform creates a Razorpay Route Transfer to the mosque's linked account, deducting the platform commission. Cash donations bypass online payment and are recorded manually.

Online donation flow:
  Donor → createOrder (platform Razorpay account)
        → Razorpay Checkout (UPI / card / net banking)
        → verifyPayment (HMAC signature check)
        → createDonation record
        → RouteTransferJob dispatched
             └─ Transfer to mosque's rp_account_id (amount − platform_fee)
                → MosqueTransfer record created
                → GenerateDonationReceipt dispatched

4.1 Payment Controller — Create Order & Verify

// app/Http/Controllers/Api/V1/PaymentController.php
class PaymentController extends Controller
{
    private Api $rp;

    public function __construct()
    {
        $this->rp = new Api(config('razorpay.key_id'), config('razorpay.key_secret'));
    }

    /**
     * Step 1 — create a Razorpay order before showing checkout.
     * Called by web Volt page or Flutter app.
     */
    public function createOrder(Request $request): JsonResponse
    {
        $request->validate([
            'amount'     => 'required|numeric|min:1',
            'purpose'    => 'required|in:donation,due,madrasa_fee,event',
            'purpose_id' => 'nullable|integer',
        ]);

        $mosque  = Mosque::findOrFail(app('current_mosque_id'));
        $amtPaise = (int) round($request->amount * 100); // Razorpay accepts paise

        $order = $this->rp->order->create([
            'amount'          => $amtPaise,
            'currency'        => 'INR',
            'receipt'         => 'mosque_'.$mosque->id.'_'.uniqid(),
            'notes'           => [
                'mosque_id'  => $mosque->id,
                'mosque_name'=> $mosque->name,
                'purpose'    => $request->purpose,
            ],
        ]);

        $platformFee    = round($request->amount * ($mosque->platform_fee_percent / 100), 2);
        $transferAmount = round($request->amount - $platformFee, 2);

        $paymentOrder = PaymentOrder::create([
            'mosque_id'       => $mosque->id,
            'rp_order_id'     => $order->id,
            'amount'          => $request->amount,
            'platform_fee'    => $platformFee,
            'transfer_amount' => $transferAmount,
            'currency'        => 'INR',
            'purpose'         => $request->purpose,
            'purpose_id'      => $request->purpose_id,
            'status'          => 'created',
            'created_by'      => auth()->id(),
        ]);

        return response()->json([
            'order_id'        => $order->id,
            'amount'          => $amtPaise,
            'currency'        => 'INR',
            'key'             => config('razorpay.key_id'),
            'payment_order_id'=> $paymentOrder->id,
            'mosque_name'     => $mosque->name,
        ]);
    }

    /**
     * Step 2 — verify HMAC signature after Razorpay Checkout callback.
     * Called immediately after successful payment on client side.
     */
    public function verifyPayment(Request $request): JsonResponse
    {
        $request->validate([
            'razorpay_order_id'   => 'required|string',
            'razorpay_payment_id' => 'required|string',
            'razorpay_signature'  => 'required|string',
        ]);

        $expected = hash_hmac(
            'sha256',
            $request->razorpay_order_id.'|'.$request->razorpay_payment_id,
            config('razorpay.key_secret')
        );

        if (!hash_equals($expected, $request->razorpay_signature)) {
            return response()->json(['error' => 'Invalid payment signature'], 422);
        }

        $paymentOrder = PaymentOrder::where('rp_order_id', $request->razorpay_order_id)->firstOrFail();
        $paymentOrder->update([
            'rp_payment_id' => $request->razorpay_payment_id,
            'rp_signature'  => $request->razorpay_signature,
            'status'        => 'paid',
        ]);

        // Dispatch Route transfer and create domain record
        CreateDonationAfterPayment::dispatch($paymentOrder);

        return response()->json(['verified' => true, 'payment_order_id' => $paymentOrder->id]);
    }
}

4.2 CreateDonationAfterPayment Job — Domain Record + Route Transfer

// app/Jobs/CreateDonationAfterPayment.php
class CreateDonationAfterPayment implements ShouldQueue
{
    public function __construct(public PaymentOrder $paymentOrder) {}

    public function handle(DonationService $service, RouteTransferService $route): void
    {
        // Create the Donation record
        $donation = $service->createFromOrder($this->paymentOrder);

        // Initiate Razorpay Route Transfer to mosque's linked account
        $mosque = $this->paymentOrder->mosque;

        if ($mosque->rp_settlements_enabled && $mosque->rp_account_id) {
            $route->transfer($this->paymentOrder, $mosque);
        }

        // Generate PDF receipt
        GenerateDonationReceipt::dispatch($donation);
    }
}

4.3 RouteTransferService — Transfer to Mosque Linked Account

// app/Services/RouteTransferService.php
class RouteTransferService
{
    private Api $rp;

    public function __construct()
    {
        $this->rp = new Api(config('razorpay.key_id'), config('razorpay.key_secret'));
    }

    public function transfer(PaymentOrder $paymentOrder, Mosque $mosque): MosqueTransfer
    {
        $amtPaise = (int) round($paymentOrder->transfer_amount * 100);

        // Razorpay Route: transfer from platform payment to mosque linked account
        $transfer = $this->rp->payment
            ->fetch($paymentOrder->rp_payment_id)
            ->transfer([
                'transfers' => [[
                    'account'  => $mosque->rp_account_id,       // acc_XXXXXXXXXX
                    'amount'   => $amtPaise,
                    'currency' => 'INR',
                    'notes'    => [
                        'mosque_id'        => $mosque->id,
                        'payment_order_id' => $paymentOrder->id,
                        'purpose'          => $paymentOrder->purpose,
                    ],
                    'on_hold'  => 0,                             // 0 = immediate settlement
                ]],
            ]);

        $trf = $transfer['items'][0];

        return MosqueTransfer::create([
            'mosque_id'             => $mosque->id,
            'payment_order_id'      => $paymentOrder->id,
            'rp_transfer_id'        => $trf['id'],               // trf_XXXXXXXXXX
            'rp_linked_account_id'  => $mosque->rp_account_id,
            'amount'                => $paymentOrder->transfer_amount,
            'platform_fee'          => $paymentOrder->platform_fee,
            'currency'              => 'INR',
            'status'                => 'processed',
            'rp_response'           => json_encode($trf),
        ]);
    }

    /**
     * Reverse a transfer (e.g. on refund request).
     */
    public function reverse(MosqueTransfer $transfer): void
    {
        $this->rp->transfer
            ->fetch($transfer->rp_transfer_id)
            ->reversal(['amount' => (int) round($transfer->amount * 100)]);

        $transfer->update(['status' => 'reversed']);
    }
}

4.4 DonationService — Receipt Number, Net Amount & PDF

// app/Services/DonationService.php
class DonationService
{
    public function createFromOrder(PaymentOrder $order): Donation
    {
        $data['receipt_number'] = $this->nextReceiptNumber();
        $data['mosque_id']      = $order->mosque_id;
        $data['payment_order_id'] = $order->id;
        $data['amount']         = $order->amount;
        $data['platform_fee']   = $order->platform_fee;
        $data['net_amount']     = $order->transfer_amount;
        $data['payment_mode']   = 'online';
        $data['date']           = now()->toDateString();
        $data['created_by']     = $order->created_by;
        return Donation::create($data);
    }

    public function createCash(array $data): Donation
    {
        // Cash/cheque donations have no RouteTransfer; net_amount = amount
        $data['receipt_number'] = $this->nextReceiptNumber();
        $data['mosque_id']      = app('current_mosque_id');
        $data['platform_fee']   = 0;
        $data['net_amount']     = $data['amount'];
        return Donation::create($data);
    }

    private function nextReceiptNumber(): string
    {
        $prefix = now()->format('m-Y');
        $last   = Donation::where('receipt_number','like',$prefix.'-%')
                          ->orderByDesc('id')->value('receipt_number');
        $seq    = $last ? ((int)substr($last, -4)) + 1 : 1;
        return $prefix.'-'.str_pad($seq, 4, '0', STR_PAD_LEFT);
    }

    public function generateReceiptPdf(Donation $donation): string
    {
        $mosque = $donation->mosque;
        $html   = view('pdf.donation-receipt', compact('donation','mosque'))->render();
        $pdf    = SnappyPdf::loadHTML($html)->setPaper('A5','portrait');
        $path   = 'mosques/'.$donation->mosque_id.'/receipts/'.$donation->receipt_number.'.pdf';
        Storage::disk('s3')->put($path, $pdf->output());
        $donation->update(['receipt_url' => $path]);
        return $path;
    }
}

4.5 Donation Volt Page — Online + Cash Tabs

{{-- resources/views/livewire/pages/donations/index.blade.php --}}
<?php
use Livewire\Attributes\Layout;
use Livewire\Volt\Component;
use Livewire\WithPagination;
use App\Models\Donation;
use App\Services\DonationService;

new #[Layout('layouts.app')] class extends Component {
    use WithPagination;

    public string $mode = 'online';   // 'online' | 'cash'
    public string $donorName = '', $donorMobile = '', $donorEmail = '',
                  $type = 'sadaqah', $referenceNote = '', $search = '';
    public float  $amount = 0;
    public bool   $showModal = false;

    // After createOrder API call, store these for the Razorpay checkout JS
    public ?string $rpOrderId = null;
    public ?int    $paymentOrderId = null;

    public function with(): array
    {
        return [
            'donations' => Donation::with('paymentOrder')
                ->when($this->search, fn($q) => $q->where('donor_name','like','%'.$this->search.'%'))
                ->latest('date')->paginate(20),
        ];
    }

    public function initOnlinePayment(): void
    {
        $this->authorize('create-donations');
        $this->validate([
            'donorName' => 'required|string|max:120',
            'amount'    => 'required|numeric|min:1',
            'type'      => 'required|in:jummah,sadaqah,zakat,eid,special,other',
        ]);

        // Create Razorpay order server-side, pass details to JS via event
        $response = app(\App\Http\Controllers\Api\V1\PaymentController::class)
            ->createOrder(request()->merge([
                'amount'  => $this->amount,
                'purpose' => 'donation',
            ]));

        $data = $response->getData(true);
        $this->rpOrderId      = $data['order_id'];
        $this->paymentOrderId = $data['payment_order_id'];

        // Dispatch browser event to trigger Razorpay checkout JS
        $this->dispatch('open-razorpay-checkout', [
            'key'       => $data['key'],
            'order_id'  => $data['order_id'],
            'amount'    => $data['amount'],
            'name'      => $data['mosque_name'],
            'prefill'   => ['name' => $this->donorName, 'contact' => $this->donorMobile, 'email' => $this->donorEmail],
        ]);
    }

    public function saveCashDonation(): void
    {
        $this->authorize('create-donations');
        $this->validate([
            'donorName' => 'required|string|max:120',
            'amount'    => 'required|numeric|min:1',
            'type'      => 'required',
        ]);

        $donation = app(DonationService::class)->createCash([
            'donor_name'     => $this->donorName,
            'donor_mobile'   => $this->donorMobile,
            'amount'         => $this->amount,
            'type'           => $this->type,
            'payment_mode'   => 'cash',
            'date'           => now()->toDateString(),
            'reference_note' => $this->referenceNote,
            'created_by'     => auth()->id(),
        ]);

        \App\Jobs\GenerateDonationReceipt::dispatch($donation);
        $this->showModal = false;
        session()->flash('success', 'Cash donation recorded!');
    }
};
?>

4.6 Razorpay Checkout JS (in Blade layout or Alpine component)

{{-- Include Razorpay JS once in layouts/app.blade.php --}}
<script src="https://checkout.razorpay.com/v1/checkout.js"></script>

{{-- Alpine/Livewire event listener — triggers when initOnlinePayment() dispatches the event --}}
<div x-data
     @open-razorpay-checkout.window="
       const opts = $event.detail[0];
       const rzp = new Razorpay({
         key:         opts.key,
         order_id:    opts.order_id,
         amount:      opts.amount,
         currency:    'INR',
         name:        opts.name,
         description: 'Mosque Donation',
         prefill:     opts.prefill,
         theme:       { color: '#10b981' },
         handler: function(response) {
           // Post back to Laravel for HMAC verification
           $wire.dispatch('payment-success', {
             razorpay_order_id:   response.razorpay_order_id,
             razorpay_payment_id: response.razorpay_payment_id,
             razorpay_signature:  response.razorpay_signature,
           });
         },
       });
       rzp.open();
     ">
</div>

4.7 Razorpay Webhook — Fallback Capture

The payment.captured webhook acts as a fallback in case the client never called verifyPayment (network drop, closed browser). Webhooks are verified with X-Razorpay-Signature.

// app/Http/Controllers/Api/V1/WebhookController.php
public function razorpay(Request $request): JsonResponse
{
    $secret = config('razorpay.webhook_secret');
    $sig    = $request->header('X-Razorpay-Signature');

    if (!hash_equals(hash_hmac('sha256', $request->getContent(), $secret), $sig)) {
        return response()->json(['error' => 'invalid signature'], 400);
    }

    $event = $request->json('event');

    match ($event) {
        'payment.captured' => $this->handleCapture($request->json('payload.payment.entity')),
        'transfer.processed'  => $this->handleTransferProcessed($request->json('payload.transfer.entity')),
        'transfer.failed'     => $this->handleTransferFailed($request->json('payload.transfer.entity')),
        'payment.failed'      => $this->handlePaymentFailed($request->json('payload.payment.entity')),
        default               => null,
    };

    return response()->json(['status' => 'ok']);
}

private function handleCapture(array $payment): void
{
    $order = PaymentOrder::where('rp_order_id', $payment['order_id'])->first();
    if (!$order || $order->status === 'paid') return; // already handled client-side

    $order->update(['rp_payment_id' => $payment['id'], 'status' => 'paid']);
    CreateDonationAfterPayment::dispatch($order);
}

private function handleTransferProcessed(array $transfer): void
{
    MosqueTransfer::where('rp_transfer_id', $transfer['id'])
        ->update(['status' => 'processed', 'settled_at' => now()]);
}

private function handleTransferFailed(array $transfer): void
{
    MosqueTransfer::where('rp_transfer_id', $transfer['id'])
        ->update(['status' => 'failed']);

    // Alert platform super-admin
    User::role('super-admin')->each(fn($u) => $u->notify(new RouteTransferFailed($transfer)));
}

private function handlePaymentFailed(array $payment): void
{
    PaymentOrder::where('rp_order_id', $payment['order_id'])
        ->update(['status' => 'failed']);
}

5. Expense Tracking (Standard & Pro)

5.1 Web — Livewire Volt Component

{{-- resources/views/livewire/pages/expenses/index.blade.php --}}
<?php
use Livewire\Attributes\Layout;
use Livewire\Volt\Component;
use App\Models\Expense;

new #[Layout('layouts.app')] class extends Component {

    public string $title = '', $category = 'maintenance', $paidTo = '',
                  $paymentMode = 'cash', $date = '', $notes = '';
    public float  $amount = 0;
    public ?int   $approvedBy = null, $eventId = null, $editId = null;
    public $billFile = null;
    public string $search = '', $filterCategory = '';
    public bool   $showModal = false;

    public function save(): void
    {
        $this->authorize('create-expenses');
        $this->validate([
            'title'    => 'required|string|max:160',
            'amount'   => 'required|numeric|min:0.01',
            'date'     => 'required|date',
            'category' => 'required',
        ]);

        $data = $this->only(['title','category','amount','date','paidTo','paymentMode','notes']);
        $data['created_by'] = auth()->id();

        if ($this->billFile) {
            $data['bill_url'] = $this->billFile->store(
                'mosques/'.app('current_mosque_id').'/bills', 's3'
            );
        }

        Expense::updateOrCreate(['id' => $this->editId], $data);
        $this->showModal = false;
        session()->flash('success', 'Expense saved!');
    }

    public function with(): array
    {
        return [
            'expenses' => Expense::query()
                ->when($this->search,         fn($q) => $q->where('title','like','%'.$this->search.'%'))
                ->when($this->filterCategory, fn($q) => $q->where('category', $this->filterCategory))
                ->latest('date')->paginate(20),

            'chartData' => Expense::selectRaw('category, SUM(amount) as total')
                ->groupBy('category')->pluck('total','category'),
        ];
    }
};
?>

6. Staff Payroll Management (Standard & Pro)

6.1 Payroll Service

// app/Services/PayrollService.php
class PayrollService
{
    public function processMonthlyPayroll(Staff $staff, int $month, int $year): Salary
    {
        $advance = StaffAdvance::where('staff_id', $staff->id)
            ->where('status', 'pending')->sum('amount');

        $salary = Salary::create([
            'mosque_id'         => $staff->mosque_id,
            'staff_id'          => $staff->id,
            'month'             => $month,
            'year'              => $year,
            'base_salary'       => $staff->monthly_salary,
            'advance_deduction' => $advance,
            'net_paid'          => max(0, $staff->monthly_salary - $advance),
            'status'            => 'pending',
        ]);

        StaffAdvance::where('staff_id', $staff->id)
            ->where('status', 'pending')
            ->update(['status' => 'deducted', 'salary_id' => $salary->id]);

        GeneratePayslip::dispatch($salary);
        return $salary;
    }
}

6.2 Payroll Volt View (Blade snippet)

@can('process-payroll')
@foreach($staffList as $staff)
  <div class="bg-white dark:bg-[#0e1912] border border-slate-200 dark:border-white/[0.06] rounded-xl p-5 flex justify-between items-center">
    <div>
      <p class="font-semibold text-slate-900 dark:text-slate-100">{{ $staff->name }}</p>
      <p class="text-xs text-slate-400">{{ ucfirst($staff->role) }}</p>
      <p class="text-sm text-emerald-600 dark:text-emerald-400 mt-1">₹{{ number_format($staff->monthly_salary, 2) }}/mo</p>
    </div>
    <div class="text-right">
      @if($staff->currentMonthSalary?->status === 'paid')
        <span class="px-2 py-0.5 rounded-full text-[10px] font-semibold bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/20">
          Paid ✓
        </span>
      @else
        <button wire:click="processPayroll({{ $staff->id }})"
                class="px-4 py-2 bg-emerald-500 hover:bg-emerald-600 text-white text-sm font-semibold rounded-lg transition-colors">
          Mark Paid
        </button>
      @endif
    </div>
  </div>
@endforeach
@endcan

7. Member Due Tracker (Standard & Pro)

7.1 Auto-Generate Monthly Dues (Scheduler)

// routes/console.php
Schedule::call(function () {
    $configs = MosqueDueConfig::all();
    foreach ($configs as $cfg) {
        User::where('mosque_id', $cfg->mosque_id)
            ->whereHas('roles', fn($q) => $q->where('name', 'member'))
            ->each(function ($user) use ($cfg) {
                MemberDue::firstOrCreate([
                    'mosque_id' => $cfg->mosque_id,
                    'member_id' => $user->id,
                    'due_date'  => now()->startOfMonth()->addDays($cfg->due_day - 1),
                ], ['amount' => $cfg->monthly_amount]);
            });
    }
})->monthlyOn(1, '00:00');

Schedule::call(function () {
    $targets = [now()->addDays(3)->toDateString(), now()->toDateString()];
    MemberDue::whereIn('due_date', $targets)
        ->where('status', 'pending')
        ->with(['member','mosque'])
        ->chunk(100, function ($dues) {
            foreach ($dues as $due) {
                SendWhatsAppMessage::dispatch(
                    $due->member->phone, 'due_reminder',
                    ['due' => $due, 'mosque' => $due->mosque]
                );
            }
        });
})->dailyAt('09:00');

7.2 Record Payment (Volt action)

public function recordPayment(int $dueId, float $paid, string $mode): void
{
    $this->authorize('record-due-payments');
    $due = MemberDue::findOrFail($dueId);
    $due->paid_amount  += $paid;
    $due->paid_date     = now();
    $due->payment_mode  = $mode;
    $due->status        = $due->paid_amount >= $due->amount ? 'paid' : 'partial';
    $due->save();
    GenerateDueReceipt::dispatch($due);
}

8. My Mosque Feed (All Plans)

8.1 API Controller

// app/Http/Controllers/Api/V1/FeedController.php
class FeedController extends Controller
{
    public function index()
    {
        return FeedPostResource::collection(
            FeedPost::with(['author','reactions'])
                ->where('status', 'approved')
                ->orderByDesc('pinned')
                ->orderByDesc('approved_at')
                ->paginate(15)
        );
    }

    public function store(Request $request)
    {
        $this->authorize('post-feed');
        $request->validate(['content' => 'required|max:500', 'category' => 'required']);

        $post = FeedPost::create([
            ...$request->only(['content','category']),
            'author_id' => auth()->id(),
            'status'    => 'pending',
        ]);

        // Notify mosque admin
        User::where('mosque_id', $post->mosque_id)
            ->role('mosque-admin')->first()
            ?->notify(new NewFeedPostPending($post));

        return new FeedPostResource($post);
    }

    public function approve(FeedPost $post)
    {
        $this->authorize('moderate-feed');
        $post->update([
            'status'      => 'approved',
            'approved_by' => auth()->id(),
            'approved_at' => now(),
        ]);

        FcmService::sendToTopic('mosque_'.$post->mosque_id, [
            'title' => 'New post on My Mosque Feed',
            'body'  => Str::limit($post->content, 80),
        ]);

        return response()->json(['approved' => true]);
    }
}

9. Prayer Times & Azan Alarm (Free Forever)

9.1 Admin — Set Prayer Times (Volt)

{{-- resources/views/livewire/pages/settings/prayer-times.blade.php --}}
<?php
use Livewire\Attributes\Layout;
use Livewire\Volt\Component;
use App\Models\PrayerTime;

new #[Layout('layouts.app')] class extends Component {

    public array $times = [
        'fajr_azan' => '', 'fajr_iqamah' => '', 'dhuhr_azan' => '', 'dhuhr_iqamah' => '',
        'asr_azan'  => '', 'asr_iqamah'  => '', 'maghrib_azan' => '', 'maghrib_iqamah' => '',
        'isha_azan' => '', 'isha_iqamah' => '', 'jummah_azan' => '',
    ];
    public string $effectiveFrom = '', $effectiveUntil = '';

    public function mount(): void
    {
        $this->authorize('view-prayer-times');
        $current = PrayerTime::where('effective_from', '<=', today())
            ->where(fn($q) => $q->whereNull('effective_until')->orWhere('effective_until', '>=', today()))
            ->latest('effective_from')->first();

        if ($current) $this->times = $current->only(array_keys($this->times));
        $this->effectiveFrom = today()->toDateString();
    }

    public function save(): void
    {
        $this->authorize('edit-prayer-times');
        $this->validate(['effectiveFrom' => 'required|date|after_or_equal:today']);

        PrayerTime::create([
            ...$this->times,
            'mosque_id'      => app('current_mosque_id'),
            'effective_from' => $this->effectiveFrom,
            'effective_until'=> $this->effectiveUntil ?: null,
            'set_by'         => auth()->id(),
        ]);

        FcmService::sendDataToTopic('mosque_'.app('current_mosque_id'), [
            'type'  => 'prayer_times_updated',
            'times' => json_encode($this->times),
        ]);

        session()->flash('success', 'Prayer times updated!');
    }
};
?>

9.2 Flutter — Azan Alarm Engine

// lib/services/azan_alarm_service.dart
class AzanAlarmService {
  static Future<void> scheduleAll(PrayerTimes times, AzanPreferences prefs) async {
    await cancelAll();
    final prayers = {
      'Fajr'   : times.fajrAzan,
      'Dhuhr'  : times.dhuhrAzan,
      'Asr'    : times.asrAzan,
      'Maghrib': times.maghribAzan,
      'Isha'   : times.ishaAzan,
    };

    for (final entry in prayers.entries) {
      final pref = prefs.forPrayer(entry.key);
      if (!pref.alarmEnabled) continue;

      final azanDT = _todayAt(entry.value);
      await _scheduleNotification(
        id:         _prayerId(entry.key),
        title:      '${entry.key} Azan',
        body:       'Time for ${entry.key} prayer',
        time:       azanDT,
        sound:      pref.tonePath,
        fullScreen: true,
      );

      if (pref.preAlertEnabled) {
        await _scheduleNotification(
          id:         _preAlertId(entry.key),
          title:      '${entry.key} in ${pref.preAlertMinutes} minutes',
          body:       'Prepare for ${entry.key} prayer',
          time:       azanDT.subtract(Duration(minutes: pref.preAlertMinutes)),
          sound:      pref.preAlertTone,
          fullScreen: false,
        );
      }
    }
  }

  static Future<void> cancelAll() async => FlutterLocalNotificationsPlugin().cancelAll();

  static DateTime _todayAt(String hhmm) {
    final p = hhmm.split(':');
    return DateTime.now().copyWith(hour: int.parse(p[0]), minute: int.parse(p[1]), second: 0);
  }

  static int _prayerId(String name)   => name.hashCode & 0xFFFF;
  static int _preAlertId(String name) => (name.hashCode + 1000) & 0xFFFF;
}

10. Zakat Calculator (Free Forever)

10.1 Service — Live Metal Prices + Calculation

// app/Services/ZakatService.php
class ZakatService
{
    public function getLiveMetalPrices(string $currency = 'INR'): array
    {
        return cache()->remember('metal_prices_'.$currency, 3600, function () use ($currency) {
            $res = Http::get('https://api.metalpriceapi.com/v1/latest', [
                'api_key'    => config('services.metal_price_api_key'),
                'base'       => $currency,
                'currencies' => 'XAU,XAG',
            ])->json();
            return [
                'gold_per_gram'   => $res['rates']['XAU'] / 31.1035,
                'silver_per_gram' => $res['rates']['XAG'] / 31.1035,
            ];
        });
    }

    public function calculate(array $input, string $currency = 'INR'): array
    {
        $prices      = $this->getLiveMetalPrices($currency);
        $goldValue   = $input['gold_grams']  * $prices['gold_per_gram'];
        $silverValue = $input['silver_grams'] * $prices['silver_per_gram'];
        $nisab       = min(612.36 * $prices['silver_per_gram'], 87.48 * $prices['gold_per_gram']);
        $total       = $input['cash'] + $goldValue + $silverValue + $input['business_goods'] + $input['receivables'];

        return [
            'gold_value'   => round($goldValue, 2),
            'silver_value' => round($silverValue, 2),
            'nisab'        => round($nisab, 2),
            'total_wealth' => round($total, 2),
            'zakat_due'    => $total >= $nisab ? round($total * 0.025, 2) : 0,
            'is_eligible'  => $total >= $nisab,
            'prices'       => $prices,
        ];
    }
}

11. Imam Meal Tracker (Pro Only)

11.1 MealRotationService

// app/Services/MealRotationService.php
class MealRotationService
{
    public function advanceDaily(): void
    {
        MealRotation::where('status', 'active')->each(function (MealRotation $rotation) {
            $members = MealRotationMember::where('rotation_id', $rotation->id)
                ->where('is_active', true)->orderBy('position')->get();
            $slots   = $rotation->meal_slots;
            $total   = $members->count();
            $pointer = $rotation->current_pointer;

            foreach ($slots as $slot) {
                $member = $members[$pointer % $total];

                MealDailyLog::firstOrCreate(
                    ['rotation_id' => $rotation->id, 'date' => today(), 'meal_type' => $slot],
                    [
                        'mosque_id'          => $rotation->mosque_id,
                        'assigned_member_id' => $member->user_id,
                        'status'             => 'pending',
                    ]
                );

                SendWhatsAppMessage::dispatch(
                    $member->user->phone, 'meal_reminder',
                    ['meal' => $slot, 'date' => today()->toFormattedDayDateString()]
                )->delay(now()->setTime(8, 0));

                $pointer++;
            }

            $rotation->update(['current_pointer' => $pointer % $total]);
        });
    }
}

11.2 Web — Today's Meal Card (Blade snippet)

<div class="bg-white dark:bg-[#0e1912] border border-slate-200 dark:border-white/[0.06] rounded-xl p-5">
  <h3 class="text-sm font-semibold text-slate-900 dark:text-slate-100 mb-4">Today's Meal Rotation</h3>
  @foreach($todayLogs as $log)
    <div class="flex justify-between items-center py-2 border-b border-slate-100 dark:border-white/[0.04] last:border-0">
      <div class="flex items-center gap-3">
        <span class="px-2 py-0.5 rounded-full text-[10px] font-semibold bg-amber-500/10 text-amber-600 dark:text-amber-400 border border-amber-500/20">
          {{ ucfirst($log->meal_type) }}
        </span>
        <span class="text-sm text-slate-900 dark:text-slate-100">{{ $log->assignedMember->name }}</span>
      </div>
      @can('edit-meal-tracker')
      <select wire:change="updateStatus({{ $log->id }}, $event.target.value)"
              class="text-xs bg-slate-50 dark:bg-[#141f16] border border-slate-200 dark:border-white/[0.06] rounded-lg px-2 py-1 text-slate-900 dark:text-slate-100">
        <option value="pending">Pending</option>
        <option value="provided">Provided ✓</option>
        <option value="not_provided">Not Provided ✗</option>
        <option value="holiday">Holiday</option>
      </select>
      @endcan
    </div>
  @endforeach
</div>

12. Madrasa / Maktab Module (Pro Only)

12.1 Attendance Sheet (Volt)

{{-- resources/views/livewire/pages/madrasa/attendance.blade.php --}}
<?php
use Livewire\Attributes\Layout;
use Livewire\Volt\Component;
use App\Models\MadrasaStudent;
use App\Models\StudentAttendance;

new #[Layout('layouts.app')] class extends Component {

    public string $date;
    public array  $attendance = [];

    public function mount(): void
    {
        $this->authorize('view-madrasa');
        $this->date = today()->toDateString();
        $this->loadAttendance();
    }

    public function loadAttendance(): void
    {
        $students = MadrasaStudent::where('is_active', true)->get();
        $existing = StudentAttendance::where('date', $this->date)->pluck('status','student_id');
        $this->attendance = $students->mapWithKeys(
            fn($s) => [$s->id => $existing[$s->id] ?? 'present']
        )->toArray();
    }

    public function saveAttendance(): void
    {
        $this->authorize('edit-madrasa');
        foreach ($this->attendance as $studentId => $status) {
            StudentAttendance::updateOrCreate(
                ['student_id' => $studentId, 'date' => $this->date,
                 'mosque_id'  => app('current_mosque_id')],
                ['status' => $status]
            );
        }
        session()->flash('success', 'Attendance saved!');
    }

    public function with(): array
    {
        return [
            'students' => MadrasaStudent::where('is_active', true)->get(),
        ];
    }
};
?>

13. Event & Project Planner (Pro Only)

13.1 Event Model

// app/Models/Event.php
class Event extends Model
{
    use BelongsToMosque, SoftDeletes;

    protected $casts = ['assigned_to' => 'array'];

    public function getActualSpendAttribute(): float
    {
        return $this->expenses()->sum('amount');
    }

    public function getBudgetVarianceAttribute(): float
    {
        return $this->budget - $this->actual_spend;
    }

    public function expenses() { return $this->hasMany(Expense::class); }
}

13.2 Publish Event to Feed (Volt action)

public function publishToFeed(int $eventId): void
{
    $this->authorize('post-feed');
    $event = Event::findOrFail($eventId);
    FeedPost::create([
        'mosque_id'   => app('current_mosque_id'),
        'author_id'   => auth()->id(),
        'content'     => 'Event: '.$event->title.' on '.Carbon::parse($event->start_date)->format('d M Y'),
        'category'    => 'event',
        'status'      => 'approved',
        'approved_by' => auth()->id(),
        'approved_at' => now(),
    ]);
    session()->flash('success', 'Event published to Feed!');
}

14. WhatsApp Business API Integration

14.1 WhatsAppService

// app/Services/WhatsAppService.php
class WhatsAppService
{
    private string $apiUrl, $phoneNumberId, $token;

    public function __construct()
    {
        $this->apiUrl        = 'https://graph.facebook.com/v19.0';
        $this->phoneNumberId = config('services.whatsapp.phone_number_id');
        $this->token         = config('services.whatsapp.token');
    }

    public function sendTemplate(string $mobile, string $template, array $vars): bool
    {
        $res = Http::withToken($this->token)->post(
            "{$this->apiUrl}/{$this->phoneNumberId}/messages",
            [
                'messaging_product' => 'whatsapp',
                'to'                => preg_replace('/[^0-9]/', '', $mobile),
                'type'              => 'template',
                'template'          => [
                    'name'       => $template,
                    'language'   => ['code' => 'en'],
                    'components' => [[
                        'type'       => 'body',
                        'parameters' => array_map(
                            fn($v) => ['type' => 'text', 'text' => $v], $vars
                        ),
                    ]],
                ],
            ]
        );
        return $res->successful();
    }
}

14.2 WhatsApp Message Templates

Template Name Use Case Variables
donation_receipt PDF receipt after donation mosque_name, donor_name, amount, receipt_no
due_reminder Due reminder 3 days before & on date member_name, amount, due_date, mosque_name
meal_reminder Day-before reminder for meal duty member_name, meal_type, date
salary_paid Payslip notification staff_name, month, net_amount
madrasa_fee Monthly fee reminder to parent student_name, amount, month
trial_expiry 30-day plan expiry warning mosque_name, expiry_date

15. Payment Platform — Razorpay Route

The My Mosque platform operates as a payment aggregator. All donor payments (donations, dues, Madrasa fees) are collected by the platform's Razorpay account and then routed to individual mosque linked accounts using Razorpay Route (Transfers API). The platform retains a configurable commission before routing.

Money Flow:
  Donor ──pays──► Platform Razorpay Account (mymosque.in)
                          │
                          ├─ deduct platform_fee (e.g. 2%)
                          │
                          └─ Route Transfer ──► Mosque Linked Account (acc_XXXX)
                                                       │
                                                       └─ Razorpay Settlement ──► Mosque Bank Account

Platform subscription fees (monthly/annual plan charges) flow only to the platform account — no Route transfer is created for them.


15.1 Mosque Onboarding — Creating a Razorpay Linked Account

Every mosque must complete this one-time KYC step before it can receive online donations.

// app/Services/LinkedAccountService.php
class LinkedAccountService
{
    private Api $rp;

    public function __construct()
    {
        $this->rp = new Api(config('razorpay.key_id'), config('razorpay.key_secret'));
    }

    /**
     * Called when mosque admin submits the KYC form.
     * Creates a Razorpay Route Linked Account for the mosque.
     */
    public function createLinkedAccount(Mosque $mosque, array $kycData): Mosque
    {
        // kycData: business_name, business_type (ngo|trust|individual), pan,
        //          bank_account_number, bank_ifsc, beneficiary_name, email, phone

        $account = $this->rp->account->create([
            'email'        => $kycData['email'],
            'profile'      => [
                'category'    => 'not_for_profit',
                'subcategory' => 'religious',
                'addresses'   => [
                    'registered' => [
                        'street1' => $mosque->city,
                        'city'    => $mosque->city,
                        'state'   => $mosque->state,
                        'postal_code' => $mosque->pincode,
                        'country' => 'IN',
                    ],
                ],
            ],
            'type'         => 'route',
            'legal_business_name' => $kycData['business_name'],
            'business_type'       => $kycData['business_type'],
            'legal_info'   => ['pan' => $kycData['pan']],
            'contact_name' => $kycData['beneficiary_name'],
            'contact_info' => ['phone' => $kycData['phone']],
        ]);

        $mosque->update([
            'rp_account_id'     => $account['id'],
            'rp_account_status' => 'pending',
            'rp_kyc_details'    => json_encode($kycData),
        ]);

        // Add bank account to the linked account
        $this->addBankAccount($account['id'], $kycData);

        // Request product configuration (route settlements)
        $this->enableRouteSettlements($account['id']);

        return $mosque->fresh();
    }

    private function addBankAccount(string $accountId, array $kycData): void
    {
        $this->rp->stakeholder->create($accountId, [
            'name'  => $kycData['beneficiary_name'],
            'email' => $kycData['email'],
            'phone' => ['primary' => $kycData['phone']],
        ]);

        // Add bank account
        Http::withBasicAuth(config('razorpay.key_id'), config('razorpay.key_secret'))
            ->post("https://api.razorpay.com/v2/accounts/{$accountId}/bank_account", [
                'ifsc_code'        => $kycData['bank_ifsc'],
                'beneficiary_name' => $kycData['beneficiary_name'],
                'account_number'   => $kycData['bank_account_number'],
                'account_type'     => 'savings',
            ]);
    }

    private function enableRouteSettlements(string $accountId): void
    {
        Http::withBasicAuth(config('razorpay.key_id'), config('razorpay.key_secret'))
            ->patch("https://api.razorpay.com/v2/accounts/{$accountId}/products/route", [
                'settlements' => ['account_number' => null], // use bank account added above
                'tnc_accepted' => true,
            ]);
    }

    /**
     * Webhook: account.activated → mosque can now receive transfers.
     */
    public function handleAccountActivated(string $accountId): void
    {
        Mosque::where('rp_account_id', $accountId)->update([
            'rp_account_status'     => 'activated',
            'rp_settlements_enabled'=> true,
        ]);
    }
}

15.2 Mosque KYC Volt Page

{{-- resources/views/livewire/pages/settings/payment-setup.blade.php --}}
<?php
use Livewire\Attributes\Layout;
use Livewire\Volt\Component;
use App\Services\LinkedAccountService;

new #[Layout('layouts.app')] class extends Component {

    public string $businessName = '', $businessType = 'trust', $pan = '',
                  $bankAccount = '', $bankIfsc = '', $beneficiaryName = '',
                  $kycEmail = '', $kycPhone = '';

    public function mount(): void
    {
        $this->authorize('manage-mosque-settings');
        $mosque = auth()->user()->mosque;
        if ($mosque->rp_account_status !== 'not_created') {
            $this->addError('general', 'Linked account already '.$mosque->rp_account_status.'.');
        }
    }

    public function submit(): void
    {
        $this->validate([
            'businessName'    => 'required|string|max:120',
            'businessType'    => 'required|in:ngo,trust,individual',
            'pan'             => ['required', 'regex:/^[A-Z]{5}[0-9]{4}[A-Z]{1}$/'],
            'bankAccount'     => 'required|string|min:9|max:18',
            'bankIfsc'        => ['required', 'regex:/^[A-Z]{4}0[A-Z0-9]{6}$/'],
            'beneficiaryName' => 'required|string|max:120',
            'kycEmail'        => 'required|email',
            'kycPhone'        => 'required|digits:10',
        ]);

        app(LinkedAccountService::class)->createLinkedAccount(
            auth()->user()->mosque,
            [
                'business_name'       => $this->businessName,
                'business_type'       => $this->businessType,
                'pan'                 => strtoupper($this->pan),
                'bank_account_number' => $this->bankAccount,
                'bank_ifsc'           => strtoupper($this->bankIfsc),
                'beneficiary_name'    => $this->beneficiaryName,
                'email'               => $this->kycEmail,
                'phone'               => $this->kycPhone,
            ]
        );

        session()->flash('success', 'KYC submitted! Razorpay will review within 2 business days.');
    }
};
?>

<div>
  <h1 class="text-xl font-bold text-slate-900 dark:text-slate-100 mb-2">Payment Setup</h1>
  <p class="text-sm text-slate-400 mb-6">
    Complete KYC to receive online donations directly in your bank account.
    Razorpay reviews your application within 2 business days.
  </p>

  @php $mosque = auth()->user()->mosque; @endphp

  {{-- Status banner --}}
  @if($mosque->rp_account_status !== 'not_created')
  <div class="mb-6 px-4 py-3 rounded-xl border
    {{ $mosque->rp_account_status === 'activated'
       ? 'bg-emerald-50 dark:bg-emerald-900/20 border-emerald-200 dark:border-emerald-500/20 text-emerald-700 dark:text-emerald-400'
       : 'bg-amber-50 dark:bg-amber-900/10 border-amber-300/50 text-amber-700 dark:text-amber-400' }}
    text-sm">
    Account status: <strong class="capitalize">{{ $mosque->rp_account_status }}</strong>
    @if($mosque->rp_settlements_enabled) — Online donations are active. @endif
  </div>
  @else
  <form wire:submit="submit" class="space-y-4 max-w-xl">
    {{-- ... form fields following the existing input style ... --}}
    <button type="submit"
            class="px-5 py-2.5 bg-emerald-500 hover:bg-emerald-600 text-white text-sm font-semibold rounded-lg transition-colors">
      Submit KYC
    </button>
  </form>
  @endif
</div>

15.3 Platform Commission & Mosque Transfer Dashboard

Super-admins and mosque-admins can see transfer history in their dashboard.

// Volt page: pages/admin/transfers.blade.php
new #[Layout('layouts.app')] class extends Component {
    public function with(): array
    {
        $isSuperAdmin = auth()->user()->hasRole('super-admin');

        return [
            'transfers' => MosqueTransfer::with(['mosque','paymentOrder'])
                ->when(!$isSuperAdmin, fn($q) => $q->where('mosque_id', app('current_mosque_id')))
                ->latest()
                ->paginate(20),

            'summary' => [
                'total_collected'   => PaymentOrder::where('status','paid')
                    ->when(!$isSuperAdmin, fn($q) => $q->where('mosque_id', app('current_mosque_id')))
                    ->sum('amount'),
                'platform_fee_total'=> PaymentOrder::where('status','paid')
                    ->when(!$isSuperAdmin, fn($q) => $q->where('mosque_id', app('current_mosque_id')))
                    ->sum('platform_fee'),
                'routed_total'      => MosqueTransfer::where('status','processed')
                    ->when(!$isSuperAdmin, fn($q) => $q->where('mosque_id', app('current_mosque_id')))
                    ->sum('amount'),
            ],
        ];
    }
};

15.4 Linked Account Webhook Events

All Razorpay webhooks go to a single endpoint. The WebhookController (Section 4.7) handles both payment and Route events.

Razorpay Event Action
payment.captured Mark payment_orders.status = paid, dispatch CreateDonationAfterPayment
payment.failed Mark payment_orders.status = failed
transfer.processed Mark mosque_transfers.status = processed, set settled_at
transfer.failed Mark mosque_transfers.status = failed, notify super-admin
account.activated Set rp_account_status = activated, rp_settlements_enabled = true
account.suspended Set rp_account_status = suspended, pause online donations for mosque
subscription.activated Upgrade mosque plan (subscription billing)
subscription.cancelled Downgrade mosque plan to free

16. Subscription Billing

Mosque plan subscriptions (Standard / Pro) are billed separately through the platform's Razorpay account using Razorpay Subscriptions. No Route transfer is created — the full amount is platform revenue.

16.1 Subscription Controller

// app/Http/Controllers/Api/V1/BillingController.php
class BillingController extends Controller
{
    public function createSubscription(Request $request): JsonResponse
    {
        $this->authorize('manage-billing');
        $request->validate(['plan' => 'required|in:standard,pro', 'cycle' => 'required|in:monthly,annual']);

        $planId = config('razorpay.plans.'.$request->plan.'.'.$request->cycle);
        $api    = new Api(config('razorpay.key_id'), config('razorpay.key_secret'));

        $sub = $api->subscription->create([
            'plan_id'         => $planId,
            'total_count'     => $request->cycle === 'annual' ? 1 : 12,
            'customer_notify' => 1,
            'notes'           => [
                'mosque_id'   => app('current_mosque_id'),
                'plan'        => $request->plan,
                'cycle'       => $request->cycle,
            ],
        ]);

        return response()->json([
            'subscription_id' => $sub->id,
            'key'             => config('razorpay.key_id'),
        ]);
    }

    public function cancelSubscription(Request $request): JsonResponse
    {
        $this->authorize('manage-billing');
        $sub = Subscription::where('mosque_id', app('current_mosque_id'))
            ->where('status', 'active')->firstOrFail();

        $api = new Api(config('razorpay.key_id'), config('razorpay.key_secret'));
        $api->subscription->fetch($sub->gateway_subscription_id)->cancel(['cancel_at_cycle_end' => 1]);

        $sub->update(['status' => 'cancelled']);
        return response()->json(['cancelled' => true]);
    }
}

16.2 Plan Gate Blade Component (unchanged)

{{-- <x-plan-gate :required="'standard'"> ... </x-plan-gate> --}}
@php
  $mosque  = auth()->user()->mosque;
  $allowed = match($required) {
    'standard' => in_array($mosque->plan, ['standard','pro']),
    'pro'      => $mosque->plan === 'pro',
    default    => true,
  };
@endphp
@if($allowed) {{ $slot }}
@else
<div class="rounded-xl border border-amber-300/50 bg-amber-50 dark:bg-amber-900/10 p-4 text-center">
  <p class="font-medium text-amber-800 dark:text-amber-400">
    This feature requires the <strong>{{ ucfirst($required) }}</strong> plan.
  </p>
  <a href="{{ route('billing.plans') }}" class="mt-2 inline-block px-4 py-2 bg-amber-500 hover:bg-amber-600 text-white text-sm font-semibold rounded-lg transition-colors">
    Upgrade Now →
  </a>
</div>
@endif

16.3 Plan Pricing

Plan Monthly Annual Includes
Free ₹0 ₹0 Donations, Members, Feed, Prayer Times, Zakat
Standard ₹499/mo ₹4,999/yr + Expenses, Payroll, Member Dues, WhatsApp, Reports
Pro ₹999/mo ₹9,999/yr + Madrasa, Events, Meal Tracker, Advanced Reports

Platform commission on all online payments: 2% per transaction (configurable per mosque via mosques.platform_fee_percent).


17. Security & Audit Trail

16.1 Model Observer — Auto Audit Logging

// app/Observers/AuditObserver.php
class AuditObserver
{
    public function created(Model $model): void  { $this->log('created', $model); }
    public function updated(Model $model): void  { $this->log('updated', $model); }
    public function deleted(Model $model): void  { $this->log('deleted', $model); }
    public function restored(Model $model): void { $this->log('restored', $model); }

    private function log(string $action, Model $model): void
    {
        AuditLog::create([
            'mosque_id'  => $model->mosque_id ?? app('current_mosque_id'),
            'user_id'    => auth()->id(),
            'action'     => $action,
            'entity'     => class_basename($model),
            'entity_id'  => $model->id,
            'old_values' => $action === 'updated' ? $model->getOriginal() : null,
            'new_values' => $action === 'deleted'  ? null : $model->getAttributes(),
            'ip_address' => request()->ip(),
        ]);
    }
}

// Register in AppServiceProvider::boot()
foreach ([Donation::class, Expense::class, Staff::class,
          Salary::class, MemberDue::class, FeedPost::class] as $model) {
    $model::observe(AuditObserver::class);
}

18. Web Dashboard — Layout & Screens

18.1 Existing Layout (layouts/app.blade.php)

The base application ships with a production-ready layout: collapsible sidebar (fixed w-60, dark bg-[#0b1510]), sticky top bar with theme switcher + notification bell, and a main scrollable content area. All mosque modules are added as new Volt pages that slot into {{ $slot }}.

layouts/app.blade.php
├── <aside> Sidebar
│   ├── Logo → /dashboard
│   ├── Overview section
│   │   └── Dashboard link
│   ├── Management section  (hasAnyRole super-admin|mosque-admin)
│   │   ├── Users
│   │   ├── Roles & Permissions
│   │   ├── Members           ← add
│   │   ├── Donations         ← add
│   │   ├── Expenses          ← add
│   │   ├── Staff / Payroll   ← add
│   │   ├── Member Dues       ← add
│   │   ├── Feed              ← add
│   │   ├── Prayer Times      ← add
│   │   ├── Reports           ← add
│   │   └── Pro features...   ← add (gated)
│   └── Account section
│       ├── Notifications
│       └── My Profile
└── <main> → {{ $slot }}

18.2 Dashboard Widgets (pages/dashboard.blade.php)

Extend the existing dashboard component to show mosque-specific stats alongside the existing Users / Roles / Permissions cards.

new #[Layout('layouts.app')] class extends Component {
    public function with(): array
    {
        $mosqueId = auth()->user()->mosque_id;
        return [
            // Existing (kept as-is)
            'userCount'       => User::where('mosque_id', $mosqueId)->count(),
            'roleCount'        => Role::count(),
            'permissionCount' => Permission::count(),
            'recentUsers'     => User::with('roles')->where('mosque_id', $mosqueId)->latest()->take(5)->get(),

            // Mosque-specific stats (add below)
            'donationToday'   => Donation::whereDate('date', today())->sum('amount'),
            'expenseMonth'    => Expense::whereMonth('date', now()->month)->sum('amount'),
            'pendingDues'     => MemberDue::where('status', 'pending')->count(),
            'memberCount'     => User::where('mosque_id', $mosqueId)->role('member')->count(),
            'prayerTimes'     => PrayerTime::where('effective_from', '<=', today())
                ->where(fn($q) => $q->whereNull('effective_until')->orWhere('effective_until', '>=', today()))
                ->latest('effective_from')->first(),
        ];
    }
};

18.3 Full Web Screen Map

Screen Route Volt Component
Login GET /login pages/auth/login (base starter)
Register GET /register pages/auth/register (base starter)
Dashboard GET /dashboard pages/dashboard (extend base)
Users GET /admin/users pages/users/index (base starter)
Roles & Perms GET /admin/roles pages/roles/index (base starter)
Notifications GET /notifications pages/notifications/index (base starter)
Profile GET /profile profile (base starter)
Members GET /admin/members pages/members/index
Donations GET /admin/donations pages/donations/index
Expenses GET /admin/expenses pages/expenses/index
Staff GET /admin/staff pages/staff/index
Payroll GET /admin/payroll pages/staff/payroll
Member Dues GET /admin/dues pages/dues/index
Community Feed GET /admin/feed pages/feed/index
Feed Moderation GET /admin/feed/pending pages/feed/moderation
Prayer Times GET /admin/settings/prayer-times pages/settings/prayer-times
Zakat Calculator GET /zakat pages/zakat/calculator
Reports GET /admin/reports pages/reports/index
Audit Logs GET /admin/audit pages/settings/audit
Madrasa GET /admin/madrasa pages/madrasa/index (Pro)
Events GET /admin/events pages/events/index (Pro)
Meal Tracker GET /admin/meal-tracker pages/meal-tracker/index (Pro)
Billing GET /billing pages/billing/plans
Super Admin GET /super-admin pages/super-admin/dashboard

18.4 UI Design Reference

All mosque module pages follow the exact same UI patterns as the existing users/index and roles/index pages:

Element Class / Pattern
Page card bg-white dark:bg-[#0e1912] border border-slate-200 dark:border-white/[0.06] rounded-xl
Primary button bg-emerald-500 hover:bg-emerald-600 text-white text-sm font-semibold rounded-lg
Danger button hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-500/10
Role badge px-2 py-0.5 rounded-full text-[10px] font-semibold bg-emerald-500/10 text-emerald-600 border border-emerald-500/20
Section label text-[10px] font-semibold text-slate-400 uppercase tracking-widest
Input bg-slate-50 dark:bg-[#141f16] border border-slate-200 dark:border-white/[0.06] rounded-lg focus:ring-2 focus:ring-emerald-500/30
Table header border-b bg-slate-50 dark:bg-[#141f16]/60 text-xs font-semibold uppercase tracking-widest text-slate-400
Flash success bg-emerald-50 dark:bg-emerald-900/20 border-emerald-200 text-emerald-700
Flash error bg-red-50 dark:bg-red-900/20 border-red-200 text-red-700
Pro badge bg-amber-500/10 text-amber-600 border border-amber-500/20
Font DM Sans (body) · Syne (headings) — loaded in app.blade.php
Primary colour emerald-500 #10b981
Dark bg #080f0a (page) · #0b1510 (sidebar) · #0e1912 (cards)

19. Flutter App — Architecture & Screen Map

19.1 GoRouter Route Map

// lib/core/router/app_router.dart
final appRouter = GoRouter(
  redirect: (ctx, state) {
    final isLoggedIn  = ref.read(authProvider).isAuthenticated;
    final isAuthRoute = state.matchedLocation.startsWith('/auth');
    if (!isLoggedIn && !isAuthRoute) return '/auth/login';
    if (isLoggedIn  &&  isAuthRoute) return '/home';
    return null;
  },
  routes: [
    GoRoute(path: '/splash',     builder: (ctx, s) => SplashScreen()),
    ShellRoute(
      builder: (ctx, s, child) => MainShell(child: child),
      routes: [
        GoRoute(path: '/home',                  builder: (ctx, s) => HomeScreen()),
        GoRoute(path: '/donations',             builder: (ctx, s) => DonationListScreen()),
        GoRoute(path: '/donations/add',         builder: (ctx, s) => DonationFormScreen()),
        GoRoute(path: '/donations/:id',         builder: (ctx, s) => DonationDetailScreen(id: s.pathParameters['id']!)),
        GoRoute(path: '/members',               builder: (ctx, s) => MemberListScreen()),
        GoRoute(path: '/feed',                  builder: (ctx, s) => FeedScreen()),
        GoRoute(path: '/feed/create',           builder: (ctx, s) => CreatePostScreen()),
        GoRoute(path: '/prayer-times',          builder: (ctx, s) => PrayerTimesScreen()),
        GoRoute(path: '/settings/azan',         builder: (ctx, s) => AzanSettingsScreen()),
        GoRoute(path: '/zakat',                 builder: (ctx, s) => ZakatScreen()),
        GoRoute(path: '/notifications',         builder: (ctx, s) => NotificationsScreen()),
        GoRoute(path: '/settings',              builder: (ctx, s) => SettingsScreen()),
      ],
    ),
    GoRoute(path: '/auth/login',   builder: (ctx, s) => LoginScreen()),
    GoRoute(path: '/auth/register',builder: (ctx, s) => RegisterScreen()),
  ],
);

19.2 Flutter Auth — Sanctum Token

The mobile app authenticates with an email + password (matching the web login) and receives a Sanctum token. Role data is returned with the user profile and used to show/hide screens client-side.

// lib/features/auth/auth_service.dart
class AuthService {
  final Dio _dio;
  AuthService(this._dio);

  Future<AuthResult> login(String email, String password) async {
    final res = await _dio.post('/api/v1/auth/login',
      data: {'email': email, 'password': password});
    await SecureStorage.write('auth_token', res.data['token']);
    return AuthResult.fromJson(res.data);  // includes user.roles[]
  }
}

class AuthInterceptor extends Interceptor {
  @override
  void onRequest(RequestOptions opts, RequestInterceptorHandler handler) async {
    final token = await SecureStorage.read('auth_token');
    if (token != null) opts.headers['Authorization'] = 'Bearer $token';
    handler.next(opts);
  }
}

19.3 Offline Sync — Hive Queue

// lib/services/offline_sync_service.dart
class OfflineSyncService {
  static late Box _queue;

  static Future<void> init() async {
    await Hive.initFlutter();
    _queue = await Hive.openBox('offline_queue');
  }

  static Future<void> enqueue(Map<String, dynamic> task) async => _queue.add(task);

  static Future<void> syncAll(Dio dio) async {
    for (final key in _queue.keys.toList()) {
      final task = _queue.get(key) as Map;
      try {
        switch (task['type']) {
          case 'donation': await dio.post('/api/v1/donations', data: task['data']); break;
        }
        await _queue.delete(key);
      } catch (e) { debugPrint('Sync failed for task $key: $e'); }
    }
  }
}

20. Multilingual Implementation

20.1 Laravel — JSON Language Files

// resources/lang/en.json
{
    "dashboard":    "Dashboard",
    "donations":    "Donations",
    "add_donation": "Add Donation",
    "donor_name":   "Donor Name",
    "amount":       "Amount",
    "prayer_times": "Prayer Times",
    "fajr": "Fajr", "dhuhr": "Dhuhr", "asr": "Asr",
    "maghrib": "Maghrib", "isha": "Isha",
    "zakat": "Zakat", "due": "Due",
    "members": "Members", "staff": "Staff"
}

// resources/lang/as.json  (Assamese)
{
    "dashboard":    "ড্যাছব'ৰ্ড",
    "donations":    "দান",
    "add_donation": "দান যোগ কৰক",
    "donor_name":   "দাতাৰ নাম",
    "prayer_times": "নামাজৰ সময়",
    "fajr": "ফজৰ", "dhuhr": "জহৰ", "asr": "আচৰ",
    "maghrib": "মাগৰিব", "isha": "ইছা"
}
// Switch language (Volt action)
public function setLanguage(string $lang): void
{
    App::setLocale($lang);
    session(['locale' => $lang]);
    auth()->user()->mosque->update(['language' => $lang]);
}

// Middleware — restore locale from mosque setting
App::setLocale(auth()->user()?->mosque?->language ?? 'en');

// In Blade templates
{{ __('donations') }}

21. Complete API Reference

Base URL: https://mymosque.in/api/v1/
Authentication: Authorization: Bearer {token} (Laravel Sanctum). The token encodes the user's role; the API enforces permissions via $this->authorize() calls backed by Spatie gates.

Method Endpoint Role Required Plan Description
POST /auth/login Free Email + password → Sanctum token
POST /auth/logout Any Free Invalidate token
GET /user Any Free Authenticated user profile + roles
GET /mosque/profile Any Free Get mosque details
PUT /mosque/profile mosque-admin Free Update mosque profile
GET /mosque/prayer-times Any Free Get current prayer times
PUT /mosque/prayer-times mosque-admin Free Update prayer times
POST /payments/create-order Any Free Create Razorpay order before checkout
POST /payments/verify Any Free Verify HMAC signature after payment
POST /payments/webhook Razorpay webhook (capture, transfer, subscription events)
GET /mosque/linked-account mosque-admin Free Linked account status + KYC progress
POST /mosque/linked-account mosque-admin Free Submit KYC, create Razorpay linked account
GET /mosque/transfers mosque-admin Free List Route transfers to mosque account
GET /donations staff+ Free List donations
POST /donations/cash staff+ Free Record cash/cheque donation (no online payment)
GET /donations/{id} staff+ Free Get single donation
PUT /donations/{id} mosque-admin Free Edit donation
DELETE /donations/{id} mosque-admin Free Soft delete donation
GET /donations/{id}/receipt mosque-admin Std+ Get PDF receipt URL
POST /donations/{id}/whatsapp mosque-admin Std+ Send receipt via WhatsApp
GET /expenses mosque-admin, staff Std+ List expenses
POST /expenses mosque-admin Std+ Create expense
PUT /expenses/{id} mosque-admin Std+ Edit expense
GET /staff mosque-admin Std+ List staff
POST /staff mosque-admin Std+ Add staff member
POST /payroll mosque-admin Std+ Process monthly salary
GET /dues mosque-admin Std+ List member dues
PUT /dues/{id} mosque-admin Std+ Record payment
POST /dues/remind mosque-admin Std+ Send bulk WhatsApp reminders
GET /members mosque-admin Free List members
POST /members mosque-admin Free Add member (creates User with member role)
PUT /members/{id} mosque-admin Free Edit member
DELETE /members/{id} mosque-admin Free Soft delete member
GET /committees mosque-admin Free List mosque committees (active + inactive)
POST /committees mosque-admin Free Create committee (name, description, established_date)
PUT /committees/{id} mosque-admin Free Edit committee name / description / status
DELETE /committees/{id} mosque-admin Free Deactivate (soft) committee
GET /committees/{id}/members mosque-admin Free List members + positions in a committee
POST /committees/{id}/members mosque-admin Free Add member to committee with position
PUT /committees/{id}/members/{userId} mosque-admin Free Change member's position
DELETE /committees/{id}/members/{userId} mosque-admin Free Remove member from committee
GET /feed Any Free Get approved feed posts
POST /feed Any Free Create post (status: pending)
PUT /feed/{id}/approve mosque-admin Free Approve post
PUT /feed/{id}/reject mosque-admin Free Reject post
POST /feed/{id}/react Any Free Add reaction
POST /zakat/calculate Any Free Calculate Zakat
GET /madrasa/students mosque-admin Pro List students
POST /madrasa/attendance mosque-admin Pro Mark student attendance
GET /events mosque-admin Pro List events
POST /events mosque-admin Pro Create event
GET /meal-rotation mosque-admin Pro Rotation + today's log
GET /reports/financial mosque-admin, staff Std+ Monthly/annual report
GET /billing/plans Any Free List available plans + pricing
POST /billing/subscribe mosque-admin Free Create Razorpay subscription (platform revenue)
DELETE /billing/subscribe mosque-admin Free Cancel active subscription
GET /audit-logs mosque-admin Free View audit trail
GET /notifications Any Free In-app notifications
POST /notifications/{id}/read Any Free Mark notification read

22. Scheduler Tasks & Queue Jobs

22.1 Scheduled Commands

Schedule Frequency Task
GenerateMonthlyDues 1st of month 00:00 Auto-create due entries for all member-role users
GenerateStudentFees 1st of month 00:01 Auto-create Madrasa fee entries
SendDueReminders Daily 09:00 WhatsApp reminders for dues due in 3 days and today
AdvanceMealRotation Daily 00:01 Compute tomorrow's meal assignments
SendMealReminders Daily 08:00 WhatsApp reminder to assigned member
ExpireFreeTrials Daily 00:05 Auto-lock features when plan expires
AutoBackup Daily 02:00 mysqldump → S3
FetchMetalPrices Hourly Cache gold/silver prices for Zakat
CleanAuditLogs Weekly Sunday 03:00 Delete audit logs older than 1 year
CleanRecycleBin Daily 03:00 Permanently delete soft-deleted records > 30 days

22.2 Queue Jobs

Job Class Queue Triggered By
CreateDonationAfterPayment payments payment.captured webhook or client verify
RouteTransferJob payments Dispatched by CreateDonationAfterPayment (online payments only)
GenerateDonationReceipt pdf Donation record created (online or cash)
GeneratePayslip pdf Monthly payroll processed
GenerateDueReceipt pdf Due payment recorded
SendWhatsAppMessage whatsapp Receipts, reminders, payslips, meal notifications
SendPushNotification notifications Feed post approved, new pending post
UploadToS3 storage Bill files, logos, photos

Run queues with: php artisan horizon — Redis-backed. Monitor at /horizon.


23. Deployment & DevOps

23.1 Server Setup

# Ubuntu 24 LTS · PHP-FPM 8.3 · MySQL 8 (RDS) · Redis (ElastiCache)
server {
    listen 443 ssl http2;
    server_name mymosque.in;
    root /var/www/mymosque/public;

    location / { try_files $uri $uri/ /index.php?$query_string; }
    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }

    add_header Strict-Transport-Security 'max-age=31536000' always;
    add_header X-Frame-Options SAMEORIGIN;
    add_header X-Content-Type-Options nosniff;
}

23.2 GitHub Actions CI/CD

name: Deploy to Production
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install PHP deps
        run: composer install --no-dev --optimize-autoloader
      - name: Run tests
        run: php artisan test --parallel
      - name: Deploy via SSH
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.PROD_HOST }}
          key:  ${{ secrets.PROD_SSH_KEY }}
          script: |
            cd /var/www/mymosque
            git pull origin main
            composer install --no-dev --optimize-autoloader
            php artisan migrate --force
            php artisan db:seed --class=RolesAndPermissionsSeeder --force
            php artisan config:cache
            php artisan route:cache
            php artisan view:cache
            php artisan queue:restart
            sudo systemctl reload php8.3-fpm

24. Environment Configuration (.env)

APP_NAME='My Mosque'
APP_ENV=production
APP_URL=https://mymosque.in

DB_CONNECTION=mysql
DB_HOST=mymosque-prod.rds.amazonaws.com
DB_PORT=3306
DB_DATABASE=mymosque
DB_USERNAME=mymosque_user
DB_PASSWORD=***

CACHE_DRIVER=redis
QUEUE_CONNECTION=redis
SESSION_DRIVER=redis
REDIS_HOST=mymosque-cache.elasticache.amazonaws.com
REDIS_PORT=6379

FILESYSTEM_DISK=s3
AWS_ACCESS_KEY_ID=***
AWS_SECRET_ACCESS_KEY=***
AWS_DEFAULT_REGION=ap-south-1
AWS_BUCKET=mymosque-prod
AWS_URL=https://cdn.mymosque.in

# WhatsApp Business API
WHATSAPP_TOKEN=***
WHATSAPP_PHONE_NUMBER_ID=***
WHATSAPP_WEBHOOK_VERIFY_TOKEN=***

# Firebase
FIREBASE_CREDENTIALS=/var/www/mymosque/storage/firebase-credentials.json

# Razorpay — Platform account (collects all payments)
RAZORPAY_KEY_ID=rzp_live_***
RAZORPAY_KEY_SECRET=***
RAZORPAY_WEBHOOK_SECRET=***

# Razorpay Route — default platform commission
RAZORPAY_PLATFORM_FEE_PERCENT=2.00

# Razorpay Subscription plans (mosque billing — platform revenue)
RAZORPAY_PLAN_STANDARD_MONTHLY=plan_***
RAZORPAY_PLAN_STANDARD_ANNUAL=plan_***
RAZORPAY_PLAN_PRO_MONTHLY=plan_***
RAZORPAY_PLAN_PRO_ANNUAL=plan_***

# Zakat
METAL_PRICE_API_KEY=***

# PDF
WKHTMLTOPDF_PATH=/usr/local/bin/wkhtmltopdf
WKHTMLTOIMAGE_PATH=/usr/local/bin/wkhtmltoimage

# Broadcasting
BROADCAST_DRIVER=pusher
PUSHER_APP_ID=***
PUSHER_APP_KEY=***
PUSHER_APP_SECRET=***
PUSHER_APP_CLUSTER=ap2

25. Developer Quick Reference

25.1 Key Artisan Commands

Command Purpose
php artisan migrate --seed Run all migrations + seed roles/permissions
php artisan db:seed --class=RolesAndPermissionsSeeder Re-seed roles & permissions only
php artisan horizon Start queue workers (keep alive via Supervisor)
php artisan schedule:run Trigger scheduler (add to cron: * * * * *)
php artisan test --parallel Run all tests in parallel
php artisan config:cache && route:cache && view:cache Production optimisation
php artisan queue:restart Restart Horizon workers after deploy
php artisan livewire:make pages/donations/index Generate new Volt page
php artisan make:observer DonationObserver --model=Donation Generate model observer
php artisan permission:cache-reset Clear Spatie permission cache after role changes

25.2 Adding a New Module (Checklist)

Follow the existing pages/users/index.blade.php and pages/roles/index.blade.php as the template for every new mosque module page.

1. php artisan make:migration create_<module>_table
2. php artisan make:model <Model> -f
3. Add 'view-<module>', 'create-<module>', 'edit-<module>', 'delete-<module>'
   permissions to RolesAndPermissionsSeeder and re-seed
4. Create resources/views/livewire/pages/<module>/index.blade.php
   - PHP class at top (new #[Layout('layouts.app')] class extends Component)
   - with() method for data, actions for mutations
   - Use @can('create-<module>') gates in the template
5. Add Volt::route() in routes/web.php under the correct middleware group
6. Add nav link to layouts/app.blade.php sidebar (under Management section)
7. Add route to API if Flutter access is needed

25.3 Key Packages — Laravel

Package Purpose
livewire/livewire ^3.0 Full-stack reactive components (Volt single-file)
spatie/laravel-permission Role & permission management (already installed)
laravel/sanctum SPA + mobile API token auth
laravel/horizon Redis queue dashboard
barryvdh/laravel-snappy PDF generation (wkhtmltopdf)
spatie/laravel-medialibrary File uploads with S3 support
razorpay/razorpay Razorpay PHP SDK — orders, transfers (Route), subscriptions, linked accounts
kreait/laravel-firebase Firebase Admin SDK (FCM push)
league/flysystem-aws-s3-v3 AWS S3 file storage
maatwebsite/excel Excel export

25.4 Key Packages — Flutter

Package Purpose
riverpod ^2.5 + flutter_riverpod State management
go_router ^13.0 Declarative navigation
dio ^5.4 HTTP client with interceptors
flutter_local_notifications Azan alarms (local, exact-time)
firebase_messaging FCM push notifications
just_audio Azan tone playback
hive_flutter Offline storage (prayer cache, offline queue)
flutter_secure_storage Secure token storage
connectivity_plus Network state monitoring
file_picker Bill/receipt file selection
flutter_pdfview In-app PDF receipt viewer
intl + flutter_localizations i18n / multilingual

25.5 Design Tokens

Token Value Usage
Primary Emerald #10b981 (emerald-500) Buttons, active nav, badges
Dark card bg #0e1912 Card backgrounds (dark mode)
Dark sidebar #0b1510 Sidebar background
Dark page #080f0a Page background (dark mode)
Amber / Pro amber-500/10 + amber-600 Pro badges, plan-gate banners
Body Font DM Sans 300/400/500/600 All UI text
Heading Font Syne 600/700 Page/section headings
Font (Arabic) Amiri / Scheherazade New RTL UI, Bismillah
Font (Bengali/Assamese) Noto Sans Bengali Regional script

25.6 Razorpay Route Integration Checklist

1. Dashboard → Settings → Razorpay (set RAZORPAY_KEY_ID / RAZORPAY_KEY_SECRET)
2. Enable Razorpay Route product on your platform account (dashboard.razorpay.com)
3. Set webhook URL: https://mymosque.in/api/v1/payments/webhook
   Events to enable: payment.captured, payment.failed,
                     transfer.processed, transfer.failed,
                     account.activated, account.suspended,
                     subscription.activated, subscription.cancelled
4. Mosque admin completes KYC via /admin/settings/payment-setup
5. Platform calls LinkedAccountService::createLinkedAccount()
6. Razorpay reviews (~2 business days) → fires account.activated webhook
7. mosque.rp_settlements_enabled = true → online donations now route automatically
8. Each donation payment: createOrder → checkout JS → verifyPayment → CreateDonationAfterPayment job → RouteTransferJob (transfer to mosque linked account)
9. Cash donations bypass online payment — use /donations/cash endpoint
10. Monitor transfers at /admin/transfers (mosque view) or super-admin dashboard

25.7 Role Summary

Role Who Key Permissions
super-admin Platform operator Everything — all mosques
mosque-admin Mosque secretary / treasurer Full mosque data, users, billing
staff Imam, muazzin, caretaker Create donations, view members, post feed
member General congregation View/post feed, view prayer times

Committee is not a Spatie role. It is a mosque-managed group of member-role users with named positions (president, vice-president, secretary, treasurer, general member). Position is a free-form string — no fixed enum. See §3.12 for the data model and §21 (Committee Management endpoints) for the API.


My Mosque · mymosque.in · Developer Guide v1.0 · June 2026 · Confidential


26. Development Phase Plan

Each phase below is a self-contained deliverable. Phases within the same tier can overlap, but cross-tier dependencies must be respected (e.g., Razorpay Route setup must precede online donation capture).


Phase 0 — Foundation ✅ (Completed)

Core infrastructure that every other feature depends on. Must be 100% done before any module work begins.

# Task Notes
0.1 Laravel 12 + Livewire 3 Volt + Tailwind + Alpine.js setup Starter kit
0.2 Spatie Laravel Permission — install & configure model_has_roles etc.
0.3 mosques table migration §3.1
0.4 mosque_id column added to users §3.2 — nullable FK, nullOnDelete
0.5 ResolveMosqueScope middleware registered §1.2
0.6 CheckMosquePlan middleware registered §1.3
0.7 SetLocale middleware — session-based lang §20
0.8 RolesAndPermissionsSeeder — 45 permissions, 4 roles §2.2
0.9 MosqueSeeder — 2 demo mosques (free + pro) Demo data
0.10 SuperAdminSeeder — platform super-admin mosque_id = null
0.11 DemoUsersSeeder — admin, staff, member per mosque All password: "password"
0.12 AppNotification in-app notification system Bell icon, database driver

Phase 1 — Auth & Public Onboarding ✅ (Completed)

Everything a mosque sees before they log in, plus the first-time registration flow.

# Task Notes
1.1 Welcome / landing page Tailwind, multilingual (en/hi/as/bn), mobile responsive
1.2 Language switcher (top bar) Session-based, 4 locales
1.3 layouts/guest.blade.php — two-panel auth shell Dark green left · cream right
1.4 Login page — email + password + remember me Rate-limited (5 attempts), eye toggle
1.5 Register Mosque — creates Mosque + mosque-admin user Free plan, unique slug
1.6 Forgot / Reset password pages Laravel built-in, restyled
1.7 Email verification page Resend button, 3-step guide
1.8 Confirm password page Secure-area guard

Phase 2 — Super Admin Platform ✅ (Completed)

The platform owner's control panel. Mosque admins cannot see these pages.

# Task Notes
2.1 Super Admin dashboard Platform-wide counts (mosques, users, revenue)
2.2 Mosque CRUD (admin/mosques) name, city, state, plan, status, waqf no.
2.3 Users management (admin/users) Mosque-scoped list; mosque selector for super-admin
2.4 Roles & permissions management (admin/roles) View / assign permissions
2.5 Send notification to any user Custom title, body, icon, color, action URL
2.6 layouts/app.blade.php — authenticated shell Sidebar, topbar, notification bell, theme switcher

Phase 3 — Mosque Dashboard & Members (Free)

The core daily-use screens for every mosque, available on all plans.

# Task Notes
3.1 Mosque Dashboard — role-aware stats Donation totals, member count, upcoming prayers
3.2 Mosque Settings — profile, logo, timezone manage-mosque-settings permission gate
3.3 Member management (admin/members) CRUD, photo, phone, address, joined date
3.4 Committee management Create committee, assign member-role users + positions (free-form)
3.5 Prayer Times management Set azan + iqamah per salah, effective date range
3.6 Audit Log viewer (admin/audit) Paginated table, filter by entity/action

Phase 4 — Donations Module (Free Forever)

Cash donations can be added immediately. Online payments require Phase 6 (Razorpay) to be wired up first.

# Task Notes
4.1 Donation list + search + filters By type, date range, payment mode
4.2 Record cash donation (manual) donor name, amount, type, receipt number
4.3 Donation receipt PDF Laravel Snappy / wkhtmltopdf
4.4 WhatsApp receipt (optional at this stage) Phase 9 dependency
4.5 Online donation checkout (Razorpay) Requires Phase 6 linked-account setup
4.6 Donation summary / totals widget Dashboard card

Phase 5 — Community Feed & Zakat Calculator (Free)

Low-dependency features; can be built in parallel with Phase 4.

# Task Notes
5.1 Feed list — all approved posts Dua, hadith, announcement, reminder, general
5.2 Create post — member / staff Text + optional media URL
5.3 Feed moderation queue (admin/feed/pending) Approve / reject; moderate-feed permission
5.4 Reactions — Ameen / MashaAllah / JazakAllah One reaction per user per post
5.5 Pin post mosque-admin only
5.6 Zakat Calculator (/zakat) Nisab check, hawl, gold/silver/cash/stock inputs; no login required

Phase 6 — Razorpay Route (Payment Platform)

Must be completed before online donations, due payments, or madrasa fees go live.

# Task Notes
6.1 Razorpay linked-account creation for mosque rp_account_id, KYC details stored in mosques
6.2 PaymentController::createOrder Platform Razorpay account, paise conversion
6.3 PaymentController::verifyPayment HMAC signature check
6.4 RouteTransferJob — platform → mosque transfer Deducts platform_fee_percent
6.5 payment_orders table & model §3.3
6.6 mosque_transfers table & model §3.4
6.7 Webhook endpoint — razorpay.webhook payment.captured, transfer.processed events
6.8 Transfer log viewer (super-admin) Settlement status, reversal tracking

Phase 7 — Standard Plan: Expenses, Staff & Payroll

Requires plan:standard middleware gate on all routes.

# Task Notes
7.1 Expense list + filters By category, date, payment mode
7.2 Add / edit expense Bill upload (S3), approved_by, event link
7.3 Staff CRUD (admin/staff) Role (imam / muazzin / caretaker / teacher / other), salary
7.4 Monthly payroll processing Generate salary record, mark paid, payslip PDF
7.5 Advance deduction from payroll Deducted from net_paid
7.6 Finance report — P&L summary Donations in vs expenses out, by month

Phase 8 — Standard Plan: Member Dues & WhatsApp

# Task Notes
8.1 Member Due creation — bulk or individual Amount, due_date, member_id
8.2 Record due payment — full / partial Updates paid_amount, computed pending_balance
8.3 Due status dashboard Pending / paid / partial / waived breakdown
8.4 Online due payment via Razorpay Phase 6 prerequisite
8.5 WhatsApp Business API integration Cloud API, template messages
8.6 SendWhatsAppMessage job Queue-based, retry on failure
8.7 Auto WhatsApp reminder — overdue dues Scheduler task (daily at 9 AM)
8.8 WhatsApp receipt on donation capture Triggered from verifyPayment

Phase 9 — Reports & Audit

# Task Notes
9.1 Reports dashboard (admin/reports) Date-range picker, export to PDF/CSV
9.2 Donation report — by type, donor, date view-reports permission
9.3 Expense report — by category, period Total outflow breakdown
9.4 Payroll report — staff cost per month
9.5 Due collection report — recovery rate
9.6 AuditLog model + observer Auto-log create/update/delete on all tenant models

Phase 10 — Subscription & Billing

# Task Notes
10.1 Plans page (/billing) Free / Standard / Pro feature comparison
10.2 Razorpay subscription — create & activate gateway_subscription_id
10.3 Webhook — subscription.activated Set plan, plan_expiry on mosque
10.4 Webhook — subscription.cancelled / expired Downgrade mosque to Free
10.5 Plan expiry reminder scheduler 7 days, 3 days, day-of emails
10.6 Billing history page Past invoices, payment IDs
10.7 Upgrade / downgrade flow Proration handled by Razorpay

Phase 11 — Pro Plan Features

Requires plan:pro middleware gate. All three modules can be built in parallel.

11A — Madrasa / Maktab Module

# Task Notes
11A.1 Student CRUD (admin/madrasa) DOB, father name, class, enrolment date
11A.2 Monthly fee management Mark paid / unpaid, generate fee receipt
11A.3 Attendance tracking Daily presence per student
11A.4 Madrasa summary report Headcount, fee collection rate

11B — Event & Project Planner

# Task Notes
11B.1 Event CRUD (admin/events) Type, dates, budget, status, assigned_to JSON
11B.2 Event expenses — link expense → event event_id FK on expenses
11B.3 Event budget tracker Spent vs budgeted
11B.4 Event-linked donation Purpose = event in payment_orders

11C — Imam Meal Tracker

# Task Notes
11C.1 Meal rotation CRUD (admin/meal-tracker) Assign host family per date
11C.2 Rotation algorithm Auto-distribute fairly by frequency
11C.3 WhatsApp reminder to host Day-before reminder via job
11C.4 Meal history log Full calendar view

Phase 12 — Flutter Mobile App (Android-first, iOS Phase 4)

Build after the web API (§21) is stable. All screens consume GET /api/v1/… Sanctum-authenticated endpoints.

# Screen / Feature Plan
12.1 Project setup — GoRouter + Riverpod + Dio + SecureStorage All
12.2 Splash + onboarding All
12.3 Login + Sanctum token auth All
12.4 Home dashboard — role-aware cards All
12.5 Donation list + add donation Free
12.6 Donation detail + receipt PDF viewer Free
12.7 Member list + profile view Free
12.8 Community Feed — view + react + create post Free
12.9 Prayer Times screen Free
12.10 Azan alarm settings (local notifications) Free
12.11 Zakat Calculator Free
12.12 Notifications screen All
12.13 FCM push notification integration All
12.14 Online donation — Razorpay Flutter SDK Free
12.15 Member Dues view + pay online Standard
12.16 Settings + profile edit + logout All
12.17 Madrasa fee view (student parent) Pro
12.18 Event listing Pro
12.19 iOS build + App Store submission Phase 4

Phase 13 — Production & DevOps

# Task Notes
13.1 Redis — session, cache, queue driver CACHE_DRIVER=redis, QUEUE_CONNECTION=redis
13.2 Laravel Horizon — queue monitoring dashboard Worker config, Supervisor
13.3 AWS S3 / Cloudinary — file storage Bills, logos, receipts
13.4 Scheduler tasks — due reminders, meal reminders, plan expiry app/Console/Kernel.php or routes/console.php
13.5 CI/CD pipeline — GitHub Actions Test → build → deploy on merge to main
13.6 Staging environment — separate DB .env.staging, seeded with demo data
13.7 wkhtmltopdf / Laravel Snappy — PDF server setup Donation receipts, payslips
13.8 Pusher / Reverb — real-time notifications Laravel Echo, WebSocket broadcast
13.9 Firebase Cloud Messaging — Flutter push firebase_messaging package
13.10 Production environment hardening APP_DEBUG=false, rate limits, HTTPS-only, CORS
13.11 Monitoring — Sentry (errors) + UptimeRobot Alert on 5xx, queue failures

Dependency Map

Phase 0 (Foundation)
  └─ Phase 1 (Auth)
       └─ Phase 2 (Super Admin)
            └─ Phase 3 (Dashboard & Members)
                 ├─ Phase 4 (Donations — cash)
                 │    └─ Phase 6 (Razorpay) → Phase 4 (online) → Phase 8.4 (online dues)
                 ├─ Phase 5 (Feed + Zakat)
                 ├─ Phase 7 (Expenses, Staff, Payroll)  [plan:standard]
                 ├─ Phase 8 (Dues + WhatsApp)           [plan:standard]
                 ├─ Phase 9 (Reports + Audit)
                 └─ Phase 10 (Billing)
                       └─ Phase 11A (Madrasa)           [plan:pro]
                       └─ Phase 11B (Events)            [plan:pro]
                       └─ Phase 11C (Meal Tracker)      [plan:pro]
Phase 0–9 stable API → Phase 12 (Flutter)
Phase 12 production-ready → Phase 13 (DevOps)

Progress Tracker

Phase Name Status
0 Foundation ✅ Complete
1 Auth & Onboarding ✅ Complete
2 Super Admin Platform ✅ Complete
3 Dashboard & Members 🔲 Not started
4 Donations 🔲 Not started
5 Feed & Zakat 🔲 Not started
6 Razorpay Route 🔲 Not started
7 Expenses, Staff & Payroll 🔲 Not started
8 Member Dues & WhatsApp 🔲 Not started
9 Reports & Audit 🔲 Not started
10 Subscription & Billing 🔲 Not started
11A Madrasa Module 🔲 Not started
11B Event & Project Planner 🔲 Not started
11C Imam Meal Tracker 🔲 Not started
12 Flutter Mobile App 🔲 Not started
13 Production & DevOps 🔲 Not started

My Mosque · mymosque.in · Developer Guide v1.0 · June 2026 · Confidential