migrate to gtea from bistbucket

This commit is contained in:
2026-03-15 17:08:23 +07:00
commit 129ca2260c
3716 changed files with 566316 additions and 0 deletions

View File

@@ -0,0 +1,246 @@
<?php
declare(strict_types=1);
namespace Modules\RestaurantDelivery\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Modules\RestaurantDelivery\Models\Delivery;
use Modules\RestaurantDelivery\Models\Rider;
use Modules\RestaurantDelivery\Services\Firebase\FirebaseService;
class AssignRiderJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* The number of times the job may be attempted.
*/
public int $tries = 3;
/**
* The maximum number of seconds the job can run before timing out.
*/
public int $timeout = 30;
/**
* Create a new job instance.
*/
public function __construct(
public readonly Delivery $delivery
) {
$this->onQueue(config('restaurant-delivery.queue.queues.assignment', 'restaurant-delivery-assignment'));
}
/**
* Execute the job.
*/
public function handle(FirebaseService $firebase): void
{
// Skip if already assigned
if ($this->delivery->rider_id) {
Log::info('Delivery already has a rider assigned', [
'delivery_id' => $this->delivery->id,
'rider_id' => $this->delivery->rider_id,
]);
return;
}
// Get assignment config
$config = config('restaurant-delivery.assignment');
// Find nearby available riders
$riders = $this->findAvailableRiders();
if ($riders->isEmpty()) {
Log::warning('No available riders found for delivery', [
'delivery_id' => $this->delivery->id,
]);
// Retry after a delay if max attempts not reached
if ($this->attempts() < $this->tries) {
$this->release(60); // Release back to queue after 1 minute
}
return;
}
// Score and rank riders
$scoredRiders = $this->scoreRiders($riders, $config['scoring']);
// Use broadcast or direct assignment
if ($config['broadcast']['enabled']) {
$this->broadcastToRiders($scoredRiders->take($config['broadcast']['max_riders']), $firebase);
} else {
$this->directAssign($scoredRiders->first());
}
}
/**
* Find available riders within the assignment radius.
*/
protected function findAvailableRiders()
{
$radius = config('restaurant-delivery.assignment.assignment_radius', 5);
$maxConcurrent = config('restaurant-delivery.assignment.max_concurrent_orders', 3);
$query = Rider::query()
->where('status', 'available')
->where('is_online', true)
->where('is_verified', true)
->whereNotNull('current_latitude')
->whereNotNull('current_longitude');
// Filter by restaurant for SaaS multi-tenant
if ($this->delivery->restaurant_id) {
$query->where('restaurant_id', $this->delivery->restaurant_id);
}
// Filter riders with less than max concurrent orders
$query->withCount(['deliveries' => function ($q) {
$q->whereIn('status', ['rider_assigned', 'rider_at_restaurant', 'picked_up', 'on_the_way', 'arrived']);
}])->having('deliveries_count', '<', $maxConcurrent);
// Calculate distance and filter by radius
// Using Haversine formula in raw query for efficiency
$lat = $this->delivery->pickup_latitude;
$lng = $this->delivery->pickup_longitude;
$query->selectRaw('
*,
(6371 * acos(
cos(radians(?)) * cos(radians(current_latitude)) * cos(radians(current_longitude) - radians(?))
+ sin(radians(?)) * sin(radians(current_latitude))
)) AS distance
', [$lat, $lng, $lat])
->having('distance', '<=', $radius)
->orderBy('distance');
return $query->get();
}
/**
* Score riders based on various criteria.
*/
protected function scoreRiders($riders, array $weights)
{
$maxDistance = config('restaurant-delivery.assignment.assignment_radius', 5);
return $riders->map(function ($rider) use ($weights, $maxDistance) {
$score = 0;
// Distance score (closer is better)
$distanceScore = (1 - ($rider->distance / $maxDistance)) * $weights['distance_weight'];
$score += $distanceScore;
// Rating score
$ratingScore = ($rider->rating / 5) * $weights['rating_weight'];
$score += $ratingScore;
// Acceptance rate score
$acceptanceScore = ($rider->acceptance_rate / 100) * $weights['acceptance_rate_weight'];
$score += $acceptanceScore;
// Current orders score (fewer is better)
$ordersScore = (1 - ($rider->deliveries_count / config('restaurant-delivery.assignment.max_concurrent_orders', 3)))
* $weights['current_orders_weight'];
$score += $ordersScore;
// Experience score (normalized by max 1000 deliveries)
$experienceScore = min($rider->total_deliveries / 1000, 1) * $weights['experience_weight'];
$score += $experienceScore;
$rider->assignment_score = $score;
return $rider;
})->sortByDesc('assignment_score');
}
/**
* Broadcast assignment request to multiple riders.
*/
protected function broadcastToRiders($riders, FirebaseService $firebase): void
{
$timeout = config('restaurant-delivery.assignment.broadcast.accept_timeout', 30);
foreach ($riders as $rider) {
// Record assignment attempt
$this->delivery->addAssignmentToHistory([
'rider_id' => $rider->id,
'type' => 'broadcast',
'score' => $rider->assignment_score,
'distance' => $rider->distance,
]);
// Send push notification to rider
if ($rider->fcm_token) {
$firebase->sendPushNotification(
$rider->fcm_token,
'New Delivery Request',
"New delivery from {$this->delivery->restaurant_name}. Distance: ".round($rider->distance, 1).' km',
[
'type' => 'delivery_request',
'delivery_id' => (string) $this->delivery->id,
'tracking_code' => $this->delivery->tracking_code,
'restaurant_name' => $this->delivery->restaurant_name,
'pickup_address' => $this->delivery->pickup_address,
'drop_address' => $this->delivery->drop_address,
'distance' => (string) $this->delivery->distance,
'timeout' => (string) $timeout,
]
);
}
}
Log::info('Delivery request broadcasted to riders', [
'delivery_id' => $this->delivery->id,
'rider_count' => $riders->count(),
]);
// Schedule timeout check
dispatch(new CheckAssignmentTimeoutJob($this->delivery))
->delay(now()->addSeconds($timeout + 5));
}
/**
* Directly assign to the best scoring rider.
*/
protected function directAssign(Rider $rider): void
{
DB::transaction(function () use ($rider) {
$this->delivery->assignRider($rider);
// Record assignment
$this->delivery->addAssignmentToHistory([
'rider_id' => $rider->id,
'type' => 'direct',
'score' => $rider->assignment_score ?? 0,
'distance' => $rider->distance ?? 0,
'assigned_at' => now()->toIso8601String(),
]);
});
Log::info('Rider directly assigned to delivery', [
'delivery_id' => $this->delivery->id,
'rider_id' => $rider->id,
]);
}
/**
* Handle a job failure.
*/
public function failed(\Throwable $exception): void
{
Log::error('Failed to assign rider to delivery', [
'delivery_id' => $this->delivery->id,
'error' => $exception->getMessage(),
]);
}
}

