Appearance
Background Jobs
Upvendo uses Laravel's queue system to offload time-consuming operations from the HTTP request cycle. Jobs handle email sending, payment capture, integration syncing, image processing, and order processing.
Key Files
Jobs live in app/Jobs/ — 102 files as of 2026-07-27, including the UberEats/ and Commission/ sub-directories. The tables below cover the notable ones and are not exhaustive; check the directory for the current set rather than treating any list here as closed.
Job Categories
Payment Jobs
| Job | Purpose | Dispatched By |
|---|---|---|
CapturePaymentJob | Capture payment after terminal confirmation | KioskOrchestrator |
ProcessOrderJob | Post-payment order processing (KDS, notifications) | PaymentCaptureService |
FinalizePaidOrderJob | Deferred post-payment side-effects: loyalty earn (+ Mplus points push), offer/reward redemption, customer timeline, kiosk customer-name update, Stripe B2B auto-invoice | PaymentCaptureService, OnlineOrderingOrchestrator::verifyPayment() |
UpdateD1StocksAfterPayment | Sync stock changes to Cloudflare D1 edge database | PaymentCaptureService |
ProcessOrderJob
The most important job in the payment flow. Dispatched after successful payment capture.
php
ProcessOrderJob::dispatch($transactionId, $locationId);Dispatch timing (from PaymentCaptureService::processOrderJob()):
- Kiosk orders: Dispatched immediately
- Online ordering (dine-in): Dispatched immediately
- Online ordering (delivery/pickup): May be delayed based on
sent_attime - Local development: Executed synchronously via
->handle()
Delayed dispatch for scheduled orders:
php
$delayInSeconds = $processTime->diffInSeconds($now, true);
ProcessOrderJob::dispatch($transactionId, $locationId)->delay($delayInSeconds);Email Jobs
| Job | Purpose | Trigger |
|---|---|---|
SendBackofficeOTPMail | Send OTP code for 2FA login | AuthService::requestBackofficeOTP() |
SendDeviceActivationCodeMail | Send device activation code | Device management |
SendGridMail | Generic email via SendGrid | Various |
SendLocalOrderReceiptMail | Send order receipt to customer | After payment capture |
SendLocalOrderReadyForPickupMail | Notify customer order is ready | KDS marks order ready |
SendLocalOrderOutForDeliveryMail | Notify customer order is out for delivery | Order status change |
SendLocalOrderDeliveredMail | Confirm delivery to customer | Order status change |
SendLocalOrderReadyDeliveryMail | Notify order ready for delivery | KDS marks order ready |
SendLocalOrderPickupCompletedMail | Confirm pickup complete | Order status change |
SendLocalOrderMissedDeliveryMail | Notify missed delivery | Delivery status |
SendLoyaltyProgramAlertMail | Loyalty program notifications | Loyalty threshold reached |
SendMerchantInvitationMail | Invite user to merchant team | Team management |
SendPasswordResetMail | Password reset link | AuthService::forgetPassword() |
SendVerificationCodeMail | Customer verification code | AuthCustomerService::sendOTP() |
SMS Jobs
| Job | Purpose |
|---|---|
SendSMS | Send SMS via configured provider (customer OTP, notifications) |
Integration Sync Jobs
| Job | Purpose | Trigger |
|---|---|---|
SyncSquareMenuJob | Sync menu catalog to Square | Menu publish |
SyncSquareInventoryJob | Sync inventory to Square | Inventory changes |
SyncSquareLocationJob | Sync location data to Square | Location update |
SyncSquareIntegrationJob | Initialize Square integration | OAuth completion |
SyncLocationToDeliverooJob | Sync location data to Deliveroo | Location update |
ExportUpvendoMenuToShopifyJob | Export menu to Shopify | Manual trigger |
ImportShopifyMenuJob | Import menu from Shopify | Manual trigger |
Kassanet (POS Integration) Jobs
| Job | Purpose |
|---|---|
ImportKassanetMenuJob | Import full menu from Kassanet POS |
ImportKassanetCategoriesJob | Import categories from Kassanet |
ImportKassanetDisplayGroupsJob | Import display groups from Kassanet |
ImportKassanetProductsJob | Import products from Kassanet |
GetKassanetBillJob | Retrieve bill from Kassanet POS |
PayKassanetBillJob | Submit payment to Kassanet POS |
Uber Eats Jobs
Located in app/Jobs/UberEats/:
| Job | Purpose |
|---|---|
ProcessUberEatsOrderNotificationJob | Process incoming Uber Eats order |
ProcessUberEatsCancelNotificationJob | Handle Uber Eats order cancellation |
ProcessUberEatsScheduledNotificationJob | Handle scheduled order updates |
Image Processing Jobs
| Job | Purpose |
|---|---|
CompressImageJob | Compress uploaded images for storage optimization |
QR Code Generation Jobs
| Job | Purpose |
|---|---|
GenerateQrCodesForSection | Generate QR codes for a table section |
GenerateQrCodesForSectionBatch | Batch generate QR codes for section |
GenerateQrCodesConsolidation | Consolidate generated QR codes |
GenerateQrCodesConsolidationBatch | Batch consolidation of QR codes |
GenerateQrCodesFinalConsolidation | Final consolidation step |
Translation Jobs
| Job | Purpose |
|---|---|
RetranslatePublishedLanguagesJob | Re-translate all published language content |
BulkTranslateJob | Bulk translation run |
Notification / Push Jobs
Firebase writes were moved off the request thread — NotificationService::create() previously looped Firebase round-trips inline.
| Job | Purpose |
|---|---|
SendFirebaseNotificationsJob | Fan out notifications to Firebase (FCM + RTDB) |
SendFirebaseReadUpdateJob | Push read-state updates to Firebase |
SendFcmBroadcastJob | Broadcast an FCM message |
SendFieldPushJob | Push to the field-ops rep app |
Webhook Processing Jobs
Inbound provider webhooks are verified synchronously, then queued.
| Job | Provider |
|---|---|
ProcessStripeWebhookJob | Stripe |
ProcessVivaWebhookJob | Viva Wallet |
ProcessSquareWebhookJob | Square |
ProcessShopifyWebhookJob | Shopify |
ProcessDeliverooWebhookJob | Deliveroo |
ProcessLightspeedKSeriesWebhookJob | Lightspeed K-Series |
UberEats/ProcessUberEatsWebhookJob | Uber Eats |
MplusKassa Sync Jobs
| Job | Purpose |
|---|---|
SyncMplusKassaIntegrationJob | Initialise the integration |
SyncMplusKassaMenuJob | Menu sync entry point |
SyncMplusKassaProductsPageJob | One page of products |
SyncMplusKassaProductsBatchJob | One batch of products |
SyncMplusKassaInventoryJob | Inventory sync |
SyncMplusKassaRelationsJob | Relations sync |
SyncMplusKassaImagesDispatchJob | Fan out image sync |
SyncMplusKassaImagesBatchJob | One batch of images |
SyncMplusKassaFinalizationJob | Finalise the sync run |
Odoo / CRM Jobs
| Job | Purpose |
|---|---|
OdooSyncPartnerJob | Sync a merchant to Odoo |
OdooErasePartnerJob | GDPR erasure of a partner |
OdooSyncResellerJob | Sync a reseller to Odoo |
OdooEraseResellerJob | GDPR erasure of a reseller |
CreateOdooLeadJob | Create a CRM lead |
Invoicing, Subscription & Payout Jobs
| Job | Purpose |
|---|---|
ProcessInvoicePaidJob | Handle an invoice paid event |
ProcessInvoiceVoidedJob | Handle an invoice voided event |
ExpireSubscriptionJob | Expire a subscription |
ConvertGracePeriodJob | Convert a grace period |
MonthlySubscriptionReceivableJob | Monthly receivable run |
ExecutePayoutBatchJob | Execute a payout batch |
SettleOnlineOrderPaymentJob | Settle an online-order payment |
SettleLightspeedKSeriesPaymentJob | Settle a Lightspeed order |
Cloudflare D1 / Stock Jobs
| Job | Purpose |
|---|---|
ReleaseD1StockReservationJob | Release an edge stock reservation |
ReconcileLocationD1ConstantsJob | Reconcile location constants to D1 |
ReconcileLocationStocksJob | Reconcile location stock to D1 |
Other
| Job | Purpose |
|---|---|
SendEmailVerificationMail | Back-office email verification |
SendFieldRepInviteMail | Invite a field rep |
UploadImageToCloudflareJob | Upload an image to Cloudflare |
ConfirmDeliverooScheduledOrderJob | Confirm a scheduled Deliveroo order |
GenerateStandManifestJob | Field-ops stand manifest |
RetryPosForLocationJob | Retry POS sync for a location |
GeneratePdf* | PDF generation family |
Dispatch Patterns
Immediate Dispatch
Most jobs are dispatched immediately to the queue:
php
SendBackofficeOTPMail::dispatch($user, $otpCode);Delayed Dispatch
Used for scheduled orders:
php
ProcessOrderJob::dispatch($transactionId, $locationId)->delay($delayInSeconds);Synchronous Execution (Local Dev)
In local environment, some jobs execute synchronously for easier debugging:
php
if (app()->isLocal()) {
(new ProcessOrderJob($transactionId, $locationId))->handle();
return;
}After-Response Dispatch
There is no defer() in the codebase. The after-response mechanism is Laravel's ->afterResponse(), applied conditionally on the dispatched job so it still runs inline under tests and Artisan commands:
php
$useAfterResponse = ! app()->runningUnitTests() && ! app()->runningInConsole();
$job = ProcessOrderJob::dispatch($transactionId, $locationId);
if ($useAfterResponse) {
$job->afterResponse();
}(PaymentCaptureService.php:103, :119-121, :128-130.)
Queue Configuration
The queue driver and connection are configured via environment variables:
| Variable | Purpose |
|---|---|
QUEUE_CONNECTION | Queue driver (redis, database, sqs, sync) |
DB_QUEUE | Default queue name for the database driver (config/queue.php:41) |
Queue Names
Queues are a fixed, named taxonomy declared as constants on App\Constants (app/Constants.php:858-887). Jobs pin themselves in their constructor via $this->onQueue(...).
| Constant | Queue name |
|---|---|
Constants::QUEUE_PAYMENTS_HIGH | payments-high |
Constants::QUEUE_PAYMENTS_MEDIUM | payments-medium |
Constants::QUEUE_EMAIL | email |
Constants::QUEUE_MENU_SYNC | menu-sync |
Constants::QUEUE_MPLUSKASSA_SYNC | mpluskassa-sync |
Constants::QUEUE_KASSANET_SYNC | kassanet-sync |
Constants::QUEUE_ODOO_SYNC | odoo-sync |
php
// ProcessOrderJob.php:41
$this->onQueue(Constants::QUEUE_PAYMENTS_HIGH);FinalizePaidOrderJob sits on payments-medium on purpose, so deferred side-effects never starve live capture jobs on payments-high, and it is dispatched last at its call sites so the kitchen never waits on it.
FinalizePaidOrderJob runs once, by design
It carries $tries = 1. Its steps are non-idempotent local writes, and single-attempt preserves the exactly-once, best-effort semantics they had when they ran inline. Do not add retries without making each step idempotent first.
It re-binds tenancy in handle() from the carried tenant database, re-loads the transaction, and runs each step under its own try/catch so one failure (MplusKassa being down, say) never skips the others. Which steps run is decided at dispatch time by flags (runLoyalty, runTimeline, runOffers, updateKioskCustomerName).
It must be dispatched after the payment/order is persisted — post-commit and past the lock closure. Dispatching inside the lock writes to the central jobs collection before COMMIT, letting a worker read a not-yet-visible (or retry-aborted) transaction. See Payment Flow for what deliberately stayed synchronous and why.
Job Structure Pattern
All jobs follow Laravel's standard structure:
php
class ExampleJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
private string $someId,
private string $tenantDatabase
) {}
public function handle(): void
{
// Set tenant database if multi-tenant
config()->set('database.connections.tenant.database', $this->tenantDatabase);
// Execute job logic
// ...
}
}Multi-Tenant Considerations
Jobs that access tenant data must set the tenant database at the start of handle():
php
config()->set('database.connections.tenant.database', $this->tenantDatabase);The tenant database name is typically passed as a constructor parameter when dispatching:
php
UpdateD1StocksAfterPayment::dispatch(
$transaction,
config('database.connections.tenant.database')
);Error Handling and Retries
Default Behavior
Laravel's queue system provides automatic retry behavior. Failed jobs are stored in the failed_jobs table.
Manual Retry
bash
php artisan queue:retry {job_id}
php artisan queue:retry allMonitoring Failed Jobs
bash
php artisan queue:failedAsync Processing Monitor (back office)
There is a shipped back-office monitor at route async-monitoring (nav icon tabler-clock-bolt) with three tabs: Summary, Recent runs and Live queue.
Live queue reads GET /back-office/async-monitoring/queue?limit=100 and shows waiting / processing / delayed / failed counts plus two tables:
| Table | Columns |
|---|---|
| Jobs | Job, Queue, State, In queue (waiting_seconds), Attempts, Runs at, Queued |
| Failed jobs | Job, Queue, Error, Failed at |
waiting_seconds is populated only for waiting jobs — processing and delayed rows render an em dash. An Auto-refresh toggle polls every 5s, and only while Live queue is the active tab.
Three endpoints — GET /async-monitoring, /summary, /queue — and the whole prefix sits behind the global-admin middleware, deliberately: the monitor exposes cross-tenant job internals (tenant DBs, exception traces).
Gating note. This page introduced a front-end gate primitive: an optional
roleon nav items and route meta, ANDed with the existing CASL/permission gate.hasRole()readsuserData.role/userData.rolesand fails closed whenuserDatais absent. Both the nav entry and the route carryrole: 'global-administrator', becauseview global-settingsis also held byglobal-supportandglobal-developer— which CASL cannot tell apart, since all three carrymanage:all.
Debugging Jobs
Common Issues
- Job not executing: Check queue worker is running (
php artisan queue:work) - Tenant database error: Verify tenant database name is passed to job constructor
- Serialization error: Ensure job constructor parameters are serializable (use IDs, not models)
- Job timing: For delayed jobs, check
sent_atcalculation inPaymentCaptureService::processOrderJob()
Logging
Jobs typically log their execution. Search for job class name in logs:
grep "ProcessOrderJob" storage/logs/laravel.logRunning Jobs Synchronously
For debugging, set QUEUE_CONNECTION=sync in .env to execute all jobs synchronously.