Complete Handover Document – UMPSA SuperApp APIs

This document is your complete end-to-end onboarding + technical guide for the microservice used by the UMPSA SuperApp. It is written for developers who are new to Laravel APIs and microservices, so it explains both concepts and concrete patterns from this project.


Table of Contents

  1. REST API Basics & How Mobile Uses This Backend
  2. Microservice Architecture & This Project’s Role
  3. Global Application Setup (AppServiceProvider)
  4. Middleware – JSON, Auth, Roles
  5. Routing Conventions & Versioning
  6. Controller Patterns – Skeletons
  7. Traits: ApiResponse & CommonFunction
  8. Error Handling – ApiException & Handler
  9. Models, DB Connections & Oracle Schemas
  10. Requests (Validation) & Resources (Transformers)
  11. Docker Setup & Local Development
  12. Environments, URLs & Deployment (2Q & UMP Staging)
  13. Your API Development Workflow (Step-By-Step)
  14. People & Support Contacts
  15. Final Checklist for New Developers

1. REST API Basics & How Mobile Uses This Backend

1.1 What is a REST API?

1.2 How the SuperApp Interacts with This API

Mobile only cares about the contract: request format + response JSON shape. Our job is to keep that contract stable and clear.


2. Microservice Architecture & This Project’s Role

2.1 Microservices Overview

The UMPSA backend is split into multiple Laravel API projects:

Each microservice will share similar patterns:


3. Global Application Setup (AppServiceProvider)

File: app/Providers/AppServiceProvider.php

This provider configures cross-cutting concerns that affect all APIs:

public function boot(): void
{
    // Use custom token model/table for Sanctum (Oracle WEBAPP_TOKENS)
    Sanctum::usePersonalAccessTokenModel(OraclePersonalAccessToken::class);

    // Rate limiting
    RateLimiter::for('api', function (Request $request) {
        // Skip for local development
        if (config('app.env') === 'local') {
            return;
        }
        return Limit::perMinute(60)
            ->by($request->user()?->id ?: $request->ip())
            ->response(function (Request $request) {
                return $this->tooManyRequests();
            });
    });

    // Log SQL queries
    DB::listen(function ($query) {
        Log::info([
            'sql'      => $query->sql,
            'bindings' => $query->bindings,
            'time'     => $query->time.'ms'
        ]);
    });

    $this->setupLogViewer();
    $this->setupScramble();
}

3.1 Sanctum Custom Token Model

3.2 API Rate Limiting (throttle:api)

3.3 SQL Query Logging

3.4 Log Viewer & API Docs (Scramble)


4. Middleware – JSON, Auth, Roles

4.1 ForceJsonRequestHeader – Force JSON Responses

File: app/Http/Middleware/ForceJsonRequestHeader.php

public function handle(Request $request, Closure $next): Response
{
    $request->headers->set('Accept', 'application/json');
    return $next($request);
}

4.2 auth:sanctum – Required Authentication

Used for endpoints that must have a logged-in user.

Route::prefix('/v1/student')->middleware(['throttle:api','auth:sanctum'])->group(function () {

    // Profile
    Route::get('/profile', [ProfileController::class, 'getStudentProfile']);
    Route::put('/profile/edit', [ProfileController::class, 'editStudentProfile']);
    Route::get('/family', [ProfileController::class, 'getStudentFamily']);
    Route::post('/family/create', [ProfileController::class, 'createFamilyMember']);
    Route::put('/family/edit/{id}', [ProfileController::class, 'editStudentFamily']);
    Route::get('/e-confirmation', [ProfileController::class, 'getStudentEConfirmation']);

    // Lejar
    Route::group(['prefix' => '/lejar'], function () {
        Route::get('/total-invoice', [LejarController::class, 'totalInvoice']);
        Route::get('/balance', [LejarController::class, 'balance']);
        Route::get('/invoices', [LejarController::class, 'invoices']);
        Route::get('/student-payments', [LejarController::class, 'studentPayments']);
        Route::get('/sponsor-payments', [LejarController::class, 'sponsorPayments']);
        Route::get('/vouchers', [LejarController::class, 'vouchers']);
    });
});

Or

Route::get('/profile', [ProfileController::class, 'getStudentProfile'])->middleware('auth:sanctum');

4.3 OptionalSanctumAuth – Optional Auth + Role Detection (auth.optional)

