Skip to content

Commit 71cb57d

Browse files
devin-ai-integration[bot]theFongcloin
authored
Add brev redeem command for coupon code redemption (#259)
* Add brev redeem command for coupon code redemption - Add RedeemCouponCode method to organization store - Create new redeem command with --org flag support - Wire command into CLI command tree - Include completion handler for org names - Display success message with credits amount and duration Co-Authored-By: Alec Fong <alecsanf@usc.edu> * Fix string formatting in redeem output --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Alec Fong <alecsanf@usc.edu> Co-authored-by: Colin McNaughton <cloin@users.noreply.github.com>
1 parent b82eedb commit 71cb57d

3 files changed

Lines changed: 144 additions & 0 deletions

File tree

pkg/cmd/cmd.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
"github.com/brevdev/brev-cli/pkg/cmd/profile"
3131
"github.com/brevdev/brev-cli/pkg/cmd/proxy"
3232
"github.com/brevdev/brev-cli/pkg/cmd/recreate"
33+
"github.com/brevdev/brev-cli/pkg/cmd/redeem"
3334
"github.com/brevdev/brev-cli/pkg/cmd/refresh"
3435
"github.com/brevdev/brev-cli/pkg/cmd/reset"
3536
"github.com/brevdev/brev-cli/pkg/cmd/runtasks"
@@ -238,6 +239,7 @@ func createCmdTree(cmd *cobra.Command, t *terminal.Terminal, loginCmdStore *stor
238239
cmd.AddCommand(ls.NewCmdLs(t, loginCmdStore, noLoginCmdStore))
239240
cmd.AddCommand(org.NewCmdOrg(t, loginCmdStore, noLoginCmdStore))
240241
cmd.AddCommand(invite.NewCmdInvite(t, loginCmdStore, noLoginCmdStore))
242+
cmd.AddCommand(redeem.NewCmdRedeem(t, loginCmdStore, noLoginCmdStore))
241243
cmd.AddCommand(portforward.NewCmdPortForwardSSH(loginCmdStore, t))
242244
cmd.AddCommand(login.NewCmdLogin(t, noLoginCmdStore, loginAuth))
243245
cmd.AddCommand(logout.NewCmdLogout(loginAuth, noLoginCmdStore))

pkg/cmd/redeem/redeem.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package redeem
2+
3+
import (
4+
"fmt"
5+
"time"
6+
7+
"github.com/brevdev/brev-cli/pkg/cmd/cmderrors"
8+
"github.com/brevdev/brev-cli/pkg/cmd/completions"
9+
"github.com/brevdev/brev-cli/pkg/cmdcontext"
10+
"github.com/brevdev/brev-cli/pkg/entity"
11+
breverrors "github.com/brevdev/brev-cli/pkg/errors"
12+
"github.com/brevdev/brev-cli/pkg/store"
13+
"github.com/brevdev/brev-cli/pkg/terminal"
14+
15+
"github.com/spf13/cobra"
16+
)
17+
18+
type RedeemStore interface {
19+
GetActiveOrganizationOrDefault() (*entity.Organization, error)
20+
GetOrganizations(options *store.GetOrganizationsOptions) ([]entity.Organization, error)
21+
RedeemCouponCode(organizationID string, code string) (*store.RedeemCouponCodeResponse, error)
22+
completions.CompletionStore
23+
}
24+
25+
func NewCmdRedeem(t *terminal.Terminal, redeemStore RedeemStore, noRedeemStore RedeemStore) *cobra.Command {
26+
var orgFlag string
27+
28+
cmd := &cobra.Command{
29+
Annotations: map[string]string{"housekeeping": ""},
30+
Use: "redeem <code>",
31+
DisableFlagsInUseLine: true,
32+
Short: "Redeem a coupon code for credits",
33+
Long: "Redeem a coupon code to add credits to your active organization",
34+
Example: `
35+
brev redeem ABC123XYZ
36+
brev redeem ABC123XYZ --org myorg
37+
`,
38+
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
39+
err := cmdcontext.InvokeParentPersistentPreRun(cmd, args)
40+
if err != nil {
41+
return breverrors.WrapAndTrace(err)
42+
}
43+
44+
return nil
45+
},
46+
Args: cmderrors.TransformToValidationError(cobra.ExactArgs(1)),
47+
RunE: func(cmd *cobra.Command, args []string) error {
48+
err := RunRedeem(t, redeemStore, args[0], orgFlag)
49+
if err != nil {
50+
return breverrors.WrapAndTrace(err)
51+
}
52+
return nil
53+
},
54+
}
55+
56+
cmd.Flags().StringVarP(&orgFlag, "org", "o", "", "organization (will override active org)")
57+
err := cmd.RegisterFlagCompletionFunc("org", completions.GetOrgsNameCompletionHandler(noRedeemStore, t))
58+
if err != nil {
59+
breverrors.GetDefaultErrorReporter().ReportError(breverrors.WrapAndTrace(err))
60+
fmt.Print(breverrors.WrapAndTrace(err))
61+
}
62+
63+
return cmd
64+
}
65+
66+
func RunRedeem(t *terminal.Terminal, redeemStore RedeemStore, code string, orgFlag string) error {
67+
startTime := time.Now()
68+
69+
var org *entity.Organization
70+
if orgFlag != "" {
71+
orgs, err := redeemStore.GetOrganizations(&store.GetOrganizationsOptions{Name: orgFlag})
72+
if err != nil {
73+
return breverrors.WrapAndTrace(err)
74+
}
75+
if len(orgs) == 0 {
76+
return fmt.Errorf("no org found with name %s", orgFlag)
77+
} else if len(orgs) > 1 {
78+
return fmt.Errorf("more than one org found with name %s", orgFlag)
79+
}
80+
81+
org = &orgs[0]
82+
} else {
83+
currOrg, err := redeemStore.GetActiveOrganizationOrDefault()
84+
if err != nil {
85+
return breverrors.WrapAndTrace(err)
86+
}
87+
if currOrg == nil {
88+
return fmt.Errorf("no orgs exist")
89+
}
90+
org = currOrg
91+
}
92+
93+
result, err := redeemStore.RedeemCouponCode(org.ID, code)
94+
if err != nil {
95+
return breverrors.WrapAndTrace(err)
96+
}
97+
98+
duration := time.Since(startTime)
99+
100+
t.Vprint(t.Green(fmt.Sprintf("✓ Successfully redeemed coupon code: %s\n", code)))
101+
if result.Data.Transaction.AmountUSD != "" {
102+
t.Vprintf(" Credits added: $%s\n", result.Data.Transaction.AmountUSD)
103+
}
104+
t.Vprintf(" Organization: %s\n", org.Name)
105+
t.Vprintf(" Duration: %v\n", duration.Round(time.Millisecond))
106+
107+
return nil
108+
}

pkg/store/organization.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,3 +213,37 @@ func GetDefaultOrNilOrg(orgs []entity.Organization) *entity.Organization {
213213
return nil
214214
}
215215
}
216+
217+
type RedeemCouponCodeRequest struct {
218+
Code string `json:"Code"`
219+
}
220+
221+
type RedeemCouponCodeResponse struct {
222+
Data struct {
223+
Transaction struct {
224+
AmountUSD string `json:"amount_usd"`
225+
} `json:"transaction"`
226+
} `json:"data"`
227+
}
228+
229+
func (s AuthHTTPStore) RedeemCouponCode(organizationID string, code string) (*RedeemCouponCodeResponse, error) {
230+
var result RedeemCouponCodeResponse
231+
path := orgPath + "/" + organizationID + "/credits/code/redeem"
232+
req := RedeemCouponCodeRequest{
233+
Code: code,
234+
}
235+
236+
res, err := s.authHTTPClient.restyClient.R().
237+
SetHeader("Content-Type", "application/json").
238+
SetResult(&result).
239+
SetBody(req).
240+
Post(path)
241+
if err != nil {
242+
return nil, breverrors.WrapAndTrace(err)
243+
}
244+
if res.IsError() {
245+
return nil, NewHTTPResponseError(res)
246+
}
247+
248+
return &result, nil
249+
}

0 commit comments

Comments
 (0)