Skip to content

feat(portin): band portin command surface for porting flows#10

Open
kshahbw wants to merge 6 commits intomainfrom
feat/portin
Open

feat(portin): band portin command surface for porting flows#10
kshahbw wants to merge 6 commits intomainfrom
feat/portin

Conversation

@kshahbw
Copy link
Copy Markdown
Contributor

@kshahbw kshahbw commented May 7, 2026

Summary

  • Adds band portin covering the six end-to-end porting flows that complete entirely via the public Numbers API (TF validate, on-net portin, automated off-net, TF Phase 1, bulk, lifecycle ops).
  • Defends against two documented Bandwidth API traps: the supp 7300 silent-fail (always polls until LastModifiedDate advances or 7300 surfaces), and the toll-free Phase 1 gate (exits 4 with the upgrade path instead of leaving an order in limbo).
  • Out-of-scope flows (port-out, manual TF, internal TF, NASC, international) are explicitly documented to fail-fast rather than strand a CLI user mid-flow.

What's in here

11 subcommands under band portin:

validate-tf, create, get, list, submit, supp, cancel,
history, notes (add/list), upload-loa, bulk (create/get/get-tns/list)

create chains --loa document upload, supports --customer-order-id + --if-not-exists for idempotent agent retries, and surfaces the orderId on partial-failure paths. supp always polls for propagation by default — the whole point of the command is to detect 7300, and a single GET fires too fast to see the change.

Verified live on stage

Smoke pass against stage.api.bandwidth.com exercised: validate-tf, create (with chained LOA), get, list (incl. --customer-order-id filter), submit path, supp (real propagation observed), notes add (returns real noteId from Location header), notes list, history, upload-loa standalone, cancel, bulk list. All cleaned up after testing.

Bugs found and fixed during integration:

  • E.164 with + is required in request bodies, not stripped 10-digit
  • GET responses don't carry OrderId (it's in the URL) — flatten threads it through
  • Stable plain shape's errorCode no longer captures HTTP 201 success codes
  • Notes need UserId field (auto-populated from active profile)
  • LOA upload needs ?documentType=LOA
  • list requires page/size; param names are startdate/enddate (not modifiedDateFrom/To)

Agent ergonomics

AGENTS.md grew with:

  • Locked v1 --plain shapes per command (table)
  • Port-in state machine with which states are stable --wait targets
  • Three-line reconciliation idiom (--customer-order-id + --if-not-exists)
  • Error-code registry (1022, 5217, 7300, 7615, 7626, 7640, 7642, 7643, 7671)

Test plan

  • go build ./... clean
  • go vet ./... clean
  • go test ./... — 27 packages pass, 12 new tests in cmd/portin/...
  • Live smoke pass on stage (validate-tf, create, get, list, submit, supp, cancel, notes, history, upload-loa, bulk list)
  • Verify --if-not-exists against an existing order on stage (idempotent return)
  • Test Phase 1 gate on a non-Phase-1 account (exit 4 with upgrade message)

kshahbw added 4 commits May 6, 2026 22:45
Adds `band portin` covering the porting flows that complete entirely via
the public Numbers API: standalone toll-free portability validation,
on-net domestic, automated off-net, toll-free Phase 1, bulk, and
lifecycle ops (notes, supp, cancel, history, document upload).

Out-of-scope flows that require Bandwidth ops or the Dashboard
(port-out, manual TF, internal TF, NASC, international) are
intentionally not surfaced — `band portin create` exits 4 with a clear
upgrade message when the account lacks Phase 1 automation, rather than
quietly producing a stuck order.

Two correctness defenses worth flagging:

  * `supp` always does a verifying GET after the PUT and exits 1 if
    error code 7300 surfaces. This catches the documented Bandwidth
    behavior where a wireless_to_wireless supp past FOC returns 200 on
    PUT but never propagates to Neustar.

  * `validate-tf` exits 1 with per-number reasons when any TN reports
    portable=false, instead of returning 0 with a buried negative
    result.

Stable `--plain` shapes are documented in AGENTS.md and locked by
golden tests for the v1 contract. `create` and `bulk create` accept
`--customer-order-id` + `--if-not-exists` for idempotent agent retries.

Adds `api.PostMultipart` for the LOA document upload, mirroring the
existing `PutRaw` pattern.
Bugs found running the full lifecycle (validate-tf, create, get, history,
notes add/list, supp, cancel, upload-loa) against stage.api.bandwidth.com:

  * Numbers are E.164 with the + prefix in request bodies, not bare 10-digit.
    The earlier code mirrored tnoption's stripE164 — which is only for the
    list-filter query param, not the body.

  * GET /portins/{id} responses don't carry OrderId (it's in the URL).
    flattenPortInResult takes a fallbackOrderID so plain output is stable.

  * The plain shape's errorCode was capturing the 201 success Status.Code
    on creates. extractErrorCode now only looks inside Errors / ErrorList
    blocks and skips HTTP 2xx codes.

  * POST /portins/{id}/notes requires a UserId field — empty value returns
    errorCode 5217. Added cmdutil.ActiveUserID() that pulls from the active
    profile's client ID and use it on every notes add.

  * Notes responses use Id and LastDateModifier (not NoteId / Timestamp);
    flattenNotes was producing empty fields.

  * POST /portins/{id}/loas requires documentType either as a query param or
    a header. Default to ?documentType=LOA — by far the most common case.

  * When create chains an LOA upload and the upload fails, the orderId is
    now surfaced in the error message so the user can retry the upload
    rather than orphaning a draft.

  * findByCustomerOrderID now treats 404 as "no match" rather than
    propagating, since the search endpoint may not exist on every account.

Open: GET /portins?... and GET /bulkPortins return 404 on stage, so
`portin list`, `portin bulk list`, and customer-order-id lookups for
`--if-not-exists` cannot find existing orders. Need to find the right
search/list path; tracked separately.
  * list: page and size are required query params on the Numbers API.
    Without them the endpoint 404s. Defaults page=1 size=30. Also fixed
    the date param names — the API uses startdate/enddate (YYYY-MM-DD),
    not modifiedDateFrom/modifiedDateTo (those are bulk-only). Added
    --tn, --order-tn, --customer-order-id, --pon filters per the docs.

  * bulk list: same page+size requirement, kept modifiedDateFrom/
    modifiedDateTo since those are correct for bulk. Added --order-date
    and --order-details for completeness.

  * findByCustomerOrderID (used by --if-not-exists): now passes the
    required page+size, so existing-order lookup actually works.

  * supp: removed --wait. Always polls for propagation by default — the
    point of the command is to detect the documented 7300 silent-fail,
    and a single GET fires too fast to see the change. Now captures the
    pre-PUT LastModifiedDate, then polls until that timestamp advances
    or 7300 surfaces or timeout expires.

  * notes add: returns the real noteId now. The endpoint responds 201
    with empty body and the new resource URL in the Location header;
    added api.Client.PostXMLReturnLocation to expose it, and parse the
    trailing path segment as noteId.

AGENTS.md updated to reflect the new supp semantics.
Closes the gaps that kept band portin merely "agent-friendly" rather
than agent-native:

  * The locked v1 --plain shapes for every porting command, so agents
    can write parsers without source-diving or trial-and-error.

  * The port-in state machine, including which transitions are stable
    targets for --wait and which are terminal.

  * The reconciliation idiom (--customer-order-id + --if-not-exists)
    in three lines instead of buried prose.

  * A small registry of error codes encountered on porting endpoints
    (1022, 5217, 7300, 7615, 7626, 7640, 7642, 7643, 7671) with where
    they surface and what to do.
@kshahbw kshahbw requested review from a team as code owners May 7, 2026 03:42
@bwappsec
Copy link
Copy Markdown

bwappsec commented May 7, 2026

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues
Code Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

kshahbw added 2 commits May 6, 2026 23:46
The Numbers API has a quirk where searching by customerOrderId without a
status filter excludes draft-state orders entirely — observed empirically
on stage. Idempotent retries on freshly-created orders (which are still
in VALIDATE_DRAFT_TFNS / VALID_DRAFT_TFNS) silently created duplicates.

Fix: iterate findByCustomerOrderID across all 17 documented status
values, short-circuiting on the first hit. Live/active states are
checked first since they're the most common idempotency target.

Also: when --if-not-exists hits an existing order, follow up with a
full GET so the returned plain shape includes focDate, numbers, etc.
The list response is summary-only; without this fetch the idempotent
return shape was missing fields.

End-to-end verified on stage: same orderId returned across retries,
full shape populated on the second call.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants