Add zero-revenue 499-A filing at $179
New service slug fcc-499a-zero for carriers with no telecom revenue: - $179 instead of $499 (no revenue analysis needed) - Minimal intake: entity, officer, filer ID, filing type only - Skips revenue schedules (blocks 3-4), USF calculations (block 5), traffic study upload, and revenue workbook generation - Fills blocks 1-2 and 6 only, all revenue lines left as zero Compliance checker: shows both options (mutually exclusive checkboxes) Order page: maps form_499a_zero to fcc-499a-zero slug Handler: detects slug and skips revenue pipeline DC Agent shown when either 499-A variant is checked Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5dabac856d
commit
3e04a8fc16
5 changed files with 98 additions and 37 deletions
|
|
@ -32,6 +32,12 @@ const COMPLIANCE_SERVICES: Record<
|
|||
erpnext_item: "FCC-499A",
|
||||
discountable: true,
|
||||
},
|
||||
"fcc-499a-zero": {
|
||||
name: "FCC Form 499-A Filing (Zero Revenue)",
|
||||
price_cents: 17900,
|
||||
erpnext_item: "FCC-499A-ZERO",
|
||||
discountable: true,
|
||||
},
|
||||
"fcc-499a-499q": {
|
||||
name: "FCC Form 499-A + 499-Q Bundle",
|
||||
price_cents: 59900,
|
||||
|
|
@ -284,6 +290,17 @@ const REQUIRED_FIELDS: Record<string, FieldSpec> = {
|
|||
],
|
||||
validators: ["lnpa_sums_100", "de_minimis_calc", "trs_base_nonnegative"],
|
||||
},
|
||||
// Zero-revenue 499-A: minimal intake — no revenue schedules, no traffic study
|
||||
"fcc-499a-zero": {
|
||||
required: [
|
||||
"entity_structure",
|
||||
"officer_1_name", "officer_1_title",
|
||||
"officer_1_street", "officer_1_city", "officer_1_state", "officer_1_zip",
|
||||
"filer_id_499",
|
||||
"filing_type",
|
||||
],
|
||||
soft: ["ceo_name", "trade_names"],
|
||||
},
|
||||
"fcc-499a-499q": {
|
||||
required: [
|
||||
"line_105_primary", "line_105_categories",
|
||||
|
|
@ -340,7 +357,7 @@ const REQUIRED_FIELDS: Record<string, FieldSpec> = {
|
|||
// Entity-level requirements (e.g. "must have an FRN on file before this
|
||||
// service can run"). Checked against the linked telecom_entity.
|
||||
const REQUIRES_ENTITY_FRN: ReadonlySet<string> = new Set([
|
||||
"rmd-filing", "cpni-certification", "fcc-499a", "fcc-499a-499q",
|
||||
"rmd-filing", "cpni-certification", "fcc-499a", "fcc-499a-zero", "fcc-499a-499q",
|
||||
"fcc-499-initial", "stir-shaken", "bdc-filing", "bdc-broadband",
|
||||
"bdc-voice", "calea-ssi", "fcc-63-11-notification", "fcc-full-compliance",
|
||||
]);
|
||||
|
|
@ -393,7 +410,7 @@ function resolveOrderFeeCents(
|
|||
// Compute the baseline per-year price for this order.
|
||||
let perYearPrice = service.price_cents;
|
||||
let baselineNote: string | undefined;
|
||||
if (["fcc-499a", "fcc-499a-499q"].includes(slug)) {
|
||||
if (["fcc-499a", "fcc-499a-zero", "fcc-499a-499q"].includes(slug)) {
|
||||
if (waive_deminimis) {
|
||||
// Waive → full price, overrides any de minimis discount
|
||||
perYearPrice = service.price_cents;
|
||||
|
|
@ -738,7 +755,7 @@ router.post("/api/v1/compliance-orders", async (req, res) => {
|
|||
.map((y: unknown) => Number(y))
|
||||
: null;
|
||||
if (myf && myf.length > 0) {
|
||||
if (!["fcc-499a", "fcc-499a-499q"].includes(service_slug)) {
|
||||
if (!["fcc-499a", "fcc-499a-zero", "fcc-499a-499q"].includes(service_slug)) {
|
||||
res.status(400).json({
|
||||
error: "multi_year_filings only supported for fcc-499a / fcc-499a-499q.",
|
||||
});
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ SERVICE_HANDLERS: dict[str, type] = {
|
|||
"rmd-filing": RMDFilingHandler,
|
||||
"cpni-certification": CPNIFilingHandler,
|
||||
"fcc-499a": Form499AHandler,
|
||||
"fcc-499a-zero": Form499AHandler, # same handler, zero_revenue flag set from slug
|
||||
"fcc-499a-499q": Form499ABundleHandler,
|
||||
"stir-shaken": StirShakenHandler,
|
||||
# BDC triple — same handler, mode auto-resolved from slug
|
||||
|
|
@ -96,6 +97,7 @@ FCC_SERVICE_SLUGS: frozenset[str] = frozenset({
|
|||
"rmd-filing",
|
||||
"cpni-certification",
|
||||
"fcc-499a",
|
||||
"fcc-499a-zero",
|
||||
"fcc-499a-499q",
|
||||
"stir-shaken",
|
||||
"bdc-filing",
|
||||
|
|
|
|||
|
|
@ -419,27 +419,29 @@ class Form499AHandler(BaseServiceHandler):
|
|||
except Exception as exc:
|
||||
logger.warning("499-A checklist PDF conversion failed: %s", exc)
|
||||
|
||||
try:
|
||||
from scripts.document_gen.templates.form_499a_revenue_workbook_generator import (
|
||||
generate_499a_revenue_workbook,
|
||||
)
|
||||
workbook_path = os.path.join(
|
||||
work_dir, f"fcc_499a_revenue_workbook_{order_number}_{date_str}.xlsx",
|
||||
)
|
||||
traffic_study = self._load_traffic_study(entity.get("id"))
|
||||
wb_result = generate_499a_revenue_workbook(
|
||||
entity_name=entity.get("legal_name", ""),
|
||||
filer_id_499=entity.get("filer_id_499", ""),
|
||||
frn=entity.get("frn", ""),
|
||||
reporting_year=int(entity.get("last_filing_year") or 0)
|
||||
or (datetime.utcnow().year - 1),
|
||||
traffic_study=traffic_study,
|
||||
output_path=workbook_path,
|
||||
)
|
||||
if wb_result:
|
||||
generated.append(wb_result)
|
||||
except Exception as exc:
|
||||
logger.warning("499-A revenue workbook generation failed: %s", exc)
|
||||
# Skip revenue workbook for zero-revenue filings
|
||||
if order_data.get("service_slug") != "fcc-499a-zero":
|
||||
try:
|
||||
from scripts.document_gen.templates.form_499a_revenue_workbook_generator import (
|
||||
generate_499a_revenue_workbook,
|
||||
)
|
||||
workbook_path = os.path.join(
|
||||
work_dir, f"fcc_499a_revenue_workbook_{order_number}_{date_str}.xlsx",
|
||||
)
|
||||
traffic_study = self._load_traffic_study(entity.get("id"))
|
||||
wb_result = generate_499a_revenue_workbook(
|
||||
entity_name=entity.get("legal_name", ""),
|
||||
filer_id_499=entity.get("filer_id_499", ""),
|
||||
frn=entity.get("frn", ""),
|
||||
reporting_year=int(entity.get("last_filing_year") or 0)
|
||||
or (datetime.utcnow().year - 1),
|
||||
traffic_study=traffic_study,
|
||||
output_path=workbook_path,
|
||||
)
|
||||
if wb_result:
|
||||
generated.append(wb_result)
|
||||
except Exception as exc:
|
||||
logger.warning("499-A revenue workbook generation failed: %s", exc)
|
||||
|
||||
return generated
|
||||
|
||||
|
|
@ -529,14 +531,24 @@ class Form499AHandler(BaseServiceHandler):
|
|||
await page.click('text="Form 499-A"')
|
||||
await human_delay()
|
||||
|
||||
revenue_lines = self._build_revenue_lines(entity, intake_data)
|
||||
is_zero_revenue = order_data.get("service_slug") == "fcc-499a-zero"
|
||||
|
||||
await self._phase_block_1(page, entity, intake_data)
|
||||
await self._phase_block_2(page, entity, intake_data)
|
||||
await self._phase_blocks_3_4(page, entity, intake_data, revenue_lines)
|
||||
await self._phase_block_5(page, entity, intake_data, revenue_lines)
|
||||
await self._phase_block_6(page, entity, intake_data)
|
||||
await self._phase_submit_traffic_study(page, entity, work_dir)
|
||||
if is_zero_revenue:
|
||||
# Zero-revenue filing: skip revenue schedules and traffic study
|
||||
logger.info("Zero-revenue 499-A — skipping blocks 3-5 and traffic study")
|
||||
revenue_lines = {}
|
||||
await self._phase_block_1(page, entity, intake_data)
|
||||
await self._phase_block_2(page, entity, intake_data)
|
||||
# Blocks 3-4 (revenue) and 5 (USF) left as zero
|
||||
await self._phase_block_6(page, entity, intake_data)
|
||||
else:
|
||||
revenue_lines = self._build_revenue_lines(entity, intake_data)
|
||||
await self._phase_block_1(page, entity, intake_data)
|
||||
await self._phase_block_2(page, entity, intake_data)
|
||||
await self._phase_blocks_3_4(page, entity, intake_data, revenue_lines)
|
||||
await self._phase_block_5(page, entity, intake_data, revenue_lines)
|
||||
await self._phase_block_6(page, entity, intake_data)
|
||||
await self._phase_submit_traffic_study(page, entity, work_dir)
|
||||
|
||||
await human_delay(1.5, 3.0)
|
||||
await page.click('button:has-text("Review")')
|
||||
|
|
|
|||
|
|
@ -144,6 +144,7 @@ window._existingOCN = "";
|
|||
var CHECK_TO_SLUG = {
|
||||
cpni:"cpni-certification", cpni_certification:"cpni-certification",
|
||||
"499a":"fcc-499a", form_499a:"fcc-499a",
|
||||
form_499a_zero:"fcc-499a-zero", "499a-zero":"fcc-499a-zero",
|
||||
"499q":"fcc-499a-499q", form_499q:"fcc-499a-499q",
|
||||
rmd:"rmd-filing", rmd_filing:"rmd-filing",
|
||||
stir:"stir-shaken", stir_shaken:"stir-shaken",
|
||||
|
|
@ -159,6 +160,7 @@ var CHECK_TO_SLUG = {
|
|||
var SLUG_META = {
|
||||
"cpni-certification": {label:"CPNI Annual Certification", price:14900, desc:"47 CFR § 64.2009 annual CPNI cert filed to FCC ECFS", order:"/order/cpni-certification"},
|
||||
"fcc-499a": {label:"FCC Form 499-A Filing", price:49900, desc:"Annual USF contribution filing at USAC E-File", order:"/order/fcc-499a"},
|
||||
"fcc-499a-zero": {label:"FCC Form 499-A (Zero Revenue)", price:17900, desc:"For carriers with no telecom revenue", order:"/order/fcc-499a"},
|
||||
"fcc-499a-499q": {label:"FCC 499-A + 499-Q Bundle", price:59900, desc:"Annual + quarterly USF projections", order:"/order/fcc-499a-499q"},
|
||||
"rmd-filing": {label:"RMD Registration / Recertification", price:21900, gov_fee:10000, gov_fee_label:"FCC RMD filing fee", desc:"Robocall Mitigation Database filing + $100 FCC filing fee", order:"/order/rmd-filing"},
|
||||
"stir-shaken": {label:"STIR/SHAKEN Implementation", price:49900, desc:"STIR/SHAKEN certificate + attestation setup", order:"/order/stir-shaken"},
|
||||
|
|
@ -180,6 +182,8 @@ var frn=params.get("frn")||"";
|
|||
var slugs=[]; var seen={};
|
||||
rawKeys.forEach(function(k){var s=CHECK_TO_SLUG[k]||k; if(SLUG_META[s]&&!seen[s]){slugs.push(s);seen[s]=1}});
|
||||
if(slugs.indexOf("fcc-499a")>=0&&slugs.indexOf("fcc-499a-499q")>=0) slugs.splice(slugs.indexOf("fcc-499a"),1);
|
||||
// Zero-revenue and full 499-A are mutually exclusive
|
||||
if(slugs.indexOf("fcc-499a")>=0&&slugs.indexOf("fcc-499a-zero")>=0) slugs.splice(slugs.indexOf("fcc-499a-zero"),1);
|
||||
|
||||
var listEl=document.getElementById("pw-svc-list");
|
||||
var entityBar=document.getElementById("pw-entity-bar");
|
||||
|
|
@ -253,6 +257,9 @@ function renderServices() {
|
|||
// Deduplicate 499a/499a-499q
|
||||
if(s==="fcc-499a"&&e.target.checked&&selectedSlugs.indexOf("fcc-499a-499q")>=0) selectedSlugs=selectedSlugs.filter(function(x){return x!=="fcc-499a-499q"});
|
||||
if(s==="fcc-499a-499q"&&e.target.checked&&selectedSlugs.indexOf("fcc-499a")>=0) selectedSlugs=selectedSlugs.filter(function(x){return x!=="fcc-499a"});
|
||||
// Zero-revenue ↔ full 499-A mutual exclusion
|
||||
if(s==="fcc-499a"&&e.target.checked) selectedSlugs=selectedSlugs.filter(function(x){return x!=="fcc-499a-zero"});
|
||||
if(s==="fcc-499a-zero"&&e.target.checked) selectedSlugs=selectedSlugs.filter(function(x){return x!=="fcc-499a"&&x!=="fcc-499a-499q"});
|
||||
// STIR/SHAKEN requires OCN — auto-add if not already present
|
||||
if(s==="stir-shaken"&&e.target.checked&&selectedSlugs.indexOf("ocn-registration")<0&&!window._hasExistingOCN) {
|
||||
selectedSlugs.push("ocn-registration");
|
||||
|
|
|
|||
|
|
@ -777,7 +777,8 @@ import Base from "../../layouts/Base.astro";
|
|||
services.push({ id: "stir_shaken", name: "STIR/SHAKEN Implementation", desc: "", price: 499 });
|
||||
break;
|
||||
case "form_499a":
|
||||
services.push({ id: "form_499a", name: "Form 499-A Filing", desc: "", price: 499 });
|
||||
services.push({ id: "form_499a", name: "Form 499-A Filing", desc: "with revenue", price: 499 });
|
||||
services.push({ id: "form_499a_zero", name: "Form 499-A (Zero Revenue)", desc: "no telecom revenue", price: 179, altOf: "form_499a" });
|
||||
if (s === "red") {
|
||||
services.push({ id: "dc_agent", name: "D.C. Registered Agent", desc: "", price: 99, priceLabel: "$99/yr" });
|
||||
}
|
||||
|
|
@ -813,10 +814,11 @@ import Base from "../../layouts/Base.astro";
|
|||
html += `<div class="space-y-2" id="service-list">`;
|
||||
|
||||
for (const svc of services) {
|
||||
const checked = svc.price > 0 && svc.id !== "stir_shaken" ? "checked" : "";
|
||||
// altOf items start unchecked (they're alternatives to the main item)
|
||||
const checked = svc.price > 0 && svc.id !== "stir_shaken" && !svc.altOf ? "checked" : "";
|
||||
const priceLabel = svc.priceLabel || (svc.price > 0 ? `$${svc.price}` : svc.desc || "included");
|
||||
html += `<label class="flex items-start gap-3 p-3 rounded-lg hover:bg-gray-50 cursor-pointer transition">
|
||||
<input type="checkbox" class="svc-checkbox mt-1 accent-pw-600" data-id="${svc.id}" data-price="${svc.price}" ${checked} />
|
||||
html += `<label class="flex items-start gap-3 p-3 rounded-lg hover:bg-gray-50 cursor-pointer transition" ${svc.altOf ? `data-alt-of="${svc.altOf}"` : ""}>
|
||||
<input type="checkbox" class="svc-checkbox mt-1 accent-pw-600" data-id="${svc.id}" data-price="${svc.price}" ${svc.altOf ? `data-alt-of="${svc.altOf}"` : ""} ${checked} />
|
||||
<div class="flex-1">
|
||||
<span class="font-medium text-gray-900">${svc.name}</span>
|
||||
${svc.desc && svc.price > 0 ? `<span class="text-xs text-gray-500 ml-1">${svc.desc}</span>` : ""}
|
||||
|
|
@ -838,6 +840,27 @@ import Base from "../../layouts/Base.astro";
|
|||
paymentIcons.classList.remove("hidden");
|
||||
reminderCta.classList.add("hidden");
|
||||
|
||||
// Mutual exclusion for alt items (e.g. 499-A vs 499-A Zero Revenue)
|
||||
ctaContent.querySelectorAll(".svc-checkbox[data-alt-of]").forEach((alt) => {
|
||||
alt.addEventListener("change", () => {
|
||||
if (alt.checked) {
|
||||
const main = ctaContent.querySelector(`.svc-checkbox[data-id="${alt.dataset.altOf}"]`);
|
||||
if (main) main.checked = false;
|
||||
updateTotal();
|
||||
}
|
||||
});
|
||||
});
|
||||
ctaContent.querySelectorAll(".svc-checkbox:not([data-alt-of])").forEach((main) => {
|
||||
main.addEventListener("change", () => {
|
||||
if (main.checked) {
|
||||
ctaContent.querySelectorAll(`.svc-checkbox[data-alt-of="${main.dataset.id}"]`).forEach((alt) => {
|
||||
alt.checked = false;
|
||||
});
|
||||
}
|
||||
updateTotal();
|
||||
});
|
||||
});
|
||||
|
||||
// Total calculation
|
||||
function updateTotal() {
|
||||
const boxes = ctaContent.querySelectorAll(".svc-checkbox");
|
||||
|
|
@ -845,8 +868,8 @@ import Base from "../../layouts/Base.astro";
|
|||
let pricedCount = 0;
|
||||
const selectedIds = [];
|
||||
|
||||
// If 499-A is unchecked, hide and uncheck DC Agent
|
||||
const f499Checked = Array.from(boxes).some((cb) => cb.dataset.id === "form_499a" && cb.checked);
|
||||
// If neither 499-A variant is checked, hide and uncheck DC Agent
|
||||
const f499Checked = Array.from(boxes).some((cb) => (cb.dataset.id === "form_499a" || cb.dataset.id === "form_499a_zero") && cb.checked);
|
||||
boxes.forEach((cb) => {
|
||||
if (cb.dataset.id === "dc_agent") {
|
||||
const label = cb.closest("label");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue