Developer Guide
My Mosque — Implementation Reference
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 |
| Laravel Snappy (wkhtmltopdf) | |
| Push Notifications | Firebase Cloud Messaging |
| Payments | Razorpay Route (marketplace) — platform collects, routes to mosque linked accounts |
| WhatsApp Business Cloud API | |
| Storage | AWS S3 / Cloudinary |
| Caching | Redis (session, queue, rate-limit) |
Table of Contents
- Project Architecture & Setup
- Role-Based Authentication
- Database Schema
- Donation Management
- Expense Tracking
- Staff Payroll Management
- Member Due Tracker
- My Mosque Feed
- Prayer Times & Azan Alarm
- Zakat Calculator
- Imam Meal Tracker
- Madrasa / Maktab Module
- Event & Project Planner
- WhatsApp Integration
- Payment Platform — Razorpay Route
- Subscription Billing
- Security & Audit Trail
- Web Dashboard — Layout & Screens
- Flutter App — Architecture & Screen Map
- Multilingual Implementation
- Complete API Reference
- Scheduler Tasks & Queue Jobs
- Deployment & DevOps
- Environment Configuration
- 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 |
Donation record created (online or cash) | |
GeneratePayslip |
Monthly payroll processed | |
GenerateDueReceipt |
Due payment recorded | |
SendWhatsAppMessage |
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