// Copyright (C) 2019 Storj Labs, Inc. // See LICENSE for copying information. package consoleapi import ( "encoding/json" "io" "net/http" "strconv" "time" "github.com/gorilla/mux" "github.com/spacemonkeygo/monkit/v3" "github.com/zeebo/errs" "go.uber.org/zap" "storj.io/storj/satellite/console" ) var ( // ErrPaymentsAPI - console payments api error type. ErrPaymentsAPI = errs.Class("consoleapi payments") mon = monkit.Package() ) // Payments is an api controller that exposes all payment related functionality. type Payments struct { log *zap.Logger service *console.Service } // NewPayments is a constructor for api payments controller. func NewPayments(log *zap.Logger, service *console.Service) *Payments { return &Payments{ log: log, service: service, } } // SetupAccount creates a payment account for the user. func (p *Payments) SetupAccount(w http.ResponseWriter, r *http.Request) { ctx := r.Context() var err error defer mon.Task()(&ctx)(&err) couponType, err := p.service.Payments().SetupAccount(ctx) if err != nil { if console.ErrUnauthorized.Has(err) { p.serveJSONError(w, http.StatusUnauthorized, err) return } p.serveJSONError(w, http.StatusInternalServerError, err) return } err = json.NewEncoder(w).Encode(couponType) if err != nil { p.log.Error("failed to write json token deposit response", zap.Error(ErrPaymentsAPI.Wrap(err))) } } // AccountBalance returns an integer amount in cents that represents the current balance of payment account. func (p *Payments) AccountBalance(w http.ResponseWriter, r *http.Request) { ctx := r.Context() var err error defer mon.Task()(&ctx)(&err) w.Header().Set("Content-Type", "application/json") balance, err := p.service.Payments().AccountBalance(ctx) if err != nil { if console.ErrUnauthorized.Has(err) { p.serveJSONError(w, http.StatusUnauthorized, err) return } p.serveJSONError(w, http.StatusInternalServerError, err) return } err = json.NewEncoder(w).Encode(&balance) if err != nil { p.log.Error("failed to write json balance response", zap.Error(ErrPaymentsAPI.Wrap(err))) } } // ProjectsCharges returns how much money current user will be charged for each project which he owns. func (p *Payments) ProjectsCharges(w http.ResponseWriter, r *http.Request) { ctx := r.Context() var err error defer mon.Task()(&ctx)(&err) w.Header().Set("Content-Type", "application/json") sinceStamp, err := strconv.ParseInt(r.URL.Query().Get("from"), 10, 64) if err != nil { p.serveJSONError(w, http.StatusBadRequest, err) return } beforeStamp, err := strconv.ParseInt(r.URL.Query().Get("to"), 10, 64) if err != nil { p.serveJSONError(w, http.StatusBadRequest, err) return } since := time.Unix(sinceStamp, 0).UTC() before := time.Unix(beforeStamp, 0).UTC() charges, err := p.service.Payments().ProjectsCharges(ctx, since, before) if err != nil { if console.ErrUnauthorized.Has(err) { p.serveJSONError(w, http.StatusUnauthorized, err) return } p.serveJSONError(w, http.StatusInternalServerError, err) return } err = json.NewEncoder(w).Encode(charges) if err != nil { p.log.Error("failed to write json response", zap.Error(ErrPaymentsAPI.Wrap(err))) } } // AddCreditCard is used to save new credit card and attach it to payment account. func (p *Payments) AddCreditCard(w http.ResponseWriter, r *http.Request) { ctx := r.Context() var err error defer mon.Task()(&ctx)(&err) bodyBytes, err := io.ReadAll(r.Body) if err != nil { p.serveJSONError(w, http.StatusBadRequest, err) return } token := string(bodyBytes) err = p.service.Payments().AddCreditCard(ctx, token) if err != nil { if console.ErrUnauthorized.Has(err) { p.serveJSONError(w, http.StatusUnauthorized, err) return } p.serveJSONError(w, http.StatusInternalServerError, err) return } } // ListCreditCards returns a list of credit cards for a given payment account. func (p *Payments) ListCreditCards(w http.ResponseWriter, r *http.Request) { ctx := r.Context() var err error defer mon.Task()(&ctx)(&err) w.Header().Set("Content-Type", "application/json") cards, err := p.service.Payments().ListCreditCards(ctx) if err != nil { if console.ErrUnauthorized.Has(err) { p.serveJSONError(w, http.StatusUnauthorized, err) return } p.serveJSONError(w, http.StatusInternalServerError, err) return } if cards == nil { _, err = w.Write([]byte("[]")) } else { err = json.NewEncoder(w).Encode(cards) } if err != nil { p.log.Error("failed to write json list cards response", zap.Error(ErrPaymentsAPI.Wrap(err))) } } // MakeCreditCardDefault makes a credit card default payment method. func (p *Payments) MakeCreditCardDefault(w http.ResponseWriter, r *http.Request) { ctx := r.Context() var err error defer mon.Task()(&ctx)(&err) cardID, err := io.ReadAll(r.Body) if err != nil { p.serveJSONError(w, http.StatusBadRequest, err) return } err = p.service.Payments().MakeCreditCardDefault(ctx, string(cardID)) if err != nil { if console.ErrUnauthorized.Has(err) { p.serveJSONError(w, http.StatusUnauthorized, err) return } p.serveJSONError(w, http.StatusInternalServerError, err) return } } // RemoveCreditCard is used to detach a credit card from payment account. func (p *Payments) RemoveCreditCard(w http.ResponseWriter, r *http.Request) { ctx := r.Context() var err error defer mon.Task()(&ctx)(&err) vars := mux.Vars(r) cardID := vars["cardId"] if cardID == "" { p.serveJSONError(w, http.StatusBadRequest, err) return } err = p.service.Payments().RemoveCreditCard(ctx, cardID) if err != nil { if console.ErrUnauthorized.Has(err) { p.serveJSONError(w, http.StatusUnauthorized, err) return } p.serveJSONError(w, http.StatusInternalServerError, err) return } } // BillingHistory returns a list of invoices, transactions and all others billing history items for payment account. func (p *Payments) BillingHistory(w http.ResponseWriter, r *http.Request) { ctx := r.Context() var err error defer mon.Task()(&ctx)(&err) w.Header().Set("Content-Type", "application/json") billingHistory, err := p.service.Payments().BillingHistory(ctx) if err != nil { if console.ErrUnauthorized.Has(err) { p.serveJSONError(w, http.StatusUnauthorized, err) return } p.serveJSONError(w, http.StatusInternalServerError, err) return } if billingHistory == nil { _, err = w.Write([]byte("[]")) } else { err = json.NewEncoder(w).Encode(billingHistory) } if err != nil { p.log.Error("failed to write json billing history response", zap.Error(ErrPaymentsAPI.Wrap(err))) } } // ApplyCouponCode applies a coupon code to the user's account. func (p *Payments) ApplyCouponCode(w http.ResponseWriter, r *http.Request) { ctx := r.Context() var err error defer mon.Task()(&ctx)(&err) // limit the size of the body to prevent excessive memory usage bodyBytes, err := io.ReadAll(io.LimitReader(r.Body, 1*1024*1024)) if err != nil { p.serveJSONError(w, http.StatusInternalServerError, err) return } couponCode := string(bodyBytes) coupon, err := p.service.Payments().ApplyCouponCode(ctx, couponCode) if err != nil { p.serveJSONError(w, http.StatusInternalServerError, err) return } if err = json.NewEncoder(w).Encode(coupon); err != nil { p.log.Error("failed to encode coupon", zap.Error(ErrPaymentsAPI.Wrap(err))) } } // GetCoupon returns the coupon applied to the user's account. func (p *Payments) GetCoupon(w http.ResponseWriter, r *http.Request) { ctx := r.Context() var err error defer mon.Task()(&ctx)(&err) w.Header().Set("Content-Type", "application/json") coupon, err := p.service.Payments().GetCoupon(ctx) if err != nil { if console.ErrUnauthorized.Has(err) { p.serveJSONError(w, http.StatusUnauthorized, err) return } p.serveJSONError(w, http.StatusInternalServerError, err) return } if err = json.NewEncoder(w).Encode(coupon); err != nil { p.log.Error("failed to encode coupon", zap.Error(ErrPaymentsAPI.Wrap(err))) } } // GetWallet returns the wallet address (with balance) already assigned to the user. func (p *Payments) GetWallet(w http.ResponseWriter, r *http.Request) { ctx := r.Context() var err error defer mon.Task()(&ctx)(&err) w.Header().Set("Content-Type", "application/json") walletInfo, err := p.service.Payments().GetWallet(ctx) if err != nil { if console.ErrUnauthorized.Has(err) { p.serveJSONError(w, http.StatusUnauthorized, err) return } p.serveJSONError(w, http.StatusInternalServerError, err) return } if err = json.NewEncoder(w).Encode(walletInfo); err != nil { p.log.Error("failed to encode wallet info", zap.Error(ErrPaymentsAPI.Wrap(err))) } } // ClaimWallet will claim a new wallet address. Returns with existing if it's already claimed. func (p *Payments) ClaimWallet(w http.ResponseWriter, r *http.Request) { ctx := r.Context() var err error defer mon.Task()(&ctx)(&err) w.Header().Set("Content-Type", "application/json") walletInfo, err := p.service.Payments().ClaimWallet(ctx) if err != nil { if console.ErrUnauthorized.Has(err) { p.serveJSONError(w, http.StatusUnauthorized, err) return } p.serveJSONError(w, http.StatusInternalServerError, err) return } if err = json.NewEncoder(w).Encode(walletInfo); err != nil { p.log.Error("failed to encode wallet info", zap.Error(ErrPaymentsAPI.Wrap(err))) } } // WalletPayments returns with the list of storjscan transactions for user`s wallet. func (p *Payments) WalletPayments(w http.ResponseWriter, r *http.Request) { ctx := r.Context() var err error defer mon.Task()(&ctx)(&err) w.Header().Set("Content-Type", "application/json") walletPayments, err := p.service.Payments().WalletPayments(ctx) if err != nil { if console.ErrUnauthorized.Has(err) { p.serveJSONError(w, http.StatusUnauthorized, err) return } p.serveJSONError(w, http.StatusInternalServerError, err) return } if err = json.NewEncoder(w).Encode(walletPayments); err != nil { p.log.Error("failed to encode payments", zap.Error(ErrPaymentsAPI.Wrap(err))) } } // serveJSONError writes JSON error to response output stream. func (p *Payments) serveJSONError(w http.ResponseWriter, status int, err error) { ServeJSONError(p.log, w, status, err) }