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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ gumroad admin users mark-compliant --user-id 2245593582708 --expected-email sell
gumroad admin users suspend --user-id 2245593582708 --expected-email seller@example.com --note "Chargeback risk confirmed"
gumroad admin users suspend-for-tos-violation --user-id 2245593582708 --expected-email seller@example.com --note "DMCA takedown notice confirmed"
gumroad admin products flag-for-tos-violation abc123 --user-id 2245593582708 --expected-email seller@example.com
gumroad admin payouts scheduled create --user-id 2245593582708 --expected-email seller@example.com --processor stripe --payout-date 2026-06-15
gumroad admin payouts scheduled list --status pending --user-id 2245593582708
gumroad admin purchases lookup --stripe-fingerprint fp_abc --limit 25
gumroad admin users watch --user-id 2245593582708 --expected-email seller@example.com --revenue-threshold 200 --note "Review next buyers"
gumroad admin users update-watch --user-id 2245593582708 --expected-email seller@example.com --revenue-threshold 500
Expand Down
1 change: 1 addition & 0 deletions internal/cmd/admin/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ func NewAdminCmd() *cobra.Command {
gumroad admin users watch --user-id <user-id> --expected-email <email> --revenue-threshold 200
gumroad admin payouts list --email <email>
gumroad admin payouts pause --user-id <user-id> --expected-email <email>
gumroad admin payouts scheduled create --user-id <user-id> --processor stripe
gumroad admin products list --email <email>
gumroad admin products view <product-id>
gumroad admin products flag-for-tos-violation <product-id> --user-id <user-id>`,
Expand Down
1 change: 1 addition & 0 deletions internal/cmd/admin/payouts/payouts.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ func NewPayoutsCmd() *cobra.Command {
gumroad admin payouts pause --user-id 2245593582708 --expected-email seller@example.com --reason "Verification pending"
gumroad admin payouts resume --user-id 2245593582708
gumroad admin payouts issue --user-id 2245593582708 --through 2026-04-30 --processor stripe --yes
gumroad admin payouts scheduled create --user-id 2245593582708 --processor stripe --yes
gumroad admin payouts scheduled list --status flagged`,
}

Expand Down
2 changes: 2 additions & 0 deletions internal/cmd/admin/payouts/scheduled.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ func newScheduledCmd() *cobra.Command {
Short: "Inspect and act on ScheduledPayout records",
Example: ` gumroad admin payouts scheduled list
gumroad admin payouts scheduled list --status flagged
gumroad admin payouts scheduled create --user-id 2245593582708 --processor stripe --yes
gumroad admin payouts scheduled execute pay_abc123 --yes
gumroad admin payouts scheduled cancel pay_abc123`,
}

cmd.AddCommand(newScheduledListCmd())
cmd.AddCommand(newScheduledCreateCmd())
cmd.AddCommand(newScheduledExecuteCmd())
cmd.AddCommand(newScheduledCancelCmd())

Expand Down
186 changes: 186 additions & 0 deletions internal/cmd/admin/payouts/scheduled_create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package payouts

import (
"net/http"
"net/url"
"strconv"
"strings"

"github.com/antiwork/gumroad-cli/internal/adminapi"
"github.com/antiwork/gumroad-cli/internal/admincmd"
"github.com/antiwork/gumroad-cli/internal/cmdutil"
"github.com/antiwork/gumroad-cli/internal/output"
"github.com/spf13/cobra"
)

type scheduledCreateRequest struct {
UserID string `json:"user_id"`
ExpectedEmail string `json:"expected_email,omitempty"`
Processor string `json:"processor"`
PayoutDate string `json:"payout_date,omitempty"`
Note string `json:"note,omitempty"`
}

type scheduledCreateResponse struct {
Success bool `json:"success"`
UserID string `json:"user_id"`
Message string `json:"message"`
ScheduledPayout scheduledPayout `json:"scheduled_payout"`
}

