Commerce Operations
Prepare catalog, customer, order, fulfillment, shipping, and support ownership.
Commerce Operations
Use this page when the workspace will sell products, manage customers, or support orders after launch.
Commerce Setup
Prepare catalog
Create real products, options, categories, and images before inviting teams to operate the workspace.
Assign order ownership
Name who handles order review, fulfillment, returns, refunds, and customer support.
Confirm shipping and discounts
Review shipping policy ownership and decide who can create or retire discounts.
Connect support habits
Make sure customer account, order, and return questions have a clear support path.
Operating Ownership
| Workflow | Owner | Before launch |
|---|---|---|
| catalog | merchandising owner | products and sellable options exist |
| orders | operations owner | order review and fulfillment path is assigned |
| shipping | logistics owner | default shipping policy is confirmed |
| discounts | commerce owner | promotion approval is clear |
| customers | support owner | account recovery and service expectations are assigned |
Shipping and Non-Shipping Products
Physical variants should keep requiresShipping enabled so paid orders create carrier fulfillment work. Digital goods, remote classes, services, and other non-shipping variants can disable shipping; those lines do not require a shipping address or shipping fee, do not appear in carrier file export/import, and are skipped by shipment item selection.
Storefront cart responses include each line's shipping requirement so checkout can stay contextual. Non-shipping-only carts skip the shipping address step and shipping summary line; mixed carts still collect shipping for the physical lines.
Mixed orders keep the two paths separate: ship the physical lines through fulfillment orders and carrier tracking, then complete any non-shipping access or service lines through the manual completion flow. Refund and return handling should distinguish physical delivery from digital/service access so shipping refunds and return shipping fees are only used when outbound or return shipping actually applies.
Return Requests
In Console, create return requests from an eligible delivered or confirmed order. Operators select the return lines explicitly, review ordered, already-returned, and remaining quantities, enter per-line restocking fees and a return shipping fee, then submit the net refund amount shown by the breakdown.
The return request records the selected line quantities, returnShippingFee, restockingFee, initialShippingRefundAmount, and refundAmount. initialShippingRefundAmount is the initial outbound shipping refunded to the customer. returnShippingFee is a return-shipping deduction charged to the customer. The net refund is:
refundAmount = sum(lineRefund) + initialShippingRefundAmount - sum(restockingFee) - returnShippingFeeThe initial shipping refund suggestion uses the order-time return shipping refund policy snapshot. Across non-rejected returns, the cumulative initial shipping refund is capped by the order shipping amount. Legacy orders without that snapshot use the order shipping amount as a manual cap and require an audit note for any positive initial shipping refund.
Manual Orders (Draft Orders)
Sales that do not go through storefront checkout — phone orders, B2B or manual sales, support-assisted reorders, and quote-before-confirm — are handled as draft orders. In the Console an operator builds a draft with line items, a customer or customer snapshot, a shipping address, a discount code, a shipping fee, and operator/customer notes, and can freely edit or discard it until it is finalized.
A draft reserves no inventory and creates no order ledger rows (orders, transactions, inventory reservations, fulfillment) before finalization. POST /api/draft-orders/:id/calculate returns a server-derived totals preview (subtotal, discount, shipping, total) with no side effects and surfaces unavailable or out-of-stock variants and an invalid discount code before finalize.
POST /api/draft-orders/:id/finalize converts the draft into a real order. Pricing, discounts, tenant checks, idempotency, and lifecycle defaults reuse the same server-derived order-creation path as storefront orders, and the resulting order records source = manual. A zero-amount draft follows the existing free-order auto-paid path; otherwise a manual pending order is created for follow-up. If the customer email is missing or invalid, finalize is rejected with a clear error and the draft stays editable.
The v1 scope covers server-priced catalog items plus an existing discount code. There is no manual or custom line pricing, currency is fixed to KRW, and finalize does not send payment or invoice links (a later phase). Draft status moves draft → finalized (linked to the created order) or discarded; a finalized or discarded draft can no longer be edited.
Launch Checklist
- ecommerce is effective on the current plan
- at least one product is ready for sale
- shipping and discount ownership are clear
- customer support owns account and order questions
- payment and webhook responsibilities are assigned before customer launch
Keep commerce launch small. A complete first product and order support path is better than a broad catalog with unclear ownership.
Commerce helpers (SDK)
The @01.software/sdk exposes shaped commerce helpers as the recommended path for common consumer flows. Helpers fold the underlying Payload queries, access policy, and response shaping into a single call.
Product media is ordered. The first product image is treated as the featured image for listing cards and the default product detail state.
| Helper | Use case |
|---|---|
createServerClient().commerce.product.detail({ slug | id, market }) | Trusted server product detail: returns { found: true, product } with variants, options, brand, categories, tags, images, videos, Shopify-shaped Product fields, and operational inventory fields. Requires server credentials. Pass market to resolve variant prices into that market; each priced variant includes resolvedPrice with amount, compare-at price, currency, market id, and source (fixed or computed). Detail allows products with status: 'published' or status: 'unlisted'; 404s return { found: false, reason } for missing, unpublished, or feature disabled. |
commerce.product.detail({ slug | id, market }) / commerce.product.detailCatalog({ slug | id, market }) / commerce.product.listingPage({ market }) / listingGroupsCatalog({ productIds, market }) | Publishable storefront reads: public-safe detail/card data without raw stock quantities. Pass market for market-resolved product ranges, group listings, and variant resolvedPrice; market listing pages reject base-currency price filters/sorts because those ranges no longer match the resolved market price. Only status: 'published' products appear in listings; use direct detail for status: 'unlisted' products. |
commerce.product.stockCheck({ items }) | Pre-cart validation: point-in-time stock availability for variants. |
commerce.carts.* | Canonical Cart lifecycle: create / retrieve / mine / merge / addItem / updateItem / removeItem / updateDiscountCodes / updateAttributes / clear. Discount codes and public attributes use full-list replacement ([] clears). Event lines use admissions[]; singular admission and the old apply/remove discount helpers are not in SDK 0.45. |
commerce.discounts.validate({ code, orderAmount, currency?, market?, items? }) | Pre-checkout discount validation. Pass currency or market whenever validating discounts that depend on absolute money amounts; fixed amounts, min-order thresholds, max caps, and fixed/tiered thresholds fail closed without an explicit matching currency or market context. |
commerce.shipping.calculate({ postalCode, orderAmount }) | Pre-checkout shipping calculation. |
commerce.checkouts.create/retrieve | Create or read a Checkout from a server-authoritative Cart. Creation returns { checkout, checkoutToken }; ordinary DTOs never echo the capability. Browser retrieval uses that token, while server retrieval uses checkoutId. Results are required discriminated unions and an Order exists only after placement. |
commerce.paymentSessions.create/retrieve/cancel | Source-compatible Toss widget_v2 PaymentSessions returning a browser-safe toss_widget action. Toss confirmation remains a typed server-only adapter action. |
commerce.paymentSessions.createPortOne/retrievePortOne/cancelPortOne | Additive PortOne hosted_v2 and distinct standard_v2 PaymentSessions. Hosted returns only HTTPS portone_checkout.checkoutUrl; Standard initially supports a PortOne Toss Payments V2 CARD channel and returns a versioned portone_sdk V2 requestPayment action with reviewed store/channel identifiers, a 6–40 character ASCII-alphanumeric immutable payment ID, server-derived money/order fields, PortOne's CURRENCY_KRW currency constant, and a required validated redirect URL. A copied Standard action has no provider-enforced revocation or expiry, so failed/absent observations and local expiry keep its one payment slot held for exact-action retry. Redirects and SDK results only trigger authoritative lookup and are never payment finality. Server-side PortOne retrieval requires write scope because authenticated lookup may converge and finalize domain state. |
createServerClient().commerce.paymentSessions.findPortOne/reconcilePortOne | Server-only full-tuple PortOne recovery facade. findPortOne reads bounded local PaymentSession/Checkout/Order/Money state without provider I/O or mutation. reconcilePortOne requires write scope plus a durable idempotency key and reuses the existing managed convergence engine. Tenant comes only from the server credential; both calls require provider account key, environment, and provider payment ID. A missing local session yields typed found: false/requires_attention without claiming or heuristically attaching the provider payment. |
createServerClient().commerce.paymentSessions.custom.create/retrieve/confirm | Bounded server-only bridge for a provider without a first-party adapter. The trusted caller independently verifies the provider API response or signed webhook, then submits the exact provider session/payment/event identities and captured Money. retrieve performs an exact lookup by either provider session ID or provider payment ID and returns only bounded status, Money, and resolved orderId; it does not list sessions. Raw PaymentSession relations, evidence, fingerprints, and credentials stay private. |
createServerClient() uses resource-owned /server/* transports for every Cart operation, Checkout create/retrieve/query, and PaymentSession create/retrieve/cancel/confirm. Public paths accept only capability- or customer-scoped credentials; the split remains exact even when request bodies match.
The custom bridge is not available from the browser or SDK root, does not make an arbitrary “paid” assertion trustworthy, and does not provide adapter-backed verification or refund authority. It records a capture only after trusted server code has independently verified the provider result. It is also not a manual/offline pending-order flow: use Draft Orders for current back-office manual orders. Storefront placement without a payment provider is a separate commerce capability.
The PortOne adapter contract exposes the full current Hosted v2 method
vocabulary: CARD_KR, N_PAY, KAKAO_PAY, TOSS_PAY,
CARD_INTERNATIONAL, PAY_PAL, UNION_PAY, ALIPAY_CN, ALIPAY_HK,
TRUE_MONEY, DANA, TOUCH_N_GO, G_CASH, WE_CHAT_PAY, KLARNA,
E_CONTEXT, GRAB_PAY_MY, GRAB_PAY_SG, SHOPEE_PAY_TH, PAY_PAY, BPI,
RABBIT_LINE_PAY, CONVENIENCE_STORE_JP, AMAZON_PAY, RAKUTEN_PAY,
D_BARAI, AU_PAY, and MERPAY. The selected method must be enabled on the
ProviderConnection. The caller must select one method and Console checks the
connection's local allowlist before Hosted creation; PortOne remains
authoritative for profile and channel support. Confirm the intended profile
and channel before going live.
For Checkout and PaymentSession mutations, create and persist one random idempotency key per logical operation and reuse it after a timeout or lost response. createCommerceIdempotencyKey() generates a valid key. Do not derive ordinary keys from Cart, Checkout, PaymentSession, Order, or provider IDs, because that collides with later operations on the same resource. Canonical failures are exposed as CommerceSDKError; branch on reason and retryable, use retryAfter for in-progress/lock backoff when present, and retain requestId for support and tracing.
Cart mutations except create/retrieve/mine also require a persisted idempotency key. Token-keyed writes require expectedRevision from the last observed Cart; after cart_revision_conflict, retrieve the Cart and begin a new logical mutation with a new key. Merge requires the key but no single revision because it resolves two Cart states atomically. A replayed lower-revision Cart is an operation receipt and must not replace a newer cached Cart.
For React, createQueryHooks(client).useProductDetailBySlug(slug) and matching hooks expose the public-safe catalog detail shape with React Query caching and automatic invalidation on mutations to related collections.
The raw client.collections.from() query builder remains available as the escape hatch for cases the helpers do not cover (bulk reads, custom filter combinations, fields not in the helper response shape). See the SDK README's "Advanced: direct Payload queries" section.
Next Actions
- Review credentials and webhooks in Integrations & Keys.
- Run commerce blockers through Launch Readiness.
- Return to Feature Planning before adding customer accounts or community workflows.