Includes: API (Express/TypeScript), Astro site, Python workers, document generators, FCC compliance tools, Canada CRTC formation, Ansible infrastructure, and deployment scripts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
176 lines
7.7 KiB
Text
176 lines
7.7 KiB
Text
---
|
||
// LNPARegionStep — Block 5 Lines 503-510 LNPA region percentages.
|
||
// 10 regions × 2 columns (Block 3 resale / Block 4 end-user).
|
||
// Each column must sum to 100.00% or 0%.
|
||
import { LNPA_REGIONS } from "../../../lib/lnpa_regions_catalog";
|
||
---
|
||
|
||
<div class="pw-step">
|
||
<h2>Revenue by LNPA region (Block 5)</h2>
|
||
<p class="pw-help">
|
||
Distribute your telecommunications revenue across the 10 Local Number
|
||
Portability Administration regions. Each column (Block 3 resale /
|
||
Block 4 end-user) must sum to exactly 100.00% — or 0 if you had no
|
||
revenue in that block. Feeds FCC Form 499-A Lines 503-510.
|
||
</p>
|
||
|
||
<div class="pw-lnpa-helper">
|
||
<button type="button" class="pw-btn-plain pw-btn" id="pw-lnpa-even">Distribute evenly</button>
|
||
<button type="button" class="pw-btn-plain pw-btn" id="pw-lnpa-from-billing">Pull from billing address distribution (CDR)</button>
|
||
</div>
|
||
|
||
<table class="pw-lnpa-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Region</th>
|
||
<th>Description</th>
|
||
<th>Block 3 %<br>(resale)</th>
|
||
<th>Block 4 %<br>(end-user)</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="pw-lnpa-body"></tbody>
|
||
<tfoot>
|
||
<tr>
|
||
<th colspan="2" style="text-align:right;">Sum:</th>
|
||
<th id="pw-lnpa-b3-sum">0.00%</th>
|
||
<th id="pw-lnpa-b4-sum">0.00%</th>
|
||
</tr>
|
||
</tfoot>
|
||
</table>
|
||
<div id="pw-lnpa-err" class="pw-err" hidden></div>
|
||
</div>
|
||
|
||
<style>
|
||
.pw-step h2 { margin: 0 0 0.5rem; color: #1a2744; }
|
||
.pw-help { color: #64748b; font-size: 0.9rem; margin-bottom: 1rem; }
|
||
.pw-lnpa-helper { display: flex; gap: 0.5rem; margin-bottom: 0.75rem; }
|
||
.pw-btn { padding: 0.4rem 0.9rem; border: 0; border-radius: 6px; font-size: 0.85rem; cursor: pointer; }
|
||
.pw-btn-plain { background: #e2e8f0; color: #1f2937; }
|
||
.pw-lnpa-table { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
|
||
.pw-lnpa-table th, .pw-lnpa-table td {
|
||
padding: 0.4rem 0.6rem; border-bottom: 1px solid #e2e8f0;
|
||
text-align: left; vertical-align: middle;
|
||
}
|
||
.pw-lnpa-table thead th { background: #f8fafc; font-size: 0.78rem; color: #475569; text-transform: uppercase; }
|
||
.pw-lnpa-table input { width: 80px; padding: 0.3rem 0.5rem; border: 1px solid #cbd5e1; border-radius: 4px; text-align: right; }
|
||
.pw-lnpa-table tfoot th { background: #f1f5f9; }
|
||
.pw-lnpa-table tfoot th[id$="-sum"][data-invalid="1"] { color: #b91c1c; }
|
||
.pw-err { color: #b91c1c; margin-top: 0.75rem; font-size: 0.9rem; }
|
||
</style>
|
||
|
||
<script>
|
||
import { LNPA_REGIONS } from "../../../lib/lnpa_regions_catalog";
|
||
|
||
const body = document.getElementById("pw-lnpa-body")!;
|
||
const b3Sum = document.getElementById("pw-lnpa-b3-sum")!;
|
||
const b4Sum = document.getElementById("pw-lnpa-b4-sum")!;
|
||
const err = document.getElementById("pw-lnpa-err") as HTMLDivElement;
|
||
|
||
for (const r of LNPA_REGIONS) {
|
||
const row = document.createElement("tr");
|
||
row.innerHTML = `
|
||
<td><strong>${r.code}</strong> — ${r.label}</td>
|
||
<td style="font-size:.78rem; color:#64748b;">${r.description}</td>
|
||
<td><input type="number" step="0.01" min="0" max="100" data-b3="${r.code}" /></td>
|
||
<td><input type="number" step="0.01" min="0" max="100" data-b4="${r.code}" /></td>
|
||
`;
|
||
body.appendChild(row);
|
||
}
|
||
|
||
function recalc() {
|
||
let s3 = 0, s4 = 0;
|
||
body.querySelectorAll<HTMLInputElement>("input[data-b3]").forEach((i) => s3 += Number(i.value) || 0);
|
||
body.querySelectorAll<HTMLInputElement>("input[data-b4]").forEach((i) => s4 += Number(i.value) || 0);
|
||
b3Sum.textContent = s3.toFixed(2) + "%";
|
||
b4Sum.textContent = s4.toFixed(2) + "%";
|
||
b3Sum.setAttribute("data-invalid", (Math.abs(s3 - 100) > 0.01 && s3 !== 0) ? "1" : "0");
|
||
b4Sum.setAttribute("data-invalid", (Math.abs(s4 - 100) > 0.01 && s4 !== 0) ? "1" : "0");
|
||
return { s3, s4 };
|
||
}
|
||
|
||
body.addEventListener("input", recalc);
|
||
|
||
document.getElementById("pw-lnpa-even")!.addEventListener("click", () => {
|
||
const pct = 100 / LNPA_REGIONS.length;
|
||
body.querySelectorAll<HTMLInputElement>("input[data-b3]").forEach((i) => i.value = pct.toFixed(2));
|
||
body.querySelectorAll<HTMLInputElement>("input[data-b4]").forEach((i) => i.value = pct.toFixed(2));
|
||
recalc();
|
||
});
|
||
|
||
document.getElementById("pw-lnpa-from-billing")!.addEventListener("click", async () => {
|
||
const s = (window as any).PWIntake.get();
|
||
if (!s.telecom_entity_id) {
|
||
err.hidden = false; err.textContent = "No telecom entity selected — can't pull billing distribution."; return;
|
||
}
|
||
try {
|
||
// Call the CDR study endpoint to get billing_state_regions_json, then convert
|
||
const profResp = await fetch(`/api/v1/cdr/profile/by-entity/${s.telecom_entity_id}`);
|
||
if (!profResp.ok) throw new Error("no CDR profile");
|
||
const { profile_id } = await profResp.json();
|
||
const year = s.intake_data?.form_year || new Date().getUTCFullYear() - 1;
|
||
const sResp = await fetch(`/api/v1/cdr/profile/${profile_id}/study?year=${year}`);
|
||
if (!sResp.ok) throw new Error("no study");
|
||
const study = await sResp.json();
|
||
const regions = study.classified_report?.billing_state_regions || {};
|
||
// regions is { "NE": 0.12, "MA": 0.08, ... } — apply to both columns
|
||
for (const r of LNPA_REGIONS) {
|
||
const pct = (regions[r.code] || 0) * 100;
|
||
const b3 = body.querySelector<HTMLInputElement>(`input[data-b3="${r.code}"]`)!;
|
||
const b4 = body.querySelector<HTMLInputElement>(`input[data-b4="${r.code}"]`)!;
|
||
b3.value = pct.toFixed(2);
|
||
b4.value = pct.toFixed(2);
|
||
}
|
||
recalc();
|
||
err.hidden = true;
|
||
} catch (e) {
|
||
err.hidden = false; err.textContent = `Could not pull billing distribution: ${(e as Error).message}`;
|
||
}
|
||
});
|
||
|
||
window.addEventListener("pw:step-shown", (evt: any) => {
|
||
if (evt.detail.step !== "lnpa_region") return;
|
||
const s = (window as any).PWIntake.get();
|
||
const existing = s.intake_data?.lnpa_region_allocations || [];
|
||
for (const a of existing) {
|
||
const b3 = body.querySelector<HTMLInputElement>(`input[data-b3="${a.region_code}"]`);
|
||
const b4 = body.querySelector<HTMLInputElement>(`input[data-b4="${a.region_code}"]`);
|
||
if (b3) b3.value = String(a.block_3_pct);
|
||
if (b4) b4.value = String(a.block_4_pct);
|
||
}
|
||
recalc();
|
||
});
|
||
|
||
window.addEventListener("pw:step-next", (evt: any) => {
|
||
const PW = (window as any).PWIntake;
|
||
if (PW.steps[PW.get().step_index] !== "lnpa_region") return;
|
||
const { s3, s4 } = recalc();
|
||
if (Math.abs(s3 - 100) > 0.01 && s3 !== 0) {
|
||
err.hidden = false; err.textContent = `Block 3 column sums to ${s3.toFixed(2)}% — must be 100.00% or 0.`; evt.preventDefault(); return;
|
||
}
|
||
if (Math.abs(s4 - 100) > 0.01 && s4 !== 0) {
|
||
err.hidden = false; err.textContent = `Block 4 column sums to ${s4.toFixed(2)}% — must be 100.00% or 0.`; evt.preventDefault(); return;
|
||
}
|
||
err.hidden = true;
|
||
const allocations: Array<{region_code: string; block_3_pct: number; block_4_pct: number}> = [];
|
||
for (const r of LNPA_REGIONS) {
|
||
const b3 = Number(body.querySelector<HTMLInputElement>(`input[data-b3="${r.code}"]`)?.value) || 0;
|
||
const b4 = Number(body.querySelector<HTMLInputElement>(`input[data-b4="${r.code}"]`)?.value) || 0;
|
||
allocations.push({ region_code: r.code, block_3_pct: b3, block_4_pct: b4 });
|
||
}
|
||
PW.patchIntakeData({ lnpa_region_allocations: allocations });
|
||
|
||
// Also PUT to the API so the /validate endpoint finds the rows
|
||
const st = PW.get();
|
||
if (st.telecom_entity_id) {
|
||
fetch(`/api/v1/lnpa-regions/entity/${st.telecom_entity_id}`, {
|
||
method: "PUT",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({
|
||
reporting_year: st.intake_data?.form_year || new Date().getUTCFullYear() - 1,
|
||
reporting_period: "annual",
|
||
allocations,
|
||
}),
|
||
}).catch(() => {});
|
||
}
|
||
});
|
||
</script>
|