feat(domain): migrate domain list to v3, add premium/pricing support (preview, do not merge) - #211
feat(domain): migrate domain list to v3, add premium/pricing support (preview, do not merge)#211jpage-godaddy wants to merge 18 commits into
Conversation
…ing support Preview migration against the domains v3 test environment (not yet in prod): - `domain list` now calls v3 `GET /domain-names` instead of the v1 list endpoint. `--status` stays repeatable (comma-joined client-side per the API's `style: form, explode: false`); the default "hide non-visible domains" view maps to `lifecycleGroups` excluding TERMINAL. Output fields and json-schema type move to the v3 `Domain` shape. - Cherry-picked the `listDomains` operation, its `statuses`/`lifecycleGroups` filtering, and Afternic premium-domain support (`Fee`/`FeeType`, `TermPrice.fees/firstTermPrice`, `RegistrationQuote.fees/inventory`, `Registration.fees`, `Consent.acknowledgedFees`) into the vendored v3 spec from the canonical source repo, since the private prototype bundle we normally sync from lags behind. Bulk `check-availability`'s domain cap corrected to the now-official 25 (was 50). - `domain quote`/`domain purchase` wire the premium-fee flow end to end: the quote's `fees`/`inventory` are surfaced and cached, and `purchase` echoes the fees back into `consent.acknowledgedFees` (required by the server for a premium purchase to succeed). - `domain available` lists every priced registration term (not just a single 1-year headline) as a nested table, including any first-term promotion. Deliberately NOT included: `replaceDNSRecord` (PUT), `setAutoRenew`, and `setTransferLock` — the first is confirmed fixed live in test but still excluded pending a decision on whether to simplify `dns set`'s existing workaround; the latter two are new v3.2 endpoints not yet wired to any command. `domain agreements` remains on v1 (no v3 equivalent yet). This targets the v3 API's test environment. The domains team has not yet shipped this batch to prod — do not merge until v3 is confirmed live in prod and the spec has been re-synced against the shipped version. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Preview migration of the Domains CLI to newer v3 Domain Lifecycle Management API shapes, including premium-domain fee acknowledgement and richer pricing/term output, backed by an updated vendored OpenAPI spec.
Changes:
- Migrate
gddy domain listto v3listDomainsand update default fields/schema to the v3Domainshape. - Add premium-domain fee plumbing: cache quote
fees, surface them indomain quoteoutput, and echo them intoconsent.acknowledgedFeesondomain purchase. - Update
domain availableto emit per-term pricing as a nestedterms[]table and sync spec changes (listDomains, fees, availability cap).
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| rust/src/quote_cache.rs | Cache premium quote fees alongside existing quote metadata. |
| rust/src/domain/suggest.rs | Update test fixtures for new v3 suggestion fields (fees/first-term/recommended). |
| rust/src/domain/quote.rs | Surface premium inventory/fees in quote output and cache fees for purchase. |
| rust/src/domain/purchase.rs | Echo cached quote fees into purchase consent; align request struct fields with new spec. |
| rust/src/domain/list.rs | Switch domain list to v3 listDomains and adjust filtering/default fields. |
| rust/src/domain/common.rs | Remove now-unused headline pricing helper (available now emits all terms). |
| rust/src/domain/available.rs | Replace headline price output with per-term terms[] nested table output. |
| rust/domains-client/src/lib.rs | Update generated-client test expectations for new consent/registration fields. |
| rust/domains-client/openapi/swagger_domains.v3.yaml | Vendor spec updates for listDomains, fee types, acknowledgedFees, and availability cap. |
| rust/domains-client/openapi/domains.oas3.json | Mirror spec updates in merged OAS3 JSON consumed by codegen. |
Suppressed comments (1)
rust/src/domain/list.rs:113
listDomainsis cursor-paginated (returns aDomainCollectionwith pagination links), but this handler only sends a single request and returnsitemsfrom the first page. For accounts with more domains than the API default page size,gddy domain listwill silently omit the remaining domains.
Consider looping while a links entry with rel == "next" exists: extract pageToken from its href, call list_domains().page_token(...) (and direction if required), and accumulate items until no next link remains (or until the CLI --limit is satisfied).
let client = make_client(&ctx).await?;
let mut req = client.list_domains();
if !statuses.is_empty() {
// `statuses` is `style: form, explode: false` — one
// comma-joined value, not repeated `statuses=` pairs
// (progenitor always seq-serializes a `Vec` as repeated pairs
// regardless of the spec's `explode` setting; see
// `comma_joined`'s doc comment / DEVEX-882).
req = req.statuses(comma_joined(statuses));
} else if visible_only {
req = req.lifecycle_groups(
comma_joined(
DEFAULT_VISIBLE_GROUPS
.into_iter()
.map(str::to_string)
.collect(),
)
.into_iter()
.map(types::DomainLifecycleGroup::from)
.collect::<Vec<_>>(),
);
}
let resp = match req.send().await {
Ok(r) => r,
Err(e) => return Err(api_error("listing domains", debug, e).await),
};
let domains: Vec<serde_json::Value> = resp
.into_inner()
.items
.unwrap_or_default()
.iter()
.map(serde_json::to_value)
.collect::<std::result::Result<_, _>>()
.map_err(|e| {
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
CachedQuote.fees's doc comment said "register" must echo the fees back into consent.acknowledgedFees; the user-facing command is `domain purchase`. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (2)
rust/src/domain/list.rs:106
listDomainsreturns a paginatedDomainCollection(default pageSize is 100, with cursor-basedpageToken+links[rel=next]). The handler currently performs a singlesend()and serializes only that page’sitems, so accounts with more than one page of domains will get truncated results.
Consider looping until there is no next link (parse pageToken/optional direction from the href) and accumulating items across pages; also consider setting pageSize to the API max (200) to reduce requests.
let resp = match req.send().await {
Ok(r) => r,
Err(e) => return Err(api_error("listing domains", debug, e).await),
};
let domains: Vec<serde_json::Value> = resp
rust/src/domain/list.rs:88
- The
statuses/lifecycleGroupsquery params require thecomma_joinedworkaround (style: form, explode: false), but there’s no regression test here to prove we keep sending a singlestatuses=ACTIVE,EXPIRED(and the default visiblelifecycleGroups=...) rather than repeatedstatuses=pairs.
Add an httpmock-based test similar to rust/src/domain/agreements.rs’s tlds_are_sent_as_a_single_comma_joined_query_param, covering both multiple --status values and the default visible-only lifecycleGroups path.
This issue also appears on line 102 of the same file.
// comma-joined value, not repeated `statuses=` pairs
// (progenitor always seq-serializes a `Vec` as repeated pairs
// regardless of the spec's `explode` setting; see
// `comma_joined`'s doc comment / DEVEX-882).
req = req.statuses(comma_joined(statuses));
Regression tests mirroring agreements.rs's existing tlds test: httpmock asserts `domain list` sends one `statuses=ACTIVE,EXPIRED` (or the default-view `lifecycleGroups=...`) query param, not repeated pairs — the exact shape the live API rejects with MISMATCH_FORMAT. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
`listDomains` is cursor-paginated (DomainCollection + links[rel=next]), but the handler only ever sent one request and returned that page's items — accounts with more domains than one API page got silently truncated results. Flagged by Copilot review. fetch_domains() now follows links[rel=next] until the API reports no further page, requesting the API's max pageSize (200) each time to minimize round trips against a tightly rate-limited API. An explicit --limit/--offset window (already wired via cli-engine's pagination pipeline) short-circuits the fetch once satisfied, so a small --limit doesn't pay for pages it'll just discard; unflagged, every domain is fetched, matching the command's pre-v3 behavior and the pagination opt-in's own documented invariant. A defensive MAX_PAGES cap treats a malformed/looping next link as an error rather than a silent partial result. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (1)
rust/src/domain/list.rs:30
MAX_PAGES’ doc comment says hitting the cap “is treated as a bug … rather than silently returned as if it were the complete list”, butfetch_domainscurrently just exits the loop and returnsOk(items)even if arel=nexttoken is still present. That’s misleading for maintainers and future debugging of truncated results.
/// Defensive cap on pages fetched for one invocation. No real account should
/// ever approach `MAX_PAGE_SIZE * MAX_PAGES` (10,000) domains; hitting this
/// is treated as a bug (a malformed or looping `next` link) rather than
/// silently returned as if it were the complete list.
const MAX_PAGES: usize = 50;
fetch_domains()'s doc comment already claimed exceeding MAX_PAGES "is treated as a bug ... rather than silently returned as if it were the complete list," but the loop just broke and returned Ok(items) regardless of whether a next page was still pending. Flagged by Copilot review. fetch_domains now returns cli_engine::Result directly (folding the domains_client-error conversion in, since the loop needs its own error path too) and only falls through past the loop when MAX_PAGES pages were exhausted with page_token still Some — every other exit returns early. Added a regression test asserting the error path fires after exactly MAX_PAGES requests against a mock that always advertises a next page. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.
Suppressed comments (2)
rust/src/domain/list.rs:343
- The pagination tests only exercise absolute
hrefvalues, but the spec’s link examples commonly use relative paths (e.g./v3/domains/...). Using a relativehrefhere would better pin the real-world behavior and would have caught theUrl::parseissue.
fn next_page_token_parses_token_and_direction_from_the_next_link() {
let links = next_link(
"https://api.example.com/v3/domains/domain-names?pageToken=abc123&pageTokenDirection=forward",
);
let (token, direction) = next_page_token(&links).expect("next link present");
rust/src/domain/available.rs:78
shared_currencylooks only atprice/renewal_price, but the handler can emitfirstTermPriceinterms. If (now or in a future API version) a term includes onlyfirstTermPricewith a currency code, the top-levelcurrencyfield would be omitted even though the information is available.
/// The currency code shared by a domain's priced terms, sourced from whichever
/// term has a price or renewal price first (all terms use the same currency in
/// practice, so one top-level field covers every row in `terms`).
fn shared_currency(prices: &[types::TermPrice]) -> Option<String> {
prices
.iter()
.find_map(|t| t.price.as_ref().or(t.renewal_price.as_ref()))
.and_then(|m| m.currency_code.as_ref())
.map(|c| c.to_string())
next_page_token() used url::Url::parse(href), which rejects relative references outright — but the v3 spec's own link examples use relative paths (e.g. /v3/domains/domain-names?...). Against a real response shaped that way, pagination would have silently stopped after page one. Flagged by Copilot review. Parse only the query string (everything after the first '?') via url::form_urlencoded instead of the whole href as a URL, which works for both relative and absolute hrefs. Added a regression test using a relative href, and switched the existing multi-page/error-path tests to relative hrefs too so they'd have caught this. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (3)
rust/src/domain/quote.rs:384
feescaching silently drops JSON serialization errors via.ok(). If serialization ever fails (e.g., a future type change), the quote will still be shown butdomain purchasewill later fail withquote_mismatchbecauseconsent.acknowledgedFeescan’t be echoed back. This should fail fast with a clear message (similar toprofile_json) instead of silently cachingNone.
fees: quote
.fees
.as_ref()
.filter(|f| !f.is_empty())
.and_then(|f| serde_json::to_value(f).ok()),
rust/src/domain/list.rs:105
- The PR description says true cursor pagination for
domain listis “deliberately not included” and that the handler fetches only the first page. This implementation now followslinks[rel=next]across pages viafetch_domains, so the description looks out of date and could mislead reviewers/users about behavior and rate-limit impact.
/// Fetch every domain matching `statuses`/`visible_only`, following v3's
/// `links[rel=next]` cursor until the API reports no further page — or until
/// `stop_at` items have been accumulated, when an explicit `--limit`/
/// `--offset` window needs no more than that many (cli-engine's own
/// pagination pipeline slices the exact window from whatever this returns;
rust/domains-client/openapi/swagger_domains.v3.yaml:1156
- A safety note explaining why
replaceDNSRecord(PUT) was removed is no longer present. Given the historical zone-wipe behavior (#136) and the PR description stating this endpoint is still deliberately excluded, keeping a short warning here helps prevent accidental reintroduction without a re-verification.
/zones/{zone}/dns-records/{recordId}:
… serialization Two more Copilot findings: - The comment explaining why replaceDNSRecord (PUT) was removed (#136 — silently wipes the zone) had been dropped from the vendored spec at some point during this branch's history, despite being deliberately restored earlier. Restored it verbatim; confirmed the rest of the file is otherwise byte-identical to origin/main for this path. - quote.rs cached a quote's `fees` via `serde_json::to_value(f).ok()`, silently discarding a serialization failure as `None`. That would surface at `domain purchase` as a confusing `quote_mismatch` instead of here. Now fails fast with a clear message, mirroring the existing `profile_json` pattern in the same function. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (3)
rust/src/domain/list.rs:43
parse_statusesuppercases each--statusvalue twice (to_uppercase()in both validation and return). This does extra allocation/work for every status value; compute the uppercase string once and reuse it for both the enum validation and the returned wire form.
fn parse_statuses(raw: &[String]) -> Result<Vec<String>> {
raw.iter()
.map(|s| {
types::DomainStatus::try_from(s.to_uppercase().as_str())
.map(|_| s.to_uppercase())
.map_err(|_| CliCoreError::message(format!("invalid --status {s:?}")))
rust/domains-client/openapi/domains.oas3.json:18
- The OpenAPI
servers[0].urlis now set to the OTE (test) host. If any code (or future consumers) uses the generated client’s default base URL from the spec, this can cause unintended calls to test instead of prod. Since the CLI already selects the environment via config (config.domains_api_url), consider keeping the spec’s server URL pointed at the production host and relying on runtime config for test/prod selection.
"servers": [
{
"url": "https://api.ote-godaddy.com",
"description": "Domains API host"
}
rust/src/domain/quote.rs:56
fees_to_jsonalways emits atypekey, but its value can benullwhenf.type_is absent. Thatnullwill leak into--output jsonand the nested table output; it’s clearer to omit the key when it’s missing (consistent with how other optional fields are handled in this file).
let mut out = json!({
"type": f.type_.as_ref().map(|t| t.to_string()),
});
…ees_to_json Two more Copilot findings: - parse_statuses called s.to_uppercase() twice per value (once for DomainStatus validation, again for the returned wire form). Compute it once and reuse it. - fees_to_json always emitted a "type" key, leaking a JSON null into --output json / the nested table when a fee's type is absent. Now omitted when missing, consistent with how every other optional field in this file is handled. (A third finding — the vendored spec's cosmetic servers[0].url — was evaluated and left as-is: this field is never read at runtime, every client construction in this codebase goes through client_with_auth with an explicitly-resolved base URL, and this branch is deliberately scoped to the test environment per the PR description, so pointing it at the OTE host is correct for now rather than a drive-by prod repoint.) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (1)
rust/src/domain/list.rs:236
stop_atis computed viactx.middleware.offset.max(0) + limit, which can overflow for large--offsetvalues (panics in debug builds; wraps in release). Using a saturating/checked add avoids overflow while preserving the existing fallback-to-usize::MAXbehavior.
let limit = ctx.middleware.limit;
let stop_at = (limit > 0).then(|| {
usize::try_from(ctx.middleware.offset.max(0) + limit).unwrap_or(usize::MAX)
});
ctx.middleware.offset.max(0) + limit is an i64 addition that panics on overflow in debug builds (and wraps in release) for a large enough --offset — an unbounded, user-controlled CLI flag. Flagged by Copilot review. saturating_add avoids that while preserving the existing fallback to usize::MAX when the sum doesn't fit. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (1)
rust/src/domain/available.rs:77
shared_currencyonly considerspriceandrenewal_pricewhen picking the currency code. If the API ever returns a term where onlyfirst_term_priceis populated (andprice/renewal_priceare absent), the command will emitfirstTermPricevalues but omit the top-levelcurrency, leaving the output ambiguous.
Consider also looking at first_term_price when determining the shared currency.
prices
.iter()
.find_map(|t| t.price.as_ref().or(t.renewal_price.as_ref()))
.and_then(|m| m.currency_code.as_ref())
.map(|c| c.to_string())
shared_currency() only checked price/renewalPrice; a term carrying only a firstTermPrice promotion (schema permits it, even though every live example so far pairs it with a price) would render firstTermPrice values with no top-level currency to interpret them against. Flagged by Copilot review. Added a regression test for that case. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
rust/src/domain/purchase.rs:299
Registration.feesis markedreadOnly: truein the v3 spec (rust/domains-client/openapi/swagger_domains.v3.yaml:2368-2377), but the execute request body is built withfees: vec![]. This is safe only if the generatedtypes::Registrationskips serializing empty vectors; otherwise the CLI would send a read-only field and risk a 400/422 from strict servers. Consider adding a unit test assertingserde_json::to_value(®istration)does not include afeeskey when it’s empty, or (if supported by the generated type) avoid setting/sending the field entirely.
expires_at: None,
// Server-populated on the response; not sent in the request.
fees: vec![],
links: vec![],
operation_id: None,
…ent as [] Both Consent.acknowledgedFees (minItems: 1 when present, "omit when the quote carries no purchase fees") and Registration.fees (readOnly) are constructed with vec![] for the common non-premium purchase. That's only correct because progenitor generates skip_serializing_if = "Vec::is_empty" for both fields — verified by inspection, now pinned by a test so a future spec/codegen change can't silently start sending an empty array and breaking every non-premium purchase. Flagged by Copilot review. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (3)
rust/domains-client/openapi/domains.oas3.json:18
- The spec’s
servers[0].urlis used byrust/tools/generate-api-catalog/src/openapi.rs:401-404to derive the catalogbaseUrl. Setting it to the OTE host will cause generated API catalog metadata to point at test instead of prod. Consider keeping the prod host as the first server (and optionally listing OTE as a secondary server) so catalog consumers default to prod.
"servers": [
{
"url": "https://api.ote-godaddy.com",
"description": "Domains API host"
}
rust/domains-client/openapi/domains.oas3.json:2091
- This schema updates availability checks to 1–25 domains (
maxItems: 25), but other descriptive text in the same spec still says 1–50 (e.g.info.descriptionand theDiscoverytag description near the top of the file). That leaves the spec internally inconsistent for readers and downstream tooling that surfaces descriptions.
"AvailabilityCheckCriteria": {
"title": "Availability Check Criteria",
"description": "Criteria for an availability check. Specifies 1\u201325 domain names and optional parameters that influence how the check is performed. This controller does not persist the check; there is no check identity or poll URL.\n",
"type": "object",
rust/domains-client/openapi/swagger_domains.v3.yaml:1663
- The AvailabilityCheckCriteria limit is updated to 1–25 domains here, but the spec still describes availability batch checks as accepting 1–50 domains elsewhere (e.g. the top-level API description around line 72, the
Discoverytag description around line 117, and thecheckAvailabilityoperation description around line 395). Those should be updated too so the spec is consistent.
AvailabilityCheckCriteria:
title: Availability Check Criteria
description: 'Criteria for an availability check. Specifies 1–25 domain names and optional parameters that influence
how the check is performed. This controller does not persist the check; there is no check identity or poll URL.
…5 cap AvailabilityCheckCriteria's maxItems was corrected 50->25 earlier this branch, but three other description strings still said 1-50 (the top- level API description, the Discovery tag description, and checkAvailability's own operation description). Flagged by Copilot review — the spec was internally inconsistent for readers and downstream tooling that surfaces these descriptions. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copilot review flagged that generate-api-catalog's resolve_catalog_base_url derives every non-prod environment's URL by substituting the host in this spec's servers[0].url, which requires that value to be prod-canonical — but the currently-checked-in domains.oas3.json on this branch has it set to OTE, since this preview branch's spec syncs were done by hand against the test environment rather than by running this script. Leaving the OTE value as-is per discussion — this branch isn't meant to merge until v3 ships to prod and gets a real spec sync, at which point running this script restores the prod-canonical value. Added a comment here so the next person regenerating the spec understands the discrepancy instead of being surprised by it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.
Suppressed comments (2)
rust/src/domain/list.rs:115
next_page_tokensilently drops an unrecognizedpageTokenDirectionvalue by converting the parse failure toNone. Sincerel=nextis treated as a guarantee that more data exists, an invalid direction should be handled like other unparseable pagination links (error rather than potentially issuing the wrong next-page request).
match key.as_ref() {
"pageToken" => token = Some(value.into_owned()),
"pageTokenDirection" => {
direction = types::ListDomainsPageTokenDirection::try_from(value.as_ref()).ok();
}
rust/src/domain/available.rs:145
- The handler can emit a top-level
currencyeven whentermsis omitted (e.g., if allTermPriceentries are filtered out byterm_to_jsondue to a missingperiod). That produces an inconsistent payload/table where currency has no corresponding term rows. Emittingcurrencyonly when at least one term is emitted keeps the output self-consistent with the schema comment.
let prices = body.prices.unwrap_or_default();
if let Some(currency) = shared_currency(&prices) {
result["currency"] = json!(currency);
}
…rrency/terms consistent Two more Copilot findings: - next_page_token silently dropped an unrecognized pageTokenDirection (e.g. a value outside backward/forward) to None instead of erroring, inconsistent with treating every other unparseable part of a guaranteed-more-data next link as a bug. Distinguished "absent" (fine, the field is documented optional) from "present but invalid" (now Unparseable) so an ordinary link with no direction still works. - domain available could emit a top-level currency with no corresponding terms rows if every TermPrice happened to lack a period (term_to_json drops those, shared_currency doesn't care about period at all) — a self-inconsistent payload. currency is now only set alongside a non-empty terms array. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.
Suppressed comments (2)
rust/src/domain/available.rs:154
termsoutput is currently gated onshared_currency(&prices)returningSome(_). The OpenAPIsimple-moneyschema allowscurrencyCodeto be absent (andformat_moneyalready handles missing currency by defaulting decimals), so this can drop all pricedtermsfrom the output even when prices are present. Emittermswhenever they exist, and only add the optional top-levelcurrencyfield when it can be derived.
if !terms.is_empty()
&& let Some(currency) = shared_currency(&prices)
{
result["currency"] = json!(currency);
result["terms"] = json!(terms);
rust/domains-client/openapi/domains.oas3.json:16
- This spec's
servers[0].urlis used as the catalog domain's prod-shapedbaseUrl(seetools/generate-api-catalog/src/openapi.rs:401-404andenvironments::resolve_catalog_base_url). Setting it to the OTE host causes non-prod environment rewriting to produce invalid hosts likeapi.ote.ote-godaddy.com(substitute_env_hostassumes a*.godaddy.comprod canonical). Keep this URL prod-canonical and rely on runtime env resolution for test/ote.
"url": "https://api.ote-godaddy.com",
My previous fix (keeping currency from appearing with no terms) went too far the other way: gating terms on shared_currency(&prices) succeeding meant terms disappeared entirely when every priced term lacked a currencyCode (itself optional per simple-money) even though the prices/terms were perfectly valid. Flagged by Copilot review. terms is now emitted whenever any priced term exists, full stop; currency is the one gated on terms being non-empty, so it still never appears with nothing to interpret it against, but pricing with no currency code no longer loses its terms entirely. (The other repeated finding this round — domains.oas3.json's OTE servers[0].url breaking generate-api-catalog's env-derivation convention — is already covered by the explanatory comment added in 574d5e6; leaving as-is per prior discussion, this branch isn't meant to merge before a real spec sync against prod.) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.
Suppressed comments (1)
rust/src/domain/available.rs:85
shared_currencycan incorrectly returnNoneeven when later terms include a currency code: it stops at the first term that has any price/renewal/first-term money, then looks for that money'scurrency_code. If that first money lackscurrency_code(it’s optional in the API), the function returnsNoneand never checks subsequent terms.
.find_map(|t| {
t.price
.as_ref()
.or(t.renewal_price.as_ref())
.or(t.first_term_price.as_ref())
shared_currency stopped at the first term with any price/renewal/ first-term money, then looked only at that money's currencyCode. Since currencyCode is itself optional, a first term whose price lacks one made the whole function return None even when a later term (or a later field on the same term) carried a valid currency. Flagged by Copilot review. Now searches every price-like value across every term for the first one with a currencyCode, rather than fixing on the first price-like value regardless of whether it has one. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Summary
Preview migration of
gddy domain listfrom the v1 API to the v3 Domain Lifecycle Management API'slistDomains, plus supporting spec sync for the domains team's latest test-environment features. This targets the v3 API's test environment — the domains team has not yet shipped this batch to prod. Do not merge until v3 is confirmed live in prod and the spec has been re-synced against the shipped version.domain listcalls v3GET /domain-namesinstead of v1.--statusstays repeatable (comma-joined client-side, matching the API'sstyle: form, explode: false); the default "hide non-visible domains" view now maps tolifecycleGroupsexcludingTERMINAL. Output fields and--schematype move to the v3Domainshape (breaking change for--output jsonconsumers).domain listfetches every page vialistDomains's cursor pagination (links[rel=next]), not just the first — an explicit--limit/--offsetwindow (cli-engine's existing pagination pipeline) short-circuits the fetch once satisfied, so a small--limitdoesn't pay for pages it'll discard; unflagged, every domain is fetched. Requests the API's maxpageSize(200) each time to minimize round trips against a tightly rate-limited API. A defensive cap (50 pages) errors rather than silently returning a partial list if anextlink ever loops or is malformed.listDomains+ its filtering, and Afternic premium-domain support (Fee/FeeType,TermPrice.fees/firstTermPrice,RegistrationQuote.fees/inventory,Registration.fees,Consent.acknowledgedFees) into the vendored v3 spec from the canonical source repo — the private prototype bundle we normally sync from lags behind. Bulkcheck-availability's domain cap corrected to the now-official 25 (was 50).domain quote/domain purchasewire the premium-fee flow end to end: a premium quote'sfees/inventoryare surfaced and cached, andpurchaseechoes the fees back intoconsent.acknowledgedFees(required server-side for the purchase to succeed).domain availablenow lists every priced registration term (1yr, 2yr, 3yr, ...) as a nested table instead of a single 1-year headline, including any first-term promotion. Also breaking for--output jsonconsumers (price/renewalPrice/period/periodLabelreplaced byterms[]).TermPrice.recommendedis deliberately not surfaced — it's a hint meant for web UIs, not CLI output.statuses/lifecycleGroupsas single comma-joined query params (not repeated pairs), and coveringdomain list's pagination loop (multi-page follow, early-exit on a satisfied--limit/--offsetwindow, theMAX_PAGESerror path, and relative vs. absolutehrefs inlinks[rel=next]).Deliberately not included:
replaceDNSRecord(PUT) — live-confirmed the old zone-wiping bug (gddy dns setwipes the entire zone — issues a PUT to a non-existent v3 dns-records endpoint that the API executes as a full-collection replace #136) is fixed in test, but still excluded from the client pending a decision on simplifyingdns set's existing create-then-delete workaround.setAutoRenew/setTransferLock— new v3.2 endpoints, not yet wired to any command.domain agreementsstays on v1 — no v3 equivalent exists yet.Test plan
cargo check,cargo clippy -- -D warnings,cargo test,cargo fmt --checkall clean--env test:domain list(default view,--statussingle/multiple,--show-hidden,--limit/--offsetagainst the pagination envelope),domain available,domain suggest,domain quote(including the fees-caching path against a premium test domain)Url::parsethat rejected the spec's own relativehrefexamples, aMAX_PAGESdoc comment that overpromised what the code actually did, a silently-dropped fee-serialization error, and a dropped safety comment on the excluded DNS PUT)🤖 Generated with Claude Code