diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0388e51
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,12 @@
+# MokoSuite standard ignores
+.claude/
+.mcp.json
+TODO.md
+vendor/
+node_modules/
+.env
+*.min.css
+*.min.js
+.DS_Store
+Thumbs.db
+*.ppk
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..d37e676
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,19 @@
+
+
+# Changelog
+
+## [Unreleased]
+
+### Added
+- **Repository** -- initial repo creation with scaffolding
+- **System Plugin** -- Extension class, service provider, 6 helpers
+- **SQL Schema** -- 8 tables: vehicles, drivers, zones, fares, rides, ratings, dispatch, shifts
+- **Admin Component** -- 6 views: dashboard, rides, vehicles, drivers, zones, fares
+- **Webservices Plugin** -- 7 API routes: rides, vehicles, drivers, zones, fares, dispatch, ratings
+- **Configuration** -- 16 settings across general, dispatch, fares, drivers, booking
+- **Access Control** -- 21 permissions for granular role-based access
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..713e6a2
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,32 @@
+# MokoSuiteTaxi
+
+Ride-hailing, dispatch, fleet management, fare zones, and driver scheduling for Joomla 6.
+
+## Quick Reference
+
+| Field | Value |
+|---|---|
+| **Package** | `pkg_mokosuitetaxi` |
+| **Layer** | 2 (requires: Client, CRM) |
+| **Language** | PHP 8.3+ |
+| **Branch** | develop on `dev`, merge to `main` (protected) |
+| **Wiki** | [MokoSuiteTaxi Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteTaxi/wiki) |
+
+## Architecture
+
+Joomla **package** -- Layer 2 add-on. CRM contacts as riders/drivers, zone-based dispatch with surge pricing.
+
+## Rules
+
+- **Never commit** `.claude/`, `.mcp.json`, `TODO.md`, `*.min.css`/`*.min.js`
+- **Attribution**: `Authored-by: Moko Consulting`
+- **Workflow directory**: `.mokogitea/`
+- **Standards**: [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoCLI/wiki)
+- **Changelog**: `[Unreleased]` only -- release system assigns versions
+
+## Coding Standards
+
+- PHP 8.3+ / Joomla 6 patterns
+- `$this->getDatabase()` in models, `Factory::getContainer()->get(DatabaseInterface::class)` in helpers
+- `Factory::getApplication()->getIdentity()` for user
+- FOR UPDATE locking on ride acceptance/dispatch to prevent race conditions
diff --git a/README.md b/README.md
index eeff7b8..21a8ded 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,44 @@
-# MokoSuiteTaxi
+
-MokoSuite Taxi — ride-hailing, dispatch, fleet management, fare zones, driver scheduling for Joomla 6
\ No newline at end of file
+# MokoSuite Taxi
+
+Ride-hailing, dispatch, fleet management, fare zones, and driver scheduling module for MokoSuite on Joomla 6.
+
+## Overview
+
+MokoSuiteTaxi is a Layer 2 module in the MokoSuite platform, building on MokoSuiteClient (Layer 0) and MokoSuiteCRM (Layer 1) to provide complete taxi and ride-hailing operations management.
+
+## Features
+
+- **Ride Management** -- on-demand, scheduled, airport, and corporate ride types
+- **Dispatch Engine** -- auto-dispatch with zone-aware driver matching and FOR UPDATE locking
+- **Fleet Management** -- vehicle tracking, insurance/inspection expiry alerts, maintenance status
+- **Driver Management** -- profiles linked to CRM contacts, approval workflow, background checks
+- **Fare Zones** -- GeoJSON polygon boundaries with radius fallback, zone-based pricing
+- **Dynamic Pricing** -- surge multiplier based on demand/supply ratio, night surcharges, peak hours
+- **Ratings** -- bidirectional rider/driver ratings with feedback tags
+- **Shift Scheduling** -- driver shift management with earnings tracking
+
+## Requirements
+
+- Joomla 6.x
+- PHP 8.3+
+- MokoSuiteClient (Layer 0)
+- MokoSuiteCRM (Layer 1)
+
+## Installation
+
+Install via Joomla Extension Manager using the package file `pkg_mokosuitetaxi.zip`.
+
+## License
+
+GNU General Public License v3.0 or later -- see [LICENSE](LICENSE).
+
+## Links
+
+- [Documentation](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteTaxi/wiki)
+- [Issues](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteTaxi/issues)
+- [MokoSuite Platform](https://mokoconsulting.tech)
diff --git a/source/packages/com_mokosuitetaxi/admin/access.xml b/source/packages/com_mokosuitetaxi/admin/access.xml
new file mode 100644
index 0000000..5ae7eda
--- /dev/null
+++ b/source/packages/com_mokosuitetaxi/admin/access.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/source/packages/com_mokosuitetaxi/admin/config.xml b/source/packages/com_mokosuitetaxi/admin/config.xml
new file mode 100644
index 0000000..860a4d8
--- /dev/null
+++ b/source/packages/com_mokosuitetaxi/admin/config.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
diff --git a/source/packages/com_mokosuitetaxi/admin/services/provider.php b/source/packages/com_mokosuitetaxi/admin/services/provider.php
new file mode 100644
index 0000000..6c33848
--- /dev/null
+++ b/source/packages/com_mokosuitetaxi/admin/services/provider.php
@@ -0,0 +1,26 @@
+
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ * Authored-by: Moko Consulting
+ */
+
+defined('_JEXEC') or die;
+
+use Joomla\CMS\Extension\ComponentInterface;
+use Joomla\CMS\Extension\MVCComponent;
+use Joomla\CMS\Dispatcher\ComponentDispatcherFactoryInterface;
+use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
+use Joomla\DI\Container;
+use Joomla\DI\ServiceProviderInterface;
+
+return new class implements ServiceProviderInterface {
+ public function register(Container $container): void {
+ $container->set(ComponentInterface::class, function (Container $container) {
+ $c = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class));
+ $c->setMVCFactory($container->get(MVCFactoryInterface::class));
+ return $c;
+ });
+ }
+};
diff --git a/source/packages/com_mokosuitetaxi/admin/src/Controller/DisplayController.php b/source/packages/com_mokosuitetaxi/admin/src/Controller/DisplayController.php
new file mode 100644
index 0000000..dc995f1
--- /dev/null
+++ b/source/packages/com_mokosuitetaxi/admin/src/Controller/DisplayController.php
@@ -0,0 +1,18 @@
+
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ * Authored-by: Moko Consulting
+ */
+
+namespace Moko\Component\MokoSuiteTaxi\Administrator\Controller;
+
+defined('_JEXEC') or die;
+
+use Joomla\CMS\MVC\Controller\BaseController;
+
+class DisplayController extends BaseController
+{
+ protected $default_view = 'taxidashboard';
+}
diff --git a/source/packages/com_mokosuitetaxi/admin/src/Model/TaxiDashboardModel.php b/source/packages/com_mokosuitetaxi/admin/src/Model/TaxiDashboardModel.php
new file mode 100644
index 0000000..04e69b5
--- /dev/null
+++ b/source/packages/com_mokosuitetaxi/admin/src/Model/TaxiDashboardModel.php
@@ -0,0 +1,17 @@
+
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ * Authored-by: Moko Consulting
+ */
+
+namespace Moko\Component\MokoSuiteTaxi\Administrator\Model;
+
+defined('_JEXEC') or die;
+
+use Joomla\CMS\MVC\Model\BaseDatabaseModel;
+
+class TaxiDashboardModel extends BaseDatabaseModel
+{
+}
diff --git a/source/packages/com_mokosuitetaxi/admin/src/View/TaxiDashboard/HtmlView.php b/source/packages/com_mokosuitetaxi/admin/src/View/TaxiDashboard/HtmlView.php
new file mode 100644
index 0000000..4c7c3c9
--- /dev/null
+++ b/source/packages/com_mokosuitetaxi/admin/src/View/TaxiDashboard/HtmlView.php
@@ -0,0 +1,23 @@
+
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ * Authored-by: Moko Consulting
+ */
+
+namespace Moko\Component\MokoSuiteTaxi\Administrator\View\TaxiDashboard;
+
+defined('_JEXEC') or die;
+
+use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
+use Joomla\CMS\Toolbar\ToolbarHelper;
+
+class HtmlView extends BaseHtmlView
+{
+ public function display($tpl = null): void
+ {
+ ToolbarHelper::title('MokoSuite Taxi - Dashboard');
+ parent::display($tpl);
+ }
+}
diff --git a/source/packages/com_mokosuitetaxi/admin/src/View/TaxiDrivers/HtmlView.php b/source/packages/com_mokosuitetaxi/admin/src/View/TaxiDrivers/HtmlView.php
new file mode 100644
index 0000000..a0f6d70
--- /dev/null
+++ b/source/packages/com_mokosuitetaxi/admin/src/View/TaxiDrivers/HtmlView.php
@@ -0,0 +1,23 @@
+
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ * Authored-by: Moko Consulting
+ */
+
+namespace Moko\Component\MokoSuiteTaxi\Administrator\View\TaxiDrivers;
+
+defined('_JEXEC') or die;
+
+use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
+use Joomla\CMS\Toolbar\ToolbarHelper;
+
+class HtmlView extends BaseHtmlView
+{
+ public function display($tpl = null): void
+ {
+ ToolbarHelper::title('MokoSuite Taxi - Drivers');
+ parent::display($tpl);
+ }
+}
diff --git a/source/packages/com_mokosuitetaxi/admin/src/View/TaxiFares/HtmlView.php b/source/packages/com_mokosuitetaxi/admin/src/View/TaxiFares/HtmlView.php
new file mode 100644
index 0000000..0914bda
--- /dev/null
+++ b/source/packages/com_mokosuitetaxi/admin/src/View/TaxiFares/HtmlView.php
@@ -0,0 +1,23 @@
+
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ * Authored-by: Moko Consulting
+ */
+
+namespace Moko\Component\MokoSuiteTaxi\Administrator\View\TaxiFares;
+
+defined('_JEXEC') or die;
+
+use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
+use Joomla\CMS\Toolbar\ToolbarHelper;
+
+class HtmlView extends BaseHtmlView
+{
+ public function display($tpl = null): void
+ {
+ ToolbarHelper::title('MokoSuite Taxi - Fare Rules');
+ parent::display($tpl);
+ }
+}
diff --git a/source/packages/com_mokosuitetaxi/admin/src/View/TaxiRides/HtmlView.php b/source/packages/com_mokosuitetaxi/admin/src/View/TaxiRides/HtmlView.php
new file mode 100644
index 0000000..127b07c
--- /dev/null
+++ b/source/packages/com_mokosuitetaxi/admin/src/View/TaxiRides/HtmlView.php
@@ -0,0 +1,23 @@
+
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ * Authored-by: Moko Consulting
+ */
+
+namespace Moko\Component\MokoSuiteTaxi\Administrator\View\TaxiRides;
+
+defined('_JEXEC') or die;
+
+use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
+use Joomla\CMS\Toolbar\ToolbarHelper;
+
+class HtmlView extends BaseHtmlView
+{
+ public function display($tpl = null): void
+ {
+ ToolbarHelper::title('MokoSuite Taxi - Rides');
+ parent::display($tpl);
+ }
+}
diff --git a/source/packages/com_mokosuitetaxi/admin/src/View/TaxiVehicles/HtmlView.php b/source/packages/com_mokosuitetaxi/admin/src/View/TaxiVehicles/HtmlView.php
new file mode 100644
index 0000000..054e4b1
--- /dev/null
+++ b/source/packages/com_mokosuitetaxi/admin/src/View/TaxiVehicles/HtmlView.php
@@ -0,0 +1,23 @@
+
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ * Authored-by: Moko Consulting
+ */
+
+namespace Moko\Component\MokoSuiteTaxi\Administrator\View\TaxiVehicles;
+
+defined('_JEXEC') or die;
+
+use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
+use Joomla\CMS\Toolbar\ToolbarHelper;
+
+class HtmlView extends BaseHtmlView
+{
+ public function display($tpl = null): void
+ {
+ ToolbarHelper::title('MokoSuite Taxi - Vehicles');
+ parent::display($tpl);
+ }
+}
diff --git a/source/packages/com_mokosuitetaxi/admin/src/View/TaxiZones/HtmlView.php b/source/packages/com_mokosuitetaxi/admin/src/View/TaxiZones/HtmlView.php
new file mode 100644
index 0000000..3ad6d82
--- /dev/null
+++ b/source/packages/com_mokosuitetaxi/admin/src/View/TaxiZones/HtmlView.php
@@ -0,0 +1,23 @@
+
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ * Authored-by: Moko Consulting
+ */
+
+namespace Moko\Component\MokoSuiteTaxi\Administrator\View\TaxiZones;
+
+defined('_JEXEC') or die;
+
+use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
+use Joomla\CMS\Toolbar\ToolbarHelper;
+
+class HtmlView extends BaseHtmlView
+{
+ public function display($tpl = null): void
+ {
+ ToolbarHelper::title('MokoSuite Taxi - Zones');
+ parent::display($tpl);
+ }
+}
diff --git a/source/packages/com_mokosuitetaxi/admin/tmpl/taxidashboard/default.php b/source/packages/com_mokosuitetaxi/admin/tmpl/taxidashboard/default.php
new file mode 100644
index 0000000..e80e602
--- /dev/null
+++ b/source/packages/com_mokosuitetaxi/admin/tmpl/taxidashboard/default.php
@@ -0,0 +1 @@
+
diff --git a/source/packages/com_mokosuitetaxi/admin/tmpl/taxidrivers/default.php b/source/packages/com_mokosuitetaxi/admin/tmpl/taxidrivers/default.php
new file mode 100644
index 0000000..9668d20
--- /dev/null
+++ b/source/packages/com_mokosuitetaxi/admin/tmpl/taxidrivers/default.php
@@ -0,0 +1 @@
+
diff --git a/source/packages/com_mokosuitetaxi/admin/tmpl/taxifares/default.php b/source/packages/com_mokosuitetaxi/admin/tmpl/taxifares/default.php
new file mode 100644
index 0000000..c6883f7
--- /dev/null
+++ b/source/packages/com_mokosuitetaxi/admin/tmpl/taxifares/default.php
@@ -0,0 +1 @@
+
diff --git a/source/packages/com_mokosuitetaxi/admin/tmpl/taxirides/default.php b/source/packages/com_mokosuitetaxi/admin/tmpl/taxirides/default.php
new file mode 100644
index 0000000..018e753
--- /dev/null
+++ b/source/packages/com_mokosuitetaxi/admin/tmpl/taxirides/default.php
@@ -0,0 +1 @@
+
diff --git a/source/packages/com_mokosuitetaxi/admin/tmpl/taxivehicles/default.php b/source/packages/com_mokosuitetaxi/admin/tmpl/taxivehicles/default.php
new file mode 100644
index 0000000..70f3d47
--- /dev/null
+++ b/source/packages/com_mokosuitetaxi/admin/tmpl/taxivehicles/default.php
@@ -0,0 +1 @@
+
diff --git a/source/packages/com_mokosuitetaxi/admin/tmpl/taxizones/default.php b/source/packages/com_mokosuitetaxi/admin/tmpl/taxizones/default.php
new file mode 100644
index 0000000..597a753
--- /dev/null
+++ b/source/packages/com_mokosuitetaxi/admin/tmpl/taxizones/default.php
@@ -0,0 +1 @@
+
diff --git a/source/packages/com_mokosuitetaxi/mokosuitetaxi.xml b/source/packages/com_mokosuitetaxi/mokosuitetaxi.xml
new file mode 100644
index 0000000..bd0bfd6
--- /dev/null
+++ b/source/packages/com_mokosuitetaxi/mokosuitetaxi.xml
@@ -0,0 +1,14 @@
+
+
+ MokoSuite Taxi
+ Moko Consulting
+ 2026-06-27
+ Copyright (C) 2026 Moko Consulting.
+ GPL-3.0-or-later
+ 06.00.00
+ Moko\Component\MokoSuiteTaxi
+
+ servicessrctmpl
+
+
+
diff --git a/source/packages/plg_system_mokosuitetaxi/language/en-GB/plg_system_mokosuitetaxi.ini b/source/packages/plg_system_mokosuitetaxi/language/en-GB/plg_system_mokosuitetaxi.ini
new file mode 100644
index 0000000..6c013ee
--- /dev/null
+++ b/source/packages/plg_system_mokosuitetaxi/language/en-GB/plg_system_mokosuitetaxi.ini
@@ -0,0 +1,2 @@
+PLG_SYSTEM_MOKOSUITETAXI="System - MokoSuite Taxi"
+PLG_SYSTEM_MOKOSUITETAXI_DESC="Ride-hailing, dispatch, fleet management, fare zones, and driver scheduling"
diff --git a/source/packages/plg_system_mokosuitetaxi/language/en-GB/plg_system_mokosuitetaxi.sys.ini b/source/packages/plg_system_mokosuitetaxi/language/en-GB/plg_system_mokosuitetaxi.sys.ini
new file mode 100644
index 0000000..6c013ee
--- /dev/null
+++ b/source/packages/plg_system_mokosuitetaxi/language/en-GB/plg_system_mokosuitetaxi.sys.ini
@@ -0,0 +1,2 @@
+PLG_SYSTEM_MOKOSUITETAXI="System - MokoSuite Taxi"
+PLG_SYSTEM_MOKOSUITETAXI_DESC="Ride-hailing, dispatch, fleet management, fare zones, and driver scheduling"
diff --git a/source/packages/plg_system_mokosuitetaxi/mokosuitetaxi.xml b/source/packages/plg_system_mokosuitetaxi/mokosuitetaxi.xml
new file mode 100644
index 0000000..bc6ab3f
--- /dev/null
+++ b/source/packages/plg_system_mokosuitetaxi/mokosuitetaxi.xml
@@ -0,0 +1,67 @@
+
+
+ System - MokoSuite Taxi
+ mokosuitetaxi
+ Moko Consulting
+ 2026-06-27
+ Copyright (C) 2026 Moko Consulting. All rights reserved.
+ GPL-3.0-or-later
+ hello@mokoconsulting.tech
+ https://mokoconsulting.tech
+ 06.00.00
+ 8.3
+ PLG_SYSTEM_MOKOSUITETAXI_DESC
+ Moko\Plugin\System\MokoSuiteTaxi
+
+ src
+ services
+ language
+ sql
+
+
+ en-GB/plg_system_mokosuitetaxi.ini
+ en-GB/plg_system_mokosuitetaxi.sys.ini
+
+ sql/install.mysql.sql
+ sql/uninstall.mysql.sql
+
+
+
+
+
+
+
+
+
diff --git a/source/packages/plg_system_mokosuitetaxi/services/provider.php b/source/packages/plg_system_mokosuitetaxi/services/provider.php
new file mode 100644
index 0000000..130e71f
--- /dev/null
+++ b/source/packages/plg_system_mokosuitetaxi/services/provider.php
@@ -0,0 +1,33 @@
+
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ * Authored-by: Moko Consulting
+ */
+
+defined('_JEXEC') or die;
+
+use Joomla\CMS\Extension\PluginInterface;
+use Joomla\CMS\Factory;
+use Joomla\CMS\Plugin\PluginHelper;
+use Joomla\DI\Container;
+use Joomla\DI\ServiceProviderInterface;
+use Joomla\Event\DispatcherInterface;
+use Moko\Plugin\System\MokoSuiteTaxi\Extension\Taxi;
+
+return new class implements ServiceProviderInterface
+{
+ public function register(Container $container): void
+ {
+ $container->set(
+ PluginInterface::class,
+ function (Container $container) {
+ $dispatcher = $container->get(DispatcherInterface::class);
+ $plugin = new Taxi($dispatcher, (array) PluginHelper::getPlugin('system', 'mokosuitetaxi'));
+ $plugin->setApplication(Factory::getApplication());
+ return $plugin;
+ }
+ );
+ }
+};
diff --git a/source/packages/plg_system_mokosuitetaxi/sql/install.mysql.sql b/source/packages/plg_system_mokosuitetaxi/sql/install.mysql.sql
new file mode 100644
index 0000000..d3c40b2
--- /dev/null
+++ b/source/packages/plg_system_mokosuitetaxi/sql/install.mysql.sql
@@ -0,0 +1,216 @@
+--
+-- MokoSuite Taxi Tables
+--
+
+CREATE TABLE IF NOT EXISTS `#__mokosuitetaxi_vehicles` (
+ `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
+ `plate_number` VARCHAR(20) NOT NULL,
+ `vin` VARCHAR(17) NOT NULL DEFAULT '',
+ `make` VARCHAR(100) NOT NULL,
+ `model` VARCHAR(100) NOT NULL,
+ `year` SMALLINT UNSIGNED NOT NULL,
+ `color` VARCHAR(50) NOT NULL DEFAULT '',
+ `vehicle_type` ENUM('sedan','suv','van','luxury','minibus','motorcycle') NOT NULL DEFAULT 'sedan',
+ `capacity` TINYINT UNSIGNED NOT NULL DEFAULT 4,
+ `fuel_type` ENUM('gasoline','diesel','electric','hybrid') NOT NULL DEFAULT 'gasoline',
+ `status` ENUM('available','in_service','maintenance','retired') NOT NULL DEFAULT 'available',
+ `insurance_expiry` DATE DEFAULT NULL,
+ `inspection_expiry` DATE DEFAULT NULL,
+ `odometer_km` INT UNSIGNED NOT NULL DEFAULT 0,
+ `photo` VARCHAR(500) NOT NULL DEFAULT '',
+ `notes` TEXT,
+ `published` TINYINT NOT NULL DEFAULT 1,
+ `created` DATETIME NOT NULL,
+ `modified` DATETIME DEFAULT NULL,
+ `created_by` INT NOT NULL DEFAULT 0,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `idx_plate` (`plate_number`),
+ KEY `idx_status` (`status`),
+ KEY `idx_type` (`vehicle_type`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+CREATE TABLE IF NOT EXISTS `#__mokosuitetaxi_drivers` (
+ `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
+ `contact_id` INT DEFAULT NULL COMMENT 'FK to #__contact_details',
+ `name` VARCHAR(255) NOT NULL,
+ `phone` VARCHAR(50) NOT NULL DEFAULT '',
+ `email` VARCHAR(255) NOT NULL DEFAULT '',
+ `license_number` VARCHAR(50) NOT NULL,
+ `license_expiry` DATE DEFAULT NULL,
+ `license_class` VARCHAR(10) NOT NULL DEFAULT '',
+ `vehicle_id` INT UNSIGNED DEFAULT NULL,
+ `status` ENUM('active','inactive','suspended','pending_approval') NOT NULL DEFAULT 'pending_approval',
+ `rating` DECIMAL(3,2) NOT NULL DEFAULT 5.00,
+ `total_rides` INT UNSIGNED NOT NULL DEFAULT 0,
+ `total_earnings` DECIMAL(12,2) NOT NULL DEFAULT 0.00,
+ `commission_rate` DECIMAL(5,2) DEFAULT NULL COMMENT 'Override platform default',
+ `photo` VARCHAR(500) NOT NULL DEFAULT '',
+ `background_check_date` DATE DEFAULT NULL,
+ `published` TINYINT NOT NULL DEFAULT 1,
+ `created` DATETIME NOT NULL,
+ `modified` DATETIME DEFAULT NULL,
+ `created_by` INT NOT NULL DEFAULT 0,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `idx_license` (`license_number`),
+ KEY `idx_contact` (`contact_id`),
+ KEY `idx_vehicle` (`vehicle_id`),
+ KEY `idx_status` (`status`),
+ KEY `idx_rating` (`rating`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+CREATE TABLE IF NOT EXISTS `#__mokosuitetaxi_zones` (
+ `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
+ `name` VARCHAR(255) NOT NULL,
+ `description` TEXT,
+ `zone_type` ENUM('pickup','dropoff','service','surge','restricted') NOT NULL DEFAULT 'service',
+ `boundary_geojson` JSON COMMENT 'GeoJSON polygon defining zone boundary',
+ `center_lat` DECIMAL(10,7) DEFAULT NULL,
+ `center_lng` DECIMAL(10,7) DEFAULT NULL,
+ `radius_km` DECIMAL(8,2) DEFAULT NULL COMMENT 'Circular zone fallback if no polygon',
+ `fare_multiplier` DECIMAL(4,2) NOT NULL DEFAULT 1.00,
+ `active_hours_start` TIME DEFAULT NULL,
+ `active_hours_end` TIME DEFAULT NULL,
+ `published` TINYINT NOT NULL DEFAULT 1,
+ `ordering` INT NOT NULL DEFAULT 0,
+ `created` DATETIME NOT NULL,
+ PRIMARY KEY (`id`),
+ KEY `idx_type` (`zone_type`),
+ KEY `idx_center` (`center_lat`, `center_lng`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+CREATE TABLE IF NOT EXISTS `#__mokosuitetaxi_fares` (
+ `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
+ `title` VARCHAR(255) NOT NULL,
+ `vehicle_type` ENUM('sedan','suv','van','luxury','minibus','motorcycle','all') NOT NULL DEFAULT 'all',
+ `zone_id` INT UNSIGNED DEFAULT NULL COMMENT 'Zone-specific pricing, NULL = default',
+ `base_fare` DECIMAL(10,2) NOT NULL DEFAULT 2.50,
+ `per_km` DECIMAL(10,2) NOT NULL DEFAULT 1.50,
+ `per_minute` DECIMAL(10,2) NOT NULL DEFAULT 0.25,
+ `minimum_fare` DECIMAL(10,2) NOT NULL DEFAULT 5.00,
+ `booking_fee` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
+ `peak_multiplier` DECIMAL(4,2) NOT NULL DEFAULT 1.00,
+ `peak_hours_start` TIME DEFAULT NULL,
+ `peak_hours_end` TIME DEFAULT NULL,
+ `night_surcharge` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
+ `airport_surcharge` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
+ `effective_from` DATE DEFAULT NULL,
+ `effective_to` DATE DEFAULT NULL,
+ `published` TINYINT NOT NULL DEFAULT 1,
+ `created` DATETIME NOT NULL,
+ PRIMARY KEY (`id`),
+ KEY `idx_vehicle_type` (`vehicle_type`),
+ KEY `idx_zone` (`zone_id`),
+ KEY `idx_effective` (`effective_from`, `effective_to`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+CREATE TABLE IF NOT EXISTS `#__mokosuitetaxi_rides` (
+ `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
+ `ride_ref` VARCHAR(20) NOT NULL,
+ `rider_contact_id` INT DEFAULT NULL COMMENT 'FK to #__contact_details',
+ `rider_name` VARCHAR(255) NOT NULL,
+ `rider_phone` VARCHAR(50) NOT NULL DEFAULT '',
+ `driver_id` INT UNSIGNED DEFAULT NULL,
+ `vehicle_id` INT UNSIGNED DEFAULT NULL,
+ `status` ENUM('requested','dispatched','accepted','arriving','in_progress','completed','cancelled','no_driver') NOT NULL DEFAULT 'requested',
+ `ride_type` ENUM('on_demand','scheduled','airport','corporate') NOT NULL DEFAULT 'on_demand',
+ `vehicle_type_requested` ENUM('sedan','suv','van','luxury','minibus','motorcycle','any') NOT NULL DEFAULT 'any',
+ `pickup_address` VARCHAR(500) NOT NULL,
+ `pickup_lat` DECIMAL(10,7) DEFAULT NULL,
+ `pickup_lng` DECIMAL(10,7) DEFAULT NULL,
+ `dropoff_address` VARCHAR(500) NOT NULL DEFAULT '',
+ `dropoff_lat` DECIMAL(10,7) DEFAULT NULL,
+ `dropoff_lng` DECIMAL(10,7) DEFAULT NULL,
+ `pickup_zone_id` INT UNSIGNED DEFAULT NULL,
+ `dropoff_zone_id` INT UNSIGNED DEFAULT NULL,
+ `scheduled_at` DATETIME DEFAULT NULL,
+ `dispatched_at` DATETIME DEFAULT NULL,
+ `accepted_at` DATETIME DEFAULT NULL,
+ `arrived_at` DATETIME DEFAULT NULL,
+ `started_at` DATETIME DEFAULT NULL,
+ `completed_at` DATETIME DEFAULT NULL,
+ `cancelled_at` DATETIME DEFAULT NULL,
+ `cancelled_by` ENUM('rider','driver','system') DEFAULT NULL,
+ `cancel_reason` VARCHAR(500) NOT NULL DEFAULT '',
+ `distance_km` DECIMAL(10,2) DEFAULT NULL,
+ `duration_minutes` DECIMAL(10,2) DEFAULT NULL,
+ `fare_id` INT UNSIGNED DEFAULT NULL,
+ `base_fare` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
+ `distance_charge` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
+ `time_charge` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
+ `surge_multiplier` DECIMAL(4,2) NOT NULL DEFAULT 1.00,
+ `surcharges` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
+ `discount` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
+ `total_fare` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
+ `driver_payout` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
+ `platform_fee` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
+ `payment_method` ENUM('cash','card','wallet','corporate') NOT NULL DEFAULT 'cash',
+ `payment_status` ENUM('pending','paid','refunded') NOT NULL DEFAULT 'pending',
+ `passenger_count` TINYINT UNSIGNED NOT NULL DEFAULT 1,
+ `notes` TEXT,
+ `route_polyline` TEXT COMMENT 'Encoded polyline of actual route',
+ `created` DATETIME NOT NULL,
+ `created_by` INT NOT NULL DEFAULT 0,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `idx_ref` (`ride_ref`),
+ KEY `idx_rider` (`rider_contact_id`),
+ KEY `idx_driver` (`driver_id`),
+ KEY `idx_vehicle` (`vehicle_id`),
+ KEY `idx_status` (`status`),
+ KEY `idx_type` (`ride_type`),
+ KEY `idx_created` (`created`),
+ KEY `idx_scheduled` (`scheduled_at`),
+ KEY `idx_pickup_zone` (`pickup_zone_id`),
+ KEY `idx_dropoff_zone` (`dropoff_zone_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+CREATE TABLE IF NOT EXISTS `#__mokosuitetaxi_ratings` (
+ `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
+ `ride_id` INT UNSIGNED NOT NULL,
+ `rated_by` ENUM('rider','driver') NOT NULL,
+ `rating` TINYINT UNSIGNED NOT NULL COMMENT '1-5 stars',
+ `comment` TEXT,
+ `tags` VARCHAR(500) NOT NULL DEFAULT '' COMMENT 'Comma-separated feedback tags',
+ `created` DATETIME NOT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `idx_ride_rater` (`ride_id`, `rated_by`),
+ KEY `idx_rating` (`rating`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+CREATE TABLE IF NOT EXISTS `#__mokosuitetaxi_dispatch` (
+ `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
+ `ride_id` INT UNSIGNED NOT NULL,
+ `driver_id` INT UNSIGNED NOT NULL,
+ `status` ENUM('offered','accepted','rejected','expired') NOT NULL DEFAULT 'offered',
+ `offered_at` DATETIME NOT NULL,
+ `responded_at` DATETIME DEFAULT NULL,
+ `distance_to_pickup_km` DECIMAL(10,2) DEFAULT NULL,
+ `eta_minutes` DECIMAL(10,2) DEFAULT NULL,
+ `attempt_number` TINYINT UNSIGNED NOT NULL DEFAULT 1,
+ PRIMARY KEY (`id`),
+ KEY `idx_ride` (`ride_id`),
+ KEY `idx_driver` (`driver_id`),
+ KEY `idx_status` (`status`),
+ KEY `idx_offered` (`offered_at`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+CREATE TABLE IF NOT EXISTS `#__mokosuitetaxi_shifts` (
+ `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
+ `driver_id` INT UNSIGNED NOT NULL,
+ `vehicle_id` INT UNSIGNED DEFAULT NULL,
+ `start_time` DATETIME NOT NULL,
+ `end_time` DATETIME DEFAULT NULL,
+ `status` ENUM('scheduled','active','completed','cancelled') NOT NULL DEFAULT 'scheduled',
+ `start_lat` DECIMAL(10,7) DEFAULT NULL,
+ `start_lng` DECIMAL(10,7) DEFAULT NULL,
+ `end_lat` DECIMAL(10,7) DEFAULT NULL,
+ `end_lng` DECIMAL(10,7) DEFAULT NULL,
+ `total_rides` INT UNSIGNED NOT NULL DEFAULT 0,
+ `total_distance_km` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
+ `total_earnings` DECIMAL(12,2) NOT NULL DEFAULT 0.00,
+ `created` DATETIME NOT NULL,
+ PRIMARY KEY (`id`),
+ KEY `idx_driver` (`driver_id`),
+ KEY `idx_vehicle` (`vehicle_id`),
+ KEY `idx_times` (`start_time`, `end_time`),
+ KEY `idx_status` (`status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
diff --git a/source/packages/plg_system_mokosuitetaxi/sql/uninstall.mysql.sql b/source/packages/plg_system_mokosuitetaxi/sql/uninstall.mysql.sql
new file mode 100644
index 0000000..6e0b8c6
--- /dev/null
+++ b/source/packages/plg_system_mokosuitetaxi/sql/uninstall.mysql.sql
@@ -0,0 +1,12 @@
+--
+-- MokoSuite Taxi — Uninstall
+--
+
+DROP TABLE IF EXISTS `#__mokosuitetaxi_shifts`;
+DROP TABLE IF EXISTS `#__mokosuitetaxi_dispatch`;
+DROP TABLE IF EXISTS `#__mokosuitetaxi_ratings`;
+DROP TABLE IF EXISTS `#__mokosuitetaxi_rides`;
+DROP TABLE IF EXISTS `#__mokosuitetaxi_fares`;
+DROP TABLE IF EXISTS `#__mokosuitetaxi_zones`;
+DROP TABLE IF EXISTS `#__mokosuitetaxi_drivers`;
+DROP TABLE IF EXISTS `#__mokosuitetaxi_vehicles`;
diff --git a/source/packages/plg_system_mokosuitetaxi/src/Extension/Taxi.php b/source/packages/plg_system_mokosuitetaxi/src/Extension/Taxi.php
new file mode 100644
index 0000000..55bf0e8
--- /dev/null
+++ b/source/packages/plg_system_mokosuitetaxi/src/Extension/Taxi.php
@@ -0,0 +1,24 @@
+
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ * Authored-by: Moko Consulting
+ */
+
+namespace Moko\Plugin\System\MokoSuiteTaxi\Extension;
+
+defined('_JEXEC') or die;
+
+use Joomla\CMS\Plugin\CMSPlugin;
+use Joomla\Event\SubscriberInterface;
+
+class Taxi extends CMSPlugin implements SubscriberInterface
+{
+ protected $autoloadLanguage = true;
+
+ public static function getSubscribedEvents(): array
+ {
+ return [];
+ }
+}
diff --git a/source/packages/plg_system_mokosuitetaxi/src/Helper/DispatchHelper.php b/source/packages/plg_system_mokosuitetaxi/src/Helper/DispatchHelper.php
new file mode 100644
index 0000000..0312d68
--- /dev/null
+++ b/source/packages/plg_system_mokosuitetaxi/src/Helper/DispatchHelper.php
@@ -0,0 +1,177 @@
+
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ * Authored-by: Moko Consulting
+ */
+
+declare(strict_types=1);
+
+namespace Moko\Plugin\System\MokoSuiteTaxi\Helper;
+
+defined('_JEXEC') or die;
+
+use Joomla\CMS\Factory;
+use Joomla\Database\DatabaseInterface;
+
+class DispatchHelper
+{
+ public static function findNearbyDrivers(
+ float $lat,
+ float $lng,
+ float $radiusKm = 10.0,
+ string $vehicleType = 'any',
+ int $limit = 10
+ ): array {
+ $db = Factory::getContainer()->get(DatabaseInterface::class);
+
+ $query = $db->getQuery(true)
+ ->select([
+ 'd.id', 'd.name', 'd.rating', 'd.vehicle_id',
+ 'v.plate_number', 'v.vehicle_type', 'v.capacity',
+ 's.start_lat AS lat', 's.start_lng AS lng',
+ ])
+ ->from($db->quoteName('#__mokosuitetaxi_drivers', 'd'))
+ ->join('INNER', $db->quoteName('#__mokosuitetaxi_shifts', 's')
+ . ' ON s.driver_id = d.id AND s.status = ' . $db->quote('active'))
+ ->join('LEFT', $db->quoteName('#__mokosuitetaxi_vehicles', 'v')
+ . ' ON v.id = d.vehicle_id')
+ ->where($db->quoteName('d.status') . ' = ' . $db->quote('active'))
+ ->where($db->quoteName('d.published') . ' = 1')
+ ->where('s.start_lat IS NOT NULL')
+ ->where('s.start_lng IS NOT NULL')
+ ->order('d.rating DESC')
+ ->setLimit($limit);
+
+ if ($vehicleType !== 'any') {
+ $query->where($db->quoteName('v.vehicle_type') . ' = ' . $db->quote($vehicleType));
+ }
+
+ $db->setQuery($query);
+ $drivers = $db->loadObjectList() ?: [];
+
+ $nearby = [];
+
+ foreach ($drivers as $driver) {
+ $distance = TaxiHelper::haversineDistance($lat, $lng, (float) $driver->lat, (float) $driver->lng);
+
+ if ($distance <= $radiusKm) {
+ $driver->distance_km = round($distance, 2);
+ $driver->eta_minutes = round($distance * 2.5, 1);
+ $nearby[] = $driver;
+ }
+ }
+
+ usort($nearby, fn($a, $b) => $a->distance_km <=> $b->distance_km);
+
+ return $nearby;
+ }
+
+ public static function dispatchRide(int $rideId, int $driverId): object
+ {
+ $db = Factory::getContainer()->get(DatabaseInterface::class);
+ $now = Factory::getDate()->toSql();
+
+ // FOR UPDATE lock to prevent race condition on ride acceptance
+ $db->transactionStart();
+
+ try {
+ $db->setQuery('SELECT id, status FROM ' . $db->quoteName('#__mokosuitetaxi_rides')
+ . ' WHERE id = ' . (int) $rideId . ' FOR UPDATE');
+ $ride = $db->loadObject();
+
+ if (!$ride || !in_array($ride->status, ['requested', 'dispatched'], true)) {
+ $db->transactionRollback();
+ throw new \RuntimeException('Ride is no longer available for dispatch');
+ }
+
+ $attempt = 1;
+ $db->setQuery($db->getQuery(true)
+ ->select('COALESCE(MAX(attempt_number), 0)')
+ ->from('#__mokosuitetaxi_dispatch')
+ ->where('ride_id = ' . (int) $rideId));
+ $attempt = (int) $db->loadResult() + 1;
+
+ $dispatch = (object) [
+ 'ride_id' => $rideId,
+ 'driver_id' => $driverId,
+ 'status' => 'offered',
+ 'offered_at' => $now,
+ 'attempt_number' => $attempt,
+ ];
+
+ $db->insertObject('#__mokosuitetaxi_dispatch', $dispatch, 'id');
+
+ $update = (object) [
+ 'id' => $rideId,
+ 'status' => 'dispatched',
+ 'driver_id' => $driverId,
+ 'dispatched_at' => $now,
+ ];
+ $db->updateObject('#__mokosuitetaxi_rides', $update, 'id');
+
+ $db->transactionCommit();
+
+ return $dispatch;
+ } catch (\Throwable $e) {
+ $db->transactionRollback();
+ throw $e;
+ }
+ }
+
+ public static function acceptRide(int $rideId, int $driverId): bool
+ {
+ $db = Factory::getContainer()->get(DatabaseInterface::class);
+ $now = Factory::getDate()->toSql();
+
+ $db->transactionStart();
+
+ try {
+ $db->setQuery('SELECT id, status, driver_id FROM ' . $db->quoteName('#__mokosuitetaxi_rides')
+ . ' WHERE id = ' . (int) $rideId . ' FOR UPDATE');
+ $ride = $db->loadObject();
+
+ if (!$ride || $ride->status !== 'dispatched' || (int) $ride->driver_id !== $driverId) {
+ $db->transactionRollback();
+ return false;
+ }
+
+ $db->updateObject('#__mokosuitetaxi_rides', (object) [
+ 'id' => $rideId,
+ 'status' => 'accepted',
+ 'accepted_at' => $now,
+ ], 'id');
+
+ $query = $db->getQuery(true)
+ ->update('#__mokosuitetaxi_dispatch')
+ ->set($db->quoteName('status') . ' = ' . $db->quote('accepted'))
+ ->set($db->quoteName('responded_at') . ' = ' . $db->quote($now))
+ ->where('ride_id = ' . (int) $rideId)
+ ->where('driver_id = ' . (int) $driverId)
+ ->where($db->quoteName('status') . ' = ' . $db->quote('offered'));
+ $db->setQuery($query)->execute();
+
+ $db->transactionCommit();
+
+ return true;
+ } catch (\Throwable $e) {
+ $db->transactionRollback();
+ throw $e;
+ }
+ }
+
+ public static function getActiveRidesForDriver(int $driverId): array
+ {
+ $db = Factory::getContainer()->get(DatabaseInterface::class);
+
+ $db->setQuery($db->getQuery(true)
+ ->select('*')
+ ->from('#__mokosuitetaxi_rides')
+ ->where('driver_id = ' . (int) $driverId)
+ ->where($db->quoteName('status') . ' IN (' . implode(',', array_map([$db, 'quote'], ['dispatched', 'accepted', 'arriving', 'in_progress'])) . ')')
+ ->order('created DESC'));
+
+ return $db->loadObjectList() ?: [];
+ }
+}
diff --git a/source/packages/plg_system_mokosuitetaxi/src/Helper/FareHelper.php b/source/packages/plg_system_mokosuitetaxi/src/Helper/FareHelper.php
new file mode 100644
index 0000000..87913b7
--- /dev/null
+++ b/source/packages/plg_system_mokosuitetaxi/src/Helper/FareHelper.php
@@ -0,0 +1,126 @@
+
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ * Authored-by: Moko Consulting
+ */
+
+declare(strict_types=1);
+
+namespace Moko\Plugin\System\MokoSuiteTaxi\Helper;
+
+defined('_JEXEC') or die;
+
+use Joomla\CMS\Factory;
+use Joomla\Database\DatabaseInterface;
+
+class FareHelper
+{
+ public static function calculateFare(
+ float $distanceKm,
+ float $durationMinutes,
+ string $vehicleType = 'sedan',
+ ?int $zoneId = null,
+ float $surgeMultiplier = 1.0
+ ): object {
+ $rule = self::getApplicableRule($vehicleType, $zoneId);
+
+ $baseFare = (float) $rule->base_fare;
+ $distanceCharge = $distanceKm * (float) $rule->per_km;
+ $timeCharge = $durationMinutes * (float) $rule->per_minute;
+
+ $subtotal = ($baseFare + $distanceCharge + $timeCharge) * $surgeMultiplier;
+ $subtotal += (float) $rule->booking_fee;
+
+ $hour = (int) date('H');
+ if ($hour >= 22 || $hour < 6) {
+ $subtotal += (float) $rule->night_surcharge;
+ }
+
+ $subtotal *= (float) ($rule->peak_multiplier ?? 1.0);
+ $total = max($subtotal, (float) $rule->minimum_fare);
+
+ return (object) [
+ 'fare_id' => (int) $rule->id,
+ 'base_fare' => $baseFare,
+ 'distance_charge' => round($distanceCharge, 2),
+ 'time_charge' => round($timeCharge, 2),
+ 'surge_multiplier' => $surgeMultiplier,
+ 'surcharges' => round((float) $rule->night_surcharge + (float) $rule->booking_fee, 2),
+ 'total_fare' => round($total, 2),
+ ];
+ }
+
+ public static function estimateFare(
+ float $pickupLat,
+ float $pickupLng,
+ float $dropoffLat,
+ float $dropoffLng,
+ string $vehicleType = 'sedan'
+ ): object {
+ $distanceKm = TaxiHelper::haversineDistance($pickupLat, $pickupLng, $dropoffLat, $dropoffLng);
+ $distanceKm *= 1.3; // road distance approximation
+ $durationMinutes = $distanceKm * 2.0;
+
+ $estimate = self::calculateFare($distanceKm, $durationMinutes, $vehicleType);
+ $estimate->estimated_distance_km = round($distanceKm, 2);
+ $estimate->estimated_duration_minutes = round($durationMinutes, 1);
+
+ return $estimate;
+ }
+
+ public static function getApplicableRule(string $vehicleType = 'sedan', ?int $zoneId = null): object
+ {
+ $db = Factory::getContainer()->get(DatabaseInterface::class);
+ $now = Factory::getDate()->format('Y-m-d');
+
+ $query = $db->getQuery(true)
+ ->select('*')
+ ->from('#__mokosuitetaxi_fares')
+ ->where($db->quoteName('published') . ' = 1')
+ ->where('(' . $db->quoteName('effective_from') . ' IS NULL OR ' . $db->quoteName('effective_from') . ' <= ' . $db->quote($now) . ')')
+ ->where('(' . $db->quoteName('effective_to') . ' IS NULL OR ' . $db->quoteName('effective_to') . ' >= ' . $db->quote($now) . ')')
+ ->order('zone_id DESC, vehicle_type DESC')
+ ->setLimit(1);
+
+ if ($zoneId) {
+ $query->where('(' . $db->quoteName('zone_id') . ' = ' . (int) $zoneId . ' OR ' . $db->quoteName('zone_id') . ' IS NULL)');
+ } else {
+ $query->where($db->quoteName('zone_id') . ' IS NULL');
+ }
+
+ $query->where('(' . $db->quoteName('vehicle_type') . ' = ' . $db->quote($vehicleType) . ' OR ' . $db->quoteName('vehicle_type') . ' = ' . $db->quote('all') . ')');
+
+ $db->setQuery($query);
+ $rule = $db->loadObject();
+
+ if (!$rule) {
+ return (object) [
+ 'id' => 0, 'base_fare' => 2.50, 'per_km' => 1.50, 'per_minute' => 0.25,
+ 'minimum_fare' => 5.00, 'booking_fee' => 0.00, 'peak_multiplier' => 1.00,
+ 'night_surcharge' => 0.00, 'airport_surcharge' => 0.00,
+ ];
+ }
+
+ return $rule;
+ }
+
+ public static function calculateDriverPayout(float $totalFare, ?float $driverCommissionRate = null): object
+ {
+ if ($driverCommissionRate === null) {
+ $params = Factory::getApplication()->bootPlugin('mokosuitetaxi', 'system')->params;
+ $driverCommissionRate = (float) ($params->get('commission_rate', 20));
+ }
+
+ $platformFee = round($totalFare * ($driverCommissionRate / 100), 2);
+ $driverPayout = round($totalFare - $platformFee, 2);
+
+ return (object) [
+ 'total_fare' => $totalFare,
+ 'commission_rate' => $driverCommissionRate,
+ 'platform_fee' => $platformFee,
+ 'driver_payout' => $driverPayout,
+ ];
+ }
+}
diff --git a/source/packages/plg_system_mokosuitetaxi/src/Helper/RideHelper.php b/source/packages/plg_system_mokosuitetaxi/src/Helper/RideHelper.php
new file mode 100644
index 0000000..d68fba0
--- /dev/null
+++ b/source/packages/plg_system_mokosuitetaxi/src/Helper/RideHelper.php
@@ -0,0 +1,211 @@
+
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ * Authored-by: Moko Consulting
+ */
+
+declare(strict_types=1);
+
+namespace Moko\Plugin\System\MokoSuiteTaxi\Helper;
+
+defined('_JEXEC') or die;
+
+use Joomla\CMS\Factory;
+use Joomla\Database\DatabaseInterface;
+
+class RideHelper
+{
+ private const STATUS_FLOW = [
+ 'requested' => ['dispatched', 'cancelled', 'no_driver'],
+ 'dispatched' => ['accepted', 'cancelled', 'no_driver'],
+ 'accepted' => ['arriving', 'cancelled'],
+ 'arriving' => ['in_progress', 'cancelled'],
+ 'in_progress' => ['completed', 'cancelled'],
+ 'completed' => [],
+ 'cancelled' => [],
+ 'no_driver' => ['requested'],
+ ];
+
+ public static function requestRide(array $data): object
+ {
+ $db = Factory::getContainer()->get(DatabaseInterface::class);
+ $now = Factory::getDate()->toSql();
+ $user = Factory::getApplication()->getIdentity();
+
+ $ride = (object) [
+ 'ride_ref' => TaxiHelper::generateRideRef(),
+ 'rider_contact_id' => isset($data['rider_contact_id']) ? (int) $data['rider_contact_id'] : null,
+ 'rider_name' => $data['rider_name'],
+ 'rider_phone' => $data['rider_phone'] ?? '',
+ 'status' => 'requested',
+ 'ride_type' => $data['ride_type'] ?? 'on_demand',
+ 'vehicle_type_requested' => $data['vehicle_type'] ?? 'any',
+ 'pickup_address' => $data['pickup_address'],
+ 'pickup_lat' => $data['pickup_lat'] ?? null,
+ 'pickup_lng' => $data['pickup_lng'] ?? null,
+ 'dropoff_address' => $data['dropoff_address'] ?? '',
+ 'dropoff_lat' => $data['dropoff_lat'] ?? null,
+ 'dropoff_lng' => $data['dropoff_lng'] ?? null,
+ 'scheduled_at' => $data['scheduled_at'] ?? null,
+ 'passenger_count' => (int) ($data['passenger_count'] ?? 1),
+ 'payment_method' => $data['payment_method'] ?? 'cash',
+ 'notes' => $data['notes'] ?? '',
+ 'created' => $now,
+ 'created_by' => $user ? (int) $user->id : 0,
+ ];
+
+ if ($ride->pickup_lat && $ride->pickup_lng && $ride->dropoff_lat && $ride->dropoff_lng) {
+ $estimate = FareHelper::estimateFare(
+ (float) $ride->pickup_lat, (float) $ride->pickup_lng,
+ (float) $ride->dropoff_lat, (float) $ride->dropoff_lng,
+ $ride->vehicle_type_requested === 'any' ? 'sedan' : $ride->vehicle_type_requested
+ );
+ $ride->fare_id = $estimate->fare_id;
+ $ride->base_fare = $estimate->base_fare;
+ $ride->distance_charge = $estimate->distance_charge;
+ $ride->time_charge = $estimate->time_charge;
+ $ride->surcharges = $estimate->surcharges;
+ $ride->total_fare = $estimate->total_fare;
+ }
+
+ $db->insertObject('#__mokosuitetaxi_rides', $ride, 'id');
+
+ return $ride;
+ }
+
+ public static function updateStatus(int $rideId, string $newStatus): bool
+ {
+ $db = Factory::getContainer()->get(DatabaseInterface::class);
+ $now = Factory::getDate()->toSql();
+
+ $db->setQuery($db->getQuery(true)
+ ->select('id, status, driver_id')
+ ->from('#__mokosuitetaxi_rides')
+ ->where('id = ' . (int) $rideId));
+ $ride = $db->loadObject();
+
+ if (!$ride) {
+ return false;
+ }
+
+ $allowed = self::STATUS_FLOW[$ride->status] ?? [];
+
+ if (!in_array($newStatus, $allowed, true)) {
+ throw new \InvalidArgumentException(
+ "Cannot transition from '{$ride->status}' to '{$newStatus}'"
+ );
+ }
+
+ $update = (object) ['id' => $rideId, 'status' => $newStatus];
+
+ $timestampMap = [
+ 'arriving' => 'arrived_at',
+ 'in_progress' => 'started_at',
+ 'completed' => 'completed_at',
+ 'cancelled' => 'cancelled_at',
+ ];
+
+ if (isset($timestampMap[$newStatus])) {
+ $field = $timestampMap[$newStatus];
+ $update->$field = $now;
+ }
+
+ $db->updateObject('#__mokosuitetaxi_rides', $update, 'id');
+
+ if ($newStatus === 'completed' && $ride->driver_id) {
+ self::updateDriverStats((int) $ride->driver_id);
+ }
+
+ return true;
+ }
+
+ public static function completeRide(int $rideId, float $distanceKm, float $durationMinutes): object
+ {
+ $db = Factory::getContainer()->get(DatabaseInterface::class);
+ $now = Factory::getDate()->toSql();
+
+ $db->setQuery($db->getQuery(true)->select('*')->from('#__mokosuitetaxi_rides')->where('id = ' . (int) $rideId));
+ $ride = $db->loadObject();
+
+ if (!$ride || $ride->status !== 'in_progress') {
+ throw new \RuntimeException('Ride is not in progress');
+ }
+
+ $vehicleType = $ride->vehicle_type_requested === 'any' ? 'sedan' : $ride->vehicle_type_requested;
+ $fare = FareHelper::calculateFare($distanceKm, $durationMinutes, $vehicleType, null, (float) $ride->surge_multiplier);
+ $payout = FareHelper::calculateDriverPayout($fare->total_fare);
+
+ $update = (object) [
+ 'id' => $rideId,
+ 'status' => 'completed',
+ 'completed_at' => $now,
+ 'distance_km' => $distanceKm,
+ 'duration_minutes' => $durationMinutes,
+ 'base_fare' => $fare->base_fare,
+ 'distance_charge' => $fare->distance_charge,
+ 'time_charge' => $fare->time_charge,
+ 'surcharges' => $fare->surcharges,
+ 'total_fare' => $fare->total_fare,
+ 'driver_payout' => $payout->driver_payout,
+ 'platform_fee' => $payout->platform_fee,
+ ];
+
+ $db->updateObject('#__mokosuitetaxi_rides', $update, 'id');
+
+ if ($ride->driver_id) {
+ self::updateDriverStats((int) $ride->driver_id);
+ }
+
+ return $update;
+ }
+
+ public static function getById(int $rideId): ?object
+ {
+ $db = Factory::getContainer()->get(DatabaseInterface::class);
+
+ $query = $db->getQuery(true)
+ ->select([
+ 'r.*',
+ 'd.name AS driver_name', 'd.phone AS driver_phone', 'd.rating AS driver_rating',
+ 'v.plate_number', 'v.make AS vehicle_make', 'v.model AS vehicle_model', 'v.color AS vehicle_color',
+ ])
+ ->from($db->quoteName('#__mokosuitetaxi_rides', 'r'))
+ ->join('LEFT', $db->quoteName('#__mokosuitetaxi_drivers', 'd') . ' ON d.id = r.driver_id')
+ ->join('LEFT', $db->quoteName('#__mokosuitetaxi_vehicles', 'v') . ' ON v.id = r.vehicle_id')
+ ->where('r.id = ' . (int) $rideId);
+
+ $db->setQuery($query);
+
+ return $db->loadObject() ?: null;
+ }
+
+ private static function updateDriverStats(int $driverId): void
+ {
+ $db = Factory::getContainer()->get(DatabaseInterface::class);
+
+ $db->setQuery($db->getQuery(true)
+ ->select(['COUNT(*) AS total_rides', 'COALESCE(SUM(driver_payout), 0) AS total_earnings'])
+ ->from('#__mokosuitetaxi_rides')
+ ->where('driver_id = ' . (int) $driverId)
+ ->where($db->quoteName('status') . ' = ' . $db->quote('completed')));
+ $stats = $db->loadObject();
+
+ $db->setQuery($db->getQuery(true)
+ ->select('COALESCE(AVG(rating), 5.00)')
+ ->from('#__mokosuitetaxi_ratings')
+ ->join('INNER', '#__mokosuitetaxi_rides AS r ON r.id = ride_id')
+ ->where('r.driver_id = ' . (int) $driverId)
+ ->where($db->quoteName('rated_by') . ' = ' . $db->quote('rider')));
+ $avgRating = (float) $db->loadResult();
+
+ $db->updateObject('#__mokosuitetaxi_drivers', (object) [
+ 'id' => $driverId,
+ 'total_rides' => (int) $stats->total_rides,
+ 'total_earnings' => (float) $stats->total_earnings,
+ 'rating' => round($avgRating, 2),
+ 'modified' => Factory::getDate()->toSql(),
+ ], 'id');
+ }
+}
diff --git a/source/packages/plg_system_mokosuitetaxi/src/Helper/TaxiHelper.php b/source/packages/plg_system_mokosuitetaxi/src/Helper/TaxiHelper.php
new file mode 100644
index 0000000..22a9b56
--- /dev/null
+++ b/source/packages/plg_system_mokosuitetaxi/src/Helper/TaxiHelper.php
@@ -0,0 +1,104 @@
+
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ * Authored-by: Moko Consulting
+ */
+
+declare(strict_types=1);
+
+namespace Moko\Plugin\System\MokoSuiteTaxi\Helper;
+
+defined('_JEXEC') or die;
+
+use Joomla\CMS\Factory;
+use Joomla\Database\DatabaseInterface;
+
+class TaxiHelper
+{
+ public static function getDashboard(): object
+ {
+ $db = Factory::getContainer()->get(DatabaseInterface::class);
+ $now = Factory::getDate()->toSql();
+
+ $db->setQuery($db->getQuery(true)
+ ->select('COUNT(*)')
+ ->from('#__mokosuitetaxi_drivers')
+ ->where($db->quoteName('status') . ' = ' . $db->quote('active'))
+ ->where($db->quoteName('published') . ' = 1'));
+ $activeDrivers = (int) $db->loadResult();
+
+ $db->setQuery($db->getQuery(true)
+ ->select('COUNT(*)')
+ ->from('#__mokosuitetaxi_rides')
+ ->where($db->quoteName('status') . ' IN (' . implode(',', array_map([$db, 'quote'], ['requested', 'dispatched', 'accepted', 'arriving', 'in_progress'])) . ')'));
+ $activeRides = (int) $db->loadResult();
+
+ $db->setQuery($db->getQuery(true)
+ ->select('COUNT(*)')
+ ->from('#__mokosuitetaxi_vehicles')
+ ->where($db->quoteName('status') . ' = ' . $db->quote('available'))
+ ->where($db->quoteName('published') . ' = 1'));
+ $availableVehicles = (int) $db->loadResult();
+
+ $db->setQuery($db->getQuery(true)
+ ->select('COALESCE(SUM(' . $db->quoteName('total_fare') . '), 0)')
+ ->from('#__mokosuitetaxi_rides')
+ ->where($db->quoteName('status') . ' = ' . $db->quote('completed'))
+ ->where($db->quoteName('payment_status') . ' = ' . $db->quote('paid'))
+ ->where('DATE(' . $db->quoteName('completed_at') . ') = CURDATE()'));
+ $todayRevenue = (float) $db->loadResult();
+
+ $db->setQuery($db->getQuery(true)
+ ->select('COUNT(*)')
+ ->from('#__mokosuitetaxi_rides')
+ ->where($db->quoteName('status') . ' = ' . $db->quote('completed'))
+ ->where('DATE(' . $db->quoteName('completed_at') . ') = CURDATE()'));
+ $todayCompleted = (int) $db->loadResult();
+
+ $db->setQuery($db->getQuery(true)
+ ->select('COALESCE(AVG(' . $db->quoteName('r.rating') . '), 0)')
+ ->from($db->quoteName('#__mokosuitetaxi_ratings', 'r'))
+ ->where($db->quoteName('r.rated_by') . ' = ' . $db->quote('rider'))
+ ->where('DATE(' . $db->quoteName('r.created') . ') >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)'));
+ $avgRating = round((float) $db->loadResult(), 2);
+
+ return (object) [
+ 'active_drivers' => $activeDrivers,
+ 'active_rides' => $activeRides,
+ 'available_vehicles' => $availableVehicles,
+ 'today_revenue' => $todayRevenue,
+ 'today_completed' => $todayCompleted,
+ 'avg_rating_30d' => $avgRating,
+ ];
+ }
+
+ public static function generateRideRef(): string
+ {
+ $db = Factory::getContainer()->get(DatabaseInterface::class);
+ $prefix = 'TX';
+
+ do {
+ $ref = $prefix . strtoupper(bin2hex(random_bytes(4)));
+ $db->setQuery($db->getQuery(true)
+ ->select('COUNT(*)')
+ ->from('#__mokosuitetaxi_rides')
+ ->where($db->quoteName('ride_ref') . ' = ' . $db->quote($ref)));
+ } while ((int) $db->loadResult() > 0);
+
+ return $ref;
+ }
+
+ public static function haversineDistance(float $lat1, float $lng1, float $lat2, float $lng2): float
+ {
+ $earthRadius = 6371.0;
+ $dLat = deg2rad($lat2 - $lat1);
+ $dLng = deg2rad($lng2 - $lng1);
+ $a = sin($dLat / 2) * sin($dLat / 2) +
+ cos(deg2rad($lat1)) * cos(deg2rad($lat2)) *
+ sin($dLng / 2) * sin($dLng / 2);
+
+ return $earthRadius * 2 * atan2(sqrt($a), sqrt(1 - $a));
+ }
+}
diff --git a/source/packages/plg_system_mokosuitetaxi/src/Helper/VehicleHelper.php b/source/packages/plg_system_mokosuitetaxi/src/Helper/VehicleHelper.php
new file mode 100644
index 0000000..9566dee
--- /dev/null
+++ b/source/packages/plg_system_mokosuitetaxi/src/Helper/VehicleHelper.php
@@ -0,0 +1,97 @@
+
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ * Authored-by: Moko Consulting
+ */
+
+declare(strict_types=1);
+
+namespace Moko\Plugin\System\MokoSuiteTaxi\Helper;
+
+defined('_JEXEC') or die;
+
+use Joomla\CMS\Factory;
+use Joomla\Database\DatabaseInterface;
+
+class VehicleHelper
+{
+ public static function getById(int $vehicleId): ?object
+ {
+ $db = Factory::getContainer()->get(DatabaseInterface::class);
+
+ $query = $db->getQuery(true)
+ ->select([
+ 'v.*',
+ 'd.name AS assigned_driver',
+ 'd.id AS assigned_driver_id',
+ ])
+ ->from($db->quoteName('#__mokosuitetaxi_vehicles', 'v'))
+ ->join('LEFT', $db->quoteName('#__mokosuitetaxi_drivers', 'd')
+ . ' ON d.vehicle_id = v.id AND d.status = ' . $db->quote('active'))
+ ->where('v.id = ' . (int) $vehicleId);
+
+ $db->setQuery($query);
+
+ return $db->loadObject() ?: null;
+ }
+
+ public static function getFleetSummary(): object
+ {
+ $db = Factory::getContainer()->get(DatabaseInterface::class);
+
+ $db->setQuery($db->getQuery(true)
+ ->select([
+ $db->quoteName('status'),
+ 'COUNT(*) AS count',
+ ])
+ ->from('#__mokosuitetaxi_vehicles')
+ ->where($db->quoteName('published') . ' = 1')
+ ->group('status'));
+
+ $rows = $db->loadObjectList('status') ?: [];
+
+ $db->setQuery($db->getQuery(true)
+ ->select([
+ $db->quoteName('vehicle_type'),
+ 'COUNT(*) AS count',
+ ])
+ ->from('#__mokosuitetaxi_vehicles')
+ ->where($db->quoteName('published') . ' = 1')
+ ->group('vehicle_type'));
+
+ $byType = $db->loadObjectList('vehicle_type') ?: [];
+
+ $db->setQuery($db->getQuery(true)
+ ->select('COUNT(*)')
+ ->from('#__mokosuitetaxi_vehicles')
+ ->where($db->quoteName('published') . ' = 1')
+ ->where('(' . $db->quoteName('insurance_expiry') . ' <= DATE_ADD(CURDATE(), INTERVAL 30 DAY)'
+ . ' OR ' . $db->quoteName('inspection_expiry') . ' <= DATE_ADD(CURDATE(), INTERVAL 30 DAY))'));
+ $expiringCount = (int) $db->loadResult();
+
+ return (object) [
+ 'by_status' => $rows,
+ 'by_type' => $byType,
+ 'expiring_soon' => $expiringCount,
+ 'total' => array_sum(array_map(fn($r) => (int) $r->count, (array) $rows)),
+ ];
+ }
+
+ public static function getExpiringDocuments(int $days = 30): array
+ {
+ $db = Factory::getContainer()->get(DatabaseInterface::class);
+ $deadline = Factory::getDate('+' . $days . ' days')->format('Y-m-d');
+
+ $db->setQuery($db->getQuery(true)
+ ->select('id, plate_number, make, model, insurance_expiry, inspection_expiry')
+ ->from('#__mokosuitetaxi_vehicles')
+ ->where($db->quoteName('published') . ' = 1')
+ ->where('(' . $db->quoteName('insurance_expiry') . ' <= ' . $db->quote($deadline)
+ . ' OR ' . $db->quoteName('inspection_expiry') . ' <= ' . $db->quote($deadline) . ')')
+ ->order('LEAST(COALESCE(insurance_expiry, ' . $db->quote('9999-12-31') . '), COALESCE(inspection_expiry, ' . $db->quote('9999-12-31') . ')) ASC'));
+
+ return $db->loadObjectList() ?: [];
+ }
+}
diff --git a/source/packages/plg_system_mokosuitetaxi/src/Helper/ZoneHelper.php b/source/packages/plg_system_mokosuitetaxi/src/Helper/ZoneHelper.php
new file mode 100644
index 0000000..50a46aa
--- /dev/null
+++ b/source/packages/plg_system_mokosuitetaxi/src/Helper/ZoneHelper.php
@@ -0,0 +1,104 @@
+
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ * Authored-by: Moko Consulting
+ */
+
+declare(strict_types=1);
+
+namespace Moko\Plugin\System\MokoSuiteTaxi\Helper;
+
+defined('_JEXEC') or die;
+
+use Joomla\CMS\Factory;
+use Joomla\Database\DatabaseInterface;
+
+class ZoneHelper
+{
+ public static function detectZone(float $lat, float $lng): ?object
+ {
+ $db = Factory::getContainer()->get(DatabaseInterface::class);
+
+ $db->setQuery($db->getQuery(true)
+ ->select('*')
+ ->from('#__mokosuitetaxi_zones')
+ ->where($db->quoteName('published') . ' = 1')
+ ->where('center_lat IS NOT NULL')
+ ->where('center_lng IS NOT NULL')
+ ->order('ordering ASC'));
+
+ $zones = $db->loadObjectList() ?: [];
+
+ foreach ($zones as $zone) {
+ $radius = (float) ($zone->radius_km ?: 50.0);
+ $distance = TaxiHelper::haversineDistance($lat, $lng, (float) $zone->center_lat, (float) $zone->center_lng);
+
+ if ($distance <= $radius) {
+ $zone->distance_from_center = round($distance, 2);
+ return $zone;
+ }
+ }
+
+ return null;
+ }
+
+ public static function getSurgeMultiplier(int $zoneId): float
+ {
+ $db = Factory::getContainer()->get(DatabaseInterface::class);
+
+ $db->setQuery($db->getQuery(true)
+ ->select('COUNT(*)')
+ ->from('#__mokosuitetaxi_rides')
+ ->where($db->quoteName('pickup_zone_id') . ' = ' . (int) $zoneId)
+ ->where($db->quoteName('status') . ' IN (' . implode(',', array_map([$db, 'quote'], ['requested', 'dispatched'])) . ')')
+ ->where($db->quoteName('created') . ' >= DATE_SUB(NOW(), INTERVAL 15 MINUTE)'));
+ $demandCount = (int) $db->loadResult();
+
+ $db->setQuery($db->getQuery(true)
+ ->select('COUNT(DISTINCT d.id)')
+ ->from($db->quoteName('#__mokosuitetaxi_drivers', 'd'))
+ ->join('INNER', $db->quoteName('#__mokosuitetaxi_shifts', 's') . ' ON s.driver_id = d.id AND s.status = ' . $db->quote('active'))
+ ->where($db->quoteName('d.status') . ' = ' . $db->quote('active')));
+ $supplyCount = max((int) $db->loadResult(), 1);
+
+ $ratio = $demandCount / $supplyCount;
+
+ if ($ratio <= 0.5) {
+ return 1.0;
+ }
+
+ if ($ratio <= 1.0) {
+ return 1.25;
+ }
+
+ if ($ratio <= 2.0) {
+ return 1.5;
+ }
+
+ $params = Factory::getApplication()->bootPlugin('mokosuitetaxi', 'system')->params;
+ $maxSurge = (float) $params->get('max_surge_multiplier', 3.0);
+
+ return min(1.5 + ($ratio - 2.0) * 0.5, $maxSurge);
+ }
+
+ public static function listZones(string $type = ''): array
+ {
+ $db = Factory::getContainer()->get(DatabaseInterface::class);
+
+ $query = $db->getQuery(true)
+ ->select('*')
+ ->from('#__mokosuitetaxi_zones')
+ ->where($db->quoteName('published') . ' = 1')
+ ->order('ordering ASC');
+
+ if ($type) {
+ $query->where($db->quoteName('zone_type') . ' = ' . $db->quote($type));
+ }
+
+ $db->setQuery($query);
+
+ return $db->loadObjectList() ?: [];
+ }
+}
diff --git a/source/packages/plg_webservices_mokosuitetaxi/mokosuitetaxi.xml b/source/packages/plg_webservices_mokosuitetaxi/mokosuitetaxi.xml
new file mode 100644
index 0000000..d8b2406
--- /dev/null
+++ b/source/packages/plg_webservices_mokosuitetaxi/mokosuitetaxi.xml
@@ -0,0 +1,10 @@
+
+
+ Web Services - MokoSuite Taxi
+ mokosuitetaxi
+ Moko Consulting
+ 06.00.00
+ GPL-3.0-or-later
+ Moko\Plugin\WebServices\MokoSuiteTaxi
+ srcservices
+
diff --git a/source/packages/plg_webservices_mokosuitetaxi/services/provider.php b/source/packages/plg_webservices_mokosuitetaxi/services/provider.php
new file mode 100644
index 0000000..39d09c9
--- /dev/null
+++ b/source/packages/plg_webservices_mokosuitetaxi/services/provider.php
@@ -0,0 +1,24 @@
+
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ * Authored-by: Moko Consulting
+ */
+
+defined('_JEXEC') or die;
+
+use Joomla\CMS\Extension\PluginInterface;
+use Joomla\CMS\Plugin\PluginHelper;
+use Joomla\DI\Container;
+use Joomla\DI\ServiceProviderInterface;
+use Joomla\Event\DispatcherInterface;
+use Moko\Plugin\WebServices\MokoSuiteTaxi\Extension\MokoSuiteTaxi;
+
+return new class implements ServiceProviderInterface {
+ public function register(Container $container): void {
+ $container->set(PluginInterface::class, function (Container $container) {
+ return new MokoSuiteTaxi($container->get(DispatcherInterface::class), (array) PluginHelper::getPlugin('webservices', 'mokosuitetaxi'));
+ });
+ }
+};
diff --git a/source/packages/plg_webservices_mokosuitetaxi/src/Extension/MokoSuiteTaxi.php b/source/packages/plg_webservices_mokosuitetaxi/src/Extension/MokoSuiteTaxi.php
new file mode 100644
index 0000000..47cc288
--- /dev/null
+++ b/source/packages/plg_webservices_mokosuitetaxi/src/Extension/MokoSuiteTaxi.php
@@ -0,0 +1,36 @@
+
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ * Authored-by: Moko Consulting
+ */
+
+namespace Moko\Plugin\WebServices\MokoSuiteTaxi\Extension;
+
+defined('_JEXEC') or die;
+
+use Joomla\CMS\Plugin\CMSPlugin;
+use Joomla\Event\SubscriberInterface;
+
+class MokoSuiteTaxi extends CMSPlugin implements SubscriberInterface
+{
+ public static function getSubscribedEvents(): array
+ {
+ return ['onBeforeApiRoute' => 'onBeforeApiRoute'];
+ }
+
+ public function onBeforeApiRoute(&$event): void
+ {
+ $router = $event->getArgument('router');
+ $opts = ['component' => 'com_mokosuitetaxi'];
+
+ $router->createCRUDRoutes('v1/mokosuitetaxi/rides', 'rides', $opts);
+ $router->createCRUDRoutes('v1/mokosuitetaxi/vehicles', 'vehicles', $opts);
+ $router->createCRUDRoutes('v1/mokosuitetaxi/drivers', 'drivers', $opts);
+ $router->createCRUDRoutes('v1/mokosuitetaxi/zones', 'zones', $opts);
+ $router->createCRUDRoutes('v1/mokosuitetaxi/fares', 'fares', $opts);
+ $router->createCRUDRoutes('v1/mokosuitetaxi/dispatch', 'dispatch', $opts);
+ $router->createCRUDRoutes('v1/mokosuitetaxi/ratings', 'ratings', $opts);
+ }
+}
diff --git a/source/pkg_mokosuitetaxi.xml b/source/pkg_mokosuitetaxi.xml
new file mode 100644
index 0000000..2c1dfb5
--- /dev/null
+++ b/source/pkg_mokosuitetaxi.xml
@@ -0,0 +1,22 @@
+
+
+ Package - MokoSuite Taxi
+ mokosuitetaxi
+ 06.00.00
+ 2026-06-27
+ Moko Consulting
+ hello@mokoconsulting.tech
+ https://mokoconsulting.tech
+ Copyright (C) 2026 Moko Consulting. All rights reserved.
+ GNU General Public License version 3 or later; see LICENSE
+ Ride-hailing, dispatch, fleet management, fare zones, and driver scheduling
+ 8.3
+
+ true
+
+ plg_system_mokosuitetaxi.zip
+
+
+ https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteTaxi/updates.xml
+
+