File: app/Http/Middleware/OptionalSanctumAuth.php
Registered in: bootstrap/app.php as auth.optional.

public function handle(Request $request, Closure $next): Response
{
    $user = null; $role = 'PUBLIC'; $userType = null;
    $token = $request->bearerToken() ?? $request->input('token');

    if ($token) {
        $parts = explode('|', $token);
        if (count($parts) === 2) {
            $hash   = hash('sha256', $parts[1]);
            $pat    = PersonalAccessToken::where('token', $hash)->first();

            if ($pat) {
                $type = $pat->tokenable_type;
                $id   = $pat->tokenable_id;

                if (str_contains($type, 'StudentMain')) {
                    $user = StudentMain::find($id);
                    if ($user) { $role = 'STUDENT'; $userType = 'student'; }
                } elseif (str_contains($type, 'StaffMain')) {
                    $user = StaffMain::find($id);
                    if ($user) { $role = 'STAFF'; $userType = 'staff'; }
                }
            }
        }
    }

    $request->merge([
        'auth_user'      => $user,
        'auth_role'      => $role,      // PUBLIC / STUDENT / STAFF
        'auth_user_type' => $userType,  // null / 'student' / 'staff'
    ]);

    $request->setUserResolver(fn () => $user);

    return $next($request);
}

Purpose:

Usage in routes:

Route::get('/calendar/all-by-month', [CalendarController::class, 'getEventsByMonth'])
    ->middleware('auth.optional');

Route::get('/events/upcoming', [EventController::class, 'upcoming'])
    ->middleware('auth.optional');

Route::apiResource('/events', EventController::class)
    ->middleware('auth.optional');

Route::group(['prefix' => '/announcements', 'middleware' => 'auth.optional'], function () {
    // ...
});

In controllers (how to filter data by role):

This middleware is key for endpoints that must behave differently for guest vs logged-in vs role.


5. Routing Conventions & Versioning

File: routes/api.php

5.1 Structure

Route::group(['prefix' => '/v1/general', 'middleware' => 'throttle:api'], function () {
    // /api/v1/general/auth/*
    // /api/v1/general/themes
    // /api/v1/general/calendar/*
    // /api/v1/general/events/*
    // /api/v1/general/contact-directory/*
    // /api/v1/general/common/*
});

Route::group(['prefix' => '/v1/student', 'middleware' => 'throttle:api'], function () {
    // /api/v1/student/profile/*
    // /api/v1/student/lejar/*
});

Rules:

5.2 Fallback (Route not found)

Route::fallback(function () {
    return Handler::handleRouteNotFoundException();
});

6. Controller Patterns – Skeletons

6.1 Non-Paginated Example – ThemesController@index

File: app/Http/Controllers/Api/ThemesController.php

public function index(Request $request)
{
    try {
        $themes = Themes::with('pageBackgrounds')
            ->where('wt_start_date', '<=', now())
            ->where('wt_end_date', '>=', now())
            ->whereIn('wt_status_id', [Themes::$ActiveStatus, Themes::$DefaultStatus])
            ->whereHas('pageBackgrounds', function ($query) {
                $query->whereIn('wpb_page', [
                    PageBackgrounds::$PAGE_BACKGROUND_IMAGE,
                    PageBackgrounds::$PAGE_BACKGROUND_IMAGE_LOGIN,
                ]);
            })
            ->get();

        if ($themes->isEmpty()) {
            throw new ModelNotFoundException(ApiException::RECORD_NOT_FOUND);
        }

        return ApiResponse::success(
            'Themes retrieved successfully',
            ThemesResource::collection($themes)
        );
    } catch (\Exception $e) {
        return Handler::handleApiException($request, $e);
    }
}

Pattern to follow (no pagination):

  1. Query using Eloquent, include relations (with, whereHas).
  2. If no result (->isEmpty()) →
    throw new ModelNotFoundException(ApiException::RECORD_NOT_FOUND);
  3. Return JSON using ApiResponse::success('Message', Resource::collection($items)).
  4. Wrap in try/catch and delegate error to Handler::handleApiException().

6.2 Paginated Example – LejarController@invoices

File: app/Http/Controllers/Api_Student/LejarController.php

