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 |
| 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 |
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": "The given data was invalid.",
"errors": {
"field_name": ["The field_name field is required."]
}
}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
"No tenant database configured"
Cause: The SetTenantDatabase middleware didn't set the tenant database, or it was set to null/empty.
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 (up to 5 retries).
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: VerifyDeliverooWebhook, VerifyShopifyWebhook, VerifySquareWebhook, VerifyUberEatsWebhook
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
"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
"Error processing Viva webhook" -> Viva Wallet webhook failures
"Error processing Uber Eats" -> Uber Eats webhook failures
"Failed to abort terminal" -> Terminal session issues
"Slow payment capture" -> Performance issues (>2s capture)
"Transaction not found" -> Missing transactions during webhook
"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
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 (optional) | redis | redis |
| Debug endpoints | Available | Available | Disabled |
| phpinfo endpoint | Available | Available | Disabled |
Sentry Integration
Production errors are tracked in Sentry:
- Payment capture operations have Sentry tracing spans
SentryBeforeSendCallback.phpfilters/enriches events before sending- Slow payment captures (>2s) generate Sentry performance alerts
Error Response Format
Standard Success
json
{
"success": true
}Standard Error (from handleException)
json
{
"message": "Error description"
}Validation Error (422)
json
{
"message": "The given data was invalid.",
"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:
- Logs the exception
- Returns appropriate HTTP status code
- Returns error message in JSON format
- Reports to Sentry in production