View File

@@ -0,0 +1,113 @@
<?php
declare(strict_types=1);
namespace Modules\RestaurantDelivery\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Modules\RestaurantDelivery\Models\Delivery;
use Modules\RestaurantDelivery\Models\RiderEarning;
use Modules\RestaurantDelivery\Services\Earnings\EarningsCalculator;
class CalculateEarningsJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* The number of times the job may be attempted.
*/
public int $tries = 3;
/**
* Create a new job instance.
*/
public function __construct(
public readonly Delivery $delivery
) {
$this->onQueue(config('restaurant-delivery.queue.queues.earnings', 'restaurant-delivery-earnings'));
}
/**
* Execute the job.
*/
public function handle(EarningsCalculator $calculator): void
{
// Skip if no rider assigned
if (! $this->delivery->rider_id) {
Log::warning('Cannot calculate earnings: no rider assigned', [
'delivery_id' => $this->delivery->id,
]);
return;
}
// Skip if earnings already exist
if ($this->delivery->riderEarning()->exists()) {
Log::info('Earnings already calculated for delivery', [
'delivery_id' => $this->delivery->id,
]);
return;
}
try {
DB::transaction(function () use ($calculator) {
// Calculate earnings
$earningsData = $calculator->calculateForDelivery($this->delivery);
// Create rider earning record
RiderEarning::create([
'rider_id' => $this->delivery->rider_id,
'delivery_id' => $this->delivery->id,
'restaurant_id' => $this->delivery->restaurant_id,
'type' => 'delivery',
'description' => "Delivery #{$this->delivery->tracking_code}",
'base_amount' => $earningsData['base_amount'],
'distance_amount' => $earningsData['distance_amount'],
'bonus_amount' => $earningsData['total_bonus'],
'penalty_amount' => $earningsData['total_penalty'],
'tip_amount' => $this->delivery->tip_amount ?? 0,
'gross_amount' => $earningsData['gross_amount'],
'commission_rate' => $earningsData['commission_rate'],
'commission_amount' => $earningsData['commission_amount'],
'net_amount' => $earningsData['net_amount'],
'currency' => config('restaurant-delivery.pricing.currency', 'BDT'),
'breakdown' => $earningsData['breakdown'],
'bonuses' => $earningsData['bonuses'],
'penalties' => $earningsData['penalties'],
'status' => 'pending',
'earned_at' => now(),
]);
Log::info('Earnings calculated for delivery', [
'delivery_id' => $this->delivery->id,
'rider_id' => $this->delivery->rider_id,
'net_amount' => $earningsData['net_amount'],
]);
});
} catch (\Exception $e) {
Log::error('Failed to calculate earnings', [
'delivery_id' => $this->delivery->id,
'error' => $e->getMessage(),
]);
throw $e;
}
}
/**
* Handle a job failure.
*/
public function failed(\Throwable $exception): void
{
Log::error('Earnings calculation job failed', [
'delivery_id' => $this->delivery->id,
'error' => $exception->getMessage(),
]);
}
}

