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
6 changes: 6 additions & 0 deletions .changeset/fresh-catalog-products.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@godaddy/gd-commerce-server': patch
'@godaddy/gd-commerce-storefront': patch
---

Load active catalog products and SKUs in merchant-defined variant order, while keeping inactive product details visible without purchase controls.
6 changes: 6 additions & 0 deletions .changeset/quiet-tax-totals.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@godaddy/gd-commerce-storefront': patch
'@godaddy/gd-commerce-server': patch
---

Show the draft-order subtotal and explain that shipping, taxes, and discounts are calculated at checkout.
12 changes: 6 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ biome-config-godaddy (packages/biome-config-godaddy)
- typecheck: tsc --noEmit
- test: vitest run

@godaddy/commerce-server (packages/commerce-server)
@godaddy/gd-commerce-server (packages/commerce-server)
- build: tsdown
- typecheck: tsc --noEmit
- lint: biome check src
Expand Down Expand Up @@ -137,9 +137,9 @@ Packages (top-level purpose)
- React component library for checkout flows; integrates with commerce APIs
- Uses tsdown for TS build and Tailwind CLI v4 for CSS build; Vitest for tests; Vite preview
- Depends on @godaddy/localizations
- @godaddy/commerce-storefront
- @godaddy/gd-commerce-storefront
- React catalog, product details, and cart components using a same-origin Commerce API
- @godaddy/commerce-server
- @godaddy/gd-commerce-server
- Express 5 routers for the storefront API, hosted checkout, and order lookup
- Uses tsdown, Vitest, and Biome; usable independently of the storefront package
- @godaddy/localizations
Expand Down Expand Up @@ -288,15 +288,15 @@ D. @godaddy/react
- Uses path alias "@/*" for src
- If adding components, follow existing patterns in src/components/checkout/** and src/components/ui/**

E. @godaddy/commerce-storefront
E. @godaddy/gd-commerce-storefront
- Opinionated React storefront; fixed same-origin `/api/commerce` server contract documented in packages/commerce-storefront/docs/server-api.md.
- Peers: React/React DOM 18 or 19, React Router 7 or 8.3+, TanStack Query 5. Host owns router/query providers; CommerceStorefront owns the cart provider and drawer.
- Build: tsdown plus local Tailwind CLI, CSS scoping, and layer removal in declared order; styles exported as ./styles.css. Artifact tests process the output through Tailwind v3 to verify host compatibility. No host Tailwind setup or global reset.
- Test: build first, then Vitest (behavior plus compiled artifact checks). Commands: build, typecheck, lint, test.
- Example: examples/commerce-storefront, port 5184, development-only in-memory server; production build needs real API routes.
- Keep credentials, merchant provisioning and platform configuration out of this client package. No dependency on the separate commerce web-component runtime.

F. @godaddy/commerce-server
F. @godaddy/gd-commerce-server
- Mount `createCommerceRouter()` at `/api/commerce` after `express.json()`. `createCommerceCatalogRouter()` installs config/catalog/cart routes; `createGoDaddyPaymentsRouter()` installs checkout/order-status routes.
- Hosts can supply `CommerceConfiguration`. The default runtime reader uses server environment variables for credentials, store/channel IDs, currency, and checkout flags; it does not read files. The API defaults to `https://api.godaddy.com`; an explicit server-controlled `apiBaseUrl` option supports alternate origins without embedding environment-specific hosts. Keep this package server-only.
- Hosts own deployment-specific configuration loading, provisioning readiness, retries, and optional `sourceApp`/`owner` attribution. Shipping options use the hosted checkout API shape; omit them to use the store configuration.
Expand All @@ -305,7 +305,7 @@ F. @godaddy/commerce-server
- Source alias `@/*` maps to `src/*` in TypeScript and Vitest. tsdown resolves it when bundling JavaScript and declarations; consumers need no alias configuration. When changing module resolution, verify a packed consumer outside the workspace.
- Validate supplied `X-Commerce-Scope` headers before catalog/cart/checkout calls. It guards against stale bindings and is not authorization. Hosts own authentication and authorization.
- Cart mutations return a refreshed cart. Keep URL cart/item IDs authoritative, allowlist PATCH fields, and only clear carts for explicit missing/expired-order errors; upstream authentication or transport failures must preserve saved carts.
- Commands: `pnpm --filter @godaddy/commerce-server build`, `typecheck`, `lint`, and `test`. See packages/commerce-server/README.md and packages/commerce-storefront/docs/server-api.md for integration details.
- Commands: `pnpm --filter @godaddy/gd-commerce-server build`, `typecheck`, `lint`, and `test`. See packages/commerce-server/README.md and packages/commerce-storefront/docs/server-api.md for integration details.

G. @godaddy/localizations
- Purpose: Localization bundles for checkout UI
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,10 @@ This monorepo contains the following packages:
| [`biome-config-godaddy`](/packages/biome-config-godaddy) | Fast Rust-based alternative to ESLint and Prettier using Biome | [![npm](https://img.shields.io/npm/v/biome-config-godaddy.svg)](https://www.npmjs.com/package/biome-config-godaddy) |
| [`@godaddy/app-connect`](/packages/app-connect) | Platform integration tools for GoDaddy apps | [![npm](https://img.shields.io/npm/v/@godaddy/app-connect.svg)](https://www.npmjs.com/package/@godaddy/app-connect) |
| [`@godaddy/react`](/packages/react) | React components and commerce API integration | [![npm](https://img.shields.io/npm/v/@godaddy/react.svg)](https://www.npmjs.com/package/@godaddy/react) |
| [@godaddy/commerce-storefront](packages/commerce-storefront) | Opinionated React catalog, product, and cart templates; [consumer example](examples/commerce-storefront) | Unreleased |
| [@godaddy/commerce-server](packages/commerce-server) | Express APIs for Commerce catalog, carts, hosted checkout, and order lookup | Unreleased |
| [@godaddy/gd-commerce-storefront](packages/commerce-storefront) | Opinionated React catalog, product, and cart templates; [consumer example](examples/commerce-storefront) | Unreleased |
| [@godaddy/gd-commerce-server](packages/commerce-server) | Express APIs for Commerce catalog, carts, hosted checkout, and order lookup | Unreleased |

`@godaddy/commerce-server` implements the same-origin API used by `@godaddy/commerce-storefront`. The packages can also be used independently with a custom client or server; see the [server integration guide](packages/commerce-server/README.md) and [storefront API contract](packages/commerce-storefront/docs/server-api.md).
`@godaddy/gd-commerce-server` implements the same-origin API used by `@godaddy/gd-commerce-storefront`. The packages can also be used independently with a custom client or server; see the [server integration guide](packages/commerce-server/README.md) and [storefront API contract](packages/commerce-storefront/docs/server-api.md).

## Why GoDaddy JavaScript?

Expand Down
4 changes: 2 additions & 2 deletions examples/commerce-storefront/README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# Independent commerce storefront example

This React Router application consumes the compiled `@godaddy/commerce-storefront` package and its shipped CSS. It runs as a standalone application without Tailwind configuration.
This React Router application consumes the compiled `@godaddy/gd-commerce-storefront` package and its shipped CSS. It runs as a standalone application without Tailwind configuration.

Run from the repository root with Node 24:

```sh
pnpm install
pnpm --filter @godaddy/commerce-storefront build
pnpm --filter @godaddy/gd-commerce-storefront build
pnpm --filter commerce-storefront-example dev
```

Expand Down
4 changes: 2 additions & 2 deletions examples/commerce-storefront/main.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { createRoot } from 'react-dom/client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { BrowserRouter, Link, Navigate, Route, Routes } from 'react-router';
import { Catalog, CartButton, CommerceStorefront, ProductDetails } from '@godaddy/commerce-storefront';
import '@godaddy/commerce-storefront/styles.css';
import { Catalog, CartButton, CommerceStorefront, ProductDetails } from '@godaddy/gd-commerce-storefront';
import '@godaddy/gd-commerce-storefront/styles.css';
import './styles.css';

const client = new QueryClient();
Expand Down
2 changes: 1 addition & 1 deletion examples/commerce-storefront/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@godaddy/commerce-storefront": "workspace:*",
"@godaddy/gd-commerce-storefront": "workspace:*",
"@tanstack/react-query": "^5.66.0",
"react": "^19",
"react-dom": "^19",
Expand Down
2 changes: 1 addition & 1 deletion examples/commerce-storefront/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { randomUUID } from 'node:crypto';
import { defineConfig, type Plugin } from 'vite';
import type { CartOrder, SKU, SKUGroup } from '@godaddy/commerce-storefront';
import type { CartOrder, SKU, SKUGroup } from '@godaddy/gd-commerce-storefront';

const money = (value: number) => ({ value, currencyCode: 'USD' });
const sku = (id: string, price: number, quantity = 20): SKU => ({
Expand Down
2 changes: 1 addition & 1 deletion packages/commerce-server/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# @godaddy/commerce-server
# @godaddy/gd-commerce-server

## 0.1.0

Expand Down
8 changes: 5 additions & 3 deletions packages/commerce-server/README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# @godaddy/commerce-server
# @godaddy/gd-commerce-server

An opinionated Express router for GoDaddy Commerce catalog, cart, and hosted checkout APIs.

```ts
import express from 'express';
import { createCommerceRouter, createRuntimeCommerceConfiguration } from '@godaddy/commerce-server';
import { createCommerceRouter, createRuntimeCommerceConfiguration } from '@godaddy/gd-commerce-server';

const app = express();
app.use(express.json());
Expand All @@ -22,7 +22,9 @@ The default configuration reads these **server-only environment variables** on e
- `GODADDY_OAUTH_CLIENT_ID` and `GODADDY_OAUTH_CLIENT_SECRET`
- `GODADDY_STORE_ID` and `GODADDY_CHANNEL_ID`
- `GODADDY_CURRENCY_CODE`
- Optional `GODADDY_CHECKOUT_CONFIGURATION`: JSON with boolean `enablePromotionCodes`, `enableTaxCollection`, and `enableShipping` fields. All three default to false when this variable is absent. Optional `shipping` accepts the checkout API's `originAddress` or `fulfillmentLocationId`; omit it to use store configuration.
- Optional `GODADDY_CHECKOUT_CONFIGURATION`: JSON with boolean `enablePromotionCodes`, `enableTaxCollection`, and `enableShipping` fields. All three default to false when this variable is absent or malformed so configuration discovery cannot prevent checkout creation. Optional `shipping` accepts the checkout API's `originAddress` or `fulfillmentLocationId`; omit it to use store configuration.

See [Checkout configuration](docs/checkout-configuration.md) for the temporary build-time synchronization flow and its eventual-consistency limitation.

The API origin defaults to `https://api.godaddy.com`. The package does not load files, provision merchants, or assign application attribution. Hosts own these concerns and any readiness checks or retries before invoking Commerce.

Expand Down
62 changes: 62 additions & 0 deletions packages/commerce-server/docs/checkout-configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Checkout configuration

## Goal

Storefronts should respond to a merchant enabling or disabling shipping, taxes, or discounts without requiring the builder agent to edit application code. This document records the current design, its temporary limitation, and the intended long-term solution.

## Decisions

- The cart remains a draft order. We will not create a checkout session when the cart is created or maintain a lazy session while the shopper edits it.
- The cart does not calculate or estimate shipping, taxes, or discounts. It displays **Subtotal** and one message: “Shipping, taxes, and discounts are calculated at checkout.” The message has the stable `commerce-cart-checkout-adjustments-note` class so an application can hide it easily.
- A checkout session is created only when the shopper selects **Checkout**. The Checkout API and the enabled commerce providers perform the actual calculations.
- Checkout capability flags are server-only. They must not be exposed by `/api/commerce/config` or supplied by browser code.
- We will defer live capability discovery in this library because the Checkout API is expected to discover enabled providers through App Registry. Until that work ships, configuration can be refreshed during an initial build or a later rebuild/redeploy.

This keeps the cart simple, avoids a second source of pricing logic, and lets shipping rates, tax rules, and discounts remain authoritative in their respective services.

## Source of truth

App Registry is the source of truth for whether the store has a provider enabled for each capability:

| Capability | App Registry action |
| --- | --- |
| Shipping | `commerce.shipping-rates.calculate` |
| Taxes | `commerce.taxes.calculate` |
| Discounts | `commerce.price-adjustment.apply` |

App Registry indicates that a capable GPA is enabled; the shipping, tax, and discount services still own their settings, rules, and calculations. The general Settings API is not a consolidated source for these three enablement states.

Commerce Admin already exposes this lookup to builders through the `commerce_checkout_configuration_get` MCP tool. Given a `storeId` and `channelId`, it validates their binding, queries enabled App Registry actions, and returns checkout flags such as `enableShipping`, `enableTaxCollection`, and `enablePromotionCodes`, plus shipping-origin readiness.

## Temporary build-time flow

Until Checkout API performs App Registry discovery itself:

1. During every initial build and rebuild/redeploy, the builder calls `commerce_checkout_configuration_get`.
2. The deployment writes the three returned feature flags to the server-only `GODADDY_CHECKOUT_CONFIGURATION` value; it does not copy the full MCP response or generate conditional application code.
3. When Checkout is selected, `commerce-server` reads that value and explicitly configures the new checkout session.
4. The enabled providers calculate live rates and adjustments during checkout.

For example:

```json
{
"enablePromotionCodes": true,
"enableTaxCollection": true,
"enableShipping": true
}
```

Restoring the server reader alone does not discover merchant settings: the builder/deployment integration must populate this value. Shipping-origin address details do not need to be copied into application code; hosted checkout can use store configuration.

The accepted stopgap is eventually consistent: a provider enabled or disabled after deployment is reflected on the next rebuild, not immediately. This is acceptable only while the Checkout API change is pending.

If the MCP lookup fails, the deployment does not receive the expected flags, or the server value is malformed, checkout falls back to all optional capabilities disabled. In particular, it sends `enableShipping: false` rather than preventing checkout creation. Builders should surface the lookup problem during the build, but it must not make the deployed checkout unusable.

## Important compatibility detail

The current checkout request builder applies explicit defaults of `false` for shipping, shipping-address collection, and tax collection. Removing `GODADDY_CHECKOUT_CONFIGURATION` without changing that behavior disables those features and is a regression. Also, omitting fields is not a complete substitute: Checkout API currently defaults shipping to enabled, but does not similarly enable address collection, taxes, or promotion codes.

## Long-term flow

Checkout API should query App Registry when a session is created and automatically enable shipping, taxes, and discounts for the store’s active GPAs. Once that is available and verified, this repository can remove the static checkout configuration and its build-time synchronization. The cart and draft-order lifecycle do not need to change.
2 changes: 1 addition & 1 deletion packages/commerce-server/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "@godaddy/commerce-server",
"name": "@godaddy/gd-commerce-server",
"version": "0.1.0",
"description": "Opinionated Express server integration for GoDaddy Commerce",
"type": "module",
Expand Down
26 changes: 16 additions & 10 deletions packages/commerce-server/src/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,14 +97,20 @@ describe('Commerce runtime configuration', () => {
});
});

it.each(['{bad json', 'null', '{"enableShipping":true}'])(
'rejects malformed checkout configuration: %s',
(raw): void => {
expect(() =>
createRuntimeCommerceConfiguration({
environment: { ...environment(), GODADDY_CHECKOUT_CONFIGURATION: raw },
}).readCheckout(),
).toThrow('Commerce config: GODADDY_CHECKOUT_CONFIGURATION');
},
);
it.each([
'{bad json',
'null',
'{"enableShipping":true}',
'{"enablePromotionCodes":false,"enableTaxCollection":false,"enableShipping":true,"shipping":{"originAddressConfigured":true}}',
])('defaults malformed checkout configuration to disabled capabilities: %s', (raw): void => {
expect(
createRuntimeCommerceConfiguration({
environment: { ...environment(), GODADDY_CHECKOUT_CONFIGURATION: raw },
}).readCheckout(),
).toEqual({
enablePromotionCodes: false,
enableTaxCollection: false,
enableShipping: false,
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,10 @@ it.each([undefined, 'https://api.example.com', 'https://api.example.com:8443'])(
const base = `http://127.0.0.1:${address.port}/api/commerce`;
const configResponse = await clientFetch(`${base}/config`);
const publicConfig = await configResponse.json();
expect(publicConfig).toEqual({ cartScope: expect.any(String), currencyCode: 'USD' });
expect(publicConfig).toEqual({
cartScope: expect.any(String),
currencyCode: 'USD',
});
const headers = { 'Content-Type': 'application/json', 'X-Commerce-Scope': publicConfig.cartScope };
expect((await clientFetch(`${base}/products`, { headers })).status).toBe(200);
expect(
Expand Down
24 changes: 17 additions & 7 deletions packages/commerce-server/src/create-checkout-session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,16 @@ describe('createCheckoutSession', () => {
},
);

it('disables optional checkout capabilities when configuration falls back to defaults', async (): Promise<void> => {
await createCheckoutSession(cart, configuration);
expect(mockGqlRequest.mock.calls[0]?.[0].variables.input).toMatchObject({
enablePromotionCodes: false,
enableTaxCollection: false,
enableShipping: false,
enableShippingAddressCollection: false,
});
});

it.each(flows)('uses only host-owned attribution for %s checkout', async (_name, params): Promise<void> => {
config = { ...config, sourceApp: 'merchant-site', owner: 'merchant-orders' };
mockGqlRequest.mockResolvedValue(response({ sourceApp: 'merchant-site' }));
Expand Down Expand Up @@ -143,13 +153,6 @@ describe('createCheckoutSession', () => {
},
);

it.each(flows)('rejects missing payment methods for %s checkout', async (_name, params): Promise<void> => {
mockGqlRequest.mockResolvedValue(response({ paymentMethods: null }));
await expect(createCheckoutSession(params, configuration)).rejects.toThrow(
'Checkout session did not configure payment methods.',
);
});

it.each([flows[1], flows[2]])(
'uses the store shipping configuration for %s checkout',
async (_name, params): Promise<void> => {
Expand Down Expand Up @@ -234,6 +237,13 @@ describe('createCheckoutSession', () => {
);
});

it.each(flows)('rejects missing payment methods for %s checkout', async (_name, params): Promise<void> => {
mockGqlRequest.mockResolvedValue(response({ paymentMethods: null }));
await expect(createCheckoutSession(params, configuration)).rejects.toThrow(
'Checkout session did not configure payment methods.',
);
});

it('uses the configured currency for non-catalog pricing', async (): Promise<void> => {
config.currencyCode = 'GBP';
await createCheckoutSession(
Expand Down
4 changes: 2 additions & 2 deletions packages/commerce-server/src/lib/commerce/cart-scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import { createHash } from 'node:crypto';
import type { Request, Response } from 'express';
import type { CommerceConfig } from './config';

type CartBinding = Pick<CommerceConfig, 'apiBaseUrl' | 'storeId' | 'channelId'>;
type CartBinding = Pick<CommerceConfig, 'apiBaseUrl' | 'storeId' | 'channelId' | 'currencyCode'>;

/** Public cache/storage scope, not an authorization credential. */
export function getCommerceCartScope(config: CartBinding): string {
return createHash('sha256')
.update(JSON.stringify([config.apiBaseUrl, config.storeId, config.channelId]))
.update(JSON.stringify([config.apiBaseUrl, config.storeId, config.channelId, config.currencyCode]))
.digest('hex')
.slice(0, 32);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export type SKUGroupSKU = SKU;

export interface SKUGroup {
id?: string | null;
status?: string | null;
name?: string | null;
label?: string | null;
description?: string | null;
Expand Down
Loading
Loading