d66e646b57
Jira issue: https://storjlabs.atlassian.net/browse/USR-820 The bonus for depositing STORJ tokens is now added as to the Stripe balance instead of the to `credits` DB table on the satellite. Existing unspent bonuses in the `credits` DB table are still processed as usual when generating invoices. They will be migrated to the Stripe balance with a separate change. The bonus is added to the Stripe balance with a separate Credit transaction. The balance transactions for the deposit and the bonus can be differentiate by their different description. The billing history is modified to list the bonus from the Stripe transactions list. The workflow for depositing STORJ tokens to the Stripe balance is improved to survive failures in the middle of the process. Change-Id: I6a1017984eae34e97c580f9093f7e51ca417b962
203 lines
5.7 KiB
Go
203 lines
5.7 KiB
Go
// Copyright (C) 2019 Storj Labs, Inc.
|
|
// See LICENSE for copying information.
|
|
|
|
package stripecoinpayments
|
|
|
|
import (
|
|
"context"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/stripe/stripe-go"
|
|
|
|
"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"
|
|
)
|
|
|
|
// 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
|
|
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
|
|
}
|