Skip to main content
Tripfy Africa uses 56 Laravel Eloquent models spread across four categories: core travel entities, commerce, content, and system configuration. Each model lives in app/Models/ and uses $guarded = ['id'] as a baseline mass-assignment guard unless otherwise noted.

Model categories

These five models form the heart of the booking workflow.

User

File: app/Models/User.phpExtends Illuminate\Foundation\Auth\User and uses the HasApiTokens, HasFactory, Notifiable, SoftDeletes, and Notify traits.Key casts
protected $casts = [
    'email_verified_at' => 'datetime',
    'password'          => 'hashed',
];
Appended attributes: last-seen-activity, user_image, fullnameRelationships
// Bookings made by this user as a traveller
public function booking()
{
    return $this->hasMany(Booking::class, 'user_id')
        ->whereIn('status', [1, 2, 3, 4, 5]);
}

// Packages this user owns as a vendor
public function packages()
{
    return $this->hasMany(Package::class, 'owner_id');
}

// Guides created by this vendor
public function guides()
{
    return $this->hasMany(Guide::class, 'created_by');
}

// Most recent transaction
public function transaction()
{
    return $this->hasOne(Transaction::class)->latest();
}

// Active subscription plan
public function activePlan()
{
    return $this->hasOne(PlanPurchase::class, 'user_id')
        ->where('status', 1)
        ->where('expiry_date', '>=', now())
        ->latest('created_at');
}

// Vendor profile information
public function vendorInfo()
{
    return $this->hasOne(VendorInfo::class, 'vendor_id');
}

// Support tickets
public function tickets()
{
    return $this->hasMany(SupportTicket::class, 'user_id');
}
Accessors
AccessorReturns
getFullnameAttribute()firstname + lastname concatenated
getUserImageAttribute()File URL via getFile($image_driver, $image)
getLastSeenActivityAttribute()true if user-is-online-{id} cache key exists
Notable methods
// Returns bookings where the user's packages were booked
public function vendorBooking()
{
    return $this->hasMany(Booking::class)
        ->whereHas('package', function ($query) {
            $query->where('owner_id', $this->id);
        })
        ->whereIn('status', [1, 2, 3, 4, 5]);
}

// Password reset sends a templated mail via the Notify trait
public function sendPasswordResetNotification($token)
{
    $this->mail($this, 'PASSWORD_RESET', [
        'message' => '<a href="' . url('password/reset', $token) . '?email=' . $this->email . '">Click To Reset Password</a>'
    ]);
}
The users table has a role column: 0 = traveller, 1 = vendor. The REST API exposes this as is_vendor boolean in all JSON responses. The same User model handles both roles — role elevation happens when KYC is approved.

Package

File: app/Models/Package.phpRepresents a tour package listed by a vendor.Key casts
protected $casts = [
    'facility'      => 'object',
    'excluded'      => 'object',
    'expected'      => 'object',
    'amenities'     => 'object',
    'places'        => 'object',
    'guides'        => 'object',
    'timeSlot'      => 'object',
    'rating'        => 'object',
    'meta_keywords' => 'array',
    'imagesUrl'     => 'array',
];
Relationships
public function owner()
{
    return $this->belongsTo(User::class, 'owner_id');
}

public function category()
{
    return $this->belongsTo(PackageCategory::class, 'package_category_id');
}

public function destination()
{
    return $this->belongsTo(Destination::class, 'destination_id');
}

public function booking()
{
    return $this->hasMany(Booking::class, 'package_id');
}

public function reviews()
{
    return $this->hasMany(Review::class, 'package_id')
        ->where('parent_review_id', null)
        ->where('status', 1);
}

public function media()
{
    return $this->hasMany(PackageMedia::class, 'package_id');
}

public function visitor()
{
    return $this->hasMany(PackageVisitor::class, 'package_id');
}

public function chat()
{
    return $this->hasMany(Chat::class, 'package_id');
}

