8a1bedd367
Currently, pending invoice payments that are made using a users token balance can get stuck in a pending state if the invoice is not able to be paid appropriately in stripe. This change addresses these stuck token invoice payments by attempting to transition them to failed if the invoice cannot be paid. Change-Id: I2b70a11c97ae5c733d05c918a1082e85bb7f73f3
140 lines
5.6 KiB
Go
140 lines
5.6 KiB
Go
// Copyright (C) 2022 Storj Labs, Inc.
|
|
// See LICENSE for copying information.
|
|
|
|
package billing
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/zeebo/errs"
|
|
|
|
"storj.io/common/currency"
|
|
"storj.io/common/uuid"
|
|
)
|
|
|
|
// TransactionStatus indicates transaction status.
|
|
type TransactionStatus string
|
|
|
|
// ErrInsufficientFunds represents err when a user balance is too low for some transaction.
|
|
var ErrInsufficientFunds = errs.New("Insufficient funds for this transaction")
|
|
|
|
// ErrNoWallet represents err when there is no wallet in the DB.
|
|
var ErrNoWallet = errs.New("wallet does not exists")
|
|
|
|
// ErrNoTransactions represents err when there is no billing transactions in the DB.
|
|
var ErrNoTransactions = errs.New("no transactions in the database")
|
|
|
|
const (
|
|
// TransactionStatusPending indicates that status of this transaction is pending.
|
|
TransactionStatusPending = "pending"
|
|
// TransactionStatusCompleted indicates that status of this transaction is complete.
|
|
TransactionStatusCompleted = "complete"
|
|
// TransactionStatusFailed indicates that status of this transaction is failed.
|
|
TransactionStatusFailed = "failed"
|
|
)
|
|
|
|
// TransactionType indicates transaction type.
|
|
type TransactionType string
|
|
|
|
const (
|
|
// TransactionTypeCredit indicates that type of this transaction is credit.
|
|
TransactionTypeCredit = "credit"
|
|
// TransactionTypeDebit indicates that type of this transaction is debit.
|
|
TransactionTypeDebit = "debit"
|
|
// TransactionTypeUnknown indicates that type of this transaction is unknown.
|
|
TransactionTypeUnknown = "unknown"
|
|
)
|
|
|
|
// TransactionsDB is an interface which defines functionality
|
|
// of DB which stores billing transactions.
|
|
//
|
|
// architecture: Database
|
|
type TransactionsDB interface {
|
|
// Insert inserts the provided primary transaction along with zero or more
|
|
// supplemental transactions that. This is NOT intended for bulk insertion,
|
|
// but rather to provide an atomic commit of one or more _related_
|
|
// transactions.
|
|
Insert(ctx context.Context, primaryTx Transaction, supplementalTx ...Transaction) (txIDs []int64, err error)
|
|
// FailPendingInvoiceTokenPayments marks all specified pending invoice token payments as failed, and refunds the pending charges.
|
|
FailPendingInvoiceTokenPayments(ctx context.Context, txIDs ...int64) error
|
|
// CompletePendingInvoiceTokenPayments updates the status of the pending invoice token payment to complete.
|
|
CompletePendingInvoiceTokenPayments(ctx context.Context, txIDs ...int64) error
|
|
// UpdateMetadata updates the metadata of the transaction.
|
|
UpdateMetadata(ctx context.Context, txID int64, metadata []byte) error
|
|
// LastTransaction returns the timestamp and metadata of the last known transaction for given source and type.
|
|
LastTransaction(ctx context.Context, txSource string, txType TransactionType) (time.Time, []byte, error)
|
|
// List returns all transactions for the specified user.
|
|
List(ctx context.Context, userID uuid.UUID) ([]Transaction, error)
|
|
// ListSource returns all transactions for the specified user and source.
|
|
ListSource(ctx context.Context, userID uuid.UUID, txSource string) ([]Transaction, error)
|
|
// GetBalance returns the current usable balance for the specified user.
|
|
GetBalance(ctx context.Context, userID uuid.UUID) (currency.Amount, error)
|
|
}
|
|
|
|
// PaymentType is an interface which defines functionality required for all billing payment types. Payment types can
|
|
// include but are not limited to Bitcoin, Ether, credit or debit card, ACH transfer, or even physical transfer of live
|
|
// goats. In each case, a source, type, and method to get new transactions must be defined by the service, though
|
|
// metadata specific to each payment type is also supported (i.e. goat hair type).
|
|
type PaymentType interface {
|
|
// Source the source of the payment
|
|
Source() string
|
|
// Type the type of the payment
|
|
Type() TransactionType
|
|
// GetNewTransactions returns new transactions that occurred after the provided last transaction received.
|
|
GetNewTransactions(ctx context.Context, lastTransactionTime time.Time, metadata []byte) ([]Transaction, error)
|
|
}
|
|
|
|
// Well-known PaymentType sources.
|
|
const (
|
|
StripeSource = "stripe"
|
|
StorjScanSource = "storjscan"
|
|
StorjScanBonusSource = "storjscanbonus"
|
|
)
|
|
|
|
// Transaction defines billing related transaction info that is stored in the DB.
|
|
type Transaction struct {
|
|
ID int64
|
|
UserID uuid.UUID
|
|
Amount currency.Amount
|
|
Description string
|
|
Source string
|
|
Status TransactionStatus
|
|
Type TransactionType
|
|
Metadata []byte
|
|
Timestamp time.Time
|
|
CreatedAt time.Time
|
|
}
|
|
|
|
// CalculateBonusAmount calculates bonus for given currency amount and bonus rate.
|
|
func CalculateBonusAmount(amount currency.Amount, bonusRate int64) currency.Amount {
|
|
bonusUnits := amount.BaseUnits() * bonusRate / 100
|
|
return currency.AmountFromBaseUnits(bonusUnits, amount.Currency())
|
|
}
|
|
|
|
func prepareBonusTransaction(bonusRate int64, source string, transaction Transaction) (Transaction, bool) {
|
|
// Bonus transactions only apply when enabled (i.e. positive rate) and
|
|
// for StorjScan transactions.
|
|
switch {
|
|
case bonusRate <= 0:
|
|
return Transaction{}, false
|
|
case source != StorjScanSource:
|
|
return Transaction{}, false
|
|
case transaction.Type != TransactionTypeCredit:
|
|
// This is defensive. Storjscan shouldn't provide "debit" transactions.
|
|
return Transaction{}, false
|
|
}
|
|
|
|
return Transaction{
|
|
UserID: transaction.UserID,
|
|
Amount: CalculateBonusAmount(transaction.Amount, bonusRate),
|
|
Description: fmt.Sprintf("STORJ Token Bonus (%d%%)", bonusRate),
|
|
Source: StorjScanBonusSource,
|
|
Status: TransactionStatusCompleted,
|
|
Type: TransactionTypeCredit,
|
|
Timestamp: transaction.Timestamp,
|
|
Metadata: append([]byte(nil), transaction.Metadata...),
|
|
}, true
|
|
}
|