Spaces:
Running
Fix admin panel auth, form title, commission data, and validation
Browse files- CRITICAL: Add auth token to ALL admin template fetch() calls (agreements,
agreement_edit, constants, bell_ringers, dropdowns, email_rules, users).
Previously only producer_emails had it, causing all admin saves to fail
with 401 Unauthorized.
- Add ?token= parameter to all admin navigation links so auth persists
when navigating between admin pages.
- Rename "Producer Intake Form" to "Commission Agreement Intake Form"
throughout (base.html, form.html, README.md).
- Move Department Code and Market Segment before Bell Ringer badge so
bell ringer auto-populates correctly.
- Add tooltip to Bell Ringer badge explaining it auto-populates.
- Fix Referral/Origination Fee tier label: "Year 1-2" -> "Year 1 & Year 2".
- Fix Life Origination Fee: "All Years" at 20% -> "Year 1" at 20%, "Year 2+" Ended.
- Add CCP + Bond Agreement incompatibility validation (client + server side).
- Add data migration in start.py to fix existing DB records on redeploy.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- README.md +2 -2
- app/rules_engine.py +9 -0
- app/seed_db.py +5 -3
- app/templates/admin/agreement_edit.html +5 -2
- app/templates/admin/agreements.html +7 -4
- app/templates/admin/bell_ringers.html +7 -4
- app/templates/admin/constants.html +5 -2
- app/templates/admin/dashboard.html +6 -6
- app/templates/admin/dropdowns.html +7 -4
- app/templates/admin/email_rules.html +6 -3
- app/templates/admin/users.html +6 -3
- app/templates/base.html +2 -2
- app/templates/form.html +39 -24
- start.py +45 -0
|
@@ -1,5 +1,5 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
emoji: π
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: gray
|
|
@@ -7,5 +7,5 @@ sdk: docker
|
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
-
#
|
| 11 |
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Commission Agreement Intake Form
|
| 3 |
emoji: π
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: gray
|
|
|
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
+
# Commission Agreement Intake Form
|
| 11 |
|
|
@@ -245,6 +245,15 @@ def compute_summary(db: Session, form_data: dict) -> dict:
|
|
| 245 |
warnings.append("An Originating Producer/Employee is selected β "
|
| 246 |
"the agreement must be Referral / Origination Fee Agreement.")
|
| 247 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 248 |
# ββ Build rows ββββββββββββββββββββββββββββββββββββββββββββ
|
| 249 |
|
| 250 |
# ROW: Producer 1
|
|
|
|
| 245 |
warnings.append("An Originating Producer/Employee is selected β "
|
| 246 |
"the agreement must be Referral / Origination Fee Agreement.")
|
| 247 |
|
| 248 |
+
# CCP is not eligible for Bond Agreement
|
| 249 |
+
is_bond = "bond" in R["name"].lower()
|
| 250 |
+
if has_cc and is_bond:
|
| 251 |
+
warnings.append("Complex Claims Practice (CCP) is not eligible for Bond Agreement.")
|
| 252 |
+
|
| 253 |
+
# Validate CE is selected if checkbox was intended
|
| 254 |
+
if cepfx and not is_ce and not cename:
|
| 255 |
+
warnings.append("Client Executive is selected but no name found β verify selection.")
|
| 256 |
+
|
| 257 |
# ββ Build rows ββββββββββββββββββββββββββββββββββββββββββββ
|
| 258 |
|
| 259 |
# ROW: Producer 1
|
|
@@ -73,10 +73,10 @@ def seed(session, reset=False):
|
|
| 73 |
{
|
| 74 |
"name": "Referral / Origination Fee Agreement", "p1_suffix": 1, "p2_suffix": 2,
|
| 75 |
"tiers": [
|
| 76 |
-
{"role": "P1", "years": "Year 1
|
| 77 |
{"role": "P1", "years": "Year 3+", "comm": 30, "credit": 100, "agr": "Standard Agreement",
|
| 78 |
"note": "Transitions to Standard Agreement"},
|
| 79 |
-
{"role": "P2", "years": "Year 1
|
| 80 |
{"role": "P2", "years": "Year 3+", "comm": None, "credit": None, "agr": "Ended"},
|
| 81 |
]
|
| 82 |
},
|
|
@@ -119,7 +119,9 @@ def seed(session, reset=False):
|
|
| 119 |
{
|
| 120 |
"name": "Life Origination Fee Agreement", "p1_suffix": 2, "p2_suffix": None,
|
| 121 |
"tiers": [
|
| 122 |
-
{"role": "P1", "years": "
|
|
|
|
|
|
|
| 123 |
]
|
| 124 |
},
|
| 125 |
{
|
|
|
|
| 73 |
{
|
| 74 |
"name": "Referral / Origination Fee Agreement", "p1_suffix": 1, "p2_suffix": 2,
|
| 75 |
"tiers": [
|
| 76 |
+
{"role": "P1", "years": "Year 1 & Year 2", "comm": 20, "credit": 100, "agr": "Referral Agreement"},
|
| 77 |
{"role": "P1", "years": "Year 3+", "comm": 30, "credit": 100, "agr": "Standard Agreement",
|
| 78 |
"note": "Transitions to Standard Agreement"},
|
| 79 |
+
{"role": "P2", "years": "Year 1 & Year 2", "comm": 10, "credit": 0, "agr": "Origination Fee Agreement"},
|
| 80 |
{"role": "P2", "years": "Year 3+", "comm": None, "credit": None, "agr": "Ended"},
|
| 81 |
]
|
| 82 |
},
|
|
|
|
| 119 |
{
|
| 120 |
"name": "Life Origination Fee Agreement", "p1_suffix": 2, "p2_suffix": None,
|
| 121 |
"tiers": [
|
| 122 |
+
{"role": "P1", "years": "Year 1", "comm": 20, "credit": 100, "agr": "Life Origination Fee Agreement"},
|
| 123 |
+
{"role": "P1", "years": "Year 2+", "comm": None, "credit": None, "agr": "Ended",
|
| 124 |
+
"note": "Life Referral is 20% Year 1 only. Year 2 it is removed."},
|
| 125 |
]
|
| 126 |
},
|
| 127 |
{
|
|
@@ -4,7 +4,7 @@
|
|
| 4 |
<div class="space-y-4">
|
| 5 |
<div class="flex items-center justify-between">
|
| 6 |
<div>
|
| 7 |
-
<a href="/admin/agreements" class="text-sm text-gray-400 hover:text-gray-600">β Back to Agreements</a>
|
| 8 |
<h1 class="text-2xl font-bold sw-blue-text mt-1">{{ agreement.name }}</h1>
|
| 9 |
</div>
|
| 10 |
<button onclick="saveTiers()" class="bg-green-600 text-white px-6 py-2 rounded-md text-sm font-semibold hover:bg-green-700">
|
|
@@ -111,6 +111,9 @@
|
|
| 111 |
</div>
|
| 112 |
|
| 113 |
<script>
|
|
|
|
|
|
|
|
|
|
| 114 |
function addTier() {
|
| 115 |
const html = `
|
| 116 |
<div class="tier-row border border-gray-200 rounded-lg p-4 bg-blue-50">
|
|
@@ -182,7 +185,7 @@ async function saveTiers() {
|
|
| 182 |
};
|
| 183 |
|
| 184 |
const resp = await fetch('/admin/api/agreements/{{ agreement.id }}/tiers', {
|
| 185 |
-
method: 'POST', headers:
|
| 186 |
body: JSON.stringify(body),
|
| 187 |
});
|
| 188 |
const data = await resp.json();
|
|
|
|
| 4 |
<div class="space-y-4">
|
| 5 |
<div class="flex items-center justify-between">
|
| 6 |
<div>
|
| 7 |
+
<a href="/admin/agreements?token={{ token }}" class="text-sm text-gray-400 hover:text-gray-600">β Back to Agreements</a>
|
| 8 |
<h1 class="text-2xl font-bold sw-blue-text mt-1">{{ agreement.name }}</h1>
|
| 9 |
</div>
|
| 10 |
<button onclick="saveTiers()" class="bg-green-600 text-white px-6 py-2 rounded-md text-sm font-semibold hover:bg-green-700">
|
|
|
|
| 111 |
</div>
|
| 112 |
|
| 113 |
<script>
|
| 114 |
+
const authToken = '{{ token }}';
|
| 115 |
+
const authHeaders = {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + authToken};
|
| 116 |
+
|
| 117 |
function addTier() {
|
| 118 |
const html = `
|
| 119 |
<div class="tier-row border border-gray-200 rounded-lg p-4 bg-blue-50">
|
|
|
|
| 185 |
};
|
| 186 |
|
| 187 |
const resp = await fetch('/admin/api/agreements/{{ agreement.id }}/tiers', {
|
| 188 |
+
method: 'POST', headers: authHeaders,
|
| 189 |
body: JSON.stringify(body),
|
| 190 |
});
|
| 191 |
const data = await resp.json();
|
|
@@ -40,7 +40,7 @@
|
|
| 40 |
</button>
|
| 41 |
</td>
|
| 42 |
<td class="px-4 py-3 text-center">
|
| 43 |
-
<a href="/admin/agreements/{{ agr.id }}" class="text-blue-600 hover:underline text-sm">Edit β</a>
|
| 44 |
</td>
|
| 45 |
</tr>
|
| 46 |
{% endfor %}
|
|
@@ -87,6 +87,9 @@
|
|
| 87 |
</div>
|
| 88 |
|
| 89 |
<script>
|
|
|
|
|
|
|
|
|
|
| 90 |
function showNewModal() { document.getElementById('newModal').classList.remove('hidden'); }
|
| 91 |
function closeNewModal() { document.getElementById('newModal').classList.add('hidden'); }
|
| 92 |
|
|
@@ -99,19 +102,19 @@ async function createAgreement() {
|
|
| 99 |
tiers: [],
|
| 100 |
};
|
| 101 |
const resp = await fetch('/admin/api/agreements/new', {
|
| 102 |
-
method: 'POST', headers:
|
| 103 |
body: JSON.stringify(body),
|
| 104 |
});
|
| 105 |
const data = await resp.json();
|
| 106 |
if (data.success) {
|
| 107 |
-
window.location.href = '/admin/agreements/' + data.id;
|
| 108 |
} else {
|
| 109 |
alert(data.error || 'Failed to create agreement');
|
| 110 |
}
|
| 111 |
}
|
| 112 |
|
| 113 |
async function toggleAgreement(id) {
|
| 114 |
-
await fetch('/admin/api/agreements/' + id + '/toggle', { method: 'POST' });
|
| 115 |
location.reload();
|
| 116 |
}
|
| 117 |
</script>
|
|
|
|
| 40 |
</button>
|
| 41 |
</td>
|
| 42 |
<td class="px-4 py-3 text-center">
|
| 43 |
+
<a href="/admin/agreements/{{ agr.id }}?token={{ token }}" class="text-blue-600 hover:underline text-sm">Edit β</a>
|
| 44 |
</td>
|
| 45 |
</tr>
|
| 46 |
{% endfor %}
|
|
|
|
| 87 |
</div>
|
| 88 |
|
| 89 |
<script>
|
| 90 |
+
const authToken = '{{ token }}';
|
| 91 |
+
const authHeaders = {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + authToken};
|
| 92 |
+
|
| 93 |
function showNewModal() { document.getElementById('newModal').classList.remove('hidden'); }
|
| 94 |
function closeNewModal() { document.getElementById('newModal').classList.add('hidden'); }
|
| 95 |
|
|
|
|
| 102 |
tiers: [],
|
| 103 |
};
|
| 104 |
const resp = await fetch('/admin/api/agreements/new', {
|
| 105 |
+
method: 'POST', headers: authHeaders,
|
| 106 |
body: JSON.stringify(body),
|
| 107 |
});
|
| 108 |
const data = await resp.json();
|
| 109 |
if (data.success) {
|
| 110 |
+
window.location.href = '/admin/agreements/' + data.id + '?token=' + authToken;
|
| 111 |
} else {
|
| 112 |
alert(data.error || 'Failed to create agreement');
|
| 113 |
}
|
| 114 |
}
|
| 115 |
|
| 116 |
async function toggleAgreement(id) {
|
| 117 |
+
await fetch('/admin/api/agreements/' + id + '/toggle', { method: 'POST', headers: {'Authorization': 'Bearer ' + authToken} });
|
| 118 |
location.reload();
|
| 119 |
}
|
| 120 |
</script>
|
|
@@ -2,7 +2,7 @@
|
|
| 2 |
{% block title %}Bell Ringer Thresholds β Admin{% endblock %}
|
| 3 |
{% block content %}
|
| 4 |
<div class="space-y-4">
|
| 5 |
-
<a href="/admin/" class="text-sm text-gray-400 hover:text-gray-600">β Admin Panel</a>
|
| 6 |
<div class="flex items-center justify-between">
|
| 7 |
<h1 class="text-2xl font-bold sw-blue-text">Bell Ringer Thresholds</h1>
|
| 8 |
<button onclick="document.getElementById('addRow').classList.toggle('hidden')"
|
|
@@ -56,10 +56,13 @@
|
|
| 56 |
</div>
|
| 57 |
</div>
|
| 58 |
<script>
|
|
|
|
|
|
|
|
|
|
| 59 |
async function saveBR(id) {
|
| 60 |
const val = parseFloat(document.getElementById('br_' + id).value);
|
| 61 |
await fetch('/admin/api/bell-ringers/' + id, {
|
| 62 |
-
method: 'POST', headers:
|
| 63 |
body: JSON.stringify({ threshold: val }),
|
| 64 |
});
|
| 65 |
document.getElementById('br_' + id).style.backgroundColor = '#d4edda';
|
|
@@ -67,12 +70,12 @@ async function saveBR(id) {
|
|
| 67 |
}
|
| 68 |
async function deleteBR(id) {
|
| 69 |
if (!confirm('Delete this threshold?')) return;
|
| 70 |
-
await fetch('/admin/api/bell-ringers/' + id, { method: 'DELETE' });
|
| 71 |
location.reload();
|
| 72 |
}
|
| 73 |
async function addBR() {
|
| 74 |
await fetch('/admin/api/bell-ringers/new', {
|
| 75 |
-
method: 'POST', headers:
|
| 76 |
body: JSON.stringify({
|
| 77 |
market_segment: document.getElementById('new_seg').value,
|
| 78 |
dept_code: document.getElementById('new_dept').value,
|
|
|
|
| 2 |
{% block title %}Bell Ringer Thresholds β Admin{% endblock %}
|
| 3 |
{% block content %}
|
| 4 |
<div class="space-y-4">
|
| 5 |
+
<a href="/admin/?token={{ token }}" class="text-sm text-gray-400 hover:text-gray-600">β Admin Panel</a>
|
| 6 |
<div class="flex items-center justify-between">
|
| 7 |
<h1 class="text-2xl font-bold sw-blue-text">Bell Ringer Thresholds</h1>
|
| 8 |
<button onclick="document.getElementById('addRow').classList.toggle('hidden')"
|
|
|
|
| 56 |
</div>
|
| 57 |
</div>
|
| 58 |
<script>
|
| 59 |
+
const authToken = '{{ token }}';
|
| 60 |
+
const authHeaders = {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + authToken};
|
| 61 |
+
|
| 62 |
async function saveBR(id) {
|
| 63 |
const val = parseFloat(document.getElementById('br_' + id).value);
|
| 64 |
await fetch('/admin/api/bell-ringers/' + id, {
|
| 65 |
+
method: 'POST', headers: authHeaders,
|
| 66 |
body: JSON.stringify({ threshold: val }),
|
| 67 |
});
|
| 68 |
document.getElementById('br_' + id).style.backgroundColor = '#d4edda';
|
|
|
|
| 70 |
}
|
| 71 |
async function deleteBR(id) {
|
| 72 |
if (!confirm('Delete this threshold?')) return;
|
| 73 |
+
await fetch('/admin/api/bell-ringers/' + id, { method: 'DELETE', headers: {'Authorization': 'Bearer ' + authToken} });
|
| 74 |
location.reload();
|
| 75 |
}
|
| 76 |
async function addBR() {
|
| 77 |
await fetch('/admin/api/bell-ringers/new', {
|
| 78 |
+
method: 'POST', headers: authHeaders,
|
| 79 |
body: JSON.stringify({
|
| 80 |
market_segment: document.getElementById('new_seg').value,
|
| 81 |
dept_code: document.getElementById('new_dept').value,
|
|
@@ -2,7 +2,7 @@
|
|
| 2 |
{% block title %}Fixed Constants β Admin{% endblock %}
|
| 3 |
{% block content %}
|
| 4 |
<div class="space-y-4">
|
| 5 |
-
<a href="/admin/" class="text-sm text-gray-400 hover:text-gray-600">β Admin Panel</a>
|
| 6 |
<h1 class="text-2xl font-bold sw-blue-text">Fixed Constants</h1>
|
| 7 |
<p class="text-gray-500 text-sm">Edit the percentages used for Client Executive and Complex Claims rows.</p>
|
| 8 |
|
|
@@ -33,10 +33,13 @@
|
|
| 33 |
</div>
|
| 34 |
</div>
|
| 35 |
<script>
|
|
|
|
|
|
|
|
|
|
| 36 |
async function saveConst(id) {
|
| 37 |
const val = parseFloat(document.getElementById('const_' + id).value);
|
| 38 |
await fetch('/admin/api/constants/' + id, {
|
| 39 |
-
method: 'POST', headers:
|
| 40 |
body: JSON.stringify({ value: val }),
|
| 41 |
});
|
| 42 |
// Brief flash to confirm
|
|
|
|
| 2 |
{% block title %}Fixed Constants β Admin{% endblock %}
|
| 3 |
{% block content %}
|
| 4 |
<div class="space-y-4">
|
| 5 |
+
<a href="/admin/?token={{ token }}" class="text-sm text-gray-400 hover:text-gray-600">β Admin Panel</a>
|
| 6 |
<h1 class="text-2xl font-bold sw-blue-text">Fixed Constants</h1>
|
| 7 |
<p class="text-gray-500 text-sm">Edit the percentages used for Client Executive and Complex Claims rows.</p>
|
| 8 |
|
|
|
|
| 33 |
</div>
|
| 34 |
</div>
|
| 35 |
<script>
|
| 36 |
+
const authToken = '{{ token }}';
|
| 37 |
+
const authHeaders = {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + authToken};
|
| 38 |
+
|
| 39 |
async function saveConst(id) {
|
| 40 |
const val = parseFloat(document.getElementById('const_' + id).value);
|
| 41 |
await fetch('/admin/api/constants/' + id, {
|
| 42 |
+
method: 'POST', headers: authHeaders,
|
| 43 |
body: JSON.stringify({ value: val }),
|
| 44 |
});
|
| 45 |
// Brief flash to confirm
|
|
@@ -6,7 +6,7 @@
|
|
| 6 |
<p class="text-gray-500 text-sm">Manage all configurable rules, dropdowns, and users without code changes.</p>
|
| 7 |
|
| 8 |
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
| 9 |
-
<a href="/admin/agreements" class="bg-white rounded-lg shadow p-5 hover:shadow-md transition group">
|
| 10 |
<div class="flex items-center justify-between">
|
| 11 |
<div>
|
| 12 |
<h3 class="font-semibold sw-blue-text group-hover:underline">Commission Agreements</h3>
|
|
@@ -16,7 +16,7 @@
|
|
| 16 |
</div>
|
| 17 |
</a>
|
| 18 |
|
| 19 |
-
<a href="/admin/constants" class="bg-white rounded-lg shadow p-5 hover:shadow-md transition group">
|
| 20 |
<div class="flex items-center justify-between">
|
| 21 |
<div>
|
| 22 |
<h3 class="font-semibold sw-blue-text group-hover:underline">Fixed Constants</h3>
|
|
@@ -26,7 +26,7 @@
|
|
| 26 |
</div>
|
| 27 |
</a>
|
| 28 |
|
| 29 |
-
<a href="/admin/bell-ringers" class="bg-white rounded-lg shadow p-5 hover:shadow-md transition group">
|
| 30 |
<div class="flex items-center justify-between">
|
| 31 |
<div>
|
| 32 |
<h3 class="font-semibold sw-blue-text group-hover:underline">Bell Ringer Thresholds</h3>
|
|
@@ -36,7 +36,7 @@
|
|
| 36 |
</div>
|
| 37 |
</a>
|
| 38 |
|
| 39 |
-
<a href="/admin/dropdowns" class="bg-white rounded-lg shadow p-5 hover:shadow-md transition group">
|
| 40 |
<div class="flex items-center justify-between">
|
| 41 |
<div>
|
| 42 |
<h3 class="font-semibold sw-blue-text group-hover:underline">Dropdown Options</h3>
|
|
@@ -46,7 +46,7 @@
|
|
| 46 |
</div>
|
| 47 |
</a>
|
| 48 |
|
| 49 |
-
<a href="/admin/email-rules" class="bg-white rounded-lg shadow p-5 hover:shadow-md transition group">
|
| 50 |
<div class="flex items-center justify-between">
|
| 51 |
<div>
|
| 52 |
<h3 class="font-semibold sw-blue-text group-hover:underline">Email Routing Rules</h3>
|
|
@@ -67,7 +67,7 @@
|
|
| 67 |
</div>
|
| 68 |
</a>
|
| 69 |
|
| 70 |
-
<a href="/admin/users" class="bg-white rounded-lg shadow p-5 hover:shadow-md transition group">
|
| 71 |
<div class="flex items-center justify-between">
|
| 72 |
<div>
|
| 73 |
<h3 class="font-semibold sw-blue-text group-hover:underline">User Management</h3>
|
|
|
|
| 6 |
<p class="text-gray-500 text-sm">Manage all configurable rules, dropdowns, and users without code changes.</p>
|
| 7 |
|
| 8 |
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
| 9 |
+
<a href="/admin/agreements?token={{ token }}" class="bg-white rounded-lg shadow p-5 hover:shadow-md transition group">
|
| 10 |
<div class="flex items-center justify-between">
|
| 11 |
<div>
|
| 12 |
<h3 class="font-semibold sw-blue-text group-hover:underline">Commission Agreements</h3>
|
|
|
|
| 16 |
</div>
|
| 17 |
</a>
|
| 18 |
|
| 19 |
+
<a href="/admin/constants?token={{ token }}" class="bg-white rounded-lg shadow p-5 hover:shadow-md transition group">
|
| 20 |
<div class="flex items-center justify-between">
|
| 21 |
<div>
|
| 22 |
<h3 class="font-semibold sw-blue-text group-hover:underline">Fixed Constants</h3>
|
|
|
|
| 26 |
</div>
|
| 27 |
</a>
|
| 28 |
|
| 29 |
+
<a href="/admin/bell-ringers?token={{ token }}" class="bg-white rounded-lg shadow p-5 hover:shadow-md transition group">
|
| 30 |
<div class="flex items-center justify-between">
|
| 31 |
<div>
|
| 32 |
<h3 class="font-semibold sw-blue-text group-hover:underline">Bell Ringer Thresholds</h3>
|
|
|
|
| 36 |
</div>
|
| 37 |
</a>
|
| 38 |
|
| 39 |
+
<a href="/admin/dropdowns?token={{ token }}" class="bg-white rounded-lg shadow p-5 hover:shadow-md transition group">
|
| 40 |
<div class="flex items-center justify-between">
|
| 41 |
<div>
|
| 42 |
<h3 class="font-semibold sw-blue-text group-hover:underline">Dropdown Options</h3>
|
|
|
|
| 46 |
</div>
|
| 47 |
</a>
|
| 48 |
|
| 49 |
+
<a href="/admin/email-rules?token={{ token }}" class="bg-white rounded-lg shadow p-5 hover:shadow-md transition group">
|
| 50 |
<div class="flex items-center justify-between">
|
| 51 |
<div>
|
| 52 |
<h3 class="font-semibold sw-blue-text group-hover:underline">Email Routing Rules</h3>
|
|
|
|
| 67 |
</div>
|
| 68 |
</a>
|
| 69 |
|
| 70 |
+
<a href="/admin/users?token={{ token }}" class="bg-white rounded-lg shadow p-5 hover:shadow-md transition group">
|
| 71 |
<div class="flex items-center justify-between">
|
| 72 |
<div>
|
| 73 |
<h3 class="font-semibold sw-blue-text group-hover:underline">User Management</h3>
|
|
@@ -2,7 +2,7 @@
|
|
| 2 |
{% block title %}Dropdown Options β Admin{% endblock %}
|
| 3 |
{% block content %}
|
| 4 |
<div class="space-y-4">
|
| 5 |
-
<a href="/admin/" class="text-sm text-gray-400 hover:text-gray-600">β Admin Panel</a>
|
| 6 |
<h1 class="text-2xl font-bold sw-blue-text">Dropdown Options</h1>
|
| 7 |
<p class="text-gray-500 text-sm">Manage the options shown in form dropdowns. Changes take effect immediately.</p>
|
| 8 |
|
|
@@ -35,23 +35,26 @@
|
|
| 35 |
{% endfor %}
|
| 36 |
</div>
|
| 37 |
<script>
|
|
|
|
|
|
|
|
|
|
| 38 |
async function addOption(category) {
|
| 39 |
const label = prompt('Enter the display label for the new ' + category.replace('_',' ') + ' option:');
|
| 40 |
if (!label) return;
|
| 41 |
const value = prompt('Internal value (leave blank to use label):', label);
|
| 42 |
await fetch('/admin/api/dropdowns/new', {
|
| 43 |
-
method: 'POST', headers:
|
| 44 |
body: JSON.stringify({ category, label, value: value || label }),
|
| 45 |
});
|
| 46 |
location.reload();
|
| 47 |
}
|
| 48 |
async function toggleOption(id) {
|
| 49 |
-
await fetch('/admin/api/dropdowns/' + id + '/toggle', { method: 'POST' });
|
| 50 |
location.reload();
|
| 51 |
}
|
| 52 |
async function deleteOption(id) {
|
| 53 |
if (!confirm('Delete this option permanently?')) return;
|
| 54 |
-
await fetch('/admin/api/dropdowns/' + id, { method: 'DELETE' });
|
| 55 |
location.reload();
|
| 56 |
}
|
| 57 |
</script>
|
|
|
|
| 2 |
{% block title %}Dropdown Options β Admin{% endblock %}
|
| 3 |
{% block content %}
|
| 4 |
<div class="space-y-4">
|
| 5 |
+
<a href="/admin/?token={{ token }}" class="text-sm text-gray-400 hover:text-gray-600">β Admin Panel</a>
|
| 6 |
<h1 class="text-2xl font-bold sw-blue-text">Dropdown Options</h1>
|
| 7 |
<p class="text-gray-500 text-sm">Manage the options shown in form dropdowns. Changes take effect immediately.</p>
|
| 8 |
|
|
|
|
| 35 |
{% endfor %}
|
| 36 |
</div>
|
| 37 |
<script>
|
| 38 |
+
const authToken = '{{ token }}';
|
| 39 |
+
const authHeaders = {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + authToken};
|
| 40 |
+
|
| 41 |
async function addOption(category) {
|
| 42 |
const label = prompt('Enter the display label for the new ' + category.replace('_',' ') + ' option:');
|
| 43 |
if (!label) return;
|
| 44 |
const value = prompt('Internal value (leave blank to use label):', label);
|
| 45 |
await fetch('/admin/api/dropdowns/new', {
|
| 46 |
+
method: 'POST', headers: authHeaders,
|
| 47 |
body: JSON.stringify({ category, label, value: value || label }),
|
| 48 |
});
|
| 49 |
location.reload();
|
| 50 |
}
|
| 51 |
async function toggleOption(id) {
|
| 52 |
+
await fetch('/admin/api/dropdowns/' + id + '/toggle', { method: 'POST', headers: {'Authorization': 'Bearer ' + authToken} });
|
| 53 |
location.reload();
|
| 54 |
}
|
| 55 |
async function deleteOption(id) {
|
| 56 |
if (!confirm('Delete this option permanently?')) return;
|
| 57 |
+
await fetch('/admin/api/dropdowns/' + id, { method: 'DELETE', headers: {'Authorization': 'Bearer ' + authToken} });
|
| 58 |
location.reload();
|
| 59 |
}
|
| 60 |
</script>
|
|
@@ -2,7 +2,7 @@
|
|
| 2 |
{% block title %}Email Routing β Admin{% endblock %}
|
| 3 |
{% block content %}
|
| 4 |
<div class="space-y-4">
|
| 5 |
-
<a href="/admin/" class="text-sm text-gray-400 hover:text-gray-600">β Admin Panel</a>
|
| 6 |
<div class="flex items-center justify-between">
|
| 7 |
<h1 class="text-2xl font-bold sw-blue-text">Email Routing Rules</h1>
|
| 8 |
<button onclick="addRule()" class="bg-green-600 text-white px-4 py-2 rounded-md text-sm font-semibold hover:bg-green-700">+ Add Rule</button>
|
|
@@ -47,9 +47,12 @@
|
|
| 47 |
</div>
|
| 48 |
</div>
|
| 49 |
<script>
|
|
|
|
|
|
|
|
|
|
| 50 |
async function saveRule(id) {
|
| 51 |
await fetch('/admin/api/email-rules/' + id, {
|
| 52 |
-
method: 'POST', headers:
|
| 53 |
body: JSON.stringify({
|
| 54 |
condition: document.getElementById('cond_' + id).value,
|
| 55 |
email: document.getElementById('email_' + id).value,
|
|
@@ -62,7 +65,7 @@ async function saveRule(id) {
|
|
| 62 |
}
|
| 63 |
async function addRule() {
|
| 64 |
await fetch('/admin/api/email-rules/new', {
|
| 65 |
-
method: 'POST', headers:
|
| 66 |
body: JSON.stringify({ condition: 'always', email: '', description: 'New rule' }),
|
| 67 |
});
|
| 68 |
location.reload();
|
|
|
|
| 2 |
{% block title %}Email Routing β Admin{% endblock %}
|
| 3 |
{% block content %}
|
| 4 |
<div class="space-y-4">
|
| 5 |
+
<a href="/admin/?token={{ token }}" class="text-sm text-gray-400 hover:text-gray-600">β Admin Panel</a>
|
| 6 |
<div class="flex items-center justify-between">
|
| 7 |
<h1 class="text-2xl font-bold sw-blue-text">Email Routing Rules</h1>
|
| 8 |
<button onclick="addRule()" class="bg-green-600 text-white px-4 py-2 rounded-md text-sm font-semibold hover:bg-green-700">+ Add Rule</button>
|
|
|
|
| 47 |
</div>
|
| 48 |
</div>
|
| 49 |
<script>
|
| 50 |
+
const authToken = '{{ token }}';
|
| 51 |
+
const authHeaders = {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + authToken};
|
| 52 |
+
|
| 53 |
async function saveRule(id) {
|
| 54 |
await fetch('/admin/api/email-rules/' + id, {
|
| 55 |
+
method: 'POST', headers: authHeaders,
|
| 56 |
body: JSON.stringify({
|
| 57 |
condition: document.getElementById('cond_' + id).value,
|
| 58 |
email: document.getElementById('email_' + id).value,
|
|
|
|
| 65 |
}
|
| 66 |
async function addRule() {
|
| 67 |
await fetch('/admin/api/email-rules/new', {
|
| 68 |
+
method: 'POST', headers: authHeaders,
|
| 69 |
body: JSON.stringify({ condition: 'always', email: '', description: 'New rule' }),
|
| 70 |
});
|
| 71 |
location.reload();
|
|
@@ -2,7 +2,7 @@
|
|
| 2 |
{% block title %}User Management β Admin{% endblock %}
|
| 3 |
{% block content %}
|
| 4 |
<div class="space-y-4">
|
| 5 |
-
<a href="/admin/" class="text-sm text-gray-400 hover:text-gray-600">β Admin Panel</a>
|
| 6 |
<div class="flex items-center justify-between">
|
| 7 |
<h1 class="text-2xl font-bold sw-blue-text">User Management</h1>
|
| 8 |
<button onclick="document.getElementById('addUser').classList.toggle('hidden')"
|
|
@@ -77,9 +77,12 @@
|
|
| 77 |
</div>
|
| 78 |
</div>
|
| 79 |
<script>
|
|
|
|
|
|
|
|
|
|
| 80 |
async function addUser() {
|
| 81 |
const resp = await fetch('/admin/api/users/new', {
|
| 82 |
-
method: 'POST', headers:
|
| 83 |
body: JSON.stringify({
|
| 84 |
full_name: document.getElementById('new_name').value,
|
| 85 |
email: document.getElementById('new_email').value,
|
|
@@ -92,7 +95,7 @@ async function addUser() {
|
|
| 92 |
else alert(data.error || 'Failed');
|
| 93 |
}
|
| 94 |
async function toggleUser(id) {
|
| 95 |
-
await fetch('/admin/api/users/' + id + '/toggle', { method: 'POST' });
|
| 96 |
location.reload();
|
| 97 |
}
|
| 98 |
</script>
|
|
|
|
| 2 |
{% block title %}User Management β Admin{% endblock %}
|
| 3 |
{% block content %}
|
| 4 |
<div class="space-y-4">
|
| 5 |
+
<a href="/admin/?token={{ token }}" class="text-sm text-gray-400 hover:text-gray-600">β Admin Panel</a>
|
| 6 |
<div class="flex items-center justify-between">
|
| 7 |
<h1 class="text-2xl font-bold sw-blue-text">User Management</h1>
|
| 8 |
<button onclick="document.getElementById('addUser').classList.toggle('hidden')"
|
|
|
|
| 77 |
</div>
|
| 78 |
</div>
|
| 79 |
<script>
|
| 80 |
+
const authToken = '{{ token }}';
|
| 81 |
+
const authHeaders = {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + authToken};
|
| 82 |
+
|
| 83 |
async function addUser() {
|
| 84 |
const resp = await fetch('/admin/api/users/new', {
|
| 85 |
+
method: 'POST', headers: authHeaders,
|
| 86 |
body: JSON.stringify({
|
| 87 |
full_name: document.getElementById('new_name').value,
|
| 88 |
email: document.getElementById('new_email').value,
|
|
|
|
| 95 |
else alert(data.error || 'Failed');
|
| 96 |
}
|
| 97 |
async function toggleUser(id) {
|
| 98 |
+
await fetch('/admin/api/users/' + id + '/toggle', { method: 'POST', headers: {'Authorization': 'Bearer ' + authToken} });
|
| 99 |
location.reload();
|
| 100 |
}
|
| 101 |
</script>
|
|
@@ -3,7 +3,7 @@
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
-
<title>{% block title %}
|
| 7 |
<script src="https://cdn.tailwindcss.com"></script>
|
| 8 |
<style>
|
| 9 |
body { font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; }
|
|
@@ -21,7 +21,7 @@
|
|
| 21 |
<nav class="sw-blue text-white shadow-lg">
|
| 22 |
<div class="max-w-7xl mx-auto px-4 py-3 flex items-center justify-between">
|
| 23 |
<div class="flex items-center space-x-6">
|
| 24 |
-
<span class="text-lg font-bold tracking-tight">SW
|
| 25 |
<a href="/form?token={{ token }}" class="text-sm text-gray-200 hover:text-white transition">Form</a>
|
| 26 |
{% if user.role in ('admin', 'superadmin') %}
|
| 27 |
<a href="/admin/?token={{ token }}" class="text-sm text-gray-200 hover:text-white transition">Admin Panel</a>
|
|
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>{% block title %}Commission Agreement Intake Form{% endblock %}</title>
|
| 7 |
<script src="https://cdn.tailwindcss.com"></script>
|
| 8 |
<style>
|
| 9 |
body { font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; }
|
|
|
|
| 21 |
<nav class="sw-blue text-white shadow-lg">
|
| 22 |
<div class="max-w-7xl mx-auto px-4 py-3 flex items-center justify-between">
|
| 23 |
<div class="flex items-center space-x-6">
|
| 24 |
+
<span class="text-lg font-bold tracking-tight">SW Commission Intake</span>
|
| 25 |
<a href="/form?token={{ token }}" class="text-sm text-gray-200 hover:text-white transition">Form</a>
|
| 26 |
{% if user.role in ('admin', 'superadmin') %}
|
| 27 |
<a href="/admin/?token={{ token }}" class="text-sm text-gray-200 hover:text-white transition">Admin Panel</a>
|
|
@@ -1,16 +1,16 @@
|
|
| 1 |
{% extends "base.html" %}
|
| 2 |
-
{% block title %}
|
| 3 |
|
| 4 |
{% block content %}
|
| 5 |
<div class="space-y-6">
|
| 6 |
-
<div
|
| 7 |
-
<h1 class="text-2xl font-bold sw-blue-text">
|
| 8 |
<p class="text-gray-500 text-sm mt-1">Complete all fields. The summary table updates <strong>live</strong>.</p>
|
| 9 |
</div>
|
| 10 |
|
| 11 |
<!-- ββ Section 1: Client Information ββββββββββββββββββββ -->
|
| 12 |
<div class="bg-white rounded-lg shadow overflow-hidden">
|
| 13 |
-
<div class="sw-blue text-white px-5 py-3 font-semibold">
|
| 14 |
<div class="px-5 py-4 space-y-3">
|
| 15 |
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
| 16 |
<div>
|
|
@@ -43,24 +43,7 @@
|
|
| 43 |
</div>
|
| 44 |
</div>
|
| 45 |
|
| 46 |
-
<!-- Bell Ringer Badge -->
|
| 47 |
-
<div id="bell_badge" class="bell-idle text-sm">π Bell Ringer: --</div>
|
| 48 |
-
|
| 49 |
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
| 50 |
-
<div>
|
| 51 |
-
<label class="block text-sm font-medium text-gray-700 mb-1">Policy Effective Date</label>
|
| 52 |
-
<input type="date" id="effective_date"
|
| 53 |
-
class="w-full border border-gray-300 rounded-md px-3 py-2 text-sm">
|
| 54 |
-
</div>
|
| 55 |
-
<div>
|
| 56 |
-
<label class="block text-sm font-medium text-gray-700 mb-1">Association</label>
|
| 57 |
-
<select id="association" class="w-full border border-gray-300 rounded-md px-3 py-2 text-sm">
|
| 58 |
-
<option value="">-- Select --</option>
|
| 59 |
-
{% for a in associations %}
|
| 60 |
-
<option value="{{ a.value }}">{{ a.label }}</option>
|
| 61 |
-
{% endfor %}
|
| 62 |
-
</select>
|
| 63 |
-
</div>
|
| 64 |
<div>
|
| 65 |
<label class="block text-sm font-medium text-gray-700 mb-1">Department Code</label>
|
| 66 |
<select id="dept_code" class="w-full border border-gray-300 rounded-md px-3 py-2 text-sm">
|
|
@@ -79,6 +62,26 @@
|
|
| 79 |
</select>
|
| 80 |
</div>
|
| 81 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
</div>
|
| 83 |
</div>
|
| 84 |
|
|
@@ -313,15 +316,27 @@ async function refreshSummary() {
|
|
| 313 |
if (!formData.agreement_id) {
|
| 314 |
document.getElementById('summary_area').innerHTML =
|
| 315 |
'<div class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded text-sm">' +
|
| 316 |
-
'
|
| 317 |
document.getElementById('warnings').innerHTML = '';
|
| 318 |
document.getElementById('submit_btn').disabled = true;
|
| 319 |
return;
|
| 320 |
}
|
| 321 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 322 |
try {
|
| 323 |
const resp = await fetch('/api/compute-summary', {
|
| 324 |
-
method: 'POST',
|
| 325 |
headers: {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + authToken},
|
| 326 |
body: JSON.stringify(formData),
|
| 327 |
});
|
|
@@ -330,7 +345,7 @@ async function refreshSummary() {
|
|
| 330 |
// Warnings
|
| 331 |
let warnHtml = '';
|
| 332 |
(data.warnings || []).forEach(w => {
|
| 333 |
-
warnHtml += `<div class="bg-amber-50 border border-amber-300 text-amber-800 px-4 py-2 rounded text-sm mb-2">
|
| 334 |
});
|
| 335 |
document.getElementById('warnings').innerHTML = warnHtml;
|
| 336 |
|
|
|
|
| 1 |
{% extends "base.html" %}
|
| 2 |
+
{% block title %}Commission Agreement Intake Form{% endblock %}
|
| 3 |
|
| 4 |
{% block content %}
|
| 5 |
<div class="space-y-6">
|
| 6 |
+
<div>
|
| 7 |
+
<h1 class="text-2xl font-bold sw-blue-text">Commission Agreement Intake Form</h1>
|
| 8 |
<p class="text-gray-500 text-sm mt-1">Complete all fields. The summary table updates <strong>live</strong>.</p>
|
| 9 |
</div>
|
| 10 |
|
| 11 |
<!-- ββ Section 1: Client Information ββββββββββββββββββββ -->
|
| 12 |
<div class="bg-white rounded-lg shadow overflow-hidden">
|
| 13 |
+
<div class="sw-blue text-white px-5 py-3 font-semibold">Servicing Information</div>
|
| 14 |
<div class="px-5 py-4 space-y-3">
|
| 15 |
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
| 16 |
<div>
|
|
|
|
| 43 |
</div>
|
| 44 |
</div>
|
| 45 |
|
|
|
|
|
|
|
|
|
|
| 46 |
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
<div>
|
| 48 |
<label class="block text-sm font-medium text-gray-700 mb-1">Department Code</label>
|
| 49 |
<select id="dept_code" class="w-full border border-gray-300 rounded-md px-3 py-2 text-sm">
|
|
|
|
| 62 |
</select>
|
| 63 |
</div>
|
| 64 |
</div>
|
| 65 |
+
|
| 66 |
+
<!-- Bell Ringer Badge (auto-populates based on revenue + department) -->
|
| 67 |
+
<div id="bell_badge" class="bell-idle text-sm cursor-default" title="Auto-populates based on estimated revenue and department code. Cannot be manually selected.">π Bell Ringer: Select department code and enter revenue</div>
|
| 68 |
+
|
| 69 |
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
| 70 |
+
<div>
|
| 71 |
+
<label class="block text-sm font-medium text-gray-700 mb-1">Policy Effective Date</label>
|
| 72 |
+
<input type="date" id="effective_date"
|
| 73 |
+
class="w-full border border-gray-300 rounded-md px-3 py-2 text-sm">
|
| 74 |
+
</div>
|
| 75 |
+
<div>
|
| 76 |
+
<label class="block text-sm font-medium text-gray-700 mb-1">Association</label>
|
| 77 |
+
<select id="association" class="w-full border border-gray-300 rounded-md px-3 py-2 text-sm">
|
| 78 |
+
<option value="">-- Select --</option>
|
| 79 |
+
{% for a in associations %}
|
| 80 |
+
<option value="{{ a.value }}">{{ a.label }}</option>
|
| 81 |
+
{% endfor %}
|
| 82 |
+
</select>
|
| 83 |
+
</div>
|
| 84 |
+
</div>
|
| 85 |
</div>
|
| 86 |
</div>
|
| 87 |
|
|
|
|
| 316 |
if (!formData.agreement_id) {
|
| 317 |
document.getElementById('summary_area').innerHTML =
|
| 318 |
'<div class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded text-sm">' +
|
| 319 |
+
'Select a <strong>Commission Agreement</strong> to generate the summary.</div>';
|
| 320 |
document.getElementById('warnings').innerHTML = '';
|
| 321 |
document.getElementById('submit_btn').disabled = true;
|
| 322 |
return;
|
| 323 |
}
|
| 324 |
|
| 325 |
+
// ββ Client-side validation: CCP + Bond incompatibility ββ
|
| 326 |
+
const agrSelect = document.getElementById('agreement');
|
| 327 |
+
const agrName = agrSelect.options[agrSelect.selectedIndex]?.text || '';
|
| 328 |
+
const ccpVal = document.getElementById('complex_claims').value;
|
| 329 |
+
if (ccpVal && agrName.toLowerCase().includes('bond')) {
|
| 330 |
+
document.getElementById('warnings').innerHTML =
|
| 331 |
+
'<div class="bg-red-50 border border-red-300 text-red-800 px-4 py-2 rounded text-sm mb-2">Complex Claims Practice (CCP) is not eligible for Bond Agreement.</div>';
|
| 332 |
+
document.getElementById('summary_area').innerHTML = '';
|
| 333 |
+
document.getElementById('submit_btn').disabled = true;
|
| 334 |
+
return;
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
try {
|
| 338 |
const resp = await fetch('/api/compute-summary', {
|
| 339 |
+
method: 'POST',
|
| 340 |
headers: {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + authToken},
|
| 341 |
body: JSON.stringify(formData),
|
| 342 |
});
|
|
|
|
| 345 |
// Warnings
|
| 346 |
let warnHtml = '';
|
| 347 |
(data.warnings || []).forEach(w => {
|
| 348 |
+
warnHtml += `<div class="bg-amber-50 border border-amber-300 text-amber-800 px-4 py-2 rounded text-sm mb-2">${w}</div>`;
|
| 349 |
});
|
| 350 |
document.getElementById('warnings').innerHTML = warnHtml;
|
| 351 |
|
|
@@ -49,6 +49,51 @@ if not s.query(User).first():
|
|
| 49 |
else:
|
| 50 |
print("Database exists.")
|
| 51 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
# Import refresh data if present (uploaded by Colab daily refresh)
|
| 53 |
REFRESH_FILE = "refresh_data.json"
|
| 54 |
if os.path.exists(REFRESH_FILE):
|
|
|
|
| 49 |
else:
|
| 50 |
print("Database exists.")
|
| 51 |
|
| 52 |
+
# ββ Data migration: fix commission tier labels βββββββββββ
|
| 53 |
+
from app.models import CommissionAgreement, CommissionTier
|
| 54 |
+
print("Checking commission data migrations...")
|
| 55 |
+
|
| 56 |
+
# Fix Referral/Origination Fee: "Year 1-2" β "Year 1 & Year 2"
|
| 57 |
+
ref_agr = s.query(CommissionAgreement).filter(
|
| 58 |
+
CommissionAgreement.name == "Referral / Origination Fee Agreement"
|
| 59 |
+
).first()
|
| 60 |
+
if ref_agr:
|
| 61 |
+
updated = 0
|
| 62 |
+
for tier in ref_agr.tiers:
|
| 63 |
+
if tier.years_label == "Year 1-2":
|
| 64 |
+
tier.years_label = "Year 1 & Year 2"
|
| 65 |
+
updated += 1
|
| 66 |
+
if updated:
|
| 67 |
+
s.commit()
|
| 68 |
+
print(f" Fixed Referral/Origination Fee: {updated} tier labels updated to 'Year 1 & Year 2'")
|
| 69 |
+
|
| 70 |
+
# Fix Life Origination Fee: "All Years" at 20% β "Year 1" at 20% + "Year 2+" Ended
|
| 71 |
+
life_agr = s.query(CommissionAgreement).filter(
|
| 72 |
+
CommissionAgreement.name == "Life Origination Fee Agreement"
|
| 73 |
+
).first()
|
| 74 |
+
if life_agr:
|
| 75 |
+
all_years_tiers = [t for t in life_agr.tiers if t.years_label == "All Years"]
|
| 76 |
+
if all_years_tiers:
|
| 77 |
+
# Update existing tier to Year 1
|
| 78 |
+
all_years_tiers[0].years_label = "Year 1"
|
| 79 |
+
# Add Year 2+ Ended tier
|
| 80 |
+
max_sort = max((t.sort_order for t in life_agr.tiers), default=0)
|
| 81 |
+
ended_tier = CommissionTier(
|
| 82 |
+
agreement_id=life_agr.id,
|
| 83 |
+
producer_role="P1",
|
| 84 |
+
years_label="Year 2+",
|
| 85 |
+
commission=None,
|
| 86 |
+
credit=None,
|
| 87 |
+
agreement_label="Ended",
|
| 88 |
+
note="Life Referral is 20% Year 1 only. Year 2 it is removed.",
|
| 89 |
+
sort_order=max_sort + 1,
|
| 90 |
+
)
|
| 91 |
+
s.add(ended_tier)
|
| 92 |
+
s.commit()
|
| 93 |
+
print(" Fixed Life Origination Fee: Year 1 at 20%, Year 2+ Ended")
|
| 94 |
+
|
| 95 |
+
print("Commission data migrations complete.")
|
| 96 |
+
|
| 97 |
# Import refresh data if present (uploaded by Colab daily refresh)
|
| 98 |
REFRESH_FILE = "refresh_data.json"
|
| 99 |
if os.path.exists(REFRESH_FILE):
|