Skip to content

Add gumroad upsells command for upsells and cross-sells#168

Merged
nyomanjyotisa merged 9 commits into
mainfrom
issue-5551-upsells-cli
Jun 23, 2026
Merged

Add gumroad upsells command for upsells and cross-sells#168
nyomanjyotisa merged 9 commits into
mainfrom
issue-5551-upsells-cli

Conversation

@nyomanjyotisa

@nyomanjyotisa nyomanjyotisa commented Jun 23, 2026

Copy link
Copy Markdown
Member

What

Adds a gumroad upsells command group wrapping the new /v2/upsells API: list, view, create, update, delete. Previously gumroad upsells returned "unknown command".

  • create / update send JSON request bodies so nested upsell_variants and the offer_code round-trip correctly (form-encoding sorts keys and turns the variants array into integer-keyed hashes).
  • update fetches the upsell first and merges only the flags you pass, so a partial edit doesn't drop the associations the API replaces wholesale. --remove-offer clears the discount.
  • Supports --json / --jq, --plain, --quiet, and --dry-run like the other commands. Man page and skill docs included.

Why

Companion to the Rails API for upsells/cross-sells (antiwork/gumroad#5551). Sellers driving their store from the CLI can now automate upsells and cross-sells alongside products, offer codes, and variants.

Part of antiwork/gumroad#5551


AI disclosure: implemented with Claude Opus 4.8 via Claude Code.


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Note

Medium Risk
New mutating seller APIs that change checkout upsell/cross-sell configuration; logic is complex but heavily validated and tested, with no auth or payment-path changes.

Overview
Adds gumroad upsells (list, view, create, update, delete) wired to /upsells, registered on the root command so the group is no longer unknown.

Create and update post JSON bodies (via PostJSON/PutJSON) so nested upsell_variants and offer_code serialize correctly. Update GETs the current upsell, merges only changed flags into a full PUT payload, and handles type switches (version upsell vs cross-sell), audience rules, and --remove-offer. Shared validation covers cross-sell vs upsell flags, discounts, and audience (--universal vs --selected-product).

Output modes match other commands (--json, --plain, --quiet, --dry-run). SKILL.md documents agent usage; a large upsells_test.go covers endpoints, payloads, merge behavior, and flag errors.

Reviewed by Cursor Bugbot for commit 8e12c21. Bugbot is set up for automated code reviews on this repo. Configure here.

Wraps the new /v2/upsells API with list, view, create, update, and delete
subcommands. create/update send JSON bodies so nested upsell_variants and
offer codes round-trip cleanly; update fetches the upsell first and merges
flag overrides so partial edits don't drop associations the API replaces
wholesale.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@nyomanjyotisa nyomanjyotisa self-assigned this Jun 23, 2026
Comment thread internal/cmd/upsells/update.go
Comment thread internal/cmd/upsells/update.go
Comment thread internal/cmd/upsells/update.go
@greptile-apps

greptile-apps Bot commented Jun 23, 2026

Copy link
Copy Markdown

Greptile Summary

Adds the gumroad upsells command group (list, view, create, update, delete) wired to /v2/upsells and registers it on the root CLI. Both create and update post JSON bodies so upsell_variants and offer_code round-trip correctly.

  • update fetches the current upsell first and merges only the changed flags into a full payload, supporting --remove-offer to clear the discount and properly normalising cross-sell/upsell fields on type conversion.
  • create validates mutual-exclusion constraints (--amount/--percent-off, --universal/--selected-product, --offer-variant on cross-sells) before sending the POST.
  • Output modes (--json/--jq, --plain, --quiet, --dry-run) and test coverage match the existing command patterns; skills/gumroad/SKILL.md is updated with usage examples.

Confidence Score: 5/5

New command group with no auth changes; the merge logic in update is the main behavioral surface and is thoroughly exercised by the test suite.

After tracing every merge path in update.go — including the universal-to-targeted conversion, type-switch normalisation, and remove-offer handling — each scenario is covered by a corresponding test and behaves correctly. The previously flagged edge case around --selected-product on a universal upsell is correctly resolved by lines 129-131 setting body["universal"] = false before the guard at line 136 can fire. No logic errors or data-loss paths were found in the changed code.

update.go warrants the closest read: it fetches the current upsell unconditionally, before the dry-run gate in runUpsellWrite, so --dry-run still issues a live GET. All other files are straightforward.

Important Files Changed

Filename Overview
internal/cmd/upsells/update.go Most complex file: fetches current upsell, computes effective cross-sell/universal state, merges flags into a full PUT body, and normalises type-conversion fields. The merge logic is thorough and backed by tests; fetchUpsell is called unconditionally before the dry-run gate in runUpsellWrite, so --dry-run still issues a live GET.
internal/cmd/upsells/upsells.go Shared types (upsell, upsellDiscount, upsellProduct, etc.), helpers (offerCodeFromFlags, validateFlagConsistency, parseOfferVariants, formatDiscount), runUpsellWrite, and printUpsellDryRun. All helpers are well-guarded; dry-run short-circuits correctly for create/update's write path.
internal/cmd/upsells/create.go Flag parsing, mutual-exclusion validation, and JSON body construction for POST /upsells. Correctly guards cross-sell audience, offer-variant/cross-sell conflict, and amount/percent-off exclusivity; dry-run makes no API calls.
internal/cmd/upsells/upsells_test.go 915-line test suite covering endpoints, flag validation, merge/clear behaviour on type conversion, plain/JSON/dry-run output, and subcommand registration. Plain view path is covered by TestView_Plain.
internal/cmd/upsells/list.go Standard GET /upsells with paged table output and plain mode; follows existing list command patterns exactly.
internal/cmd/upsells/view.go GET /upsells/:id with labelled-line detail view, plain single-row mode, and paged version-upgrade list; no issues.
internal/cmd/upsells/delete.go Confirmation-gated DELETE /upsells/:id using standard cmdutil.ConfirmAction / RunRequestWithSuccess helpers; consistent with other destructive commands.
internal/cmd/root.go Single-line import and AddCommand registration for the new upsells package; no issues.
skills/gumroad/SKILL.md Adds upsells section with list/create/view/update/delete examples and flag reference; accurate and complete.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[gumroad upsells] --> B[list]
    A --> C[view id]
    A --> D[create]
    A --> E[update id]
    A --> F[delete id]

    D --> D1{Validate flags}
    D1 -->|cross-sell conflict / missing audience / bad offer-variant| D2[UsageError]
    D1 -->|valid| D3[Build JSON body]
    D3 --> D4[POST /upsells]
    D4 --> D5[renderUpsellWriteResult]

    E --> E1{RequireAnyFlagChanged?}
    E1 -->|no| E2[UsageError]
    E1 -->|yes| E3[fetchUpsell GET /upsells/id]
    E3 --> E4[currentUpsellBody]
    E4 --> E5[Merge changed flags]
    E5 --> E6{opts.DryRun?}
    E6 -->|yes| E7[printUpsellDryRun]
    E6 -->|no| E8[PUT /upsells/id JSON]
    E8 --> E9[renderUpsellWriteResult]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[gumroad upsells] --> B[list]
    A --> C[view id]
    A --> D[create]
    A --> E[update id]
    A --> F[delete id]

    D --> D1{Validate flags}
    D1 -->|cross-sell conflict / missing audience / bad offer-variant| D2[UsageError]
    D1 -->|valid| D3[Build JSON body]
    D3 --> D4[POST /upsells]
    D4 --> D5[renderUpsellWriteResult]

    E --> E1{RequireAnyFlagChanged?}
    E1 -->|no| E2[UsageError]
    E1 -->|yes| E3[fetchUpsell GET /upsells/id]
    E3 --> E4[currentUpsellBody]
    E4 --> E5[Merge changed flags]
    E5 --> E6{opts.DryRun?}
    E6 -->|yes| E7[printUpsellDryRun]
    E6 -->|no| E8[PUT /upsells/id JSON]
    E8 --> E9[renderUpsellWriteResult]
Loading

Reviews (7): Last reviewed commit: "Require an audience for a cross-sell" | Re-trigger Greptile

Comment thread internal/cmd/upsells/upsells_test.go Outdated
Address Cursor Bugbot findings on the fetch-then-merge update path:
- drop the inherited variant_id when --product changes without --variant, so
  a version ID from the old product is not paired with the new product
- gate --selected-product and --offer-variant on flags.Changed so an explicit
  empty --selected-product clears the set
- omit product_ids when the result is universal

Add tests for these paths plus dry-run/plain/view rendering, raising package
coverage above the 85% gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread internal/cmd/upsells/update.go
Comment thread internal/cmd/upsells/update.go
Comment thread internal/cmd/upsells/create.go
Follow the API change that preserves omitted associations on update:
- send explicit empty values (variant_id, product_ids, offer_code) to clear,
  instead of dropping keys which now means "leave unchanged"
- clear the offered version when the product changes without a new --variant
- drop product_ids for a universal cross-sell unless --selected-product is given
- drop version upgrades when an upsell becomes a cross-sell
- skip product_ids on a universal create

Drop the section-separator comments from the test file per the no-comments
style rule.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread internal/cmd/upsells/create.go Outdated
Match the update path so a create with an empty --selected-product does not
POST a product_ids array containing blank entries.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread internal/cmd/upsells/update.go Outdated
Comment thread internal/cmd/upsells/create.go
- create: --selected-product requires --cross-sell and conflicts with --universal
- update: --universal and --selected-product cannot be combined; passing
  --selected-product alone retargets a universal cross-sell (sets universal false)
- create only sends product_ids for a targeted cross-sell

Erroring on contradictory flags is clearer than silently dropping one.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread internal/cmd/upsells/update.go
Comment thread internal/cmd/upsells/create.go
Add a single consistency check shared by create and update: cross-sell-only
flags (--variant, --universal, --selected-product, --replace-selected-products)
require a cross-sell, and --offer-variant is rejected on a cross-sell. For
update the check runs against the resulting offer type (current upsell merged
with the changed flags), so editing a version upsell with cross-sell-only flags
is rejected and passing --selected-product alone retargets a universal
cross-sell.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread internal/cmd/upsells/update.go
Symmetric to clearing version upgrades on a cross-sell: when the resulting
offer type is a version upsell, clear variant_id, product_ids, universal, and
replace_selected_products so a converting PUT does not carry stale cross-sell
targeting fields.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ee99747. Configure here.

Comment thread internal/cmd/upsells/upsells.go
nyomanjyotisa and others added 2 commits June 23, 2026 14:40
upsells update --product on a version upsell kept the old product's
upsell_variants in the merged body, so the API rejected them (the variants
don't belong to the new product) and the product change failed. Clear the
version mappings on a product change unless new ones are given via
--offer-variant, mirroring how the offered version is cleared.

Found by exercising the CLI against a local server.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The create help says a cross-sell takes --universal or --selected-product, but
neither was enforced, so a cross-sell could be created (or an upsell converted)
with no audience -- it would never show to any buyer. Require --universal or at
least one --selected-product for a cross-sell on create, and for the resulting
cross-sell on update (including clearing the selected products of a non-universal
cross-sell).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@nyomanjyotisa nyomanjyotisa merged commit 10af476 into main Jun 23, 2026
6 checks passed
@nyomanjyotisa nyomanjyotisa deleted the issue-5551-upsells-cli branch June 23, 2026 19:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant