2019-10-10 18:12:23 +01:00
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
package stripecoinpayments
import (
2019-10-23 13:04:54 +01:00
"context"
2019-11-05 13:16:02 +00:00
"fmt"
2019-11-15 14:59:39 +00:00
"math/big"
"sync"
2019-10-23 13:04:54 +01:00
"time"
2019-10-29 16:04:34 +00:00
"github.com/stripe/stripe-go"
2019-10-15 12:23:54 +01:00
"github.com/stripe/stripe-go/client"
2019-10-10 18:12:23 +01:00
"github.com/zeebo/errs"
2019-10-23 13:04:54 +01:00
"go.uber.org/zap"
2019-11-05 13:16:02 +00:00
"gopkg.in/spacemonkeygo/monkit.v2"
2019-10-15 12:23:54 +01:00
2019-11-15 14:27:44 +00:00
"storj.io/storj/satellite/accounting"
2019-11-05 13:16:02 +00:00
"storj.io/storj/satellite/console"
2019-10-15 12:23:54 +01:00
"storj.io/storj/satellite/payments"
2019-10-17 15:04:50 +01:00
"storj.io/storj/satellite/payments/coinpayments"
2019-10-10 18:12:23 +01:00
)
2019-11-04 10:54:25 +00:00
var (
// Error defines stripecoinpayments service error.
Error = errs . Class ( "stripecoinpayments service error" )
2019-10-10 18:12:23 +01:00
2019-11-04 10:54:25 +00:00
mon = monkit . Package ( )
)
2019-10-10 18:12:23 +01:00
2019-10-17 15:04:50 +01:00
// Config stores needed information for payment service initialization.
2019-10-15 12:23:54 +01:00
type Config struct {
2019-10-29 16:04:34 +00:00
StripeSecretKey string ` help:"stripe API secret key" default:"" `
2019-11-18 11:38:43 +00:00
StripePublicKey string ` help:"stripe API public key" default:"" `
2019-10-29 16:04:34 +00:00
CoinpaymentsPublicKey string ` help:"coinpayments API public key" default:"" `
2019-10-31 16:56:54 +00:00
CoinpaymentsPrivateKey string ` help:"coinpayments API private key key" default:"" `
2019-10-29 16:04:34 +00:00
TransactionUpdateInterval time . Duration ` help:"amount of time we wait before running next transaction update loop" devDefault:"1m" releaseDefault:"30m" `
AccountBalanceUpdateInterval time . Duration ` help:"amount of time we wait before running next account balance update loop" devDefault:"3m" releaseDefault:"1h30m" `
2019-11-15 14:59:39 +00:00
ConversionRatesCycleInterval time . Duration ` help:"amount of time we wait before running next conversion rates update loop" devDefault:"1m" releaseDefault:"10m" `
2019-10-10 18:12:23 +01:00
}
2019-10-15 12:23:54 +01:00
// Service is an implementation for payment service via Stripe and Coinpayments.
2019-11-04 12:30:07 +00:00
//
// architecture: Service
2019-10-15 12:23:54 +01:00
type Service struct {
2019-11-15 14:59:39 +00:00
log * zap . Logger
db DB
projectsDB console . Projects
usageDB accounting . ProjectAccounting
stripeClient * client . API
coinPayments * coinpayments . Client
2019-11-15 14:27:44 +00:00
PerObjectPrice int64
EgressPrice int64
TBhPrice int64
2019-11-15 14:59:39 +00:00
mu sync . Mutex
rates coinpayments . CurrencyRateInfos
ratesErr error
2019-10-10 18:12:23 +01:00
}
2019-10-15 12:23:54 +01:00
// NewService creates a Service instance.
2019-11-15 14:27:44 +00:00
func NewService ( log * zap . Logger , config Config , db DB , projectsDB console . Projects , usageDB accounting . ProjectAccounting , perObjectPrice , egressPrice , tbhPrice int64 ) * Service {
2019-11-19 17:56:18 +00:00
backendConfig := & stripe . BackendConfig {
LeveledLogger : log . Sugar ( ) ,
}
stripeClient := client . New ( config . StripeSecretKey ,
& stripe . Backends {
API : stripe . GetBackendWithConfig ( stripe . APIBackend , backendConfig ) ,
Connect : stripe . GetBackendWithConfig ( stripe . ConnectBackend , backendConfig ) ,
Uploads : stripe . GetBackendWithConfig ( stripe . UploadsBackend , backendConfig ) ,
} ,
)
2019-10-17 15:04:50 +01:00
2019-10-23 13:04:54 +01:00
coinPaymentsClient := coinpayments . NewClient (
2019-10-17 15:04:50 +01:00
coinpayments . Credentials {
PublicKey : config . CoinpaymentsPublicKey ,
PrivateKey : config . CoinpaymentsPrivateKey ,
} ,
)
2019-10-10 18:12:23 +01:00
2019-10-15 12:23:54 +01:00
return & Service {
2019-11-15 14:27:44 +00:00
log : log ,
db : db ,
projectsDB : projectsDB ,
usageDB : usageDB ,
stripeClient : stripeClient ,
coinPayments : coinPaymentsClient ,
TBhPrice : tbhPrice ,
PerObjectPrice : perObjectPrice ,
EgressPrice : egressPrice ,
2019-10-10 18:12:23 +01:00
}
}
2019-10-11 16:00:35 +01:00
2019-10-15 12:23:54 +01:00
// Accounts exposes all needed functionality to manage payment accounts.
2019-10-17 15:42:18 +01:00
func ( service * Service ) Accounts ( ) payments . Accounts {
2019-10-15 12:23:54 +01:00
return & accounts { service : service }
2019-10-11 16:00:35 +01:00
}
2019-10-23 13:04:54 +01:00
// updateTransactionsLoop updates all pending transactions in a loop.
func ( service * Service ) updateTransactionsLoop ( ctx context . Context ) ( err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
2019-10-29 16:04:34 +00:00
const limit = 25
2019-10-23 13:04:54 +01:00
before := time . Now ( )
2019-11-05 13:16:02 +00:00
txsPage , err := service . db . Transactions ( ) . ListPending ( ctx , 0 , limit , before )
2019-10-23 13:04:54 +01:00
if err != nil {
return err
}
if err := service . updateTransactions ( ctx , txsPage . IDList ( ) ) ; err != nil {
return err
}
for txsPage . Next {
2019-10-29 16:04:34 +00:00
if err = ctx . Err ( ) ; err != nil {
return err
2019-10-23 13:04:54 +01:00
}
2019-11-05 13:16:02 +00:00
txsPage , err = service . db . Transactions ( ) . ListPending ( ctx , txsPage . NextOffset , limit , before )
2019-10-23 13:04:54 +01:00
if err != nil {
return err
}
if err := service . updateTransactions ( ctx , txsPage . IDList ( ) ) ; err != nil {
return err
}
}
return nil
}
// updateTransactions updates statuses and received amount for given transactions.
func ( service * Service ) updateTransactions ( ctx context . Context , ids coinpayments . TransactionIDList ) ( err error ) {
defer mon . Task ( ) ( & ctx , ids ) ( & err )
if len ( ids ) == 0 {
service . log . Debug ( "no transactions found, skipping update" )
return nil
}
infos , err := service . coinPayments . Transactions ( ) . ListInfos ( ctx , ids )
if err != nil {
return err
}
var updates [ ] TransactionUpdate
2019-10-29 16:04:34 +00:00
var applies coinpayments . TransactionIDList
2019-10-23 13:04:54 +01:00
for id , info := range infos {
updates = append ( updates ,
TransactionUpdate {
TransactionID : id ,
Status : info . Status ,
Received : info . Received ,
} ,
)
2019-10-29 16:04:34 +00:00
// moment of transition to received state, which indicates
// that customer funds were accepted, so we can apply this
// amount to customer balance. So we create intent to update
// customer balance in the future.
if info . Status == coinpayments . StatusReceived {
applies = append ( applies , id )
}
}
2019-11-05 13:16:02 +00:00
return service . db . Transactions ( ) . Update ( ctx , updates , applies )
2019-10-29 16:04:34 +00:00
}
// applyAccountBalanceLoop fetches all unapplied transaction in a loop, applying transaction
// received amount to stripe customer balance.
func ( service * Service ) updateAccountBalanceLoop ( ctx context . Context ) ( err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
const limit = 25
before := time . Now ( )
2019-11-05 13:16:02 +00:00
txsPage , err := service . db . Transactions ( ) . ListUnapplied ( ctx , 0 , limit , before )
2019-10-29 16:04:34 +00:00
if err != nil {
return err
}
for _ , tx := range txsPage . Transactions {
if err = ctx . Err ( ) ; err != nil {
return err
}
if err = service . applyTransactionBalance ( ctx , tx ) ; err != nil {
return err
}
}
for txsPage . Next {
if err = ctx . Err ( ) ; err != nil {
return err
}
2019-11-05 13:16:02 +00:00
txsPage , err := service . db . Transactions ( ) . ListUnapplied ( ctx , txsPage . NextOffset , limit , before )
2019-10-29 16:04:34 +00:00
if err != nil {
return err
}
for _ , tx := range txsPage . Transactions {
if err = ctx . Err ( ) ; err != nil {
return err
}
if err = service . applyTransactionBalance ( ctx , tx ) ; err != nil {
return err
}
}
2019-10-23 13:04:54 +01:00
}
2019-10-29 16:04:34 +00:00
return nil
}
// applyTransactionBalance applies transaction received amount to stripe customer balance.
func ( service * Service ) applyTransactionBalance ( ctx context . Context , tx Transaction ) ( err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
2019-11-05 13:16:02 +00:00
cusID , err := service . db . Customers ( ) . GetCustomerID ( ctx , tx . AccountID )
2019-10-29 16:04:34 +00:00
if err != nil {
return err
}
2019-11-15 14:59:39 +00:00
rate , err := service . db . Transactions ( ) . GetLockedRate ( ctx , tx . ID )
if err != nil {
return err
}
2019-11-05 13:16:02 +00:00
if err = service . db . Transactions ( ) . Consume ( ctx , tx . ID ) ; err != nil {
2019-10-29 16:04:34 +00:00
return err
}
2019-11-21 13:23:16 +00:00
cents := convertToCents ( rate , & tx . Received )
2019-10-29 16:04:34 +00:00
params := & stripe . CustomerBalanceTransactionParams {
2019-11-19 17:56:18 +00:00
Amount : stripe . Int64 ( - cents ) ,
2019-10-29 16:04:34 +00:00
Customer : stripe . String ( cusID ) ,
Currency : stripe . String ( string ( stripe . CurrencyUSD ) ) ,
Description : stripe . String ( "storj token deposit" ) ,
}
params . AddMetadata ( "txID" , tx . ID . String ( ) )
2019-11-15 14:59:39 +00:00
// TODO: 0 amount will return an error, how to handle that?
2019-10-29 16:04:34 +00:00
_ , err = service . stripeClient . CustomerBalanceTransactions . New ( params )
return err
2019-10-23 13:04:54 +01:00
}
2019-11-05 13:16:02 +00:00
2019-11-15 14:59:39 +00:00
// UpdateRates fetches new rates and updates service rate cache.
func ( service * Service ) UpdateRates ( ctx context . Context ) ( err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
rates , err := service . coinPayments . ConversionRates ( ) . Get ( ctx )
service . mu . Lock ( )
defer service . mu . Unlock ( )
service . rates = rates
service . ratesErr = err
return err
}
// GetRate returns conversion rate for specified currencies.
func ( service * Service ) GetRate ( ctx context . Context , curr1 , curr2 coinpayments . Currency ) ( _ * big . Float , err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
service . mu . Lock ( )
defer service . mu . Unlock ( )
if service . ratesErr != nil {
return nil , Error . Wrap ( err )
}
info1 , ok := service . rates [ curr1 ]
if ! ok {
return nil , Error . New ( "no rate for currency %s" , curr1 )
}
info2 , ok := service . rates [ curr2 ]
if ! ok {
return nil , Error . New ( "no rate for currency %s" , curr2 )
}
return new ( big . Float ) . Quo ( & info1 . RateBTC , & info2 . RateBTC ) , nil
}
2019-11-05 13:16:02 +00:00
// PrepareInvoiceProjectRecords iterates through all projects and creates invoice records if
// none exists.
func ( service * Service ) PrepareInvoiceProjectRecords ( ctx context . Context , period time . Time ) ( err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
const limit = 25
now := time . Now ( ) . UTC ( )
utc := period . UTC ( )
start := time . Date ( utc . Year ( ) , utc . Month ( ) , 1 , 0 , 0 , 0 , 0 , time . UTC )
end := time . Date ( utc . Year ( ) , utc . Month ( ) + 1 , 1 , 0 , 0 , 0 , 0 , time . UTC )
if end . After ( now ) {
return Error . New ( "prepare is for past periods only" )
}
projsPage , err := service . projectsDB . List ( ctx , 0 , limit , end )
if err != nil {
return Error . Wrap ( err )
}
if err = service . createProjectRecords ( ctx , projsPage . Projects , start , end ) ; err != nil {
return Error . Wrap ( err )
}
for projsPage . Next {
if err = ctx . Err ( ) ; err != nil {
return Error . Wrap ( err )
}
projsPage , err = service . projectsDB . List ( ctx , projsPage . NextOffset , limit , end )
if err != nil {
return Error . Wrap ( err )
}
if err = service . createProjectRecords ( ctx , projsPage . Projects , start , end ) ; err != nil {
return Error . Wrap ( err )
}
}
return nil
}
// createProjectRecords creates invoice project record if none exists.
func ( service * Service ) createProjectRecords ( ctx context . Context , projects [ ] console . Project , start , end time . Time ) ( err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
var records [ ] CreateProjectRecord
for _ , project := range projects {
if err = ctx . Err ( ) ; err != nil {
return err
}
if err = service . db . ProjectRecords ( ) . Check ( ctx , project . ID , start , end ) ; err != nil {
if err == ErrProjectRecordExists {
continue
}
return err
}
// TODO: account for usage data.
records = append ( records ,
CreateProjectRecord {
ProjectID : project . ID ,
Storage : 0 ,
Egress : 0 ,
Objects : 0 ,
} ,
)
}
return service . db . ProjectRecords ( ) . Create ( ctx , records , start , end )
}
// InvoiceApplyProjectRecords iterates through unapplied invoice project records and creates invoice line items
// for stripe customer.
func ( service * Service ) InvoiceApplyProjectRecords ( ctx context . Context ) ( err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
const limit = 25
before := time . Now ( )
recordsPage , err := service . db . ProjectRecords ( ) . ListUnapplied ( ctx , 0 , limit , before )
if err != nil {
return Error . Wrap ( err )
}
if err = service . applyProjectRecords ( ctx , recordsPage . Records ) ; err != nil {
return Error . Wrap ( err )
}
for recordsPage . Next {
if err = ctx . Err ( ) ; err != nil {
return Error . Wrap ( err )
}
recordsPage , err = service . db . ProjectRecords ( ) . ListUnapplied ( ctx , recordsPage . NextOffset , limit , before )
if err != nil {
return Error . Wrap ( err )
}
if err = service . applyProjectRecords ( ctx , recordsPage . Records ) ; err != nil {
return Error . Wrap ( err )
}
}
return nil
}
// applyProjectRecords applies invoice intents as invoice line items to stripe customer.
func ( service * Service ) applyProjectRecords ( ctx context . Context , records [ ] ProjectRecord ) ( err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
for _ , record := range records {
if err = ctx . Err ( ) ; err != nil {
return err
}
proj , err := service . projectsDB . Get ( ctx , record . ProjectID )
if err != nil {
return err
}
cusID , err := service . db . Customers ( ) . GetCustomerID ( ctx , proj . OwnerID )
if err != nil {
if err == ErrNoCustomer {
continue
}
return err
}
if err = service . createInvoiceItems ( ctx , cusID , proj . Name , record ) ; err != nil {
return err
}
}
return nil
}
// createInvoiceItems consumes invoice project record and creates invoice line items for stripe customer.
func ( service * Service ) createInvoiceItems ( ctx context . Context , cusID , projName string , record ProjectRecord ) ( err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
if err = service . db . ProjectRecords ( ) . Consume ( ctx , record . ID ) ; err != nil {
return err
}
// TODO: add and apply pricing.
projectItem := & stripe . InvoiceItemParams {
Amount : stripe . Int64 ( 0 ) ,
Currency : stripe . String ( string ( stripe . CurrencyUSD ) ) ,
Customer : stripe . String ( cusID ) ,
Description : stripe . String ( fmt . Sprintf ( "project %s" , projName ) ) ,
Period : & stripe . InvoiceItemPeriodParams {
End : stripe . Int64 ( record . PeriodEnd . Unix ( ) ) ,
Start : stripe . Int64 ( record . PeriodStart . Unix ( ) ) ,
} ,
}
projectItem . AddMetadata ( "projectID" , record . ProjectID . String ( ) )
_ , err = service . stripeClient . InvoiceItems . New ( projectItem )
return err
}
// CreateInvoices lists through all customers and creates invoices.
func ( service * Service ) CreateInvoices ( ctx context . Context ) ( err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
const limit = 25
before := time . Now ( )
cusPage , err := service . db . Customers ( ) . List ( ctx , 0 , limit , before )
if err != nil {
return Error . Wrap ( err )
}
for _ , cus := range cusPage . Customers {
if err = ctx . Err ( ) ; err != nil {
return Error . Wrap ( err )
}
if err = service . createInvoice ( ctx , cus . ID ) ; err != nil {
return Error . Wrap ( err )
}
}
for cusPage . Next {
if err = ctx . Err ( ) ; err != nil {
return Error . Wrap ( err )
}
cusPage , err = service . db . Customers ( ) . List ( ctx , cusPage . NextOffset , limit , before )
if err != nil {
return Error . Wrap ( err )
}
for _ , cus := range cusPage . Customers {
if err = ctx . Err ( ) ; err != nil {
return Error . Wrap ( err )
}
if err = service . createInvoice ( ctx , cus . ID ) ; err != nil {
return Error . Wrap ( err )
}
}
}
return nil
}
// createInvoice creates invoice for stripe customer. Returns nil error if there are no
// pending invoice line items for customer.
func ( service * Service ) createInvoice ( ctx context . Context , cusID string ) ( err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
_ , err = service . stripeClient . Invoices . New (
& stripe . InvoiceParams {
Customer : stripe . String ( cusID ) ,
AutoAdvance : stripe . Bool ( true ) ,
} ,
)
if err != nil {
if stripeErr , ok := err . ( * stripe . Error ) ; ok {
switch stripeErr . Code {
case stripe . ErrorCodeInvoiceNoCustomerLineItems :
return nil
default :
return err
}
}
}
return nil
}