Version: 1.0.0
Last Updated: 2026-02-09
Author: Axion Platform Documentation
The Page-by-Page Collection System is a sophisticated multi-channel conversation flow engine designed for structured data collection, customer engagement, and automated collections processes. The system integrates tightly with the Dialer Queue infrastructure to enable scalable, asynchronous contact processing across multiple communication channels.
┌─────────────────────────────────────────────────────────────────┐
│ Page-by-Page Collection System │
└─────────────────────────────────────────────────────────────────┘
│
┌────────────────────────┼────────────────────────┐
│ │ │
┌───────▼────────┐ ┌─────────▼────────┐ ┌─────────▼─────────┐
│ Queue System │ │ Page Navigation │ │ Workflow Engine │
│ │ │ │ │ │
│ • def_queues │ │ • ObjConversation│ │ • ObjWorkflow │
│ • RabbitMQ │◄───┤ • Page Triggers │───►│ • FORMFLOW nodes │
│ • MongoDB │ │ • State Tracking │ │ • Service calls │
└────────┬───────┘ └─────────┬────────┘ └─────────┬─────────┘
│ │ │
│ ┌───────▼────────┐ │
│ │ Contact State │ │
└─────────────►│ Tracking │◄─────────────┘
│ │
│ • Current page │
│ • User context │
│ • Response data│
└────────────────┘
| Component | Location | Purpose |
|---|---|---|
| ObjConversation | factory.core/ObjConversation.py |
Page navigation engine |
| ObjMessageQueue | factory.core/ObjMessageQueue.py |
Queue management (RabbitMQ) |
| ObjWorkflow | factory.core/ObjWorkflow.py |
Business logic orchestration |
| def_queues | local.processing/schema/package.janee/tables/ |
Queue definitions |
| def_conversation_page | local.processing/schema/package.janee/tables/ |
Page definitions |
Table: def_queues
Location: local.processing/schema/package.janee/tables/def_queues.yaml
Primary Keys:
- Block (char 36) # Organizational grouping
- Queuecode (char 255) # Unique queue identifier
Additional Columns:
- RiskLevel (int) # Priority/risk scoring
- Package (char 36) # Package context
- Description (char 255) # Human-readable description
Table: def_queues_queries
Location: local.processing/schema/package.janee/tables/def_queues_queries.yaml
Stores SQL queries for:
Class: ObjMessageQueue
File: factory.core/ObjMessageQueue.py
# Load contacts from database to queue
message_queue.load(
context="collections",
queue="outbound_dialer",
sql="SELECT guid, contact_id, phone FROM collections_queue WHERE status='pending'"
)
# Push individual contact to queue
message_queue.push(
context="collections",
queue="outbound_dialer",
guid="contact_guid_123",
payload={"phone": "0821234567", "account": "ACC001"}
)
# Pop contacts for dialer processing
contacts = message_queue.pop_list(
context="collections",
queue="outbound_dialer",
max=100 # Batch size
)
pending → mq → processing → complete
│ │ │
│ │ └──► error (retry logic)
│ │
│ └──► expired (timeout)
│
└──► filtered (exclusion rules)
Table: def_conversation_page
Location: local.processing/schema/package.janee/tables/def_conversation_page.yaml
Primary Keys:
- ConversationCode (char 50) # Conversation flow identifier
- Package (char 50) # Package context
- PageNo (int 11) # Page sequence number
Page Content:
- HeaderText (text) # Page prompt/question
- Option1..5 (char 255) # Static option text (max 5)
- Option1..5Callback (text) # Navigation target for each option
- Option1..5Display (char 1) # Y/N visibility flags
Dynamic Options:
- OptionOverrideSql (text) # SQL query for dynamic options
- OptionOverrideShow (char 50) # Column to display from SQL results
SQL Hooks:
- PreSql (text) # Execute before page render
- PostSql (text) # Execute after user response
- ReRouteSql (text) # Dynamic page routing SQL
Behavior:
- TimeoutSeconds (int) # Page-level timeout
- MaxRetries (int) # Retry attempts for invalid input
- MediaUrl (varchar 500) # Attachment URL (images, PDFs)
Scenario: Payment collection conversation
-- Page 1: Welcome
ConversationCode: 'PAYMENT_COLLECT'
PageNo: 1
HeaderText: 'Hello {name}, you have an outstanding balance of R{amount}.
Would you like to make a payment?'
Option1: '1. Yes, pay now'
Option1Callback: '2' -- Navigate to page 2
Option2: '2. Arrange payment plan'
Option2Callback: '3' -- Navigate to page 3
Option3: '3. Speak to agent'
Option3Callback: 'ESCALATE' -- Trigger workflow
-- Page 2: Payment amount
ConversationCode: 'PAYMENT_COLLECT'
PageNo: 2
HeaderText: 'How much would you like to pay? (Min R{min_payment})'
OptionOverrideSql: 'SELECT CONCAT("R", amount) as display,
amount as value
FROM payment_options
WHERE account_id = $account_id$'
OptionOverrideShow: 'display'
PostSql: 'INSERT INTO payment_requests (account_id, amount, request_date)
VALUES ($account_id$, $user_input$, NOW())'
-- Page 3: Payment plan options
ConversationCode: 'PAYMENT_COLLECT'
PageNo: 3
HeaderText: 'Select a payment plan:'
Option1: '1. 3 months'
Option1Callback: 'WORKFLOW:PLAN_3MONTH'
Option2: '2. 6 months'
Option2Callback: 'WORKFLOW:PLAN_6MONTH'
Option3: '3. 12 months'
Option3Callback: 'WORKFLOW:PLAN_12MONTH'
File: factory.core/ObjConversation.py
class ObjConversation:
def read_conversation(self, incoming_sender_id, incoming_text):
"""
Main conversation orchestrator.
Flow:
1. Load conversation definition
2. Read/restore contact state
3. Get current page detail
4. Execute PreSql
5. Build option set
6. Match user input (read_intent)
7. Execute callback/routing
8. Record response (data_line)
9. Execute PostSql
10. Return next page
"""
pass
def get_page_detail(self, pageno):
"""Load page definition with Redis caching."""
pass
def read_contact(self):
"""
Load contact conversation state.
Returns:
- Current PageNo
- Pagination Offset
- Conversation GUID
- Last update timestamp
Handles:
- State restoration after timeout
- Pagination position
- Conversation expiration
"""
pass
def read_intent(self, page_no, user_input):
"""
Match user input to available options.
Supports:
- Numeric selection (1, 2, 3...)
- Keyword matching (fuzzy search)
- Regular expression patterns
Returns matched option callback.
"""
pass
def data_line(self, pageno, response):
"""
Record user response to data_conversation_{code} table.
Tracks:
- Page visited
- User response
- Timestamp
- Session context
"""
pass
Navigate directly to a specific page number:
Option1Callback: '5' # Jump to page 5
Navigate to a named workflow:
Option1Callback: 'WORKFLOW:PAYMENT_PROCESSOR'
Exit conversation:
Option1Callback: 'EXIT'
Execute SQL to determine next page based on business logic:
ReRouteSql: |
SELECT
CASE
WHEN balance < 1000 THEN '10' -- Small balance page
WHEN balance < 5000 THEN '11' -- Medium balance page
ELSE '12' -- Large balance page
END as next_page
FROM accounts
WHERE account_id = $account_id$
The query must return a single value (page number or callback).
Execute actions after user response:
PostSql: |
-- Record payment promise
INSERT INTO payment_promises (account_id, amount, promise_date, source)
VALUES ($account_id$, $user_input$, NOW(), 'conversation');
-- Update queue status
UPDATE collections_queue
SET status = 'promised', last_contact = NOW()
WHERE account_id = $account_id$;
-- Trigger workflow
INSERT INTO workflow_queue (workflow_code, context, payload)
VALUES ('PROMISE_CONFIRMATION', 'collections',
JSON_OBJECT('account_id', $account_id$, 'amount', $user_input$));
When user input doesn't match numeric options, the system uses fuzzy matching:
# User types: "yes please"
# Matches: Option1 = "Yes, pay now" (fuzzy match score > 0.7)
# Triggers: Option1Callback
Handle page-level timeouts:
TimeoutSeconds: 300 # 5 minutes
# On timeout:
# - State preserved
# - Conversation suspended
# - Can be resumed on next contact
Transition to workflow nodes:
Option1Callback: 'WORKFLOW:PAYMENT_FLOW'
# In workflow definition (Def_Workflow_node):
NodeCode: 'PAYMENT_FLOW'
NodeType: 'FORMFLOW' # or 'SERVICE', 'CALC', etc.
┌─────────────────────────────────────────────────────────────────────┐
│ Dialer Queue Workflow │
└─────────────────────────────────────────────────────────────────────┘
1. QUEUE POPULATION
┌─────────────────┐
│ def_queues_ │ SQL Query:
│ queries │ SELECT contact_id, phone, account_id
└────────┬────────┘ FROM collections_candidates
│ WHERE last_contact < DATE_SUB(NOW(), INTERVAL 7 DAY)
│ AND status = 'active'
▼ AND balance > 0
┌─────────────────┐
│ ObjMessageQueue │ message_queue.load(context, queue, sql)
│ .load() │
└────────┬────────┘
│
▼
┌─────────────────┐
│ RabbitMQ │ Message Format:
│ Queue │ {
│ "outbound_ │ "guid": "contact_guid_123",
│ dialer" │ "payload": {
└────────┬────────┘ "phone": "0821234567",
│ "account_id": "ACC001",
│ "conversation_code": "PAYMENT_COLLECT"
│ }
│ }
2. DIALER PROCESSING
│
▼
┌─────────────────┐
│ Dialer Agent │ contacts = message_queue.pop_list(queue, max=100)
│ (Worker) │
└────────┬────────┘
│
├──► Dial contact phone number
│
▼
┌─────────────────┐
│ Call Connected │ If answered:
└────────┬────────┘ - Initialize conversation
│ - Set ConversationCode from payload
│
3. PAGE NAVIGATION
│
▼
┌─────────────────┐
│ ServeConversation│ POST /conversation/{conversation_code}
│ Webhook │ Body: {
└────────┬────────┘ "sender_id": "0821234567",
│ "text": "",
│ "account_id": "ACC001"
▼ }
┌─────────────────┐
│ ObjConversation │ conversation.read_conversation(sender_id, text)
│ .read_ │
│ conversation() │
└────────┬────────┘
│
├──► Read contact state (current page)
│
├──► Load page definition (def_conversation_page)
│
├──► Execute PreSql (prepare page data)
│
├──► Build options (static or OptionOverrideSql)
│
▼
┌─────────────────┐
│ Render Page 1 │ "Hello {name}, outstanding balance R{amount}."
│ │ "1. Pay now"
│ │ "2. Payment plan"
│ │ "3. Speak to agent"
└────────┬────────┘
│
4. USER RESPONSE
│
▼ (User presses "1")
┌─────────────────┐
│ ObjConversation │ conversation.read_intent(page_no, "1")
│ .read_intent() │ → Matches Option1
└────────┬────────┘ → Returns Option1Callback = "2"
│
├──► Record response (data_line)
│
├──► Execute PostSql (update status)
│
├──► Update contact state (PageNo = 2)
│
▼
┌─────────────────┐
│ Render Page 2 │ "How much would you like to pay?"
│ │ (Dynamic options from OptionOverrideSql)
│ │ "1. R500"
│ │ "2. R1000"
│ │ "3. R1500"
└────────┬────────┘ "4. Other amount"
│
▼ (User presses "2")
┌─────────────────┐
│ Execute │ PostSql: INSERT INTO payment_requests...
│ PostSql │
└────────┬────────┘
│
├──► Check callback: 'WORKFLOW:PAYMENT_PROCESSOR'
│
▼
┌─────────────────┐
│ Trigger │ workflow.execute(
│ Workflow │ code='PAYMENT_PROCESSOR',
│ │ context={'account_id': 'ACC001', 'amount': 1000}
└────────┬────────┘ )
│
5. WORKFLOW EXECUTION
│
▼
┌─────────────────┐
│ ObjWorkflow │ Nodes:
│ PAYMENT_ │ 1. Validate payment amount
│ PROCESSOR │ 2. Create payment transaction
└────────┬────────┘ 3. Send confirmation SMS
│ 4. Update account balance
│ 5. Close conversation
▼
┌─────────────────┐
│ Update Queue │ UPDATE collections_queue
│ Status │ SET status = 'payment_received'
└────────┬────────┘ WHERE account_id = 'ACC001'
│
▼
┌─────────────────┐
│ Conversation │ "Thank you! Your payment of R1000 has been received."
│ Complete │ "Receipt sent via SMS."
└─────────────────┘ (Exit conversation)
Update queue status after conversation:
-- After successful page completion
UPDATE collections_queue
SET
status = 'contacted',
last_contact = NOW(),
page_reached = $current_page$,
outcome = $conversation_result$
WHERE guid = $queue_guid$;
-- On conversation error/timeout
UPDATE collections_queue
SET
status = 'retry',
retry_count = retry_count + 1,
next_retry = DATE_ADD(NOW(), INTERVAL retry_interval HOUR)
WHERE guid = $queue_guid$
AND retry_count < max_retries;
Table: data_conversation_contact_{conversation_code}
Created dynamically per conversation
CREATE TABLE data_conversation_contact_payment_collect (
ConversationCode VARCHAR(50),
SenderId VARCHAR(100), -- Phone number or user ID
ConversationGuid VARCHAR(100), -- Unique session identifier
Channel VARCHAR(50), -- whatsapp, sms, console, discord
PageNo INT, -- Current page position
Offset INT, -- Pagination offset for multi-page options
LastUpdate DATETIME, -- Last interaction timestamp
ExpiryDate DATETIME, -- Session expiration
Context JSON, -- Additional session context
PRIMARY KEY (ConversationCode, SenderId)
);
Contact state is preserved across:
Code: ObjConversation.py:read_contact() (lines 624-697)
def read_contact(self):
"""
Read/restore contact conversation state.
Returns:
- Current PageNo (where to resume)
- Offset (pagination position)
- ConversationGuid (session ID)
- Expiration status
"""
sql = f"""
SELECT PageNo, Offset, ConversationGuid, ExpiryDate
FROM data_conversation_contact_{self.conversation_code}
WHERE SenderId = '{self.sender_id}'
"""
result = self.sql_get_values(sql)
if result:
# Restore state
self.current_page = result[0]
self.offset = result[1]
self.conversation_guid = result[2]
expiry = result[3]
# Check expiration
if expiry and expiry < datetime.now():
# Reset to page 1 (conversation expired)
self.reset_conversation()
else:
# New conversation
self.initialize_conversation()
For pages with many dynamic options (e.g., 50+ accounts), pagination allows users to navigate through options:
# Page with pagination
OptionOverrideSql: |
SELECT
account_number,
CONCAT('Account ', account_number, ' - R', balance) as display_text
FROM accounts
WHERE customer_id = $customer_id$
LIMIT 5 OFFSET $offset$
Navigation:
Transition from workflow to conversation page:
# In Def_Workflow_node table:
NodeCode: 'COLLECT_PAYMENT'
NodeType: 'FORMFLOW'
NodeFormContext: 'PAYMENT_COLLECT' # ConversationCode
NodeFormTemplate: 'page'
NodeFormGuid: '1' # Start at page 1
Workflow Node Code: factory.workflow/ObjWorkflowFlow.py (lines 70-148)
if node_type == 'FORMFLOW':
# Pass context to conversation system
run_context["target"] = "form"
run_context["target_context"] = node_form_context # ConversationCode
run_context["target_guid"] = node_form_guid # PageNo
run_context["target_template"] = "page"
# Transfer control to ObjConversation
return run_context
From page callback to workflow:
# In def_conversation_page:
Option1Callback: 'WORKFLOW:PAYMENT_CONFIRMATION'
# Workflow triggered with context:
# {
# "conversation_code": "PAYMENT_COLLECT",
# "sender_id": "0821234567",
# "page_no": 5,
# "user_response": "1000",
# "account_id": "ACC001"
# }
Complex flows can alternate between workflows and pages:
Workflow Node 1 (Calculate balance)
↓
FORMFLOW → Page 1 (Show balance, ask for payment)
↓ (User selects payment)
Page Callback → Workflow Node 2 (Process payment)
↓
FORMFLOW → Page 2 (Confirmation message)
↓
Workflow Node 3 (Update records, send receipt)
Step 1: Define Queue
-- def_queues table
INSERT INTO def_queues (Block, Queuecode, Package, Description)
VALUES ('PAYMENTS', 'PAYMENT_OUTBOUND', 'collections',
'Outbound payment collection queue');
-- def_queues_queries table
INSERT INTO def_queues_queries (Block, Queuecode, QuerySql)
VALUES ('PAYMENTS', 'PAYMENT_OUTBOUND',
'SELECT
a.account_id as guid,
c.phone_number as phone,
a.balance as amount,
c.first_name as name
FROM accounts a
JOIN customers c ON a.customer_id = c.customer_id
WHERE a.balance > 0
AND a.status = ''active''
AND a.last_contact < DATE_SUB(NOW(), INTERVAL 7 DAY)
LIMIT 1000');
Step 2: Define Conversation Pages
-- Page 1: Initial contact
INSERT INTO def_conversation_page
(ConversationCode, Package, PageNo, HeaderText,
Option1, Option1Callback, Option2, Option2Callback, Option3, Option3Callback)
VALUES
('PAYMENT_COLLECT', 'collections', 1,
'Hello {name}, you have an outstanding balance of R{amount}. Would you like to make a payment?',
'1. Yes, pay now', '2',
'2. Not right now', 'EXIT',
'3. Dispute balance', 'WORKFLOW:DISPUTE_HANDLER');
-- Page 2: Payment amount selection
INSERT INTO def_conversation_page
(ConversationCode, Package, PageNo, HeaderText,
OptionOverrideSql, OptionOverrideShow, PostSql)
VALUES
('PAYMENT_COLLECT', 'collections', 2,
'How much would you like to pay?',
'SELECT
CONCAT(''R'', amount) as display,
amount as value,
CONCAT(''PAY:'', amount) as callback
FROM payment_options
WHERE account_id = $account_id$',
'display',
'INSERT INTO payment_requests (account_id, amount, request_date, source)
VALUES ($account_id$, $user_input$, NOW(), ''conversation'');
UPDATE collections_queue
SET status = ''payment_promised'', last_contact = NOW()
WHERE account_id = $account_id$');
Step 3: Load Queue
from factory.core.ObjMessageQueue import ObjMessageQueue
mq = ObjMessageQueue()
# Load contacts from queue query into RabbitMQ
mq.load(
context="collections",
queue="payment_outbound",
sql="""
SELECT
a.account_id as guid,
JSON_OBJECT(
'phone', c.phone_number,
'account_id', a.account_id,
'balance', a.balance,
'name', c.first_name,
'conversation_code', 'PAYMENT_COLLECT'
) as payload
FROM accounts a
JOIN customers c ON a.customer_id = c.customer_id
WHERE a.balance > 0
LIMIT 100
"""
)
Step 4: Dialer Agent Processing
# Dialer agent pops contacts from queue
contacts = mq.pop_list(
context="collections",
queue="payment_outbound",
max=10
)
for contact in contacts:
# Initiate call
phone = contact['payload']['phone']
conversation_code = contact['payload']['conversation_code']
# On answer, start conversation
conversation = ObjConversation()
conversation.conversation_code = conversation_code
conversation.sender_id = phone
conversation.context = contact['payload']
# Start at page 1
response = conversation.read_conversation(
incoming_sender_id=phone,
incoming_text="" # Initial prompt
)
# Send via channel (SMS/WhatsApp/Voice)
send_message(phone, response['message'])
Scenario: Customer satisfaction survey with conditional branching
-- Page 1: Overall satisfaction
INSERT INTO def_conversation_page
(ConversationCode, Package, PageNo, HeaderText,
Option1, Option1Callback, Option2, Option2Callback,
Option3, Option3Callback, Option4, Option4Callback,
ReRouteSql, PostSql)
VALUES
('SATISFACTION_SURVEY', 'surveys', 1,
'How satisfied are you with our service?',
'1. Very satisfied', 'ROUTE',
'2. Satisfied', 'ROUTE',
'3. Dissatisfied', 'ROUTE',
'4. Very dissatisfied', 'ROUTE',
-- Routing logic based on response
'SELECT
CASE $user_input$
WHEN ''1'' THEN ''2'' -- Positive: go to testimonial page
WHEN ''2'' THEN ''2''
WHEN ''3'' THEN ''3'' -- Negative: go to feedback page
WHEN ''4'' THEN ''3''
END as next_page',
-- Record response
'INSERT INTO survey_responses (sender_id, question, response, response_date)
VALUES ($sender_id$, ''satisfaction'', $user_input$, NOW())');
-- Page 2: Positive feedback (testimonial request)
INSERT INTO def_conversation_page
(ConversationCode, Package, PageNo, HeaderText,
Option1, Option1Callback, Option2, Option2Callback)
VALUES
('SATISFACTION_SURVEY', 'surveys', 2,
'Thank you! Would you be willing to share a testimonial?',
'1. Yes, I would', '4', -- Go to testimonial page
'2. No thanks', 'EXIT'); -- End survey
-- Page 3: Negative feedback (issue collection)
INSERT INTO def_conversation_page
(ConversationCode, Package, PageNo, HeaderText,
OptionOverrideSql, OptionOverrideShow, PostSql)
VALUES
('SATISFACTION_SURVEY', 'surveys', 3,
'We''re sorry to hear that. What can we improve?',
'SELECT issue_code as value, issue_text as display,
CONCAT(''LOG:'', issue_code) as callback
FROM feedback_issues
ORDER BY priority',
'display',
'INSERT INTO customer_feedback (sender_id, issue_type, feedback_date)
VALUES ($sender_id$, $user_input$, NOW());
INSERT INTO workflow_queue (workflow_code, context, payload)
VALUES (''ESCALATE_FEEDBACK'', ''support'',
JSON_OBJECT(''sender_id'', $sender_id$, ''issue'', $user_input$))');
-- Page 4: Testimonial collection
INSERT INTO def_conversation_page
(ConversationCode, Package, PageNo, HeaderText, PostSql)
VALUES
('SATISFACTION_SURVEY', 'surveys', 4,
'Please reply with your testimonial (free text):',
'INSERT INTO testimonials (customer_id, testimonial_text, submitted_date)
VALUES ($sender_id$, $user_input$, NOW());
UPDATE customers SET allow_testimonial = ''Y'' WHERE phone = $sender_id$');
-- Page 1: Debt notification
INSERT INTO def_conversation_page
(ConversationCode, Package, PageNo, HeaderText,
PreSql, Option1, Option1Callback, Option2, Option2Callback, Option3, Option3Callback)
VALUES
('DEBT_COLLECTION', 'collections', 1,
'Account {account_number}: Overdue balance R{balance}. Payment due {due_date}.',
-- Calculate payment options based on balance
'CREATE TEMPORARY TABLE payment_opts AS
SELECT
account_id,
balance,
ROUND(balance, 2) as full_payment,
ROUND(balance * 0.5, 2) as partial_payment,
ROUND(balance / 3, 2) as installment
FROM accounts WHERE account_id = $account_id$',
'1. Pay full amount (R{full_payment})', '2',
'2. Pay 50% now (R{partial_payment})', '3',
'3. Arrange payment plan', '4');
-- Page 2: Full payment confirmation
INSERT INTO def_conversation_page
(ConversationCode, Package, PageNo, HeaderText,
PostSql, Option1, Option1Callback)
VALUES
('DEBT_COLLECTION', 'collections', 2,
'Confirm payment of R{full_payment} for account {account_number}?',
'INSERT INTO payment_promises (account_id, promise_amount, promise_date, promise_type)
VALUES ($account_id$, (SELECT full_payment FROM payment_opts), NOW(), ''full'');
INSERT INTO workflow_queue (workflow_code, payload)
VALUES (''PROCESS_PAYMENT'', JSON_OBJECT(''account_id'', $account_id$,
''amount'', (SELECT full_payment FROM payment_opts)))',
'1. Confirm', 'WORKFLOW:PAYMENT_PROCESSOR');
-- Page 3: Partial payment confirmation
INSERT INTO def_conversation_page
(ConversationCode, Package, PageNo, HeaderText,
PostSql, Option1, Option1Callback)
VALUES
('DEBT_COLLECTION', 'collections', 3,
'Confirm partial payment of R{partial_payment}. Remaining balance will be due in 30 days.',
'INSERT INTO payment_promises (account_id, promise_amount, promise_date, promise_type)
VALUES ($account_id$, (SELECT partial_payment FROM payment_opts), NOW(), ''partial'')',
'1. Confirm', 'WORKFLOW:PAYMENT_PROCESSOR');
-- Page 4: Payment plan options
INSERT INTO def_conversation_page
(ConversationCode, Package, PageNo, HeaderText,
OptionOverrideSql, OptionOverrideShow, PostSql)
VALUES
('DEBT_COLLECTION', 'collections', 4,
'Select payment plan duration:',
'SELECT
months as value,
CONCAT(months, '' months - R'', ROUND(balance / months, 2), ''/month'') as display,
CONCAT(''PLAN:'', months) as callback
FROM (SELECT 3 as months UNION SELECT 6 UNION SELECT 12) plans,
payment_opts
WHERE balance > 0',
'display',
'INSERT INTO payment_plans (account_id, plan_months, monthly_amount, start_date)
VALUES ($account_id$, $user_input$,
(SELECT ROUND(balance / $user_input$, 2) FROM payment_opts), NOW())');
Symptom: Contact state not progressing
Diagnosis:
-- Check contact state
SELECT * FROM data_conversation_contact_payment_collect
WHERE SenderId = '0821234567';
-- Check last response
SELECT * FROM data_conversation_payment_collect
WHERE SenderId = '0821234567'
ORDER BY created_at DESC
LIMIT 5;
Resolution:
-- Reset contact to page 1
UPDATE data_conversation_contact_payment_collect
SET PageNo = 1, Offset = 0, LastUpdate = NOW()
WHERE SenderId = '0821234567';
Symptom: Contacts not popping from RabbitMQ
Diagnosis:
# Check queue depth
from factory.core.ObjMessageQueue import ObjMessageQueue
mq = ObjMessageQueue()
count = mq.get_queue_count(context="collections", queue="payment_outbound")
print(f"Queue depth: {count}")
Resolution:
# Manually pop and inspect
contacts = mq.pop_list(context="collections", queue="payment_outbound", max=1)
print(contacts)
# Check MongoDB tracking
db = mq.get_mongo_db()
cursor = db.workflow_queue_context.find({"queue": "payment_outbound"})
for doc in cursor:
print(doc)
Symptom: OptionOverrideSql returns empty
Diagnosis:
# Test SQL directly
sql = """
SELECT account_number, balance
FROM accounts
WHERE customer_id = 'CUST001'
"""
results = conversation.sql_get_array(sql)
print(results)
Resolution:
$account_id$ must be in context)Symptom: Conversation expires prematurely
Diagnosis:
-- Check expiry settings
SELECT TimeoutSeconds, MaxRetries
FROM def_conversation_page
WHERE ConversationCode = 'PAYMENT_COLLECT'
AND PageNo = 1;
Resolution:
-- Increase timeout
UPDATE def_conversation_page
SET TimeoutSeconds = 600 -- 10 minutes
WHERE ConversationCode = 'PAYMENT_COLLECT';
Symptom: WORKFLOW:* callback doesn't execute
Diagnosis:
-- Check workflow queue
SELECT * FROM workflow_queue
WHERE workflow_code = 'PAYMENT_PROCESSOR'
ORDER BY created_at DESC
LIMIT 10;
-- Check workflow definition
SELECT * FROM Def_Workflow
WHERE WorkflowCode = 'PAYMENT_PROCESSOR';
Resolution:
# Manually trigger workflow
from factory.core.ObjWorkflow import ObjWorkflow
wf = ObjWorkflow()
result = wf.execute(
code='PAYMENT_PROCESSOR',
context={'account_id': 'ACC001', 'amount': 1000}
)
Enable conversation debugging:
conversation = ObjConversation()
conversation.DO_DEBUG = True # Enable debug output
Logs will show:
Active conversations:
SELECT
ConversationCode,
COUNT(*) as active_contacts,
AVG(PageNo) as avg_page,
MAX(PageNo) as max_page
FROM data_conversation_contact_payment_collect
WHERE LastUpdate > DATE_SUB(NOW(), INTERVAL 1 HOUR)
GROUP BY ConversationCode;
Conversation completion rate:
SELECT
c.ConversationCode,
COUNT(DISTINCT c.SenderId) as total_contacts,
SUM(CASE WHEN c.PageNo >= (SELECT MAX(PageNo) FROM def_conversation_page
WHERE ConversationCode = c.ConversationCode)
THEN 1 ELSE 0 END) as completed,
ROUND(100 * SUM(CASE WHEN c.PageNo >= (SELECT MAX(PageNo) FROM def_conversation_page
WHERE ConversationCode = c.ConversationCode)
THEN 1 ELSE 0 END) / COUNT(*), 2) as completion_rate
FROM data_conversation_contact_payment_collect c
WHERE c.LastUpdate > DATE_SUB(NOW(), INTERVAL 7 DAY)
GROUP BY c.ConversationCode;
Queue processing metrics:
SELECT
status,
COUNT(*) as count,
AVG(TIMESTAMPDIFF(MINUTE, created_at, last_contact)) as avg_time_to_contact
FROM collections_queue
WHERE created_at > DATE_SUB(NOW(), INTERVAL 7 DAY)
GROUP BY status;
| Version | Date | Author | Changes |
|---|---|---|---|
| 1.0.0 | 2026-02-09 | Axion Platform | Initial documentation |
Questions or Issues?
Contact the Axion development team or open an issue in the project repository.