// Geographic associations
public function countryTake() { return $this->belongsTo(Country::class, 'country', 'id'); }
public function stateTake()   { return $this->belongsTo(State::class, 'state', 'id'); }
public function cityTake()    { return $this->belongsTo(City::class, 'city', 'id'); }
Key accessors
// Returns disabled date ranges where maximumTravelers is already reached
public function getBookingsAttribute()
{
    $bookings = Booking::select(['id', 'date', 'package_id', DB::raw('SUM(total_person) as total_person_sum')])
        ->where('package_id', $this->id)
        ->groupBy(['date', 'package_id'])
        ->get();

    foreach ($bookings as $booking) {
        if ((int) $booking->total_person_sum >= (int) $this->maximumTravelers) {
            $disabledRanges[] = [
                'date'    => $booking->date,
                'message' => 'No available space for this date.',
            ];
        }
    }
    return $disabledRanges;
}

// Returns per-date remaining space
public function getBookingsSpaceAttribute() { ... }

// Cached review count
public function getReviewCountAttribute() { ... }

// Average star rating
public function getReviewAverageAttribute()
{
    return round($this->reviews()->avg('rating'), 1);
}
Static helper
// Eager-loads all relations needed for a full package detail page
public static function withAllRelations()
{
    return self::with([
        'category:id,name',
        'destination:id,title,slug',
        'media',
        'owner',
        'countryTake:id,name',
        'stateTake:id,name',
        'cityTake:id,name',
        'reviews.user:id,firstname,lastname,username,image,image_driver,address_one',
        'reviews.reply.user:id,firstname,lastname,username,image,image_driver,address_one',
        'chat.reply',
    ]);
}

Booking

File: app/Models/Booking.phpRecords a traveller’s reservation for a package. Uses Prunable to auto-delete stale unpaid bookings.Key casts
protected $casts = [
    'adult_info'  => 'array',
    'child_info'  => 'array',
    'infant_info' => 'array',
];
Booking status values
ValueMeaning
0Pending payment
1Confirmed (accepted by vendor)
2Completed
3Cancelled / rejected
4Refunded
5Processing (awaiting vendor acceptance)
Relationships
public function user()
{
    return $this->belongsTo(User::class, 'user_id');
}

public function package()
{
    return $this->belongsTo(Package::class, 'package_id');
}

public function depositable()
{
    return $this->morphOne(Deposit::class, 'depositable');
}
Auto-generated identifiers
// Ordered UUID for public-facing URLs (/bookings/{uid})
// Format: BK-XXXXXXXXXXXXXXXX (16 random hex chars) for trx_id
protected static function booted(): void
{
    static::creating(function (Booking $booking) {
        if (empty($booking->uid)) {
            $booking->uid = (string) Str::orderedUuid();
        }
        if (empty($booking->trx_id)) {
            $booking->trx_id = self::generateTrxId();
        }
    });
}
Pruning rule
// Stale unpaid bookings (status=0, date older than 5 days) are pruned automatically
public function prunable(): Builder
{
    return static::where('date', '<=', now()->subDays(5))->where('status', 0);
}

Review

File: app/Models/Review.phpStores traveller reviews for packages, with threaded replies via parent_review_id.Key casts
protected $casts = [
    'rating' => 'object',
];
Relationships
public function package()
{
    return $this->belongsTo(Package::class, 'package_id');
}

public function user()
{
    return $this->belongsTo(User::class, 'user_id');
}

// Approved replies to this review
public function reply()
{
    return $this->hasMany(Review::class, 'parent_review_id')->where('status', 1);
}

// All replies including unapproved ones
public function allReplies()
{
    return $this->hasMany(Review::class, 'parent_review_id');
}
Top-level reviews have parent_review_id = null. Reviews must have status = 1 to appear publicly. Reviews with status = 0 are pending admin approval.

Destination

File: app/Models/Destination.phpRepresents a Zanzibar or Tanzania tourism destination that packages are grouped under.Key casts
protected $casts = [
    'place' => 'object',
];
Relationships
// All packages at this destination
public function packages()
{
    return $this->hasMany(Package::class, 'destination_id');
}