func newScheduledCreateCmd() *cobra.Command {
var (
targetFlags mutationFlags
processor string
payoutDate string
note string
)

cmd := &cobra.Command{
Use: "create",
Short: "Create a delayed scheduled payout for a suspended user",
Long: `Create a ScheduledPayout row for a suspended user with unpaid balance.

The server defaults --payout-date to 21 days from the current UTC date. Pass a
YYYY-MM-DD date to schedule a specific UTC payout date. --processor is required
and must be stripe or paypal.

This schedules money movement. --yes is required.`,
Example: ` gumroad admin payouts scheduled create --user-id 2245593582708 --processor stripe --yes
gumroad admin payouts scheduled create --user-id 2245593582708 --expected-email seller@example.com --processor paypal --payout-date 2026-06-15 --note "Appeal window closes before payout." --yes`,
Args: cmdutil.ExactArgs(0),
RunE: func(c *cobra.Command, args []string) error {
opts := cmdutil.OptionsFrom(c)
target, err := resolveMutationTarget(c, targetFlags)
if err != nil {
return err
}
if processor == "" {
return cmdutil.MissingFlagError(c, "--processor")
}
normalizedProcessor := strings.ToLower(strings.TrimSpace(processor))
if normalizedProcessor != "stripe" && normalizedProcessor != "paypal" {
return cmdutil.UsageErrorf(c, "--processor must be one of: stripe, paypal")
}
if err := cmdutil.RequireDateFlag(c, "payout-date", payoutDate); err != nil {
return err
}

req := scheduledCreateRequest{
UserID: target.UserID,
ExpectedEmail: target.ExpectedEmail,
Processor: normalizedProcessor,
PayoutDate: payoutDate,
Note: note,
}

confirmMsg := "Schedule " + normalizedProcessor + " payout for user_id " + target.UserID + "? This schedules money movement."
if payoutDate != "" {
confirmMsg = "Schedule " + normalizedProcessor + " payout for user_id " + target.UserID + " on " + payoutDate + "? This schedules money movement."
}
ok, err := cmdutil.ConfirmAction(opts, confirmMsg)
if err != nil {
return err
}
if !ok {
return cmdutil.PrintCancelledAction(opts, "schedule payout for user_id "+target.UserID, target.UserID)
}

path := "scheduled_payouts"
if opts.DryRun {
return cmdutil.PrintDryRunRequest(opts, http.MethodPost, adminapi.AdminPath(path), scheduledCreateDryRunParams(req))
}

data, err := admincmd.FetchPostJSON(opts, "Creating scheduled payout...", path, req)
if err != nil {
return err
}
if opts.UsesJSONOutput() {
return cmdutil.PrintJSONResponse(opts, data)
}

decoded, err := cmdutil.DecodeJSON[scheduledCreateResponse](data)
if err != nil {
return err
}
return renderScheduledCreate(opts, fallbackStr(decoded.UserID, target.UserID), decoded)
},
}

addMutationFlags(cmd, &targetFlags)
cmd.Flags().StringVar(&processor, "processor", "", "Payout processor: stripe or paypal (required)")
cmd.Flags().StringVar(&payoutDate, "payout-date", "", "UTC payout date in YYYY-MM-DD (defaults server-side to today + 21 days)")
cmd.Flags().StringVar(&note, "note", "", "Optional payout note recorded on the user")

return cmd
}

func scheduledCreateDryRunParams(req scheduledCreateRequest) url.Values {
params := url.Values{}
params.Set("user_id", req.UserID)
if req.ExpectedEmail != "" {
params.Set("expected_email", req.ExpectedEmail)
}
params.Set("processor", req.Processor)
if req.PayoutDate != "" {
params.Set("payout_date", req.PayoutDate)
}
if req.Note != "" {
params.Set("note", req.Note)
}
return params
}

func renderScheduledCreate(opts cmdutil.Options, userID string, resp scheduledCreateResponse) error {
headline := fallbackStr(resp.Message, "Scheduled payout created")
payout := resp.ScheduledPayout

if opts.PlainOutput {
return output.PrintPlain(opts.Out(), [][]string{{
strconv.FormatBool(resp.Success),
headline,
userID,
payout.ExternalID,
formatScheduledAmount(payout),
payout.Status,
payout.ScheduledAt,
payout.Processor,
}})
}

if opts.Quiet {
return nil
}

style := opts.Style()
if err := output.Writeln(opts.Out(), style.Green(headline)); err != nil {
return err
}
if err := writeUserIDLine(opts.Out(), headline, userID); err != nil {
return err
}
if payout.ExternalID != "" {
if err := output.Writef(opts.Out(), "Payout ID: %s\n", payout.ExternalID); err != nil {
return err
}
}
if payout.AmountCents != 0 {
if err := output.Writef(opts.Out(), "Amount: %s\n", formatScheduledAmount(payout)); err != nil {
return err
}
}
if payout.Status != "" {
if err := output.Writef(opts.Out(), "Status: %s\n", payout.Status); err != nil {
return err
}
}
if payout.ScheduledAt != "" {
if err := output.Writef(opts.Out(), "Scheduled: %s\n", payout.ScheduledAt); err != nil {
return err
}
}
if payout.Processor != "" {
return output.Writef(opts.Out(), "Processor: %s\n", payout.Processor)
}
return nil
}
1 change: 1 addition & 0 deletions internal/cmd/admin/payouts/scheduled_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type scheduledPayout struct {
Status string `json:"status"`
Action string `json:"action"`
ScheduledAt string `json:"scheduled_at"`
Processor string `json:"processor"`
ExecutedAt string `json:"executed_at"`
CreatedAt string `json:"created_at"`
CreatedBy scheduledPayoutCreator `json:"created_by"`
Expand Down
Loading
Loading