feat(portin): band portin command surface for porting flows#10
Open
feat(portin): band portin command surface for porting flows#10
Conversation
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.
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
band portincovering 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).LastModifiedDateadvances or 7300 surfaces), and the toll-free Phase 1 gate (exits 4 with the upgrade path instead of leaving an order in limbo).What's in here
11 subcommands under
band portin:createchains--loadocument upload, supports--customer-order-id+--if-not-existsfor idempotent agent retries, and surfaces the orderId on partial-failure paths.suppalways 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.comexercised:validate-tf,create(with chained LOA),get,list(incl.--customer-order-idfilter),submitpath,supp(real propagation observed),notes add(returns realnoteIdfrom Location header),notes list,history,upload-loastandalone,cancel,bulk list. All cleaned up after testing.Bugs found and fixed during integration:
+is required in request bodies, not stripped 10-digiterrorCodeno longer captures HTTP 201 success codesUserIdfield (auto-populated from active profile)?documentType=LOAlistrequires page/size; param names arestartdate/enddate(notmodifiedDateFrom/To)Agent ergonomics
AGENTS.md grew with:
--plainshapes per command (table)--waittargets--customer-order-id+--if-not-exists)Test plan
go build ./...cleango vet ./...cleango test ./...— 27 packages pass, 12 new tests incmd/portin/...--if-not-existsagainst an existing order on stage (idempotent return)