Version: 1.0.0
Last Updated: 2026-02-09
Author: Axion Platform Documentation
The Markdown-Driven Collection Ecosystem transforms debt collection operations by using simple .md files as dynamic UI templates that interact with a central Sentral Data Mart (ADM). This ADM acts as a sophisticated state machine, ensuring debtors follow a strict, compliant path while providing agents with script-driven, context-aware interfaces.
✅ Script-Driven Consistency - Every agent sees the same compliant script
✅ State Machine Control - ADM ensures proper collection flow progression
✅ Real-Time Updates - Edit a .md file, update all agents instantly
✅ Automated Reporting - Compliance-ready reports generated automatically
✅ Performance Tracking - Live EDC (External Debt Collector) metrics
✅ Zero Code Deployment - Business users can update scripts without developers
┌─────────────────────────────────────────────────────────────────┐
│ Markdown-Driven Collection Ecosystem │
└─────────────────────────────────────────────────────────────────┘
│
┌────────────────────────┼────────────────────────┐
│ │ │
┌───────▼────────┐ ┌─────────▼────────┐ ┌─────────▼─────────┐
│ .md Screen │ │ Sentral ADM │ │ Dialer Queue │
│ Templates │ │ State Machine │ │ │
│ │ │ │ │ │
│ • discovery.md │◄──►│ • debtor_master │◄──►│ • Contact pop │
│ • negotiation │ │ • installment_ │ │ • Queue status │
│ • supervisor │ │ ledger │ │ • Next action │
└────────────────┘ │ • audit_trail │ └───────────────────┘
│ • state tracking │
└──────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ Collection Ecosystem Architecture │
└─────────────────────────────────────────────────────────────────────┘
1. MARKDOWN TEMPLATES LAYER
┌─────────────────┐
│ discovery.md │ Agent screens with:
│ negotiation.md │ • Frontmatter (ADM config)
│ supervisor_ov.md│ • Script templates
│ edc_dashboard.md│ • Form definitions
└────────┬────────┘ • ADM triggers
│
│ Template Variables: {{client.name}}, {{adm.state}}
▼
2. RENDERING ENGINE
┌─────────────────┐
│ ObjMarkdown │ • Parse frontmatter
│ Renderer │ • Inject ADM data
└────────┬────────┘ • Render forms
│ • Handle triggers
│
▼
3. SENTRAL DATA MART (ADM)
┌─────────────────────────────────────────┐
│ State Machine Tables: │
│ │
│ • debtor_master │
│ - Current state (NEW, IN_NEGOTIATION) │
│ - Contact history │
│ - Verification flags │
│ │
│ • installment_ledger │
│ - PTP records │
│ - Payment history │
│ - Broken promises │
│ │
│ • state_transitions │
│ - State change audit │
│ - Trigger history │
│ - Agent actions │
│ │
│ • feedback_repository │
│ - Agent notes │
│ - Compliance records │
│ - Disposition codes │
└────────┬────────────────────────────────┘
│
│ State Transition Events
▼
4. WORKFLOW ORCHESTRATION
┌─────────────────┐
│ ObjWorkflow │ Triggered by ADM:
│ Engine │ • MOVE_TO_NEGOTIATION
└────────┬────────┘ • SET_FLAG_UNCOOPERATIVE
│ • STATE_PTP_ACTIVE
│
▼
5. DIALER QUEUE INTEGRATION
┌─────────────────┐
│ ObjMessageQueue │ • Pop next contact based on ADM state
│ (RabbitMQ) │ • Update queue status on state change
└─────────────────┘ • Mute accounts (PTP_ACTIVE for 30 days)
New ADM Tables:
-- Core state machine table
CREATE TABLE debtor_master (
client_id VARCHAR(50) PRIMARY KEY,
acc_no VARCHAR(50) UNIQUE NOT NULL,
name VARCHAR(255),
id_number VARCHAR(13),
phone_primary VARCHAR(20),
phone_alternative VARCHAR(20),
email VARCHAR(255),
-- State Machine
current_state ENUM(
'NEW',
'TRACE_PENDING',
'IN_NEGOTIATION',
'PTP_ACTIVE',
'PTP_BROKEN',
'LEGAL_REFERRAL',
'SETTLED',
'WRITTEN_OFF'
) NOT NULL DEFAULT 'NEW',
state_entered_date DATETIME DEFAULT CURRENT_TIMESTAMP,
previous_state VARCHAR(50),
-- Verification
identity_verified BOOLEAN DEFAULT FALSE,
employment_status ENUM('Employed', 'Unemployed', 'Pensioner', 'Unknown'),
-- Financial
balance_principal DECIMAL(12,2),
balance_interest DECIMAL(12,2),
balance_fees DECIMAL(12,2),
balance_total DECIMAL(12,2),
-- Assignment
assigned_agency VARCHAR(100),
assigned_agent VARCHAR(100),
assignment_date DATETIME,
-- Flags
flag_uncooperative BOOLEAN DEFAULT FALSE,
flag_dispute BOOLEAN DEFAULT FALSE,
flag_vulnerable BOOLEAN DEFAULT FALSE,
-- Metadata
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_state (current_state),
INDEX idx_agency (assigned_agency),
INDEX idx_state_date (current_state, state_entered_date)
);
-- Installment ledger (PTPs and payments)
CREATE TABLE installment_ledger (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
client_id VARCHAR(50) NOT NULL,
-- PTP Details
ptp_amount DECIMAL(12,2) NOT NULL,
ptp_date DATE NOT NULL,
ptp_method ENUM('Debit Order', 'EFT', 'Direct Deposit', 'Cash', 'Other'),
ptp_created_date DATETIME DEFAULT CURRENT_TIMESTAMP,
ptp_created_by VARCHAR(100),
-- Status
ptp_status ENUM('PENDING', 'ACTIVE', 'KEPT', 'BROKEN', 'CANCELLED') DEFAULT 'ACTIVE',
-- Payment Tracking
payment_received_date DATETIME,
payment_amount DECIMAL(12,2),
payment_reference VARCHAR(100),
-- Broken Promise Tracking
broken_date DATETIME,
broken_reason TEXT,
-- Agent Notes
notes TEXT,
propensity_score INT, -- 1-10 likelihood of payment
FOREIGN KEY (client_id) REFERENCES debtor_master(client_id),
INDEX idx_client (client_id),
INDEX idx_ptp_date (ptp_date),
INDEX idx_status (ptp_status)
);
-- State transition audit
CREATE TABLE state_transitions (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
client_id VARCHAR(50) NOT NULL,
from_state VARCHAR(50) NOT NULL,
to_state VARCHAR(50) NOT NULL,
trigger_type ENUM('MANUAL', 'AUTO', 'WORKFLOW', 'SCHEDULE') NOT NULL,
trigger_source VARCHAR(255), -- Screen ID or workflow name
agent_id VARCHAR(100),
agency_name VARCHAR(100),
transition_date DATETIME DEFAULT CURRENT_TIMESTAMP,
reason TEXT,
FOREIGN KEY (client_id) REFERENCES debtor_master(client_id),
INDEX idx_client (client_id),
INDEX idx_date (transition_date)
);
-- Feedback repository
CREATE TABLE feedback_repository (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
client_id VARCHAR(50) NOT NULL,
interaction_date DATETIME DEFAULT CURRENT_TIMESTAMP,
screen_id VARCHAR(100), -- e.g., 'SCREEN_DISCOVERY'
disposition ENUM('CONTACTED', 'NO_ANSWER', 'WRONG_NUMBER', 'REFUSED', 'PTP', 'BROKEN_PTP'),
agent_notes TEXT,
compliance_script_followed BOOLEAN DEFAULT TRUE,
adm_state_before VARCHAR(50),
adm_state_after VARCHAR(50),
agent_id VARCHAR(100),
agency_name VARCHAR(100),
-- Captured form data (JSON)
form_data JSON,
FOREIGN KEY (client_id) REFERENCES debtor_master(client_id),
INDEX idx_client (client_id),
INDEX idx_date (interaction_date)
);
┌─────────────────────────────────────────────────────────────────┐
│ ADM State Machine │
└─────────────────────────────────────────────────────────────────┘
NEW
│
│ Identity Verified?
│
├─[YES]────────────────────┐
│ ▼
│ IN_NEGOTIATION
│ │
│ │ PTP Captured?
│ │
│ ├─[YES]──► PTP_ACTIVE
│ │ │
│ │ │ Payment Received?
│ │ │
│ │ ├─[YES]──► SETTLED
│ │ │
│ │ └─[NO after 30d]──► PTP_BROKEN
│ │ │
│ │ │
│ └─[NO]──► Still in negotiation ◄───────┘
│
└─[NO]──► TRACE_PENDING
│
│ New contact found?
│
├─[YES]──► Return to NEW
│
└─[NO after 90d]──► LEGAL_REFERRAL
Flags (orthogonal to states):
• flag_uncooperative - Set when client refuses to cooperate
• flag_dispute - Set when client disputes debt
• flag_vulnerable - Set for vulnerable customers (elderly, medical issues)
| State | Description | Entry Criteria | Exit Criteria | Dialer Behavior |
|---|---|---|---|---|
| NEW | Fresh assignment | Account assigned to agency | Identity verified OR traced | High priority (daily) |
| TRACE_PENDING | Contact details incorrect | Wrong number confirmed | New contact found | Medium priority (weekly) |
| IN_NEGOTIATION | Active discussion with debtor | Identity verified | PTP captured OR escalated | High priority (2x daily) |
| PTP_ACTIVE | Promise to Pay recorded | PTP form submitted | Payment received OR broken | Muted until PTP date - 2 days |
| PTP_BROKEN | Broken promise | Payment not received by date + grace | New PTP OR legal referral | Urgent priority (3x daily) |
| LEGAL_REFERRAL | Escalated to legal action | Manual escalation OR auto-escalate | Settlement OR write-off | Remove from dialer |
| SETTLED | Debt paid in full | Payment confirmed | - | Archive |
| WRITTEN_OFF | Debt written off | Manual write-off | - | Archive |
Automatic Transitions:
-- Auto-transition PTP_ACTIVE → PTP_BROKEN (scheduled workflow)
UPDATE debtor_master
SET current_state = 'PTP_BROKEN',
previous_state = 'PTP_ACTIVE',
state_entered_date = NOW()
WHERE current_state = 'PTP_ACTIVE'
AND client_id IN (
SELECT client_id FROM installment_ledger
WHERE ptp_status = 'ACTIVE'
AND ptp_date < DATE_SUB(NOW(), INTERVAL 2 DAY) -- Grace period
AND payment_received_date IS NULL
);
-- Auto-escalate to LEGAL_REFERRAL after 90 days in TRACE_PENDING
UPDATE debtor_master
SET current_state = 'LEGAL_REFERRAL',
previous_state = 'TRACE_PENDING',
state_entered_date = NOW()
WHERE current_state = 'TRACE_PENDING'
AND state_entered_date < DATE_SUB(NOW(), INTERVAL 90 DAY);
Manual Transitions (via Markdown triggers):
Markdown screen files use YAML frontmatter to define ADM integration:
---
id: SCREEN_DISCOVERY # Unique screen identifier
target_adm: "debtor_master" # Primary ADM table to update
required_state: ["NEW", "TRACE_PENDING"] # Only show for these states
permissions: ["agent", "supervisor"] # Role-based access
timeout_seconds: 300 # Screen idle timeout
audit_required: true # Record in feedback_repository
---
Access ADM data using double-brace syntax:
**Reference:** {{client.acc_no}} | **Current Status:** {{adm.state}}
**Total Owed:** R {{client.balance_total}}
**Agent:** {{user.name}} | **Agency:** {{user.agency_name}}
Available Contexts:
| Context | Source | Example Variables |
|---|---|---|
{{client.*}} |
debtor_master table | acc_no, name, id_number, balance_total |
{{adm.*}} |
Current ADM state | state, state_entered_date, previous_state |
{{user.*}} |
Logged-in agent | name, agency_name, group_name, permissions |
{{stats.*}} |
Aggregate metrics | count_new, val_ptp, active_agents |
{{date.*}} |
Current date/time | now, today, month, year |
{{form.*}} |
Form submission data | ptp_amount, notes, verified |
Define interactive forms inline:
### [Form: Data Enrichment]
> {type: checkbox, label: "Identity Confirmed", key: "verified", required: true}
> {type: text, label: "Alternative Phone", key: "alt_phone", validation: "phone"}
> {type: select, label: "Employment Status", options: ["Employed", "Unemployed", "Pensioner"], key: "emp_status"}
> {type: currency, label: "Installment Amount", key: "ptp_amount", min: 100}
> {type: date, label: "Payment Date", key: "ptp_date", min_date: "tomorrow"}
> {type: textarea, label: "Agent Notes", key: "notes", max_length: 500}
Supported Field Types:
| Type | Description | Attributes |
|---|---|---|
text |
Single-line text | validation (email, phone, id_number) |
textarea |
Multi-line text | max_length, rows |
checkbox |
Boolean flag | default (true/false) |
select |
Dropdown list | options (array), default |
radio |
Radio buttons | options (array) |
currency |
Money input | min, max, currency_symbol |
date |
Date picker | min_date, max_date |
number |
Numeric input | min, max, step |
Define state transitions triggered by buttons:
### [ADM Triggers]
* [Verified & Ready] -> ADM: `MOVE_TO_NEGOTIATION`
* [Wrong Number] -> ADM: `MOVE_TO_TRACE`
* [Refused to ID] -> ADM: `SET_FLAG_UNCOOPERATIVE`
* [Save PTP] -> ADM: `STATE_PTP_ACTIVE` -> Dialer: `MUTE_FOR_30_DAYS`
Trigger Format:
* [Button Label] -> ADM: `ACTION_NAME` [-> Dialer: `DIALER_ACTION`]
Standard ADM Actions:
| Action | Effect | SQL Operation |
|---|---|---|
MOVE_TO_NEGOTIATION |
State: NEW → IN_NEGOTIATION | UPDATE debtor_master SET current_state='IN_NEGOTIATION' |
MOVE_TO_TRACE |
State: → TRACE_PENDING | UPDATE debtor_master SET current_state='TRACE_PENDING' |
STATE_PTP_ACTIVE |
State: → PTP_ACTIVE | UPDATE + INSERT installment_ledger |
STATE_PTP_BROKEN |
State: → PTP_BROKEN | UPDATE debtor_master + installment_ledger.ptp_status='BROKEN' |
SET_FLAG_UNCOOPERATIVE |
Set flag, remain in state | UPDATE debtor_master SET flag_uncooperative=TRUE |
SET_FLAG_DISPUTE |
Set dispute flag | UPDATE debtor_master SET flag_dispute=TRUE |
ESCALATE_TO_LEGAL |
State: → LEGAL_REFERRAL | UPDATE debtor_master SET current_state='LEGAL_REFERRAL' |
Standard Dialer Actions:
| Action | Effect |
|---|---|
MUTE_FOR_30_DAYS |
Remove from dialer queue for 30 days |
SET_URGENT |
Increase priority to 3x daily |
SET_NORMAL |
Reset to standard priority |
REMOVE_FROM_QUEUE |
Permanently remove from dialer |
File: resource.screens/collections/discovery.md
---
id: SCREEN_DISCOVERY
target_adm: "debtor_master"
required_state: ["NEW", "TRACE_PENDING"]
permissions: ["agent"]
timeout_seconds: 300
audit_required: true
---
# Contact Discovery
**Reference:** {{client.acc_no}} | **Current Status:** {{adm.state}}
**Name on File:** {{client.name}} | **ID Number:** {{client.id_number}}
**Primary Phone:** {{client.phone_primary}}
---
## [Verification Script]
"Good day, I am calling from **{{user.agency_name}}**. I am looking for **{{client.name}}**."
**[Pause for response]**
"For security purposes, please confirm your ID number or Date of Birth."
**[Wait for verification]**
---
## [Form: Data Enrichment]
> {type: checkbox, label: "Identity Confirmed", key: "verified", required: true}
### Contact Update
> {type: text, label: "Alternative Phone", key: "alt_phone", validation: "phone"}
> {type: text, label: "Email Address", key: "email", validation: "email"}
### Additional Information
> {type: select, label: "Employment Status", options: ["Employed", "Unemployed", "Pensioner", "Self-Employed", "Student"], key: "emp_status"}
> {type: select, label: "Best Time to Contact", options: ["Morning (8-12)", "Afternoon (12-17)", "Evening (17-20)"], key: "best_time"}
### Agent Notes
> {type: textarea, label: "Additional Notes", key: "notes", max_length: 500, placeholder: "Document any relevant information about the contact..."}
---
## [ADM Triggers]
* [Verified & Ready] -> ADM: `MOVE_TO_NEGOTIATION`
* [Wrong Number] -> ADM: `MOVE_TO_TRACE`
* [Refused to Identify] -> ADM: `SET_FLAG_UNCOOPERATIVE`
* [Disputes Debt] -> ADM: `SET_FLAG_DISPUTE`
---
## [Compliance Notes]
⚠️ **Reminder:** You must verify the debtor's identity before discussing debt details (NCA Section 129).
📋 **Script Adherence:** This script is compliant with NCR regulations. Do not deviate.
File: resource.screens/collections/negotiation.md
---
id: SCREEN_PTP
target_adm: "installment_ledger"
required_state: ["IN_NEGOTIATION"]
permissions: ["agent"]
timeout_seconds: 600
audit_required: true
---
# Promise to Pay (PTP) Capture
**Client:** {{client.name}} | **Account:** {{client.acc_no}}
**Total Owed:** R {{client.balance_total}}
---
## [Negotiation Script]
"Thank you for confirming your identity. I see you have an outstanding balance of **R {{client.balance_total}}**."
**[Review balance breakdown]**
- Principal: R {{client.balance_principal}}
- Interest: R {{client.balance_interest}}
- Fees: R {{client.balance_fees}}
"We would like to work with you to resolve this. What payment can you commit to today?"
---
## [Form: Arrangement]
### Payment Details
> {type: currency, label: "Installment Amount", key: "ptp_amount", min: 100, max: "{{client.balance_total}}", required: true}
> {type: date, label: "Payment Date", key: "ptp_date", min_date: "tomorrow", max_date: "+30days", required: true}
> {type: select, label: "Payment Method", options: ["Debit Order", "EFT", "Direct Deposit", "Cash Deposit", "Other"], key: "ptp_method", required: true}
### Debtor Contact Preference
> {type: select, label: "Reminder Preference", options: ["SMS", "Email", "WhatsApp", "No Reminder"], key: "reminder_pref", default: "SMS"}
### Agent Assessment
> {type: number, label: "Propensity to Pay (1-10)", key: "propensity_score", min: 1, max: 10, default: 5}
> {type: textarea, label: "Agent Notes on Propensity", key: "notes", max_length: 500, placeholder: "Why did you assign this score? Any red flags or positive indicators?"}
---
## [PTP Confirmation Script]
"Thank you. I am recording a promise to pay **R {{form.ptp_amount}}** on **{{form.ptp_date}}** via **{{form.ptp_method}}**."
"You will receive a confirmation SMS shortly. Please keep to this commitment to avoid further escalation."
---
## [ADM Triggers]
* [Save PTP] -> ADM: `STATE_PTP_ACTIVE` -> Dialer: `MUTE_FOR_30_DAYS`
* [Debtor Requests More Time] -> ADM: `STAY_IN_NEGOTIATION` -> Dialer: `SET_CALLBACK_7_DAYS`
* [Debtor Refuses to Pay] -> ADM: `SET_FLAG_UNCOOPERATIVE` -> Dialer: `SET_URGENT`
* [Escalate to Legal] -> ADM: `ESCALATE_TO_LEGAL` -> Dialer: `REMOVE_FROM_QUEUE`
---
## [Compliance Notes]
✅ **PTP Recorded:** All PTPs are legally binding and will be enforced.
📞 **Follow-Up:** System will auto-schedule follow-up 2 days before PTP date.
⚖️ **Legal Threshold:** Accounts > 90 days overdue may be escalated if no PTP.
File: resource.screens/collections/broken_ptp.md
---
id: SCREEN_BROKEN_PTP
target_adm: "installment_ledger"
required_state: ["PTP_BROKEN"]
permissions: ["agent", "supervisor"]
timeout_seconds: 300
audit_required: true
---
# Broken Promise Follow-Up
**Client:** {{client.name}} | **Account:** {{client.acc_no}}
**Original PTP:** R {{ptp.amount}} due {{ptp.date}} ❌ **NOT RECEIVED**
---
## [Broken Promise Script]
"Good day {{client.name}}, I am calling from **{{user.agency_name}}** regarding your account **{{client.acc_no}}**."
"Our records show that a payment of **R {{ptp.amount}}** was due on **{{ptp.date}}**, but we have not received it."
**[Wait for response]**
"Can you explain what prevented you from making this payment?"
---
## [Form: Broken Promise Analysis]
### Reason for Non-Payment
> {type: select, label: "Reason Given", options: ["Financial Hardship", "Forgot Payment", "Banking Error", "Disputed Amount", "Refused to Discuss", "Other"], key: "broken_reason", required: true}
### New Arrangement
> {type: checkbox, label: "New PTP Offered", key: "new_ptp_offered"}
**[Conditional: If new_ptp_offered = true]**
> {type: currency, label: "New PTP Amount", key: "new_ptp_amount", min: 100}
> {type: date, label: "New PTP Date", key: "new_ptp_date", min_date: "tomorrow"}
### Agent Assessment
> {type: number, label: "Revised Propensity Score (1-10)", key: "propensity_score", min: 1, max: 10}
> {type: textarea, label: "Notes", key: "notes", max_length: 500}
---
## [ADM Triggers]
* [New PTP Accepted] -> ADM: `STATE_PTP_ACTIVE` -> Dialer: `MUTE_FOR_30_DAYS`
* [Escalate to Legal] -> ADM: `ESCALATE_TO_LEGAL` -> Dialer: `REMOVE_FROM_QUEUE`
* [Mark Uncooperative] -> ADM: `SET_FLAG_UNCOOPERATIVE` -> Dialer: `SET_URGENT`
* [Schedule Callback] -> ADM: `STAY_IN_NEGOTIATION` -> Dialer: `SET_CALLBACK_3_DAYS`
---
## [Compliance Notes]
⚠️ **Escalation Threshold:** 2 broken PTPs = automatic legal referral recommendation.
📝 **Documentation:** All broken promise reasons must be recorded for compliance.
File: resource.screens/supervisor/group_overview.md
---
id: SCREEN_SUPERVISOR_OVERVIEW
target_adm: "debtor_master"
required_state: ["*"] # All states
permissions: ["supervisor", "manager"]
refresh_seconds: 30
audit_required: false
---
# Supervisor State Overview
**Group:** {{user.group_name}} | **Active Agents:** {{stats.active_agents}}
**Last Refreshed:** {{date.now}}
---
## [Data Mart Summary]
| ADM State | Volume | Value (ZAR) | Avg Age (Days) |
| :--- | :---: | ---: | :---: |
| **New Assignments** | {{adm.count_new}} | R {{adm.val_new}} | {{adm.avg_age_new}} |
| **In Trace** | {{adm.count_trace}} | R {{adm.val_trace}} | {{adm.avg_age_trace}} |
| **Active Negotiation** | {{adm.count_negotiation}} | R {{adm.val_negotiation}} | {{adm.avg_age_negotiation}} |
| **Active PTPs** | {{adm.count_ptp}} | R {{adm.val_ptp}} | {{adm.avg_age_ptp}} |
| **Broken Promises** | {{adm.count_bpp}} | R {{adm.val_bpp}} | {{adm.avg_age_bpp}} |
| **Legal Referral** | {{adm.count_legal}} | R {{adm.val_legal}} | {{adm.avg_age_legal}} |
**Total Portfolio:** {{adm.count_total}} accounts | R {{adm.val_total}}
---
## [Performance Metrics]
### Today's Activity
- **PTPs Captured:** {{stats.ptps_today}} (R {{stats.ptps_value_today}})
- **Payments Received:** {{stats.payments_today}} (R {{stats.payments_value_today}})
- **Contacts Made:** {{stats.contacts_today}}
- **Right Party Contact Rate:** {{stats.rpc_rate_today}}%
### This Month
- **Total Recovery:** R {{stats.recovery_month}}
- **PTP Conversion Rate:** {{stats.ptp_conversion_month}}%
- **Kept PTP Rate:** {{stats.kept_ptp_month}}%
- **Broken PTP Rate:** {{stats.broken_ptp_month}}%
---
## [System Alerts]
> **⚠️ Alert:** {{adm.stuck_records}} records have been in 'IN_NEGOTIATION' for > 48 hours without activity.
>
> **📊 Trend:** Broken PTP rate increased {{stats.bpp_trend}}% compared to last week.
>
> **⏰ Reminder:** {{adm.ptp_due_tomorrow}} PTPs are due tomorrow. Ensure follow-up calls scheduled.
---
## [Agent Leaderboard - This Week]
| Agent | PTPs | Value | Kept Rate | RPC % |
| :--- | :---: | ---: | :---: | :---: |
| {{agent1.name}} | {{agent1.ptps}} | R {{agent1.value}} | {{agent1.kept_rate}}% | {{agent1.rpc}}% |
| {{agent2.name}} | {{agent2.ptps}} | R {{agent2.value}} | {{agent2.kept_rate}}% | {{agent2.rpc}}% |
| {{agent3.name}} | {{agent3.ptps}} | R {{agent3.value}} | {{agent3.kept_rate}}% | {{agent3.rpc}}% |
---
## [Quick Actions]
* [Export Current Portfolio] -> Report: `PORTFOLIO_SUMMARY_CSV`
* [View Stuck Records] -> Filter: `STATE=IN_NEGOTIATION AND age>48h`
* [Refresh Dashboard] -> Reload: `NOW`
File: resource.screens/supervisor/edc_dashboard.md
---
id: SCREEN_EDC_DASHBOARD
target_adm: "debtor_master"
required_state: ["*"]
permissions: ["manager", "client"]
refresh_seconds: 60
audit_required: false
---
# EDC Performance Dashboard
**Reporting Period:** {{date.month}} ({{date.year}})
**Client:** {{client.organization_name}}
---
## [EDC Comparative Analysis]
| EDC Agency | Accounts | Portfolio Value | Recovery Rate | PTP Conversion | Kept PTP % | RPC % |
| :--- | :---: | ---: | :---: | :---: | :---: | :---: |
| **Agency Alpha** | {{edc1.count}} | R {{edc1.portfolio}} | {{edc1.recovery}}% | {{edc1.ptp_conv}}% | {{edc1.kept_ptp}}% | {{edc1.rpc}}% |
| **Agency Beta** | {{edc2.count}} | R {{edc2.portfolio}} | {{edc2.recovery}}% | {{edc2.ptp_conv}}% | {{edc2.kept_ptp}}% | {{edc2.rpc}}% |
| **Agency Gamma** | {{edc3.count}} | R {{edc3.portfolio}} | {{edc3.recovery}}% | {{edc3.ptp_conv}}% | {{edc3.kept_ptp}}% | {{edc3.rpc}}% |
**Industry Benchmark:**
- Recovery Rate: 12-15%
- PTP Conversion: 25-35%
- Kept PTP: 60-70%
- RPC: 50-65%
---
## [Live Performance Feed]
> **🔵 Agency Alpha** just moved Client `{{last_activity.client}}` to `PTP_ACTIVE` (R {{last_activity.amount}})
>
> **🟢 Agency Gamma** reported 50 `TRACE_REQUIRED` updates in the last hour
>
> **🟡 Agency Beta** captured 15 PTPs totaling R {{beta.ptps_hour}} in the past hour
---
## [State Distribution by EDC]
### Agency Alpha
- NEW: {{edc1.state_new}} | NEGOTIATION: {{edc1.state_neg}} | PTP_ACTIVE: {{edc1.state_ptp}} | BROKEN: {{edc1.state_bpp}}
### Agency Beta
- NEW: {{edc2.state_new}} | NEGOTIATION: {{edc2.state_neg}} | PTP_ACTIVE: {{edc2.state_ptp}} | BROKEN: {{edc2.state_bpp}}
### Agency Gamma
- NEW: {{edc3.state_new}} | NEGOTIATION: {{edc3.state_neg}} | PTP_ACTIVE: {{edc3.state_ptp}} | BROKEN: {{edc3.state_bpp}}
---
## [Compliance Scores]
| EDC Agency | Script Adherence | Data Quality | Response Time | Overall Score |
| :--- | :---: | :---: | :---: | :---: |
| **Agency Alpha** | {{edc1.script}}% | {{edc1.data_quality}}% | {{edc1.response_time}}h | {{edc1.compliance_score}}/100 |
| **Agency Beta** | {{edc2.script}}% | {{edc2.data_quality}}% | {{edc2.response_time}}h | {{edc2.compliance_score}}/100 |
| **Agency Gamma** | {{edc3.script}}% | {{edc3.data_quality}}% | {{edc3.response_time}}h | {{edc3.compliance_score}}/100 |
---
## [Export Options]
* [Export Monthly Report] -> Report: `EDC_MONTHLY_COMPARISON_PDF`
* [Export Raw Data] -> Report: `EDC_TRANSACTIONS_CSV`
* [Schedule Weekly Email] -> Workflow: `SCHEDULE_EDC_REPORT_EMAIL`
File: resource.screens/reports/feedback_report.md
Trigger: Generated automatically after each ADM state transition
---
id: REPORT_EDC_FEEDBACK
target_adm: "feedback_repository"
permissions: ["system"]
auto_generate: true
output_format: "PDF"
---
# Collection Feedback Report
**Report Generated:** {{date.now}}
**Client ID:** {{client.id}} | **Account:** {{client.acc_no}}
**EDC Agency:** {{user.agency_name}} | **Agent:** {{user.name}}
---
## Client Information
| Field | Value |
| :--- | :--- |
| **Name** | {{client.name}} |
| **ID Number** | {{client.id_number}} |
| **Phone** | {{client.phone_primary}} |
| **Balance** | R {{client.balance_total}} |
---
## Interaction History
**Interaction Time:** {{interaction.date}}
**Screen Used:** {{interaction.screen_id}}
**Disposition:** {{interaction.disposition}}
### State Transition
- **Previous State:** {{adm.old_state}}
- **New State:** {{adm.new_state}}
- **Transition Trigger:** {{transition.trigger_source}}
---
## Captured Content
### Agent Notes
{{form.notes}}
### Form Data
{{form.data_json}}
---
## Sentral Data Mart Audit
### Changes Made
- Identity Verified: {{form.verified}}
- Employment Status: {{form.emp_status}}
- Alternative Phone: {{form.alt_phone}}
### PTP Details (if applicable)
- **Amount:** R {{form.ptp_amount}}
- **Date:** {{form.ptp_date}}
- **Method:** {{form.ptp_method}}
- **Propensity Score:** {{form.propensity_score}}/10
### Next System Action
- **Scheduled Action:** {{adm.next_action}}
- **Execution Date:** {{adm.next_action_date}}
- **Dialer Status:** {{dialer.status}}
---
## Compliance Verification
✅ **Script Followed:** {{compliance.script_followed}}
✅ **Identity Verified:** {{compliance.id_verified}}
✅ **NCA Section 129 Compliant:** {{compliance.nca_compliant}}
✅ **Audit Trail Complete:** {{compliance.audit_complete}}
---
## Signatures
**Agent:** {{user.name}} ({{user.id}})
**Supervisor Reviewed:** {{supervisor.name}} ({{supervisor.date}})
---
*This report is auto-generated by the Sentral Data Mart and is admissible for compliance and legal purposes.*
Workflow Node: ADM_STATE_TRANSITION
Type: SERVICE
Service: ObjServiceADMTransition
File: factory.service/package.collections/ObjServiceADMTransition.py
"""
ObjServiceADMTransition - Sentral Data Mart State Machine Orchestrator
Handles state transitions triggered by Markdown screens.
"""
DO_DEBUG: bool = False
import os
import sys
import json
from datetime import datetime, timedelta
base_path = os.getcwd()
paths = ["", "/factory.core", "/factory.service"]
for relative_path in paths:
sys.path.append(base_path + relative_path)
import ObjData
class ObjServiceADMTransition(ObjData.ObjData):
"""Execute ADM state transitions with validation and audit."""
def __init__(self, db=0):
super().__init__(db)
self._isa = "ObjServiceADMTransition"
self._version = "1.0.0"
self._updatedate = "2026-02-09"
def process(self, context: dict) -> dict:
"""
Execute state transition.
Context must contain:
- client_id: Client identifier
- action: ADM action name (e.g., 'MOVE_TO_NEGOTIATION')
- agent_id: Agent performing action
- agency_name: Agency name
- form_data: Captured form data (optional)
- dialer_action: Dialer action (optional)
"""
client_id = context.get("client_id")
action = context.get("action")
agent_id = context.get("agent_id")
agency_name = context.get("agency_name")
form_data = context.get("form_data", {})
dialer_action = context.get("dialer_action")
# Get current state
current_state = self._get_current_state(client_id)
# Determine new state based on action
new_state = self._resolve_action_to_state(action, current_state, form_data)
# Validate transition
if not self._validate_transition(current_state, new_state):
context["_service_status"] = "error"
context["_service_message"] = f"Invalid transition: {current_state} → {new_state}"
return context
# Execute transition
try:
# Update debtor_master
self._update_debtor_state(client_id, current_state, new_state, form_data)
# Record state transition
self._record_transition(client_id, current_state, new_state, action, agent_id, agency_name)
# Handle special actions (PTPs, flags, etc.)
self._execute_special_actions(action, client_id, form_data)
# Execute dialer action if specified
if dialer_action:
self._execute_dialer_action(client_id, dialer_action, form_data)
# Record feedback
self._record_feedback(client_id, context)
context["_service_status"] = "success"
context["_service_message"] = f"State transition: {current_state} → {new_state}"
context["adm_state"] = new_state
except Exception as e:
self.debug(f"Error in state transition: {e}")
context["_service_status"] = "error"
context["_service_message"] = str(e)
return context
def _get_current_state(self, client_id: str) -> str:
"""Get current ADM state for client."""
sql = f"""
SELECT current_state
FROM debtor_master
WHERE client_id = '{self.escape_sql(client_id)}'
"""
result = self.sql_get_value(sql)
return result if result else "UNKNOWN"
def _resolve_action_to_state(self, action: str, current_state: str, form_data: dict) -> str:
"""Map action to target state."""
action_map = {
"MOVE_TO_NEGOTIATION": "IN_NEGOTIATION",
"MOVE_TO_TRACE": "TRACE_PENDING",
"STATE_PTP_ACTIVE": "PTP_ACTIVE",
"STATE_PTP_BROKEN": "PTP_BROKEN",
"ESCALATE_TO_LEGAL": "LEGAL_REFERRAL",
"STAY_IN_NEGOTIATION": current_state, # No state change
"SET_FLAG_UNCOOPERATIVE": current_state, # Flag only
"SET_FLAG_DISPUTE": current_state,
}
return action_map.get(action, current_state)
def _validate_transition(self, from_state: str, to_state: str) -> bool:
"""Validate state transition is allowed."""
valid_transitions = {
"NEW": ["IN_NEGOTIATION", "TRACE_PENDING"],
"TRACE_PENDING": ["NEW", "IN_NEGOTIATION", "LEGAL_REFERRAL"],
"IN_NEGOTIATION": ["PTP_ACTIVE", "LEGAL_REFERRAL", "IN_NEGOTIATION"],
"PTP_ACTIVE": ["PTP_BROKEN", "SETTLED", "PTP_ACTIVE"],
"PTP_BROKEN": ["PTP_ACTIVE", "LEGAL_REFERRAL", "IN_NEGOTIATION"],
"LEGAL_REFERRAL": ["SETTLED", "WRITTEN_OFF"],
"SETTLED": [],
"WRITTEN_OFF": [],
}
# Same state is always valid (for flag-only actions)
if from_state == to_state:
return True
allowed = valid_transitions.get(from_state, [])
return to_state in allowed
def _update_debtor_state(self, client_id: str, old_state: str, new_state: str, form_data: dict):
"""Update debtor_master with new state and form data."""
# Build dynamic SET clause based on form_data
set_clauses = [
f"current_state = '{self.escape_sql(new_state)}'",
f"previous_state = '{self.escape_sql(old_state)}'",
"state_entered_date = NOW()",
"updated_at = NOW()"
]
# Update fields from form_data
if form_data.get("verified"):
set_clauses.append("identity_verified = TRUE")
if form_data.get("alt_phone"):
set_clauses.append(f"phone_alternative = '{self.escape_sql(form_data['alt_phone'])}'")
if form_data.get("email"):
set_clauses.append(f"email = '{self.escape_sql(form_data['email'])}'")
if form_data.get("emp_status"):
set_clauses.append(f"employment_status = '{self.escape_sql(form_data['emp_status'])}'")
sql = f"""
UPDATE debtor_master
SET {', '.join(set_clauses)}
WHERE client_id = '{self.escape_sql(client_id)}'
"""
self.sql_execute(sql)
def _record_transition(self, client_id: str, from_state: str, to_state: str,
trigger: str, agent_id: str, agency_name: str):
"""Record state transition in audit table."""
sql = f"""
INSERT INTO state_transitions
(client_id, from_state, to_state, trigger_type, trigger_source,
agent_id, agency_name, transition_date)
VALUES (
'{self.escape_sql(client_id)}',
'{self.escape_sql(from_state)}',
'{self.escape_sql(to_state)}',
'MANUAL',
'{self.escape_sql(trigger)}',
'{self.escape_sql(agent_id)}',
'{self.escape_sql(agency_name)}',
NOW()
)
"""
self.sql_execute(sql)
def _execute_special_actions(self, action: str, client_id: str, form_data: dict):
"""Execute action-specific operations."""
if action == "STATE_PTP_ACTIVE":
# Insert PTP record
sql = f"""
INSERT INTO installment_ledger
(client_id, ptp_amount, ptp_date, ptp_method, ptp_created_by,
notes, propensity_score, ptp_status)
VALUES (
'{self.escape_sql(client_id)}',
{form_data.get('ptp_amount', 0)},
'{form_data.get('ptp_date')}',
'{self.escape_sql(form_data.get('ptp_method', 'Unknown'))}',
'{self.escape_sql(form_data.get('agent_id', ''))}',
'{self.escape_sql(form_data.get('notes', ''))}',
{form_data.get('propensity_score', 5)},
'ACTIVE'
)
"""
self.sql_execute(sql)
elif action == "SET_FLAG_UNCOOPERATIVE":
sql = f"""
UPDATE debtor_master
SET flag_uncooperative = TRUE
WHERE client_id = '{self.escape_sql(client_id)}'
"""
self.sql_execute(sql)
elif action == "SET_FLAG_DISPUTE":
sql = f"""
UPDATE debtor_master
SET flag_dispute = TRUE
WHERE client_id = '{self.escape_sql(client_id)}'
"""
self.sql_execute(sql)
elif action == "STATE_PTP_BROKEN":
# Update installment ledger
sql = f"""
UPDATE installment_ledger
SET ptp_status = 'BROKEN',
broken_date = NOW(),
broken_reason = '{self.escape_sql(form_data.get('broken_reason', 'Not specified'))}'
WHERE client_id = '{self.escape_sql(client_id)}'
AND ptp_status = 'ACTIVE'
ORDER BY ptp_created_date DESC
LIMIT 1
"""
self.sql_execute(sql)
def _execute_dialer_action(self, client_id: str, dialer_action: str, form_data: dict):
"""Execute dialer queue action."""
if dialer_action == "MUTE_FOR_30_DAYS":
# Update collections_queue to mute
mute_until = (datetime.now() + timedelta(days=30)).strftime("%Y-%m-%d")
sql = f"""
UPDATE collections_queue
SET status = 'muted',
mute_until = '{mute_until}',
last_action = 'PTP_ACTIVE'
WHERE client_id = '{self.escape_sql(client_id)}'
"""
self.sql_execute(sql)
elif dialer_action == "SET_URGENT":
sql = f"""
UPDATE collections_queue
SET priority = 'URGENT',
daily_attempts = 3
WHERE client_id = '{self.escape_sql(client_id)}'
"""
self.sql_execute(sql)
elif dialer_action == "REMOVE_FROM_QUEUE":
sql = f"""
UPDATE collections_queue
SET status = 'removed',
removed_date = NOW(),
removed_reason = 'Legal escalation'
WHERE client_id = '{self.escape_sql(client_id)}'
"""
self.sql_execute(sql)
elif dialer_action.startswith("SET_CALLBACK_"):
# Extract days from action (e.g., "SET_CALLBACK_7_DAYS" → 7)
days = int(dialer_action.split("_")[-2])
callback_date = (datetime.now() + timedelta(days=days)).strftime("%Y-%m-%d")
sql = f"""
UPDATE collections_queue
SET status = 'callback',
callback_date = '{callback_date}'
WHERE client_id = '{self.escape_sql(client_id)}'
"""
self.sql_execute(sql)
def _record_feedback(self, client_id: str, context: dict):
"""Record interaction in feedback repository."""
form_data = context.get("form_data", {})
form_data_json = json.dumps(form_data)
sql = f"""
INSERT INTO feedback_repository
(client_id, screen_id, disposition, agent_notes,
adm_state_before, adm_state_after, agent_id, agency_name, form_data)
VALUES (
'{self.escape_sql(client_id)}',
'{self.escape_sql(context.get('screen_id', ''))}',
'{self.escape_sql(context.get('disposition', 'CONTACTED'))}',
'{self.escape_sql(form_data.get('notes', ''))}',
'{self.escape_sql(context.get('adm_state_before', ''))}',
'{self.escape_sql(context.get('adm_state', ''))}',
'{self.escape_sql(context.get('agent_id', ''))}',
'{self.escape_sql(context.get('agency_name', ''))}',
'{self.escape_sql(form_data_json)}'
)
"""
self.sql_execute(sql)
def main():
"""CLI for testing state transitions."""
import typer
app = typer.Typer()
@app.command()
def transition(
client_id: str,
action: str,
agent_id: str = "TEST_AGENT",
agency_name: str = "TEST_AGENCY"
):
"""Execute a state transition."""
service = ObjServiceADMTransition()
context = {
"client_id": client_id,
"action": action,
"agent_id": agent_id,
"agency_name": agency_name,
"form_data": {}
}
result = service.process(context)
print(f"Status: {result.get('_service_status')}")
print(f"Message: {result.get('_service_message')}")
print(f"New State: {result.get('adm_state')}")
app()
if __name__ == "__main__":
main()
Update queue based on ADM state:
-- Collections queue table
CREATE TABLE collections_queue (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
client_id VARCHAR(50) NOT NULL,
-- Queue Status
status ENUM('active', 'muted', 'callback', 'removed') DEFAULT 'active',
priority ENUM('NORMAL', 'HIGH', 'URGENT') DEFAULT 'NORMAL',
-- Scheduling
callback_date DATE,
mute_until DATE,
daily_attempts INT DEFAULT 2,
-- Metadata
last_contact DATETIME,
last_action VARCHAR(100),
removed_date DATETIME,
removed_reason TEXT,
FOREIGN KEY (client_id) REFERENCES debtor_master(client_id),
INDEX idx_status (status),
INDEX idx_priority (priority),
INDEX idx_callback (callback_date)
);
-- Sync queue with ADM states
CREATE EVENT sync_queue_with_adm
ON SCHEDULE EVERY 5 MINUTE
DO
BEGIN
-- Remove settled accounts
UPDATE collections_queue cq
JOIN debtor_master dm ON cq.client_id = dm.client_id
SET cq.status = 'removed',
cq.removed_reason = 'Account settled'
WHERE dm.current_state = 'SETTLED'
AND cq.status != 'removed';
-- Set urgent for broken PTPs
UPDATE collections_queue cq
JOIN debtor_master dm ON cq.client_id = dm.client_id
SET cq.priority = 'URGENT',
cq.daily_attempts = 3
WHERE dm.current_state = 'PTP_BROKEN'
AND cq.status = 'active';
-- Unmute PTPs 2 days before due date
UPDATE collections_queue cq
JOIN debtor_master dm ON cq.client_id = dm.client_id
JOIN installment_ledger il ON dm.client_id = il.client_id
SET cq.status = 'active',
cq.mute_until = NULL
WHERE dm.current_state = 'PTP_ACTIVE'
AND il.ptp_status = 'ACTIVE'
AND il.ptp_date <= DATE_ADD(CURDATE(), INTERVAL 2 DAY)
AND cq.status = 'muted';
END;
-- Dialer pop query (prioritized by ADM state and age)
SELECT
dm.client_id,
dm.acc_no,
dm.name,
dm.phone_primary,
dm.current_state,
dm.balance_total,
cq.priority,
DATEDIFF(NOW(), dm.state_entered_date) as state_age_days
FROM collections_queue cq
JOIN debtor_master dm ON cq.client_id = dm.client_id
WHERE cq.status = 'active'
AND (cq.callback_date IS NULL OR cq.callback_date <= CURDATE())
AND dm.current_state NOT IN ('SETTLED', 'WRITTEN_OFF', 'LEGAL_REFERRAL')
ORDER BY
-- Priority: Urgent first
CASE cq.priority
WHEN 'URGENT' THEN 1
WHEN 'HIGH' THEN 2
WHEN 'NORMAL' THEN 3
END,
-- Then by state priority
CASE dm.current_state
WHEN 'PTP_BROKEN' THEN 1
WHEN 'IN_NEGOTIATION' THEN 2
WHEN 'NEW' THEN 3
WHEN 'TRACE_PENDING' THEN 4
ELSE 5
END,
-- Then by age (older first)
state_age_days DESC
LIMIT 100;
# Create ADM tables
mysql -u axion -p axion < local.processing/schema/package.collections/adm_tables.sql
# Verify tables
mysql -u axion -p axion -e "SHOW TABLES LIKE '%debtor%'"
mysql -u axion -p axion -e "SHOW TABLES LIKE '%installment%'"
mkdir -p resource.screens/collections
mkdir -p resource.screens/supervisor
mkdir -p resource.screens/reports
Copy the markdown files from the examples above into the appropriate directories.
File: factory.text/ObjMarkdownScreen.py
"""
ObjMarkdownScreen - Render Markdown screens with ADM integration
"""
import os
import sys
import yaml
import re
from pathlib import Path
base_path = os.getcwd()
paths = ["", "/factory.core"]
for path in paths:
sys.path.append(base_path + path)
import ObjData
class ObjMarkdownScreen(ObjData.ObjData):
"""Render Markdown screens with template variable injection."""
def __init__(self, db=0):
super().__init__(db)
self._isa = "ObjMarkdownScreen"
self._version = "1.0.0"
def render_screen(self, screen_path: str, context: dict) -> dict:
"""
Load and render a markdown screen.
Args:
screen_path: Path to .md file
context: Data context for template variables
Returns:
{
'frontmatter': {...}, # Parsed YAML frontmatter
'html': '...', # Rendered HTML
'forms': [{...}], # Extracted form definitions
'triggers': [{...}] # ADM trigger buttons
}
"""
# Read markdown file
with open(screen_path, 'r') as f:
content = f.read()
# Parse frontmatter
frontmatter, body = self._parse_frontmatter(content)
# Inject template variables
body = self._inject_variables(body, context)
# Extract forms
forms = self._extract_forms(body)
# Extract triggers
triggers = self._extract_triggers(body)
# Convert to HTML (simplified - use markdown library in production)
html = self._markdown_to_html(body)
return {
'frontmatter': frontmatter,
'html': html,
'forms': forms,
'triggers': triggers
}
def _parse_frontmatter(self, content: str) -> tuple:
"""Extract YAML frontmatter from markdown."""
match = re.match(r'^---\n(.*?)\n---\n(.*)', content, re.DOTALL)
if match:
frontmatter = yaml.safe_load(match.group(1))
body = match.group(2)
return frontmatter, body
return {}, content
def _inject_variables(self, body: str, context: dict) -> str:
"""Replace {{var}} with context values."""
def replace(match):
var_path = match.group(1)
parts = var_path.split('.')
value = context
for part in parts:
value = value.get(part, '')
if not isinstance(value, dict):
break
return str(value)
return re.sub(r'\{\{([^}]+)\}\}', replace, body)
def _extract_forms(self, body: str) -> list:
"""Extract form definitions from markdown."""
forms = []
# Extract lines starting with "> {type:"
form_pattern = r'>\s*\{([^}]+)\}'
for match in re.finditer(form_pattern, body):
field_def = match.group(1)
# Parse field definition
field = {}
for part in field_def.split(','):
if ':' in part:
key, value = part.split(':', 1)
field[key.strip()] = value.strip().strip('"\'')
forms.append(field)
return forms
def _extract_triggers(self, body: str) -> list:
"""Extract ADM triggers from markdown."""
triggers = []
# Pattern: * [Button Label] -> ADM: `ACTION` [-> Dialer: `DIALER_ACTION`]
pattern = r'\*\s*\[([^\]]+)\]\s*->\s*ADM:\s*`([^`]+)`(?:\s*->\s*Dialer:\s*`([^`]+)`)?'
for match in re.finditer(pattern, body):
triggers.append({
'label': match.group(1),
'adm_action': match.group(2),
'dialer_action': match.group(3) if match.group(3) else None
})
return triggers
def _markdown_to_html(self, body: str) -> str:
"""Convert markdown to HTML (simplified)."""
# In production, use a library like markdown2 or mistune
# This is a placeholder
return f"<div class='markdown-content'>{body}</div>"
File: factory.pages/WebPageCollection.py
"""
WebPageCollection - Serve markdown collection screens
"""
from fastapi import Request
from fastapi.responses import HTMLResponse
import os
import sys
Path = os.getcwd() + "/factory.core/"
sys.path.append(Path)
Path = os.getcwd() + "/factory.text/"
sys.path.append(Path)
import WebServer
import ObjMarkdownScreen
@WebServer.app.get("/collection/screen/{screen_id}")
def render_collection_screen(screen_id: str, request: Request):
"""Render a markdown collection screen."""
# Build context from database
context = _build_screen_context(screen_id, request)
# Load markdown screen
screen_path = f"resource.screens/collections/{screen_id}.md"
renderer = ObjMarkdownScreen.ObjMarkdownScreen()
rendered = renderer.render_screen(screen_path, context)
# Return HTML response
return HTMLResponse(content=rendered['html'])
@WebServer.app.post("/collection/screen/{screen_id}/submit")
def submit_collection_screen(screen_id: str, request: Request):
"""Handle form submission and ADM trigger."""
# Parse form data
form_data = await request.json()
# Get ADM action from form
adm_action = form_data.get('_adm_action')
dialer_action = form_data.get('_dialer_action')
# Execute state transition workflow
from factory.core.ObjWorkflow import ObjWorkflow
wf = ObjWorkflow()
result = wf.execute(
code='ADM_STATE_TRANSITION',
context={
'client_id': form_data.get('client_id'),
'action': adm_action,
'dialer_action': dialer_action,
'agent_id': request.user.id,
'agency_name': request.user.agency,
'form_data': form_data,
'screen_id': screen_id
}
)
return {"status": "success", "new_state": result.get('adm_state')}
def _build_screen_context(screen_id: str, request: Request) -> dict:
"""Build template variable context for screen."""
# This would query the database to build the context
# Placeholder implementation
return {
'client': {...},
'adm': {...},
'user': {...},
'stats': {...}
}
# Start web server
python ServeWebsite.py
# Access screen
curl http://localhost:9000/collection/screen/discovery
# Submit form
curl -X POST http://localhost:9000/collection/screen/discovery/submit \
-H "Content-Type: application/json" \
-d '{"client_id": "CLI001", "_adm_action": "MOVE_TO_NEGOTIATION", "verified": true}'
# Role-based access control
SCREEN_PERMISSIONS = {
'SCREEN_DISCOVERY': ['agent', 'supervisor'],
'SCREEN_PTP': ['agent', 'supervisor'],
'SCREEN_SUPERVISOR_OVERVIEW': ['supervisor', 'manager'],
'SCREEN_EDC_DASHBOARD': ['manager', 'client']
}
def check_screen_access(screen_id: str, user_role: str) -> bool:
"""Verify user has permission to access screen."""
allowed_roles = SCREEN_PERMISSIONS.get(screen_id, [])
return user_role in allowed_roles
All actions are logged in multiple tables:
state_transitions - ADM state changesfeedback_repository - Agent interactionsworkflow_queue_context - Workflow executionsScript Compliance:
feedback_repository.compliance_script_followedData Protection:
debtor_master.consent_date-- Archive old records (scheduled event)
CREATE EVENT archive_settled_accounts
ON SCHEDULE EVERY 1 MONTH
DO
BEGIN
-- Move settled accounts to archive
INSERT INTO debtor_master_archive
SELECT * FROM debtor_master
WHERE current_state = 'SETTLED'
AND state_entered_date < DATE_SUB(NOW(), INTERVAL 12 MONTH);
DELETE FROM debtor_master
WHERE current_state = 'SETTLED'
AND state_entered_date < DATE_SUB(NOW(), INTERVAL 12 MONTH);
END;
| Version | Date | Author | Changes |
|---|---|---|---|
| 1.0.0 | 2026-02-09 | Axion Platform | Initial documentation |
Questions or Implementation Support?
Contact the Axion development team for assistance with deploying the Markdown-Driven Collection Ecosystem.