Appearance
Common Errors and Debugging
This guide covers the most frequently encountered errors in the Upvendo backend, their causes, and how to diagnose and resolve them.
HTTP Status Codes Used
| Code | Meaning | When Returned |
|---|---|---|
| 200 | Success | Standard successful response |
| 400 | Bad Request | Invalid input, malformed payload, failed validation |
| 401 | Unauthenticated | Missing/invalid/expired JWT token |
| 402 | Payment Required | Kiosk/KDS device without an active subscription (TokenType.php:152, in checkDeviceSubscription()) |
| 403 | Forbidden | Valid token but insufficient permissions or wrong user type |
| 404 | Not Found | Resource doesn't exist or wrong tenant database |
| 409 | Conflict | Duplicate resource, idempotency conflict (Viva Wallet) |
| 422 | Unprocessable Entity | Laravel validation errors |
| 429 | Too Many Requests | Rate limiting (online ordering endpoints) |
| 500 | Server Error | Unhandled exceptions, database errors |
| 503 | Service Unavailable | Unconfigured Uber Eats webhook secret (VerifyUberEatsWebhook.php:22), or a failed dependency check on /health (HealthController.php:57) |
Authentication Errors
401 "Unauthenticated"
Code Path: JwtAuthenticate middleware or Authenticate middleware
Causes:
- No
Authorization: Bearer {token}header in request - Token has expired (check
expclaim) - Token signature invalid (JWT secret mismatch)
- Token payload malformed
Diagnosis:
- Decode the JWT token at jwt.io (use the debugger, not the verifier unless you have the secret)
- Check the
exptimestamp against current time - Verify
JWT_SECRETin.envmatches what was used to sign the token - Check if the user/device/customer model exists in the database
Files to check:
app/Http/Middleware/JwtAuthenticate.php (line 40-48)
app/Services/JwtService.php401 "Invalid token"
Same middleware as above. The token was present but JwtService::validateToken() returned null.
Common Causes:
- Token signed with a different secret (e.g., staging token used against production)
- Token structure is corrupt
- Token was manually modified
403 "Unauthorized: Invalid user type"
Code Path: JwtAuthenticate middleware (type guard check)
Cause: Token type doesn't match the route's required type. For example:
- Customer token used on a backoffice route (requires
type:backoffice) - User token used on a kiosk route (requires
type:kiosk)
Diagnosis:
- Decode the JWT, check the
typefield - Verify the route middleware stack to see expected types
403 "Unauthorized: Insufficient permissions"
Code Path: CheckPermission middleware
Cause: The authenticated user's role does not include the required permission.
Diagnosis:
- Check which permission the route requires (look at route definition)
- Check the user's assigned roles in MongoDB
- Check the role's permission list
- Verify the user has access to the specific location (for location-scoped permissions)
Files to check:
app/Http/Middleware/CheckPermission.php
app/Services/PermissionService.php
app/Constants/Permissions.php (for permission constant names)Validation Errors (422)
Laravel returns 422 with validation error details:
json
{
"message": "(Laravel-generated summary of the validation errors)",
"errors": {
"field_name": ["The field_name field is required."]
}
}message is not a fixed string -- there is no invalidJson override in bootstrap/app.php, and no code path in the backend emits "The given data was invalid." (that was Laravel's wording on much older versions; production runs laravel/framework v13.18.1). Always read errors for the per-field detail rather than matching on message.
Where validation happens:
- Request classes in
app/Http/Requests/(FormRequest objects) - Each controller action that accepts input has a corresponding Request class
Common Validation Failures:
- Required fields missing
- Invalid UUID/ObjectId format
- Enum value not in allowed set
- Numeric fields receiving string values
- Image upload exceeding size limits
Database Errors
Tenant database not set
There is no "No tenant database configured" error message -- SetTenantDatabase does not error when the tenant DB is missing. If the JWT carries no tenant_database claim and the request user cannot be resolved to a database, the middleware silently leaves the connection unchanged (app/Http/Middleware/SetTenantDatabase.php:70-83), so the symptom is 404s / empty result sets rather than a message.
Its actual failure responses are:
401 Unauthenticated-- no bearer token (SetTenantDatabase.php:58)401 Invalid token--JwtService::validateToken()returned null (SetTenantDatabase.php:66)500 Internal Server Error-- an exception during the switch, logged asTenant database switch attempt failed(SetTenantDatabase.php:86-91)
Diagnosis:
- Check JWT token for
tenant_databaseclaim - Verify the vendor's database name exists
- Check the middleware order in the route group
MongoDB Write Conflict
Error: Write conflict or duplicate key error during transactions.
Code Path: DBTransactionTrait::executeWithTransactionRetry()
Cause: Two concurrent operations tried to modify the same document. The retry mechanism handles this automatically -- 3 attempts by default, with exponential backoff (100ms * 2^(attempt-1) plus 0-50ms jitter). See app/Traits/DBTransactionTrait.php:201-260.
The payment capture path is the one caller that raises this: PaymentCaptureService.php:183 passes 5. Do not confuse either figure with the 5-attempt default of the lock retry helpers (CachingTrait::executeWithDatabaseLockAndRetry, app/Traits/CachingTrait.php:571) -- that is a different mechanism.
If retries are exhausted:
- Check for hot documents (frequently updated by multiple requests)
- Consider redesigning the data access pattern
- Check if webhook handlers are processing duplicates
"Model not found" / 404 on Retrieve
Code Path: Repository retrieve() methods
Causes:
- Document was deleted
- Wrong tenant database (query running against wrong DB)
- ObjectId format incorrect (string vs MongoDB ObjectId)
- Soft-deleted document (need
withTrashed: true)
Diagnosis:
- Verify the document exists in MongoDB directly
- Check which database the query is running against
- Enable query logging to see the actual database being queried
Payment Errors
"Square Terminal device not configured for this device"
Code Path: PaymentService::processSquarePayment()
Cause: Device doesn't have a Square Terminal ID assigned.
Fix: Configure the Square Terminal device code in the BackOffice device settings.
409 Conflict on Viva Wallet Terminal Sale
Code Path: PaymentService::processVivaWalletPayment()
Cause: The idempotency key was already used for a previous terminal sale attempt.
Resolution: The code automatically regenerates the idempotency key and retries. If the error persists:
- Check
all_idempotency_keyson the transaction - Verify the terminal isn't stuck on a previous payment
- May need to abort the terminal session manually
"Error verifying payment"
Code Path: PaymentService::verifyPayment()
Cause: Viva Wallet API returned an error when attempting to retrieve or capture the transaction.
Diagnosis:
- Check
payment_snapshot.orderCodeexists - Verify the merchant ID is correct
- Check Viva Wallet dashboard for the transaction status
"Payment capture failed"
Code Path: PaymentCaptureService::capturePayment()
Cause: Exception during the capture flow. Logged with full trace.
Diagnosis:
- Search logs for the idempotency key
- Check if the transaction was already captured (duplicate webhook)
- Look for database write conflicts in the trace
Integration Errors
Webhook Signature Verification Failures
Middleware: nine verify.* aliases are registered in bootstrap/app.php:41-49, and they do not all use the same mechanism:
| Mechanism | Middleware |
|---|---|
| HMAC-SHA256 signature | VerifyShopifyWebhook, VerifySquareWebhook, VerifyUberEatsWebhook, VerifyDeliverooWebhook, VerifyShopCaisseWebhook, VerifyMplusKassaWebhook |
| HTTP Basic auth (no HMAC) | VerifyLightspeedKSeriesWebhook |
| Shared bearer token | VerifyCrmWebhook (401 Invalid CRM webhook token) |
| IP allow-list | VerifyVivaWebhookIp -- Viva does not send per-request signatures |
Cause: The webhook secret in .env doesn't match the provider's configuration.
Fix:
- Re-check the webhook secret in the provider's dashboard
- Ensure the raw request body is used for signature computation
- For Shopify: verify HMAC is computed correctly against
X-Shopify-Hmac-Sha256 - For Uber Eats: an unconfigured secret aborts with 503
Webhook verification is not configured(VerifyUberEatsWebhook.php:22), not 401/403 -- a 401 there means a missing or mismatched signature
"Shopify integration not found"
Code Path: WebhookController::shopifyWebhook()
Cause: Webhook received from a shop domain that doesn't match any enabled integration.
Fix:
- Verify the shop name in the
ThirdPartyIntegrationcollection - Check if the integration was disabled
Third-Party API Timeouts
Cause: External API (Viva Wallet, Square, Deliveroo, Uber Eats, Shopify) is slow or down.
Diagnosis:
- Check the provider's status page
- Look for timeout exceptions in logs
- Verify network connectivity from the server
Rate Limiting
429 Too Many Requests
Affected endpoints:
POST /customer/{slug}/order-history-- 5 requests per minuteGET /customer/{slug}/order-detail/{orderId}-- 10 requests per minute
Rate limiting is disabled in local environment.
Multi-Tenant Errors
Queries returning wrong data
Cause: Tenant database not properly set before query execution.
Diagnosis:
- Check middleware order ensures
SetTenantDatabaseruns before controller - In jobs, verify tenant database is set in
handle()method - In webhook handlers, verify tenant resolution logic
Cross-tenant data leakage
Prevention:
- All tenant-scoped queries go through the
tenantdatabase connection - The
SetTenantDatabasemiddleware resets the connection per-request - Never use the default connection for tenant data
Debugging Toolkit
Log Searching
Key log patterns to search for:
"Payment capture failed" -> Payment processing errors (PaymentCaptureService.php:488)
"Error processing Viva webhook" -> Viva Wallet webhook failures (WebhookController.php:203)
"Uber Eats integration not found" -> Uber Eats job could not resolve the integration
(ProcessUberEatsOrderNotificationJob.php:86)
"Uber Eats webhook" -> Uber Eats webhook intake; the error variants are
"Missing integration identifiers in Uber Eats webhook"
and "No integration found for Uber Eats webhook payload"
(UberEatsWebhookService.php:38, :48)
"Failed to abort terminal session" -> Terminal session issues (PaymentService.php:671)
"Slow payment capture detected" -> Performance issues (PaymentCaptureService.php:519)
"Transaction not found" -> Missing transactions during webhook
(ProcessVivaWebhookJob.php:148, :195, :354)
"Location not found" -> Invalid location in webhook
"Write conflict" -> Database concurrency issuesDatabase Inspection
MongoDB queries for common investigations:
javascript
// Find transaction by order number
db.transactions.findOne({ order_no: "ORDER-123" })
// Find transaction by idempotency key
db.transactions.findOne({ "payment_snapshot.idempotency_key": "idemp_xxx" })
// Find recent failed transactions
// `status` holds the capitalised App\Enums\OrderStatuses values -- "unpaid" matches nothing
db.transactions.find({ status: "Unpaid", updated_at: { $gte: new Date(Date.now() - 3600000) } })
// Check device activation status
db.devices.findOne({ activation_code: "ABC123" })Environment-Specific Behavior
| Feature | Local | Staging | Production |
|---|---|---|---|
| Auto-success payments | Configurable | Off | Off |
| Rate limiting | Disabled | Enabled | Enabled |
| Stripe webhook verification | Skipped | Enabled | Enabled |
| Queue processing | sync | sync | database (MongoDB-backed, collection jobs) |
| Debug endpoints | Available (behind e2e.auth) | Disabled | Disabled |
| phpinfo endpoint | Available (behind e2e.auth) | Disabled | Disabled |
Redis is not a queue driver here. config/queue.php:16 defaults to the database connection, whose driver is mongodb writing to the jobs collection (config/queue.php:37-41); the per-environment matrix is recorded in docs/PUSHER_ENV_CONFIGURATION.md. Redis is the cache store (CACHE_STORE=redis).
The whole dev/debug route group -- /phpinfo included -- is registered only when GeneralHelper::isTestEnv() is true, and that helper is ! app()->isProduction() && ! app()->environment('staging') (app/Helpers/GeneralHelper.php:9-12), so the routes do not exist on staging or production. Where they do register they additionally sit behind e2e.auth (E2EAuthMiddleware), which requires the E2E_API_KEY token (routes/api/guest.php:128-139).
Sentry Integration
Errors from every non-local environment are tracked in Sentry (staging included) -- the integration is registered in bootstrap/app.php:84-85:
- Payment capture operations have Sentry tracing spans
app/Services/SentryBeforeSendCallback.phpfilters/enriches events before sending- Slow payment captures (>2s) generate Sentry performance alerts
Error Response Format
Standard Success
json
{
"status": "success"
}This is the canonical envelope, produced by the base Controller::sendSuccess() (app/Http/Controllers/Controller.php:57-60) and used by ~177 controller actions. A handful of legacy endpoints (5 at the time of writing) still return {"success": true} -- treat status as the canonical shape.
Standard Error (from handleException)
json
{
"message": "Error description"
}Validation Error (422)
json
{
"message": "(Laravel-generated summary of the validation errors)",
"errors": {
"field": ["Validation message"]
}
}Controller Error Handling Pattern
All controllers use the same exception handling:
php
try {
$result = $this->service->doSomething($request->validated());
} catch (\Throwable $th) {
$this->handleException($th); // Base Controller method
}
return response()->json($result);The handleException() method in the base Controller class (app/Http/Controllers/Controller.php:35-55):
- 500s: logs the exception (via
RequestLogRepositoryinto thelogscollection, plus the system log), then aborts with the generic message "An unexpected error occurred" unlessapp.debugis on -- the real exception message is never returned to the client in production (Controller.php:192-196). - 4xx: passes the real exception message through, and is deliberately not written to the system logs at error level (client errors are expected and would otherwise alert;
Controller.php:53-55,:161). ModelNotFoundExceptionbecomes a 404 with "Unable to locate the {model} you requested." (Controller.php:37-39).- Sentry capture is not wired inside
handleException()--handleExceptionByEnvironment()has no active body. Reporting comes fromIntegration::handles($exceptions)inbootstrap/app.php:84-85, which applies to every non-local environment (staging included).