Appearance
Payment Debugging Guide
This guide provides a systematic approach to investigating payment failures in Upvendo. Payments flow through multiple services and providers, so debugging requires tracing through several layers.
Quick Reference: Key Files
| File | What to Look For |
|---|---|
app/Services/Payment/PaymentService.php | Payment intent creation, terminal interaction |
app/Services/Payment/PaymentCaptureService.php | Capture logic, post-payment processing |
app/Services/Payment/TransactionService.php | Transaction creation, state management |
app/Services/Orchestrators/KioskOrchestrator.php | Kiosk payment orchestration |
app/Services/Orchestrators/OnlineOrderingOrchestrator.php | Online payment orchestration |
app/Services/Orchestrators/VivaWebhookOrchestrator.php | Viva webhook processing |
app/Services/Orchestrators/StripeWebhookOrchestrator.php | Stripe webhook processing |
app/Services/Common/VivaWalletService.php | Viva Wallet API calls |
app/Services/BackOffice/SquareIntegrationService.php | Square API calls |
Investigation Flowchart
Step 1: Identify the Transaction
Start by finding the transaction in MongoDB:
javascript
// By order number (displayed to customer)
db.transactions.findOne({ order_no: "ORDER-123" })
// By idempotency key (from logs)
db.transactions.findOne({ "payment_snapshot.idempotency_key": "idemp_..." })
// By time range and location
db.transactions.find({
location_id: ObjectId("..."),
created_at: { $gte: ISODate("2025-03-27T10:00:00Z"), $lte: ISODate("2025-03-27T11:00:00Z") }
}).sort({ created_at: -1 })Step 2: Check Transaction Status
| Status | What It Means | Next Step |
|---|---|---|
pending | Transaction created, payment not initiated | Go to "Payment Not Initiated" |
awaiting_payment | Sent to terminal/checkout, waiting | Go to "Payment Stuck Awaiting" |
complete | Payment captured successfully | Payment worked, check post-payment |
unpaid | Payment attempted but not verified | Go to "Payment Not Verified" |
cancelled | Transaction was cancelled | Check cancellation reason |
Step 3: Check payment_snapshot
The payment_snapshot field contains provider-specific data:
javascript
// Viva Wallet fields
payment_snapshot: {
idempotency_key: "idemp_...",
sessionId: "idemp_...",
transactionId: "...", // Viva transaction ID
orderCode: "...", // Viva order code
details: { ... }, // Full Viva transaction details
sessionDetails: { ... }, // Terminal session info
webhookData: { ... }, // Raw webhook payload
capture: { ... }, // Capture response
is_free: false,
auto_success: false
}
// Square fields
payment_snapshot: {
idempotency_key: "idemp_...",
order_id: "...",
webhookData: { status: "COMPLETED", ... }
}Common Payment Failures
1. Payment Not Initiated (Stuck in "pending")
Symptoms: Transaction exists but terminal never prompted for payment.
Possible Causes:
- API error when calling
PaymentService::processPaymentIntent() - Terminal not reachable
- Device not properly configured
Investigation:
- Check application logs around the transaction creation time
- Look for errors from
VivaWalletService::initiateTerminalSale()orSquareIntegrationService::createTerminalCheckout() - Verify the device has a valid terminal ID:
- Viva:
device.viva_wallet_terminal_id - Square:
device.square_terminal_id
- Viva:
2. Terminal Not Prompting for Payment
Symptoms: API call succeeds but physical terminal shows nothing.
Possible Causes:
- Terminal offline or disconnected
- Terminal paired to wrong account
- Terminal firmware needs update
- Network connectivity issue between terminal and provider
Investigation:
- Check the terminal status in the provider's dashboard (Viva/Square)
- Verify terminal serial number matches device configuration
- Check if
initiateTerminalSale()returned a successful response - For Viva: check if session was created (look at
payment_snapshot.sessionId)
3. 409 Conflict on Terminal Sale
Symptoms: HttpException with 409 status from Viva Wallet API.
Cause: The idempotency key was already used for this terminal.
How the Code Handles It:
processVivaWalletPayment()
-> initiateTerminalSale() throws 409
-> Catch HttpException(409)
-> updateIdempotencyKey() generates new key
-> Retry initiateTerminalSale() with new keyIf it still fails after retry:
- Check
all_idempotency_keyson the transaction for key history - The terminal may have a stuck session from a previous payment
- May need to manually abort via Viva dashboard
4. Payment Stuck in "awaiting_payment"
Symptoms: Customer paid on terminal, but transaction never moved to "complete".
Possible Causes:
A. Webhook not received:
- Webhook URL misconfigured in provider dashboard
- Webhook signature verification failing
- Server returned error to webhook (provider stops retrying)
Investigation:
- Check provider dashboard for webhook delivery status
- Search logs for webhook-related errors
- Verify webhook secret in
.envmatches provider
B. Webhook received but capture failed:
- Search logs:
"Payment capture failed"with the idempotency key - Check for write conflicts (concurrent capture attempts)
- Verify tenant database resolution worked
C. Webhook received but transaction not found:
- Search logs:
"Transaction not found for" - Check if location ID in webhook data matches the transaction's location
- Verify tenant database was correctly resolved
5. Payment Shows "unpaid" After Capture
Symptoms: capturePayment() ran but status is "unpaid" instead of "complete".
Cause: The payment provider reported a non-success status.
For Viva Wallet:
php
$newStatus = match ($transactionDetails['statusId'] ?? null) {
'F' => 'complete', // F = Finalized
default => 'unpaid' // Any other status
};- Check
payment_snapshot.details.statusId-- should be "F" for success - If not "F", check the Viva Wallet dashboard for the actual transaction status
For Square:
php
$newStatus = match ($webhookData['status'] ?? null) {
'COMPLETED' => 'complete',
default => 'unpaid'
};- Check
payment_snapshot.webhookData.status-- should be "COMPLETED"
6. Duplicate Payment Capture
Symptoms: Post-payment actions (inventory, loyalty, KDS) run twice.
Cause: Two capture attempts race past the lock.
Prevention Mechanisms:
- Cache lock:
payment_verification_{idempotencyKey}(20s TTL) - Database lock in Viva webhook:
payment_processing_{channel}_{key}(30s TTL) - Viva duplicate detection: SHA-256 webhook hash comparison
Investigation:
- Check if
capturePayment()was called from both webhook and polling - Look for concurrent log entries with the same idempotency key
- Check if the cache lock expired too quickly (>20s processing time)
7. Online Ordering Payment Not Verified
Symptoms: Customer completed payment on checkout page, but order never appears.
Investigation for Viva Wallet:
- Check
payment_snapshot.orderCodeexists on the transaction - Search for
POST /verify-paymentin access logs - Check if Viva webhook (
TRANSACTION_PAYMENT_CREATED) was received - Verify the transaction exists via Viva API:
retrieveTransactionByOrderCode()
Investigation for Square:
- Check Square dashboard for the payment status
- Verify Square webhook was received
- Check
payment_snapshot.webhookData
8. Preauthorization Capture Failure
Symptoms: Online ordering payment preauthorized but capture fails.
Code Path: PaymentService::verifyPayment() -> VivaWalletService::captureTransaction()
Check:
payment_snapshot.capturefield for the capture response- If
capture.Success === false, the capture was rejected - Log message:
"Error capturing transaction: {idempotencyKey}" - Common cause: preauth expired (too much time between auth and capture)
Diagnostic Commands
Check Transaction State
javascript
// Full transaction with payment details
db.transactions.findOne(
{ order_no: "ORDER-123" },
{ status: 1, payment_snapshot: 1, all_idempotency_keys: 1, invalid_idempotency_keys: 1, order_status: 1, receipt_no: 1 }
)Check for Stuck Locks
javascript
// Find cache locks related to payment
db.cache.find({ key: /payment_verification/ })
db.cache.find({ key: /payment_processing/ })
// Clear stuck lock manually (use with caution)
db.cache.deleteOne({ key: "payment_verification_idemp_xxx" })Check Webhook Processing
javascript
// Viva webhook recent processing records
db.cache.find({ key: /viva_webhook_recent/ }).sort({ _id: -1 }).limit(10)
// Failed SEPA invoice cache
db.cache.find({ key: /failed_sepa_invoice/ })Provider-Specific Debugging
Viva Wallet
| Field | Where to Find | Purpose |
|---|---|---|
sessionId | payment_snapshot.sessionId | Terminal session identifier |
orderCode | payment_snapshot.orderCode | Viva order reference |
transactionId | payment_snapshot.transactionId | Viva transaction reference |
statusId | payment_snapshot.details.statusId | "F" = success, others = failure |
Viva Status Codes:
F= Finalized (success)A= Active (pending)E= ErrorC= Cancelled
Square
| Field | Where to Find | Purpose |
|---|---|---|
terminal_checkout_id | external_data.square.terminal_checkout_id | Square checkout reference |
order_id | payment_snapshot.order_id | Square order reference |
status | payment_snapshot.webhookData.status | "COMPLETED" = success |
Square Terminal Statuses:
PENDING= Waiting for customerIN_PROGRESS= Customer interactingCOMPLETED= Payment successfulCANCELLED= CancelledTIMED_OUT= No response from terminal
Stripe (Subscriptions)
Stripe is used for subscription billing, not direct order payments. For subscription payment issues:
- Check Stripe dashboard for the subscription status
- Look at
invoice.payment_failedwebhook handler - Check SEPA recovery logic in
StripeWebhookOrchestrator
Escalation Checklist
Before escalating a payment issue, gather:
- Transaction
_idandorder_no - Transaction
statusandpayment_snapshotcontents - Device ID and location ID
- Provider (Viva Wallet or Square)
- Provider-side transaction/order reference
- Relevant log entries (search by idempotency key)
- Timestamp of the issue
- Whether this is a one-off or recurring pattern