4bcf308a04
Jira: https://storjlabs.atlassian.net/browse/USR-822 The balance history in Satellite GUI display the deposit bonuses as separate rows. These bonuses used to be stored in the satellite DB. We recently started depositing the bonus directly to the Stripe balance and migrated old bonuses to Stripe metadata. This change displays all billing history entirely from Stripe, so we can remove the `credits` and `credits_spendings` DB tables in a next step. Change-Id: I14c304c66ec47c6a51f5b8508f11470cf36c4e24
244 lines
6.7 KiB
Go
244 lines
6.7 KiB
Go
// Copyright (C) 2019 Storj Labs, Inc.
|
|
// See LICENSE for copying information.
|
|
|
|
package stripecoinpayments
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/stripe/stripe-go"
|
|
"go.uber.org/zap"
|
|
|
|
"storj.io/common/uuid"
|
|
"storj.io/storj/satellite/payments"
|
|
"storj.io/storj/satellite/payments/coinpayments"
|
|
)
|
|
|
|
const (
|
|
// StripeDepositTransactionDescription is the description for Stripe
|
|
// balance transactions representing STORJ deposits.
|
|
StripeDepositTransactionDescription = "STORJ deposit"
|
|
|
|
// StripeDepositBonusTransactionDescription is the description for Stripe
|
|
// balance transactions representing bonuses received for STORJ deposits.
|
|
StripeDepositBonusTransactionDescription = "STORJ deposit bonus"
|
|
|
|
// StripeMigratedDepositBonusTransactionDescription is the description for
|
|
// Stripe balance transactions representing bonuses migrated from the
|
|
// 'credits' table of the satellite DB.
|
|
StripeMigratedDepositBonusTransactionDescription = "Migrated STORJ deposit bonus"
|
|
)
|
|
|
|
// ensure that storjTokens implements payments.StorjTokens.
|
|
var _ payments.StorjTokens = (*storjTokens)(nil)
|
|
|
|
// storjTokens implements payments.StorjTokens.
|
|
//
|
|
// architecture: Service
|
|
type storjTokens struct {
|
|
service *Service
|
|
}
|
|
|
|
// Deposit creates new deposit transaction with the given amount returning
|
|
// ETH wallet address where funds should be sent. There is one
|
|
// hour limit to complete the transaction. Transaction is saved to DB with
|
|
// reference to the user who made the deposit.
|
|
func (tokens *storjTokens) Deposit(ctx context.Context, userID uuid.UUID, amount int64) (_ *payments.Transaction, err error) {
|
|
defer mon.Task()(&ctx, userID, amount)(&err)
|
|
|
|
customerID, err := tokens.service.db.Customers().GetCustomerID(ctx, userID)
|
|
if err != nil {
|
|
return nil, Error.Wrap(err)
|
|
}
|
|
|
|
c, err := tokens.service.stripeClient.Customers().Get(customerID, nil)
|
|
if err != nil {
|
|
return nil, Error.Wrap(err)
|
|
}
|
|
|
|
rate, err := tokens.service.GetRate(ctx, coinpayments.CurrencySTORJ, coinpayments.CurrencyUSD)
|
|
if err != nil {
|
|
return nil, Error.Wrap(err)
|
|
}
|
|
|
|
tokenAmount := convertFromCents(rate, amount).SetPrec(payments.STORJTokenPrecision)
|
|
|
|
tx, err := tokens.service.coinPayments.Transactions().Create(ctx,
|
|
&coinpayments.CreateTX{
|
|
Amount: *tokenAmount,
|
|
CurrencyIn: coinpayments.CurrencySTORJ,
|
|
CurrencyOut: coinpayments.CurrencySTORJ,
|
|
BuyerEmail: c.Email,
|
|
},
|
|
)
|
|
if err != nil {
|
|
return nil, Error.Wrap(err)
|
|
}
|
|
|
|
key, err := coinpayments.GetTransacationKeyFromURL(tx.CheckoutURL)
|
|
if err != nil {
|
|
return nil, Error.Wrap(err)
|
|
}
|
|
|
|
if err = tokens.service.db.Transactions().LockRate(ctx, tx.ID, rate); err != nil {
|
|
return nil, Error.Wrap(err)
|
|
}
|
|
|
|
cpTX, err := tokens.service.db.Transactions().Insert(ctx,
|
|
Transaction{
|
|
ID: tx.ID,
|
|
AccountID: userID,
|
|
Address: tx.Address,
|
|
Amount: tx.Amount,
|
|
Status: coinpayments.StatusPending,
|
|
Key: key,
|
|
Timeout: tx.Timeout,
|
|
},
|
|
)
|
|
if err != nil {
|
|
return nil, Error.Wrap(err)
|
|
}
|
|
|
|
return &payments.Transaction{
|
|
ID: payments.TransactionID(tx.ID),
|
|
Amount: *payments.TokenAmountFromBigFloat(&tx.Amount),
|
|
Rate: *rate,
|
|
Address: tx.Address,
|
|
Status: payments.TransactionStatusPending,
|
|
Timeout: tx.Timeout,
|
|
Link: tx.CheckoutURL,
|
|
CreatedAt: cpTX.CreatedAt,
|
|
}, nil
|
|
}
|
|
|
|
// ListTransactionInfos fetches all transactions from the database for specified user, reconstructing checkout link.
|
|
func (tokens *storjTokens) ListTransactionInfos(ctx context.Context, userID uuid.UUID) (_ []payments.TransactionInfo, err error) {
|
|
defer mon.Task()(&ctx, userID)(&err)
|
|
|
|
txs, err := tokens.service.db.Transactions().ListAccount(ctx, userID)
|
|
if err != nil {
|
|
return nil, Error.Wrap(err)
|
|
}
|
|
|
|
var infos []payments.TransactionInfo
|
|
for _, tx := range txs {
|
|
link := coinpayments.GetCheckoutURL(tx.Key, tx.ID)
|
|
|
|
var status payments.TransactionStatus
|
|
switch tx.Status {
|
|
case coinpayments.StatusPending:
|
|
status = payments.TransactionStatusPending
|
|
case coinpayments.StatusReceived:
|
|
status = payments.TransactionStatusPaid
|
|
case coinpayments.StatusCancelled:
|
|
status = payments.TransactionStatusCancelled
|
|
default:
|
|
// unknown
|
|
status = payments.TransactionStatus(tx.Status.String())
|
|
}
|
|
|
|
rate, err := tokens.service.db.Transactions().GetLockedRate(ctx, tx.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
infos = append(infos,
|
|
payments.TransactionInfo{
|
|
ID: []byte(tx.ID),
|
|
Amount: *payments.TokenAmountFromBigFloat(&tx.Amount),
|
|
Received: *payments.TokenAmountFromBigFloat(&tx.Received),
|
|
AmountCents: convertToCents(rate, &tx.Amount),
|
|
ReceivedCents: convertToCents(rate, &tx.Received),
|
|
Address: tx.Address,
|
|
Status: status,
|
|
Link: link,
|
|
ExpiresAt: tx.CreatedAt.Add(tx.Timeout),
|
|
CreatedAt: tx.CreatedAt,
|
|
},
|
|
)
|
|
}
|
|
|
|
return infos, nil
|
|
}
|
|
|
|
// ListDepositBonuses returns all deposit bonuses from Stripe associated with user.
|
|
func (tokens *storjTokens) ListDepositBonuses(ctx context.Context, userID uuid.UUID) (_ []payments.DepositBonus, err error) {
|
|
defer mon.Task()(&ctx, userID)(&err)
|
|
|
|
cusID, err := tokens.service.db.Customers().GetCustomerID(ctx, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var bonuses []payments.DepositBonus
|
|
|
|
customer, err := tokens.service.stripeClient.Customers().Get(cusID, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for key, value := range customer.Metadata {
|
|
if !strings.HasPrefix(key, "credit_") {
|
|
continue
|
|
}
|
|
|
|
var credit payments.Credit
|
|
err = json.Unmarshal([]byte(value), &credit)
|
|
if err != nil {
|
|
tokens.service.log.Error("Error unmarshaling credit history from Stripe metadata",
|
|
zap.String("Customer ID", cusID),
|
|
zap.String("Metadata Key", key),
|
|
zap.String("Metadata Value", value),
|
|
zap.Error(err),
|
|
)
|
|
continue
|
|
}
|
|
|
|
bonuses = append(bonuses,
|
|
payments.DepositBonus{
|
|
TransactionID: payments.TransactionID(credit.TransactionID),
|
|
AmountCents: credit.Amount,
|
|
Percentage: 10,
|
|
CreatedAt: credit.Created,
|
|
},
|
|
)
|
|
}
|
|
|
|
it := tokens.service.stripeClient.CustomerBalanceTransactions().List(&stripe.CustomerBalanceTransactionListParams{Customer: stripe.String(cusID)})
|
|
for it.Next() {
|
|
tx := it.CustomerBalanceTransaction()
|
|
|
|
if tx.Type != stripe.CustomerBalanceTransactionTypeAdjustment {
|
|
continue
|
|
}
|
|
|
|
if tx.Description != StripeDepositBonusTransactionDescription {
|
|
continue
|
|
}
|
|
|
|
percentage := int64(10)
|
|
percentageStr, ok := tx.Metadata["percentage"]
|
|
if ok {
|
|
percentage, err = strconv.ParseInt(percentageStr, 10, 64)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
bonuses = append(bonuses,
|
|
payments.DepositBonus{
|
|
TransactionID: []byte(tx.Metadata["txID"]),
|
|
AmountCents: -tx.Amount,
|
|
Percentage: percentage,
|
|
CreatedAt: time.Unix(tx.Created, 0),
|
|
},
|
|
)
|
|
}
|
|
|
|
return bonuses, nil
|
|
}
|