3b751a35c5
Removes database tables and functionality related to our custom coupon implementation because it has been superseded by the Stripe coupon and promo code system. Requires implementations of the payments Invoices interface to return coupon usages along with invoices. Change-Id: Iac52d2ff64afca8cc4dbb2d1f20e6ad4b39ddfde
375 lines
9.7 KiB
Go
375 lines
9.7 KiB
Go
// Copyright (C) 2019 Storj Labs, Inc.
|
|
// See LICENSE for copying information.
|
|
|
|
package consoleapi
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io"
|
|
"io/ioutil"
|
|
"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)
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
// 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 := ioutil.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 := ioutil.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)))
|
|
}
|
|
}
|
|
|
|
// TokenDeposit creates new deposit transaction and info about address and amount of newly created tx.
|
|
func (p *Payments) TokenDeposit(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
var err error
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
var requestData struct {
|
|
Amount int64 `json:"amount"`
|
|
}
|
|
|
|
if err = json.NewDecoder(r.Body).Decode(&requestData); err != nil {
|
|
p.serveJSONError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
if requestData.Amount < 0 {
|
|
p.serveJSONError(w, http.StatusBadRequest, errs.New("amount can not be negative"))
|
|
return
|
|
}
|
|
if requestData.Amount == 0 {
|
|
p.serveJSONError(w, http.StatusBadRequest, errs.New("amount should be greater than zero"))
|
|
return
|
|
}
|
|
|
|
tx, err := p.service.Payments().TokenDeposit(ctx, requestData.Amount)
|
|
if err != nil {
|
|
if console.ErrUnauthorized.Has(err) {
|
|
p.serveJSONError(w, http.StatusUnauthorized, err)
|
|
return
|
|
}
|
|
|
|
p.serveJSONError(w, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
var responseData struct {
|
|
Address string `json:"address"`
|
|
Amount float64 `json:"amount"`
|
|
TokenAmount string `json:"tokenAmount"`
|
|
Rate string `json:"rate"`
|
|
Status string `json:"status"`
|
|
Link string `json:"link"`
|
|
ExpiresAt time.Time `json:"expires"`
|
|
}
|
|
|
|
responseData.Address = tx.Address
|
|
responseData.Amount = float64(requestData.Amount) / 100
|
|
responseData.TokenAmount = tx.Amount.AsDecimal().String()
|
|
responseData.Rate = tx.Rate.StringFixed(8)
|
|
responseData.Status = tx.Status.String()
|
|
responseData.Link = tx.Link
|
|
responseData.ExpiresAt = tx.CreatedAt.Add(tx.Timeout)
|
|
|
|
err = json.NewEncoder(w).Encode(responseData)
|
|
if err != nil {
|
|
p.log.Error("failed to write json token deposit 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 := ioutil.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)))
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|