View File

@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
namespace Modules\RestaurantDelivery\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use Modules\RestaurantDelivery\Enums\DeliveryStatus;
use Modules\RestaurantDelivery\Models\Delivery;
class CheckAssignmentTimeoutJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* The number of times the job may be attempted.
*/
public int $tries = 1;
/**
* Create a new job instance.
*/
public function __construct(
public readonly Delivery $delivery
) {
$this->onQueue(config('restaurant-delivery.queue.queues.assignment', 'restaurant-delivery-assignment'));
}
/**
* Execute the job.
*/
public function handle(): void
{
// Refresh delivery from database
$delivery = $this->delivery->fresh();
// Skip if already assigned
if ($delivery->rider_id && $delivery->status === DeliveryStatus::RIDER_ASSIGNED) {
Log::info('Delivery already assigned, skipping timeout check', [
'delivery_id' => $delivery->id,
'rider_id' => $delivery->rider_id,
]);
return;
}
// Skip if cancelled or completed
if (in_array($delivery->status, [DeliveryStatus::CANCELLED, DeliveryStatus::DELIVERED, DeliveryStatus::FAILED])) {
Log::info('Delivery is no longer active, skipping assignment', [
'delivery_id' => $delivery->id,
'status' => $delivery->status->value,
]);
return;
}
// Check if max reassignments reached
$maxReassignments = config('restaurant-delivery.assignment.max_reassignments', 3);
if ($delivery->reassignment_count >= $maxReassignments) {
Log::warning('Max reassignments reached for delivery', [
'delivery_id' => $delivery->id,
'reassignment_count' => $delivery->reassignment_count,
]);
return;
}
// Increment reassignment count
$delivery->increment('reassignment_count');
// Retry assignment
Log::info('Assignment timed out, retrying', [
'delivery_id' => $delivery->id,
'attempt' => $delivery->reassignment_count,
]);
dispatch(new AssignRiderJob($delivery));
}
/**
* Handle a job failure.
*/
public function failed(\Throwable $exception): void
{
Log::error('Assignment timeout check failed', [
'delivery_id' => $this->delivery->id,
'error' => $exception->getMessage(),
]);
}
}

View File

@@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
namespace Modules\RestaurantDelivery\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Modules\RestaurantDelivery\Models\LocationLog;
use Modules\RestaurantDelivery\Models\Rider;
class CleanupStaleLocationsJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* The number of times the job may be attempted.
*/
public int $tries = 1;
/**
* Create a new job instance.
*/
public function __construct(
public readonly int $daysToKeep = 7
) {
$this->onQueue(config('restaurant-delivery.queue.queues.analytics', 'restaurant-delivery-analytics'));
}
/**
* Execute the job.
*/
public function handle(): void
{
// Delete old location logs
$deletedLogs = LocationLog::where('recorded_at', '<', now()->subDays($this->daysToKeep))
->delete();
Log::info('Cleaned up old location logs', [
'deleted_count' => $deletedLogs,
'days_kept' => $this->daysToKeep,
]);
// Mark riders as offline if no recent location update
$offlineThreshold = config('restaurant-delivery.firebase.location.offline_threshold', 120);
$ridersMarkedOffline = Rider::where('is_online', true)
->where('last_location_update', '<', now()->subSeconds($offlineThreshold))
->update(['is_online' => false]);
Log::info('Marked riders as offline', [
'count' => $ridersMarkedOffline,
'threshold_seconds' => $offlineThreshold,
]);
// Clear stale cache entries
$this->clearStaleCacheEntries();
}
/**
* Clear stale cache entries.
*/
protected function clearStaleCacheEntries(): void
{
// Get all online riders and clear their old cache entries if needed
$riders = Rider::where('is_online', false)->get(['id']);
foreach ($riders as $rider) {
Cache::forget("rider_location_{$rider->id}");
Cache::forget("last_animation_point_{$rider->id}");
}
Log::info('Cleared stale cache entries', [
'rider_count' => $riders->count(),
]);
}
/**
* Handle a job failure.
*/
public function failed(\Throwable $exception): void
{
Log::error('Cleanup stale locations job failed', [
'error' => $exception->getMessage(),
]);
}
}

