migrate to gtea from bistbucket
This commit is contained in:
@@ -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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user