Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 17 additions & 8 deletions rust/src/domain/available.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ use serde_json::json;
use domains_client::types;

use super::common::{
api_error, fees_to_json, format_money, make_client, period_label, validate_domain_name,
PRICE_EXCLUDES_TAXES_AND_FEES, api_error, fees_to_json, format_money, make_client,
period_label, validate_domain_name,
};
use crate::idn::DomainFormArgs;
use crate::next_action::next_action;
Expand Down Expand Up @@ -48,7 +49,7 @@ fn view_columns() -> Vec<TableColumn> {
.essential(true),
TableColumn::new("firstTermPrice", "First-Term Price").align(Alignment::Right),
TableColumn::new("renewalPrice", "Renewal Price").align(Alignment::Right),
TableColumn::new("fees", "Fees").essential(true),
TableColumn::new("fees", "Premium Fees").essential(true),
]),
]
}
Expand Down Expand Up @@ -125,10 +126,15 @@ pub(super) fn command() -> RuntimeCommandSpec {
CommandSpec::from_args::<AvailableArgs>("available", "Check whether a domain is available")
.with_long(
"Check whether a domain can be registered, and at what price. \
--check-type fast trades accuracy for speed; full is authoritative \
(the `definitive` field tells you which you got). Lists every \
registration term the API prices (1 year, 2 years, …), including \
any first-term promotion.",
--check-type fast trades accuracy for speed; full is \
authoritative (the `definitive` field tells you which you got). \
Lists every registration term the API prices, with any \
promotional discounts applied.\n\
\n\
Prices exclude taxes and fees, such as the ICANN fee, which are \
added when you register. `Premium Fees` lists only the extra \
purchase cost of a premium domain; it is empty for an ordinary \
domain.",
)
.with_system("domain")
.with_tier(Tier::Read)
Expand Down Expand Up @@ -189,8 +195,11 @@ pub(super) fn command() -> RuntimeCommandSpec {
let cmd = CommandResult::new(result);
if body.available.unwrap_or(false) {
Ok(cmd.with_next_actions(vec![
next_action("domain quote <domain>", "Price a registration")
.with_param("domain", NextActionParam::value(resolved_domain)),
next_action(
"domain quote <domain>",
format!("Price a registration. {PRICE_EXCLUDES_TAXES_AND_FEES}"),
)
.with_param("domain", NextActionParam::value(resolved_domain)),
]))
} else {
Ok(cmd.with_next_actions(vec![
Expand Down
31 changes: 30 additions & 1 deletion rust/src/domain/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,31 @@
//! client, money formatting, argument helpers, and API-error rendering. Each
//! `gddy domain` subcommand lives in its own sibling module and draws from here.

use cli_engine::{CliCoreError, CommandContext, Credential, Result};
use cli_engine::{CliCoreError, CommandContext, Credential, NextAction, NextActionParam, Result};
use serde_json::json;

use crate::environments;
use crate::next_action::next_action;

use domains_client::types;

/// Appended to the next-step text under every price a domain command shows. The
/// domains API quotes the registration price only: the ICANN fee and any tax
/// are added when the order is placed, so the total charged is higher.
pub(super) const PRICE_EXCLUDES_TAXES_AND_FEES: &str = "Prices exclude applicable taxes and fees.";

/// Points at the completed order, which is where the total actually charged
/// (including tax) can be read — the registration and operation records only
/// carry the quoted price. `--wait` because a new order takes a few seconds to
/// become visible.
pub(super) fn order_next_action(order_id: String) -> NextAction {
next_action(
"shopping order get <order-id> --wait",
"See the total charged, including any taxes and fees",
)
.with_param("order-id", NextActionParam::value(order_id))
Comment thread
jpage-godaddy marked this conversation as resolved.
}

/// The ISO-4217 minor-unit exponent for a currency — how many implied decimal
/// places a [`types::SimpleMoney`] `value` carries (the v3 spec defers money
/// formatting to ISO 4217). Sourced from the `iso_currency` crate's maintained
Expand Down Expand Up @@ -540,6 +558,17 @@ mod tests {
}
}

#[test]
fn order_next_action_uses_a_structured_param_and_waits_for_the_order() {
let action = order_next_action("2211623318".to_owned());
assert_eq!(action.command, "gddy shopping order get <order-id> --wait");
assert_eq!(
action.params["order-id"].value.as_deref(),
Some("2211623318")
);
assert!(action.description.contains("taxes and fees"));
}

#[test]
fn fees_to_json_renders_type_amount_and_currency() {
let fee = types::Fee {
Expand Down
41 changes: 13 additions & 28 deletions rust/src/domain/guides/domain-purchase.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,11 @@ Registering a domain is a **two-step, quote-then-purchase** flow:
2. `gddy domain purchase --quote-token <token> --agree --confirm` accepts that
quote and registers the domain.

A purchase is **paid and not reversible**, so `purchase` has two gates
(`--agree`, `--confirm`) on top of the token. Because the token locks the price
and settings you reviewed, you're charged exactly what the quote showed.

Registration completes **asynchronously**: `purchase` submits the registration
and waits briefly for the registry, reporting the operation's `status`
(`COMPLETED` when done). The domain then appears in `gddy domain list`. If
`purchase` gives up waiting before reaching a terminal status, it still
succeeded — check on it later (the ID it printed) with:
A purchase is **paid and not reversible**, so `purchase` has two gates (`--agree`, `--confirm`) on top of the token. The quote token locks the registration price and settings you reviewed.

**The quoted price does not include taxes and fees.** The ICANN fee and any applicable taxes are added when you register, so the total charged is higher than the price shown by `suggest`, `available` and `quote`. Whether tax applies depends on the billing address of your payment method. The `Premium Fees` column is something else: it lists the extra cost of a premium domain and is empty for a non-premium domain. Once the registration completes, `purchase` gives you the order ID, and `gddy shopping order get <order-id>` shows the total charged.

Registration completes **asynchronously**: `purchase` submits the registration and waits briefly for the registry, reporting the operation's `status` (`COMPLETED` when done). The domain then appears in `gddy domain list`. If `purchase` gives up waiting before reaching a terminal status, it will give you an operation ID which you can use to check its progress with:
`gddy domain operation status <operation-id>`.

## The flow
Expand Down Expand Up @@ -52,31 +48,24 @@ succeeded — check on it later (the ID it printed) with:
gddy domain purchase --quote-token <token> --agree --confirm
```

The quote is cached locally (next to your `contacts.toml`), so **run `quote` and
`purchase` on the same machine**, within the token's ~10-minute lifetime. If the
token has expired or isn't found, just re-run `gddy domain quote`.
The quote is cached locally (next to your `contacts.toml`), so **run `quote` and `purchase` on the same machine**, within the token's ~10-minute lifetime. If the token has expired or isn't found, just re-run `gddy domain quote`.

### The three decisions

- **The quote token** selects *which* quote you're buying — the exact domain,
settings, and locked price you reviewed.
settings, and registration price you reviewed.
- `--agree` is your **consent** to the quote's legal agreements. Run `purchase`
without it and it lists the agreements you must accept.
- `--confirm` acknowledges that the purchase **charges your account**. Without
it, the command stops before buying.

AI assistants: Before using `--agree` or `--confirm`, show the end user the exact price from `domain quote` (including any premium/registration fees) and each required legal agreement's title AND link (from `quote`'s `requiredAgreements`), and obtain their explicit confirmation to proceed. Do not summarize an agreement by name only — the user must be able to click through and read it. Do not infer confirmation from a general request to buy or register a domain.
AI assistants: Before using `--agree` or `--confirm`, show the end user the quoted price from `domain quote` (including any premium fees), tell them that taxes and fees such as the ICANN fee are added on top so the total charged will be higher, and show each required legal agreement's title and URL (from `quote`'s `requiredAgreements`), and obtain their explicit confirmation to proceed. The user must be able to click through and read the agreements. Do not infer confirmation from a general request to buy or register a domain.

## Contacts

A registration has four contact roles: **registrant**, **admin**, **billing**,
and **tech**. If you don't supply them, the API uses your GoDaddy account's
default contacts — which is usually what you want. Contacts are part of the
quote, so configure them *before* you quote.
A registration has four contact roles: **registrant**, **admin**, **billing**, and **tech**. If you don't supply them, the API uses your GoDaddy account's default contacts — which is usually what you want. Contacts are part of the quote, so configure them *before* you quote.

If you register many domains and want to set your own contacts once, save them
in a `contacts.toml` in your `gddy` config directory. The quickest start is to
generate a template and edit it:
If you register many domains and want to set your own contacts once, save them in a `contacts.toml` in your `gddy` config directory. The quickest start is to generate a template and edit it:

```
gddy domain contact init
Expand All @@ -88,10 +77,7 @@ That writes a starter `contacts.toml` (with every role commented out) to:
- macOS: `~/Library/Application Support/gddy/contacts.toml`
- Windows: `%APPDATA%\gddy\contacts.toml`

Open it, uncomment the role(s) you want, and fill in the values. Each role is an
optional table: a role you omit falls back to the account default; a role you
uncomment must be **complete** (the required fields below), or the file fails to
load.
Open it, uncomment the role(s) you want, and fill in the values. Each role is an optional table: a role you omit falls back to the account default; a role you uncomment must be **complete** (the required fields below), or the file fails to load.

```toml
[registrant]
Expand Down Expand Up @@ -122,6 +108,5 @@ country = "US"
# default for it.
```

Required fields per role: `name_first`, `name_last`, `email`, `phone`,
`address1`, `city`, `state`, `postal_code`, `country`. Optional fields:
`organization`, `address2`.
Required fields per role: `name_first`, `name_last`, `email`, `phone`, `address1`, `city`, `state`, `postal_code`, `country`.
Optional fields: `organization`, `address2`.
13 changes: 7 additions & 6 deletions rust/src/domain/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,13 @@ pub(crate) use common::{api_error, format_api_error, make_client};
/// from `quote` that points at it — so a purchase's price is always relayed to
/// the end user before the account is actually charged. See AGENTS.md's
/// "Purchase confirmation" section.
pub(crate) const AGENT_PURCHASE_CONFIRMATION_INSTRUCTIONS: &str = "AI assistants: Before using --agree or --confirm, show the end user the exact \
price from `domain quote` (including any fees) and each required legal \
agreement's title AND link (from `domain quote`'s `requiredAgreements`), and \
obtain their explicit confirmation to proceed. Do not summarize an agreement \
by name only — the user must be able to click through and read it. Do not \
infer confirmation from a general request to buy or register a domain.";
pub(crate) const AGENT_PURCHASE_CONFIRMATION_INSTRUCTIONS: &str = "AI assistants: Before using --agree or --confirm, show the end user the \
quoted price from `domain quote`, including any disclosed fees. Tell them \
that taxes and fees such as the ICANN fee are added on top, so the total \
charged will be higher. Also, show each required legal agreement's title and \
URL, and obtain the user's explicit confirmation to proceed. The user must \
be able to click through and read the agreements. Do not infer confirmation \
from a general request to buy or register a domain.";

pub fn module() -> Module {
Module::new("Domains", |_ctx| {
Expand Down
7 changes: 6 additions & 1 deletion rust/src/domain/operation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ use serde_json::json;

use domains_client::types;

use super::common::{api_error, format_operation_error, is_terminal_status, make_client};
use super::common::{
api_error, format_operation_error, is_terminal_status, make_client, order_next_action,
};
use crate::idn::DomainFormArgs;
use crate::next_action::next_action;
use crate::output_schema::output_schema;
Expand Down Expand Up @@ -123,6 +125,9 @@ fn status_command() -> RuntimeCommandSpec {
.with_param("domain", NextActionParam::value(domain)),
);
}
if let Some(order_id) = result.get("orderId").and_then(|v| v.as_str()) {
actions.push(order_next_action(order_id.to_owned()));
}
} else {
actions.push(
next_action(
Expand Down
Loading
Loading