// All bookings at this destination via packages
public function bookings()
{
    return $this->hasManyThrough(
        Booking::class,
        Package::class,
        'destination_id',
        'package_id'
    );
}

// Visitor tracking
public function visitor()
{
    return $this->hasMany(DestinationVisitor::class, 'destination_id');
}

// Geographic associations
public function countryTake() { return $this->belongsTo(Country::class, 'country', 'id'); }
public function stateTake()   { return $this->belongsTo(State::class, 'state', 'id'); }
public function cityTake()    { return $this->belongsTo(City::class, 'city', 'id'); }
These models handle payments, payouts, and vendor subscriptions.

Transaction

File: app/Models/Transaction.phpThe central ledger model. Transaction IDs are prefixed with T and generated inside a database lock to prevent duplicates.Relationships
public function user()
{
    return $this->belongsTo(User::class, 'user_id', 'id');
}

// Polymorphic — can belong to a Deposit, Payout, or Booking
public function transactional()
{
    return $this->morphTo();
}
TRX ID generation
public static function generateOrderNumber()
{
    return DB::transaction(function () {
        $lastOrder = self::lockForUpdate()->orderBy('id', 'desc')->first();
        // ... increments from last ID, prefixes with 'T'
        return 'T' . $newOrderNumber;
    });
}

Deposit

File: app/Models/Deposit.phpRecords an incoming payment from a user. Transaction IDs are prefixed with D.Key casts
protected $casts = [
    'information' => 'object',
];
Relationships
public function user()        { return $this->belongsTo(User::class, 'user_id', 'id'); }
public function gateway()     { return $this->belongsTo(Gateway::class, 'payment_method_id', 'id'); }
public function gatewayable() { return $this->morphTo(); }   // auto, manual, or user gateway
public function depositable() { return $this->morphTo(); }   // Booking or Fund
public function transactional() {
    return $this->morphOne(Transaction::class, 'transactional');
}
Status values: 0 = pending, 1 = success, 2 = requested (manual), 3 = rejected

Payout

File: app/Models/Payout.phpRecords a vendor withdrawal request. Transaction IDs are prefixed with P.Fillable fields: user_id, payout_method_id, payout_currency_code, amount, charge, net_amount, amount_in_base_currency, charge_in_base_currency, net_amount_in_base_currency, trx_id, status, information, meta_field, feedbackKey casts
protected $casts = [
    'information' => 'object',
    'meta_field'  => 'object',
];
Relationships
public function user()         { return $this->belongsTo(User::class, 'user_id'); }
public function method()       { return $this->belongsTo(PayoutMethod::class, 'payout_method_id'); }
public function transactional() {
    return $this->morphOne(Transaction::class, 'transactional');
}
Status values: 1 = pending, 2 = approved, 3 = rejected

Gateway

File: app/Models/Gateway.phpConfigures automatic and manual payment gateways. Gateways with id < 1000 are automatic; id >= 1000 are manual.Key casts
protected $casts = [
    'currency'             => 'object',
    'supported_currency'   => 'object',
    'receivable_currencies'=> 'object',
    'parameters'           => 'object',
    'currencies'           => 'object',
    'extra_parameters'     => 'object',
];
Scopes
public function scopeAutomatic() { return $this->where('id', '<', 1000); }
public function scopeManual()    { return $this->where('id', '>=', 1000); }

Coupon

File: app/Models/Coupon.phpDiscount codes applicable at checkout.

Plan & PlanPurchase

File: app/Models/Plan.php, app/Models/PlanPurchase.phpVendors subscribe to plans that define listing quotas and featured-listing allowances. PlanPurchase records the active subscription per vendor with an expiry_date.
These models drive the CMS and multi-language content layer.

Blog

File: app/Models/Blog.phpKey casts
protected $casts = ['meta_keywords' => 'array'];
Relationships
public function details()  { return $this->hasOne(BlogDetails::class, 'blog_id'); }
public function comments() {
    return $this->hasMany(BlogComment::class, 'blog_id')
        ->where('parent_comment_id', null);
}
public function category() { return $this->belongsTo(BlogCategory::class); }
Static helpers
// Get recent blogs, optionally excluding one ID
public static function getRecentBlogs($existId = null, $limit = 4)
{
    return self::orderBy('created_at', 'desc')
        ->when($existId != null, fn($q) => $q->where('id', '!=', $existId))
        ->limit($limit)
        ->get();
}

// Count published blogs in a category
public static function BlogsCountById($category_id)
{
    return Blog::where('category_id', $category_id)->where('status', 1)->count();
}

Page & PageDetail

File: app/Models/Page.php, app/Models/PageDetail.phpCMS pages with per-language detail records in PageDetail.

Language

File: app/Models/Language.phpControls the active UI languages across the platform.

Content & ContentDetails

File: app/Models/Content.php, app/Models/ContentDetails.phpGeneric translatable content blocks (e.g., hero text, section copy) with per-language detail records in ContentDetails.
These models configure global platform behaviour.

BasicControl

File: app/Models/BasicControl.phpSingleton settings record. Clears the ConfigureSetting cache after every save.
protected static function boot()
{
    parent::boot();
    static::saved(function () {
        \Cache::forget('ConfigureSetting');
    });
}
Read site-wide settings using the basicControl() helper:
$basic = basicControl();
$freeLimit = $basic->free_listing;

NotificationTemplate

File: app/Models/NotificationTemplate.phpHolds email and SMS template bodies keyed by a template code (e.g., BOOKING_ACCEPTED, KYC_APPROVED). The Notify trait looks up these templates when sending notifications.

Admin

File: app/Models/Admin.phpAdministrator accounts, separate from User. Admins manage the backend panel.

Other system models

ModelPurpose
ActivityLogPolymorphic audit trail for packages and users
InAppNotificationPolymorphic in-app notification records
FireBaseTokenPolymorphic FCM device tokens for push notifications
NotificationSettingsPer-user notification preferences
MaintenanceModeToggles site maintenance state
ManageMenuDynamic navigation menu entries
GoogleMapApiGoogle Maps API key configuration
SmsControlSMS gateway configuration
ManualSmsConfigManual SMS provider settings
SupportTicketUser support tickets
SupportTicketMessageMessages within a ticket thread
SupportTicketAttachmentFile attachments on ticket messages
SubscriberNewsletter subscribers
KycKYC form schema definitions
UserKycUser KYC submission records
VendorInfoExtended vendor profile (social links, plan usage)
AmenityPackage amenity / safety item definitions
PackageCategoryTour package categories
PackageMediaGallery images for a package
PackageVisitorView-count records per package
DestinationVisitorView-count records per destination
GuideTour guide profiles linked to a user via created_by
ChatPackage-level chat messages
Country / State / CityGeographic reference data
FundWallet top-up records
UserGatewayPer-user gateway configurations for vendor payouts
RazorpayContactRazorpay contact records for payout routing
PayoutMethodPayout method configurations
FileStorageTracks uploaded file paths and drivers

Model naming conventions

ConventionExample
Model classPackageCategory (PascalCase)
Table namepackage_categories (snake_case plural)
Foreign keypackage_id, owner_id (snake_case + _id)
Polymorphic typedepositable_type / depositable_id

Guide model — JSON-based relationship

Guide does not use a standard foreign key to link to packages. Instead, package records store an array of guide codes in a JSON column:
// Guide.php
public function packages()
{
    return Package::whereJsonContains('guides', $this->code);
}

// Package.php
public function guides()
{
    return Guide::whereIn('code', $this->guides ?? []);
}
The owning User record is found via:
public function user()
{
    return $this->belongsTo(User::class, 'created_by');
}

Build docs developers (and LLMs) love