// Copyright (C) 2019 Storj Labs, Inc. // See LICENSE for copying information. package consoleapi import ( "context" "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/private/web" "storj.io/storj/satellite/console" "storj.io/storj/satellite/payments" "storj.io/storj/satellite/payments/billing" ) 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 accountFreezeService *console.AccountFreezeService } // NewPayments is a constructor for api payments controller. func NewPayments(log *zap.Logger, service *console.Service, accountFreezeService *console.AccountFreezeService) *Payments { return &Payments{ log: log, service: service, accountFreezeService: accountFreezeService, } } // 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))) } } // triggerAttemptPaymentIfFrozen checks if the account is frozen and if frozen, will trigger attempt to pay outstanding invoices. func (p *Payments) triggerAttemptPaymentIfFrozen(ctx context.Context) (err error) { defer mon.Task()(&ctx)(&err) userID, err := p.service.GetUserID(ctx) if err != nil { return err } isFrozen, err := p.accountFreezeService.IsUserFrozen(ctx, userID) if err != nil { return err } if isFrozen { err = p.service.Payments().AttemptPayOverdueInvoices(ctx) if err != nil { return err } err = p.accountFreezeService.UnfreezeUser(ctx, userID) if err != nil { return err } } return nil } // 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 } err = p.triggerAttemptPaymentIfFrozen(ctx) if err != nil { 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 } err = p.triggerAttemptPaymentIfFrozen(ctx) if err != nil { 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 { status := http.StatusInternalServerError if payments.ErrInvalidCoupon.Has(err) { status = http.StatusBadRequest } else if payments.ErrCouponConflict.Has(err) { status = http.StatusConflict } p.serveJSONError(w, status, 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 } if errs.Is(err, billing.ErrNoWallet) { p.serveJSONError(w, http.StatusNotFound, 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))) } } // GetProjectUsagePriceModel returns the project usage price model for the user. func (p *Payments) GetProjectUsagePriceModel(w http.ResponseWriter, r *http.Request) { ctx := r.Context() var err error defer mon.Task()(&ctx)(&err) w.Header().Set("Content-Type", "application/json") pricing, err := p.service.Payments().GetProjectUsagePriceModel(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(pricing); err != nil { p.log.Error("failed to encode project usage price model", zap.Error(ErrPaymentsAPI.Wrap(err))) } } // serveJSONError writes JSON error to response output stream. func (p *Payments) serveJSONError(w http.ResponseWriter, status int, err error) { web.ServeJSONError(p.log, w, status, err) }