View File

@@ -0,0 +1,140 @@
<?php
declare(strict_types=1);
namespace Modules\RestaurantDelivery\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Modules\RestaurantDelivery\Models\Rider;
use Modules\RestaurantDelivery\Models\RiderEarning;
use Modules\RestaurantDelivery\Models\RiderPayout;
class ProcessPayoutJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* The number of times the job may be attempted.
*/
public int $tries = 3;
/**
* Create a new job instance.
*/
public function __construct(
public readonly Rider $rider,
public readonly ?string $paymentMethod = null
) {
$this->onQueue(config('restaurant-delivery.queue.queues.earnings', 'restaurant-delivery-earnings'));
}
/**
* Execute the job.
*/
public function handle(): void
{
// Get pending earnings
$pendingEarnings = $this->rider->earnings()
->where('status', 'pending')
->whereNull('payout_id')
->get();
if ($pendingEarnings->isEmpty()) {
Log::info('No pending earnings for rider', [
'rider_id' => $this->rider->id,
]);
return;
}
// Calculate total
$totalAmount = $pendingEarnings->sum('net_amount');
$minimumPayout = config('restaurant-delivery.earnings.payout.minimum_amount', 500);
if ($totalAmount < $minimumPayout) {
Log::info('Pending amount below minimum payout threshold', [
'rider_id' => $this->rider->id,
'pending_amount' => $totalAmount,
'minimum' => $minimumPayout,
]);
return;
}
try {
DB::transaction(function () use ($pendingEarnings, $totalAmount) {
// Create payout record
$payout = RiderPayout::create([
'rider_id' => $this->rider->id,
'restaurant_id' => $this->rider->restaurant_id,
'amount' => $totalAmount,
'currency' => config('restaurant-delivery.pricing.currency', 'BDT'),
'payment_method' => $this->paymentMethod ?? $this->rider->preferred_payment_method,
'payment_details' => $this->getPaymentDetails(),
'status' => 'pending',
'earnings_count' => $pendingEarnings->count(),
'period_start' => $pendingEarnings->min('earned_at'),
'period_end' => $pendingEarnings->max('earned_at'),
]);
// Link earnings to payout
RiderEarning::whereIn('id', $pendingEarnings->pluck('id'))
->update([
'payout_id' => $payout->id,
'status' => 'processing',
]);
Log::info('Payout created for rider', [
'payout_id' => $payout->id,
'rider_id' => $this->rider->id,
'amount' => $totalAmount,
'earnings_count' => $pendingEarnings->count(),
]);
});
} catch (\Exception $e) {
Log::error('Failed to create payout', [
'rider_id' => $this->rider->id,
'error' => $e->getMessage(),
]);
throw $e;
}
}
/**
* Get payment details based on payment method.
*/
protected function getPaymentDetails(): array
{
$method = $this->paymentMethod ?? $this->rider->preferred_payment_method;
return match ($method) {
'bank_transfer' => [
'bank_name' => $this->rider->bank_name,
'account_number' => $this->rider->bank_account_number,
'branch' => $this->rider->bank_branch,
],
'bkash', 'nagad', 'rocket' => [
'provider' => $method,
'number' => $this->rider->mobile_banking_number,
],
default => [],
};
}
/**
* Handle a job failure.
*/
public function failed(\Throwable $exception): void
{
Log::error('Payout processing job failed', [
'rider_id' => $this->rider->id,
'error' => $exception->getMessage(),
]);
}
}

View File

@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace Modules\RestaurantDelivery\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use Modules\RestaurantDelivery\Services\Firebase\FirebaseService;
class SendPushNotificationJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* The number of times the job may be attempted.
*/
public int $tries = 3;
/**
* The maximum number of seconds to wait before retrying the job.
*/
public int $backoff = 10;
/**
* Create a new job instance.
*/
public function __construct(
public readonly string $token,
public readonly string $title,
public readonly string $body,
public readonly array $data = []
) {
$this->onQueue(config('restaurant-delivery.queue.queues.notifications', 'restaurant-delivery-notifications'));
}
/**
* Execute the job.
*/
public function handle(FirebaseService $firebase): void
{
try {
$firebase->sendPushNotification(
$this->token,
$this->title,
$this->body,
$this->data
);
Log::info('Push notification sent', [
'title' => $this->title,
'data' => $this->data,
]);
} catch (\Exception $e) {
Log::error('Failed to send push notification', [
'title' => $this->title,
'error' => $e->getMessage(),
]);
throw $e;
}
}
/**
* Handle a job failure.
*/
public function failed(\Throwable $exception): void
{
Log::error('Push notification job failed permanently', [
'title' => $this->title,
'error' => $exception->getMessage(),
]);
}
}