packetdial: add onboard-domain wrapper (GUI Add-a-Domain -> 3-call API flow)
onboard-domain runs POST /domains -> addresses/validate (gen E911 pidflo) -> addresses/create from one JSON body (domain fields + optional `emergency` block), gated --confirm. Reverse- engineered from the OITVOIP wizard screenshots; live-created the real client domain vwp.91912.service (Valley Wide Plastering) + E911 address, and proved the wrapper with a throwaway create->delete (no leftovers, vwp intact). Documented GUI->API mapping + the two manual gaps (voicemail user-defaults, email-send-from-address pending the packetdial.com mailbox) + the domain-type "no"-on-create quirk. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -167,6 +167,10 @@ def main(argv=None) -> int:
|
||||
sp = sub.add_parser("create-moh"); sp.add_argument("domain"); sp.add_argument("--body", required=True); sp.add_argument("--confirm", action="store_true")
|
||||
sp = sub.add_parser("delete-moh"); sp.add_argument("domain"); sp.add_argument("index"); sp.add_argument("--confirm", action="store_true")
|
||||
|
||||
# onboard a new client domain (wizard -> 3-call flow): domain + optional E911 address
|
||||
sp = sub.add_parser("onboard-domain", help="create a domain (+ optional E911 address) from one JSON body")
|
||||
sp.add_argument("--body"); sp.add_argument("--body-file"); sp.add_argument("--confirm", action="store_true")
|
||||
|
||||
# --- raw escape hatch ---
|
||||
sp = sub.add_parser("raw", help="raw request against any v2 path")
|
||||
sp.add_argument("method", choices=["GET", "POST", "PUT", "PATCH", "DELETE"])
|
||||
@@ -379,6 +383,20 @@ def main(argv=None) -> int:
|
||||
_require_confirm(args, "DELETE MOH", f"{args.domain}/{args.index}")
|
||||
_emit(client.delete_moh(args.domain, args.index))
|
||||
|
||||
elif args.cmd == "onboard-domain":
|
||||
if args.body_file:
|
||||
with open(args.body_file, encoding="utf-8") as fh:
|
||||
body = json.load(fh)
|
||||
else:
|
||||
body = _parse_body(args.body)
|
||||
if not isinstance(body, dict) or not body.get("domain"):
|
||||
print("[ERROR] onboard-domain needs a JSON body with 'domain'", file=sys.stderr); sys.exit(2)
|
||||
has_em = "emergency" in body
|
||||
_require_confirm(args, "ONBOARD domain",
|
||||
f"{body['domain']} (reseller {body.get('reseller','?')})"
|
||||
+ (" + E911 address" if has_em else " (no E911 block)"))
|
||||
_emit(client.onboard_domain(body))
|
||||
|
||||
elif args.cmd == "raw":
|
||||
body = _parse_body(args.body)
|
||||
if args.method != "GET":
|
||||
|
||||
@@ -565,3 +565,36 @@ class NetSapiensClient:
|
||||
return self.request(
|
||||
"GET", f"domains/{urllib.parse.quote(domain)}/transcriptions", params=filters or None
|
||||
)
|
||||
|
||||
# ======================================================================
|
||||
# ORCHESTRATION — onboard a new client domain (mirrors the GUI "Add a Domain"
|
||||
# wizard: Basic + Defaults + Limitations -> POST /domains; Emergency tab ->
|
||||
# validate (generates the pidflo) -> create E911 address). Live-proven against
|
||||
# vwp.91912.service 2026-06-22.
|
||||
# ======================================================================
|
||||
def onboard_domain(self, body: dict) -> dict:
|
||||
"""Create a domain and (optionally) its E911 address in one flow.
|
||||
|
||||
body = the POST /domains fields PLUS an optional "emergency" sub-object
|
||||
(the address fields). Returns {"domain":..., "address_validate":...,
|
||||
"address":...}. Requires body["domain"] (full, e.g. vwp.91912.service).
|
||||
"""
|
||||
body = dict(body)
|
||||
emergency = body.pop("emergency", None)
|
||||
body.setdefault("synchronous", "yes")
|
||||
domain = body.get("domain")
|
||||
if not domain:
|
||||
raise PacketDialError("onboard_domain: body must include 'domain' (e.g. acme.91912.service)")
|
||||
result = {"domain": self.create_domain(body)}
|
||||
if emergency:
|
||||
d = urllib.parse.quote(domain)
|
||||
v = self.request("POST", f"domains/{d}/addresses/validate", json_body=dict(emergency))
|
||||
vobj = v[0] if isinstance(v, list) and v else v
|
||||
addr = dict(emergency)
|
||||
addr["address-formatted-pidflo"] = vobj.get("address-formatted-pidflo")
|
||||
addr["emergency-address-id"] = vobj.get("emergency-address-id")
|
||||
result["address_validate"] = {"status": (vobj or {}).get("ValidationStatus")
|
||||
or (vobj.get("address-formatted-pidflo") or {}).get("ValidationStatus")
|
||||
if isinstance(vobj, dict) else None}
|
||||
result["address"] = self.create_address(domain, addr)
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user