public function invoices(Request $request)
{
    try {
        $studentId = $request->user()->sm_student_id;
        $perPage   = $request->input('per_page', ApiResponse::$perPage);
        $page      = $request->input('page', 1);

        $invoices = CinvoiceHead::select(
                'CH_INVOICE_NO as INVOICE_NO',
                'CH_ACADEMIC_YEAR as ACADEMIC_YEAR',
                'CH_INVOICE_DESC as INVOICE_DESCRIPTION',
                'CH_TOTAL_AMT as AMOUNT'
            )
            ->where('CH_CUST_ID', $studentId)
            ->whereIn('CH_STATUS', ['APPRV', 'PAID'])
            ->where('CH_CUST_TYPE', 'STUD')
            ->paginate($perPage, ['*'], 'page', $page);

        if ($invoices->isEmpty()) {
            throw new ModelNotFoundException(ApiException::RECORD_NOT_FOUND);
        }

        return ApiResponse::paginated(
            InvoiceResource::collection($invoices),
            'Invoices retrieved successfully'
        );
    } catch (\Exception $e) {
        return Handler::handleApiException($request, $e);
    }
}

How pagination works (step-by-step for juniors):

  1. Request from mobile:

  2. Controller reads params:

    $perPage = $request->input('per_page', ApiResponse::$perPage); // default 15
    $page    = $request->input('page', 1);
    
  3. Use ->paginate(...) on query:

    $invoices = CinvoiceHead::...->paginate($perPage, ['*'], 'page', $page);
    
  4. If no data on that page ($invoices->isEmpty()) → throw ModelNotFoundException.

  5. Return paginated response via trait:

    public static function paginated($paginator, string $message = 'Success'): JsonResponse {
        return response()->json([
            'status'     => 'success',
            'message'    => $message,
            'data'       => $paginator->items(),
            'pagination' => [
                'first'        => $paginator->url(1),
                'last'         => $paginator->url($paginator->lastPage()),
                'next'         => $paginator->nextPageUrl()
                                    ? $paginator->nextPageUrl() . '?per_page=' . $paginator->perPage()
                                    : null,
                'prev'         => $paginator->previousPageUrl()
                                    ? $paginator->previousPageUrl() . '?per_page=' . $paginator->perPage()
                                    : null,
                'total'        => $paginator->total(),
                'per_page'     => $paginator->perPage(),
                'next_page'    => $paginator->nextPageUrl() ? $paginator->currentPage() + 1 : null,
                'prev_page'    => $paginator->previousPageUrl() ? $paginator->currentPage() - 1 : null,
                'current_page' => $paginator->currentPage(),
                'last_page'    => $paginator->lastPage(),
                'from'         => $paginator->firstItem(),
            ],
        ], Response::HTTP_OK);
    }
    
  6. Mobile receives JSON like:

    {
      "status": "success",
      "message": "Invoices retrieved successfully",
      "data": [
        /* current page invoices */
      ],
      "pagination": {
        "first": "...page=1",
        "last": "...page=n",
        "next": "...page=3&per_page=10",
        "prev": "...page=1&per_page=10",
        "total": 123,
        "per_page": 10,
        "next_page": 3,
        "prev_page": 1,
        "current_page": 2,
        "last_page": 13,
        "from": 11
      }
    }
    

For any new paginated API:


7. Traits: ApiResponse & CommonFunction

7.1 ApiResponse

File: app/Traits/ApiResponse.php

Central place to standardize all API JSON responses:

trait ApiResponse
{
    public static $perPage = 15;
    public static $limit   = 5;

    public static function success(string $message = 'Success', $data = null, int $status = 200): JsonResponse { ... }
    public static function error(string $message = 'Error', $errors = null, int $status = 400): JsonResponse { ... }
    public static function validationError(string $message = 'Validation failed', $errors = null): JsonResponse { ... }
    public static function unauthorized(string $message = 'Unauthorized'): JsonResponse { ... }
    public static function forbidden(string $message = 'Forbidden'): JsonResponse { ... }
    public static function notFound(string $message = 'Resource not found'): JsonResponse { ... }
    public static function serverError(string $message = 'Internal server error'): JsonResponse { ... }
    public static function created(string $message = 'Resource created successfully', $data = null): JsonResponse { ... }
    public static function noContent(): JsonResponse { ... }
    public static function paginated($paginator, string $message = 'Success'): JsonResponse { ... }
    public static function tooManyRequests(string $message = 'Too many requests'): JsonResponse { ... }
}

Controller usage examples:

7.2 CommonFunction

Used in ThemesController (e.g. parseDate), and can be extended for reusable helpers (date formatting, etc.). Pattern: put common small helpers in traits instead of repeating across controllers.


8. Error Handling – ApiException & Handler

8.1 ApiException – Central Error Messages

File: app/Exceptions/ApiException.php

class ApiException extends Exception
{
    // Authentication errors
    const UNAUTHORIZED         = 'You are not authorized to access this API.';
    const INVALID_CREDENTIALS  = 'The provided credentials are incorrect.';
    const TOKEN_EXPIRED        = 'Your session has expired. Please login again.';
    // Validation
    const VALIDATION_ERROR     = 'The provided data is invalid.';
    const RECORD_NOT_FOUND     = 'The requested data was not found.';
    // Database
    const DATABASE_ERROR       = 'A database error occurred. Please try again.';
    const DATABASE_CONNECTION_ERROR = 'Unable to connect to database. Please try again later.';
    const DUPLICATE_ENTRY      = 'This record already exists.';
    const FOREIGN_KEY_CONSTRAINT = 'Cannot delete this record as it is being used elsewhere.';
    // Resource / permission / server / etc...
    const ROUTE_NOT_FOUND      = 'The requested route does not exist.';
    // ...
    public static function throw(string $message, int $code = 400): self
    {
        return new self($message, $code);
    }
}

Usage:

8.2 Handler – Centralized API Error Responses

File: app/Exceptions/Handler.php

public function render($request, Throwable $e)
{
    // Only handle API requests
    if ($request->is('api/*') || $request->expectsJson()) {
        return $this->handleApiException($request, $e);
    }

    return parent::render($request, $e);
}

handleApiException maps exceptions to ApiResponse:

Controller pattern:

try {
    // main logic
} catch (\Exception $e) {
    return Handler::handleApiException($request, $e);
}

9. Models, DB Connections & Oracle Schemas

9.1 Multiple DB Connections

You use Oracle with multiple users/schemas. Each user holds related tables:

In Laravel:

9.2 Dev DB Connection (2Q) – Using dbver or SQL Client

Connect to Oracle dev DB (45.127.5.74, xepdb1) with:

Use this to:


10. Requests (Validation) & Resources (Transformers)

10.1 Form Requests

For POST/PUT endpoints, use Form Request classes instead of inline validation.

Inline validation is still used in some places (e.g. ThemesController@show), but the recommended style for new APIs is separate Request classes.

10.2 Resources

Resources shape outgoing JSON:

Purpose:

Create new Resource:

docker exec -it apisz-corp php artisan make:resource StateResource

Use in controller:

return ApiResponse::success(
    'States retrieved successfully',
    StateResource::collection($states)
);

11. Docker Setup & Local Development

11.1 Docker Compose

File: docker-compose.yml

services:
  app:
    platform: linux/amd64
    build:
      context: .
      dockerfile: .docker/Dockerfile
    container_name: apisz-corp
    volumes:
      - ./application:/var/www:cached
      - /var/www/vendor
    environment:
      - ORACLE_HOME=/usr/lib/oracle/21/client64
      - LD_LIBRARY_PATH=/usr/lib/oracle/21/client64/lib
      - TNS_ADMIN=/opt/oracle/instantclient/network/admin
    networks:
      - apisz-corp-net

  webserver:
    image: nginx:alpine
    container_name: nginx_server-apisz-corp
    ports:
      - "8001:80"
    volumes:
      - ./application:/var/www:cached
      - ./.docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
      - ./.docker/nginx/logs:/var/log/nginx
    depends_on:
      - app
    networks:
      - apisz-corp-net

networks:
  apisz-corp-net:
    driver: bridge

11.2 Running Locally

11.3 Running Artisan Commands (Important Pattern)

Always include container prefix:

docker exec -it apisz-corp php artisan <command>

Common commands:

Important: For other microservices, only the container name changes (e.g. apisz-student).


12. Environments, URLs & Deployment (2Q & UMP Staging)

12.1 Root URLs

Important:

UMP staging (api-corp-stag.umpsa.edu.my) is only accessible inside UMP WiFi/network. Outside UMP you must use VPN or ask someone inside to access.

12.2 Deployment to 2Q Staging (After Pushing Code)

