This playbook covers common support scenarios for operating BillerJacket in production. All operations assume access to the Razor Pages admin UI, Azure Portal, and Application Insights.
Every API request, queue message, and database write carries a CorrelationId. This is the primary tool for debugging.
| Source | Where to Look |
|---|---|
| API response | Returned in response body or X-Correlation-Id header |
| Service Bus message | ServiceBusMessage.CorrelationId property |
| Database records | CorrelationId column on Payment, PaymentAttempt, WebhookEvent, CommunicationLog, AuditLog |
| Application Insights | Custom dimension correlationId in traces |
- Start with the CorrelationId from the API response or customer report
- Query Application Insights:
traces | where customDimensions.correlationId == "<id>" | order by timestamp asc
- Query database for related records:
SELECT * FROM AuditLogs WHERE CorrelationId = '<id>' ORDER BY OccurredAt; SELECT * FROM CommunicationLogs WHERE CorrelationId = '<id>'; SELECT * FROM PaymentAttempts WHERE CorrelationId = '<id>';
To understand everything that happened to an invoice:
-- Invoice details
SELECT * FROM Invoices WHERE InvoiceId = '<id>';
-- Line items
SELECT * FROM InvoiceLineItems WHERE InvoiceId = '<id>' ORDER BY LineNumber;
-- Payments applied
SELECT * FROM Payments WHERE InvoiceId = '<id>' ORDER BY AppliedAt;
-- Payment attempts (including failures)
SELECT * FROM PaymentAttempts WHERE InvoiceId = '<id>' ORDER BY AttemptedAt;
-- Communications sent
SELECT * FROM CommunicationLogs WHERE InvoiceId = '<id>' ORDER BY SentAt;
-- Dunning state
SELECT * FROM InvoiceDunningStates WHERE InvoiceId = '<id>';
-- Audit trail
SELECT * FROM AuditLogs
WHERE EntityType = 'Invoice' AND EntityId = '<id>'
ORDER BY OccurredAt;The Razor Pages UI at /support/dlq shows dead-lettered messages across all queues.
In Azure Portal:
- Navigate to Service Bus namespace
sb-billerjacket-{env} - Select the queue (e.g.,
email-send) - Click "Dead-letter queue" tab
- View messages with their dead-letter reason
| Reason | Meaning | Action |
|---|---|---|
bad_envelope |
Message body couldn't be deserialized | Check publisher serialization. Usually a bug -- fix and redeploy. |
unknown_message_type |
MessageType string not recognized by consumer | Version mismatch between publisher and consumer. Deploy consumer first. |
processing_failed |
Non-transient exception during handling | Check ErrorMessage. Fix root cause, then replay. |
| (auto) MaxDeliveryCount exceeded | Transient failures exhausted all retries | Investigate underlying service (SQL, email provider). Replay after fix. |
Via UI: Navigate to /support/dlq, find the message, click "Replay."
Via Azure Portal:
- Open the DLQ for the queue
- Peek the message to inspect its content
- Copy the message body
- Re-publish to the main queue (Service Bus Explorer or code)
Via API: POST /api/webhooks/{id}/replay for webhook events.
All message handlers are idempotent. Replaying a message that was already partially processed will not cause double-processing (idempotency keys, webhook deduplication, etc.).
-
Check
WebhookEventstable:SELECT * FROM WebhookEvents WHERE WebhookEventId = '<id>';
-
Check
ProcessingStatus:Received: Message was stored but worker hasn't picked it up yet. Checkwebhook-ingestqueue depth.Processing: Worker is actively handling it. If stuck, check for long-running operations.Failed: CheckErrorMessagecolumn for details.Processed: Completed successfully.
-
Check Application Insights for the CorrelationId:
traces | where customDimensions.correlationId == "<correlationId>" | where customDimensions.messageType == "webhook.received" | order by timestamp asc
Via UI: Navigate to /support/webhooks, find the event, click "Replay."
Via API: POST /api/webhooks/{id}/replay
This publishes a WebhookReplayRequested message. The worker reprocesses the stored payload idempotently.
-
Check idempotency key:
SELECT * FROM IdempotencyKeys WHERE TenantId = '<tenantId>' AND Operation = 'ApplyPayment' AND KeyValue = '<idempotencyKey>';
If a row exists, the payment was already processed (check
ResponseJson). -
Check payment attempts:
SELECT * FROM PaymentAttempts WHERE InvoiceId = '<invoiceId>' ORDER BY AttemptedAt DESC;
Look at
Status,FailureCode,FailureMessage. -
Check the
payment-commandsqueue:- Is the message still in the queue (not yet processed)?
- Is it in the DLQ?
-
Check Application Insights:
traces | where customDimensions.feature == "Payment" | where customDimensions.correlationId == "<correlationId>"
Idempotency keys prevent double processing. To verify:
- Query
IdempotencyKeysfor the key value - Check that only one
Paymentrecord exists for that idempotency key - Verify
Invoice.PaidAmountmatches the sum ofPayments.Amount
-
Verify the tenant has an active dunning plan:
SELECT * FROM DunningPlans WHERE TenantId = '<tenantId>' AND IsActive = 1;
-
Check the invoice has a dunning state:
SELECT * FROM InvoiceDunningStates WHERE InvoiceId = '<invoiceId>';
If no row exists, the daily evaluation hasn't run or the invoice wasn't overdue when it ran.
-
Check
NextActionAt:- If in the future: the next reminder is scheduled, not due yet.
- If in the past: the evaluation job may not have run. Check
dunning-evaluatequeue and worker logs.
-
Check
CommunicationLogsfor sent reminders:SELECT * FROM CommunicationLogs WHERE InvoiceId = '<invoiceId>' AND Type = 'Dunning' ORDER BY SentAt;
Check CurrentStepNumber against the plan's total steps. The dunning system should not exceed the plan's step count. If it did, check for duplicate EvaluateDunningCommand processing (look at CorrelationIds in logs).
-
Check
CommunicationLogs:SELECT * FROM CommunicationLogs WHERE InvoiceId = '<invoiceId>' AND Channel = 'Email' ORDER BY SentAt DESC;
-
If status is
Failed: checkErrorMessagefor provider error details. -
If no row exists: the email message may still be in the
email-sendqueue or DLQ. -
Check
email-sendqueue depth and DLQ in Azure Portal.
traces
| where severityLevel >= 3
| where customDimensions.feature != ""
| summarize count() by tostring(customDimensions.feature),
tostring(customDimensions.operation)
| order by count_ desctraces
| where customDimensions.component == "Worker"
| where customDimensions.operation == "ProcessMessage"
| extend duration = todouble(customDimensions.durationMs)
| where duration > 5000
| project timestamp, customDimensions.messageType, duration,
customDimensions.correlationId
| order by duration desctraces
| where customDimensions.feature == "Payment"
| where customDimensions.operation == "ApplyPayment"
| where severityLevel >= 3
| where timestamp > ago(7d)
| project timestamp, customDimensions.correlationId,
customDimensions.tenantId, messagetraces
| where customDimensions.feature == "Dunning"
| where timestamp > ago(1d)
| summarize count() by tostring(customDimensions.operation),
tostring(customDimensions.tenantId)- Check DLQ message count across all queues
- Verify dunning evaluation ran (check logs for
EvaluateDunningCommand) - Review failed payments in the last 24 hours
- Review
WebhookEventswithProcessingStatus = 'Failed' - Check aging report for invoices overdue > 90 days
- Review Application Insights for error trends
- Get the CorrelationId
- Trace through Application Insights
- Query related database tables
- Check DLQ for related messages
- Replay if safe (all handlers are idempotent)
- Document findings in incident log