2Q server:

On Windows – recommended: MobaXterm.
On macOS – recommended: Termius (or Terminal + ssh).

Deployment steps:

  1. Go to project folder (General project)

    cd /var/www/UMP/apisz-corp/
    
  2. Check git status

    git status
    
  3. Pull latest code

    git pull
    
  4. Optimize inside Docker container

    docker exec -it apisz-corp php artisan optimize
    
  5. Verify on 2Q staging


13. My API Development Workflow (Step-By-Step)

This is the my personal style already use – new devs should follow this exactly.

13.1 Step 1 – Start from API List & Screens

  1. Open API list spreadsheet:
    Api list – Comparison UI
  2. Pick a module (e.g. Profile, Lejar, State list).
  3. Get the UI screen from mobile FE team.
  4. Update the API list sheet with:

13.2 Step 2 – Check Existing Queries (UMP SQL)

  1. Open Existing Query Reference:
    Existing query sheet
  2. Search by module name / feature.
  3. Identify:
  4. Map tables to ERD to understand relationships.

13.3 Step 3 – Check ERD & DB Tables

  1. Open ERD Drive by module:
    ERD Drive (Modul Umum / Modul Pelajar / Modul Staff)
  2. Locate the relevant diagram (e.g. Lejar, Profile).
  3. Use dbver / SQL client to confirm tables on dev DB:
  4. If tables exist in dev DB → proceed.
  5. If tables missing → replicate for development (next step).

13.4 Step 4 – Replicate Missing Tables for Dev

If a table used in query is not in dev DB:

  1. Check ERD for table structure (columns, types, PK/FK).

  2. Copy table + column details and use AI (or manual) to generate:

  3. Run migration + seeder:

    docker exec -it apisz-corp php artisan migrate --path=database/migrations/..(Your Migration file name)..php
    docker exec -it apisz-corp php artisan db:seed --class=YourSeeder
    
  4. If ERD doesn’t have the table mentioned in query:

To inspect real staging data (UMP staging DB):

13.5 Step 5 – Implement the API (Laravel)

  1. Create controller (if needed):

    docker exec -it apisz-corp php artisan make:controller Api/ModuleNameController
    
  2. Add route in routes/api.php:

  3. Implement method in controller using skeletons:

  4. Create Resource to shape JSON:

    docker exec -it apisz-corp php artisan make:resource Module/FeatureResource
    
  5. For POST/PUT, create Request class:

    docker exec -it apisz-corp php artisan make:request StoreFeatureRequest
    
  6. Use OptionalSanctumAuth correctly if role-based filtering is needed for those api that does’nt use auth:sanctum:

13.6 Step 6 – Test & Update Documentation

  1. Test locally via http://localhost:8001/docs/api (Scramble UI) or Postman.

  2. Ensure:

  3. Update API list sheet:

  4. After push to Git, deploy to 2Q staging (see section 12.2) so mobile frontend team can integrate.


14. People & Support Contacts


15. Final Checklist for New Developers


Appendix: Quick Reference Commands

Docker Commands

# Start containers
docker compose up -d

# Stop containers
docker compose down

# View logs
docker logs apisz-corp
docker logs nginx_server-apisz-corp

Artisan Commands (Always with Docker prefix)

# Optimize
docker exec -it apisz-corp php artisan optimize

# Make controller
docker exec -it apisz-corp php artisan make:controller Api/MyController

# Make model with migration
docker exec -it apisz-corp php artisan make:model Models/MyModel -m

# Make resource
docker exec -it apisz-corp php artisan make:resource MyResource

# Make request
docker exec -it apisz-corp php artisan make:request StoreMyRequest

# Make migration
docker exec -it apisz-corp php artisan make:migration create_my_table

# Run migration
docker exec -it apisz-corp php artisan migrate

# Cache
docker exec -it apisz-corp php artisan config:cache
docker exec -it apisz-corp php artisan route:cache
docker exec -it apisz-corp php artisan cache:clear

Deployment Commands (2Q Server)

# SSH into server
ssh root@45.127.5.74 -p 22

# Navigate to project
cd /var/www/UMP/apisz-corp/

# Git operations
git status
git pull

# Optimize
docker exec -it apisz-corp php artisan optimize

Document Version: 1.0
Last Updated: 2025
Created By: Satiya Ganes (sG)