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"
2020-01-28 23:36:54 +00:00
"github.com/shopspring/decimal"
2019-11-08 20:40:39 +00:00
"github.com/spacemonkeygo/monkit/v3"
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-10-15 12:23:54 +01:00
2020-01-29 00:57:15 +00:00
"storj.io/common/memory"
2020-03-30 10:08:50 +01:00
"storj.io/common/uuid"
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-11-26 17:58:51 +00:00
// ErrNoCouponUsages indicates that there are no coupon usages.
ErrNoCouponUsages = errs . Class ( "stripecoinpayments no coupon usages" )
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
2020-03-16 19:34:15 +00:00
// fetchLimit sets the maximum amount of items before we start paging on requests
const fetchLimit = 100
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" `
2020-03-13 16:07:39 +00:00
AutoAdvance bool ` help:"toogle autoadvance feature for invoice creation" default:"false" `
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
2020-01-28 23:36:54 +00:00
ByteHourCents decimal . Decimal
EgressByteCents decimal . Decimal
ObjectHourCents decimal . Decimal
2020-01-24 13:38:53 +00:00
// BonusRate amount of percents
BonusRate int64
2020-03-16 19:34:15 +00:00
// Coupon Values
CouponValue int64
CouponDuration int64
CouponProjectLimit memory . Size
// Minimum CoinPayment to create a coupon
MinCoinPayment int64
2019-11-15 14:59:39 +00:00
2020-03-13 16:07:39 +00:00
//Stripe Extended Features
AutoAdvance bool
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.
2020-03-16 19:34:15 +00:00
func NewService ( log * zap . Logger , config Config , db DB , projectsDB console . Projects , usageDB accounting . ProjectAccounting , storageTBPrice , egressTBPrice , objectPrice string , bonusRate , couponValue , couponDuration int64 , couponProjectLimit memory . Size , minCoinPayment int64 ) ( * Service , error ) {
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
2020-01-28 23:36:54 +00:00
tbMonthDollars , err := decimal . NewFromString ( storageTBPrice )
if err != nil {
return nil , err
}
2020-01-29 05:06:01 +00:00
egressTBDollars , err := decimal . NewFromString ( egressTBPrice )
2020-01-28 23:36:54 +00:00
if err != nil {
return nil , err
}
objectMonthDollars , err := decimal . NewFromString ( objectPrice )
if err != nil {
return nil , err
2019-10-10 18:12:23 +01:00
}
2020-01-28 23:36:54 +00:00
// change the precision from dollars to cents
tbMonthCents := tbMonthDollars . Shift ( 2 )
2020-01-29 05:06:01 +00:00
egressTBCents := egressTBDollars . Shift ( 2 )
2020-01-28 23:36:54 +00:00
objectHourCents := objectMonthDollars . Shift ( 2 )
// get per hour prices from storage and objects
hoursPerMonth := decimal . New ( 30 * 24 , 0 )
tbHourCents := tbMonthCents . Div ( hoursPerMonth )
objectHourCents = objectHourCents . Div ( hoursPerMonth )
2020-01-29 05:06:01 +00:00
// convert tb to bytes for storage and egress
2020-01-28 23:36:54 +00:00
byteHourCents := tbHourCents . Div ( decimal . New ( 1000000000000 , 0 ) )
2020-01-29 05:06:01 +00:00
egressByteCents := egressTBCents . Div ( decimal . New ( 1000000000000 , 0 ) )
2020-01-28 23:36:54 +00:00
return & Service {
2020-03-16 19:34:15 +00:00
log : log ,
db : db ,
projectsDB : projectsDB ,
usageDB : usageDB ,
stripeClient : stripeClient ,
coinPayments : coinPaymentsClient ,
ByteHourCents : byteHourCents ,
EgressByteCents : egressByteCents ,
ObjectHourCents : objectHourCents ,
BonusRate : bonusRate ,
CouponValue : couponValue ,
CouponDuration : couponDuration ,
CouponProjectLimit : couponProjectLimit ,
MinCoinPayment : minCoinPayment ,
AutoAdvance : config . AutoAdvance ,
2020-01-28 23:36:54 +00:00
} , nil
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 )
before := time . Now ( )
2020-03-16 19:34:15 +00:00
txsPage , err := service . db . Transactions ( ) . ListPending ( ctx , 0 , fetchLimit , 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
}
2020-03-16 19:34:15 +00:00
txsPage , err = service . db . Transactions ( ) . ListPending ( ctx , txsPage . NextOffset , fetchLimit , 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.
2020-01-29 00:57:15 +00:00
func ( service * Service ) updateTransactions ( ctx context . Context , ids TransactionAndUserList ) ( err error ) {
2019-10-23 13:04:54 +01:00
defer mon . Task ( ) ( & ctx , ids ) ( & err )
if len ( ids ) == 0 {
service . log . Debug ( "no transactions found, skipping update" )
return nil
}
2020-01-29 00:57:15 +00:00
infos , err := service . coinPayments . Transactions ( ) . ListInfos ( ctx , ids . IDList ( ) )
2019-10-23 13:04:54 +01:00
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 ,
} ,
)
2020-01-14 13:38:32 +00:00
// moment of transition to completed state, which indicates
// that customer funds were accepted and transferred to our
// account, so we can apply this amount to customer balance.
// Therefore, create intent to update customer balance in the future.
if info . Status == coinpayments . StatusCompleted {
2019-10-29 16:04:34 +00:00
applies = append ( applies , id )
}
2020-01-29 00:57:15 +00:00
userID := ids [ id ]
rate , err := service . db . Transactions ( ) . GetLockedRate ( ctx , id )
if err != nil {
service . log . Error ( fmt . Sprintf ( "could not add promotional coupon for user %s" , userID . String ( ) ) , zap . Error ( err ) )
continue
}
cents := convertToCents ( rate , & info . Received )
2020-03-16 19:34:15 +00:00
if cents >= service . MinCoinPayment {
err = service . Accounts ( ) . Coupons ( ) . AddPromotionalCoupon ( ctx , userID )
2020-01-29 00:57:15 +00:00
if err != nil {
service . log . Error ( fmt . Sprintf ( "could not add promotional coupon for user %s" , userID . String ( ) ) , zap . Error ( err ) )
continue
}
}
2019-10-29 16:04:34 +00:00
}
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 )
before := time . Now ( )
2020-03-16 19:34:15 +00:00
txsPage , err := service . db . Transactions ( ) . ListUnapplied ( ctx , 0 , fetchLimit , 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
}
2020-03-16 19:34:15 +00:00
txsPage , err = service . db . Transactions ( ) . ListUnapplied ( ctx , txsPage . NextOffset , fetchLimit , 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 )
2020-01-24 13:38:53 +00:00
if err != nil {
return err
}
credit := payments . Credit {
UserID : tx . AccountID ,
Amount : cents / 100 * service . BonusRate ,
TransactionID : tx . ID ,
}
return service . db . Credits ( ) . InsertCredit ( ctx , credit )
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 )
now := time . Now ( ) . UTC ( )
utc := period . UTC ( )
start := time . Date ( utc . Year ( ) , utc . Month ( ) , 1 , 0 , 0 , 0 , 0 , time . UTC )
2020-03-13 16:07:39 +00:00
end := time . Date ( utc . Year ( ) , utc . Month ( ) + 1 , 0 , 0 , 0 , 0 , 0 , time . UTC )
2019-11-05 13:16:02 +00:00
if end . After ( now ) {
return Error . New ( "prepare is for past periods only" )
}
2020-03-16 19:34:15 +00:00
projsPage , err := service . projectsDB . List ( ctx , 0 , fetchLimit , end )
2019-11-05 13:16:02 +00:00
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 )
}
2020-03-16 19:34:15 +00:00
projsPage , err = service . projectsDB . List ( ctx , projsPage . NextOffset , fetchLimit , end )
2019-11-05 13:16:02 +00:00
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
2020-01-07 10:41:19 +00:00
var usages [ ] CouponUsage
2020-02-11 14:48:28 +00:00
var creditsSpendings [ ] CreditsSpending
2019-11-05 13:16:02 +00:00
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
}
2020-01-07 10:41:19 +00:00
usage , err := service . usageDB . GetProjectTotal ( ctx , project . ID , start , end )
if err != nil {
return err
}
2019-11-05 13:16:02 +00:00
// TODO: account for usage data.
records = append ( records ,
CreateProjectRecord {
ProjectID : project . ID ,
2020-01-07 10:41:19 +00:00
Storage : usage . Storage ,
Egress : usage . Egress ,
Objects : usage . ObjectCount ,
2019-11-05 13:16:02 +00:00
} ,
)
2020-01-07 10:41:19 +00:00
coupons , err := service . db . Coupons ( ) . ListByProjectID ( ctx , project . ID )
if err != nil {
return err
}
2020-01-28 23:36:54 +00:00
currentUsagePrice := service . calculateProjectUsagePrice ( usage . Egress , usage . Storage , usage . ObjectCount ) . TotalInt64 ( )
2020-01-07 10:41:19 +00:00
2020-02-11 14:48:28 +00:00
amountToChargeFromCoupon := int64 ( 0 )
2020-01-24 13:38:53 +00:00
2020-01-07 10:41:19 +00:00
// TODO: only for 1 coupon per project
for _ , coupon := range coupons {
2020-02-11 14:48:28 +00:00
amountToChargeFromCoupon = currentUsagePrice
2020-01-07 10:41:19 +00:00
if coupon . IsExpired ( ) {
if err = service . db . Coupons ( ) . Update ( ctx , coupon . ID , payments . CouponExpired ) ; err != nil {
return err
}
2020-02-11 14:48:28 +00:00
amountToChargeFromCoupon = 0
2020-01-07 10:41:19 +00:00
continue
}
alreadyChargedAmount , err := service . db . Coupons ( ) . TotalUsage ( ctx , coupon . ID )
if err != nil {
return err
}
remaining := coupon . Amount - alreadyChargedAmount
2020-01-24 13:38:53 +00:00
if amountToChargeFromCoupon >= remaining {
amountToChargeFromCoupon = remaining
2020-01-07 10:41:19 +00:00
}
usages = append ( usages , CouponUsage {
Period : start ,
2020-01-24 13:38:53 +00:00
Amount : amountToChargeFromCoupon ,
2020-01-07 10:41:19 +00:00
Status : CouponUsageStatusUnapplied ,
CouponID : coupon . ID ,
} )
}
2020-02-11 14:48:28 +00:00
leftAfterCoupons := currentUsagePrice - amountToChargeFromCoupon
if leftAfterCoupons == 0 {
continue
}
userBonuses , err := service . db . Credits ( ) . Balance ( ctx , project . OwnerID )
if err != nil {
return err
}
if userBonuses > 0 {
if leftAfterCoupons >= userBonuses {
leftAfterCoupons = userBonuses
}
amountChargedFromBonuses := leftAfterCoupons
creditSpendingID , err := uuid . New ( )
if err != nil {
return err
}
creditsSpendings = append ( creditsSpendings , CreditsSpending {
2020-03-30 10:08:50 +01:00
ID : creditSpendingID ,
2020-02-11 14:48:28 +00:00
Amount : amountChargedFromBonuses ,
UserID : project . OwnerID ,
ProjectID : project . ID ,
Status : CreditsSpendingStatusUnapplied ,
} )
}
2019-11-05 13:16:02 +00:00
}
2020-02-11 14:48:28 +00:00
return service . db . ProjectRecords ( ) . Create ( ctx , records , usages , creditsSpendings , start , end )
2019-11-05 13:16:02 +00:00
}
// 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 )
2020-01-07 10:41:19 +00:00
before := time . Now ( ) . UTC ( )
2019-11-05 13:16:02 +00:00
2020-03-16 19:34:15 +00:00
recordsPage , err := service . db . ProjectRecords ( ) . ListUnapplied ( ctx , 0 , fetchLimit , before )
2019-11-05 13:16:02 +00:00
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 )
}
2020-03-16 19:34:15 +00:00
recordsPage , err = service . db . ProjectRecords ( ) . ListUnapplied ( ctx , recordsPage . NextOffset , fetchLimit , before )
2019-11-05 13:16:02 +00:00
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
}
2020-01-28 23:36:54 +00:00
projectPrice := service . calculateProjectUsagePrice ( record . Egress , record . Storage , record . Objects )
2020-01-07 10:41:19 +00:00
2019-11-05 13:16:02 +00:00
projectItem := & stripe . InvoiceItemParams {
2020-03-13 16:07:39 +00:00
Currency : stripe . String ( string ( stripe . CurrencyUSD ) ) ,
Customer : stripe . String ( cusID ) ,
2019-11-05 13:16:02 +00:00
Period : & stripe . InvoiceItemPeriodParams {
Start : stripe . Int64 ( record . PeriodStart . Unix ( ) ) ,
2020-03-13 16:07:39 +00:00
End : stripe . Int64 ( record . PeriodEnd . Unix ( ) ) ,
2019-11-05 13:16:02 +00:00
} ,
}
projectItem . AddMetadata ( "projectID" , record . ProjectID . String ( ) )
2020-03-13 16:07:39 +00:00
projectItem . Description = stripe . String ( fmt . Sprintf ( "Project %s - Storage" , projName ) )
projectItem . Amount = stripe . Int64 ( projectPrice . Storage . IntPart ( ) )
_ , err = service . stripeClient . InvoiceItems . New ( projectItem )
if err != nil {
return err
}
projectItem . Description = stripe . String ( fmt . Sprintf ( "Project %s - Egress Bandwidth" , projName ) )
projectItem . Amount = stripe . Int64 ( projectPrice . Egress . IntPart ( ) )
_ , err = service . stripeClient . InvoiceItems . New ( projectItem )
if err != nil {
return err
}
projectItem . Description = stripe . String ( fmt . Sprintf ( "Project %s - Object Fee" , projName ) )
projectItem . Amount = stripe . Int64 ( projectPrice . Objects . IntPart ( ) )
2019-11-05 13:16:02 +00:00
_ , err = service . stripeClient . InvoiceItems . New ( projectItem )
return err
}
2020-01-07 10:41:19 +00:00
// InvoiceApplyCoupons iterates through unapplied project coupons and creates invoice line items
// for stripe customer.
2019-11-26 17:58:51 +00:00
func ( service * Service ) InvoiceApplyCoupons ( ctx context . Context ) ( err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
2020-01-07 10:41:19 +00:00
before := time . Now ( ) . UTC ( )
2019-11-26 17:58:51 +00:00
2020-03-16 19:34:15 +00:00
usagePage , err := service . db . Coupons ( ) . ListUnapplied ( ctx , 0 , fetchLimit , before )
2019-11-26 17:58:51 +00:00
if err != nil {
return Error . Wrap ( err )
}
2020-01-07 10:41:19 +00:00
if err = service . applyCoupons ( ctx , usagePage . Usages ) ; err != nil {
2019-11-26 17:58:51 +00:00
return Error . Wrap ( err )
}
2020-01-07 10:41:19 +00:00
for usagePage . Next {
2019-11-26 17:58:51 +00:00
if err = ctx . Err ( ) ; err != nil {
return Error . Wrap ( err )
}
2020-03-16 19:34:15 +00:00
usagePage , err = service . db . Coupons ( ) . ListUnapplied ( ctx , usagePage . NextOffset , fetchLimit , before )
2019-11-26 17:58:51 +00:00
if err != nil {
return Error . Wrap ( err )
}
2020-01-07 10:41:19 +00:00
if err = service . applyCoupons ( ctx , usagePage . Usages ) ; err != nil {
2019-11-26 17:58:51 +00:00
return Error . Wrap ( err )
}
}
return nil
}
2020-01-07 10:41:19 +00:00
// applyCoupons applies concrete coupon usage as invoice line item.
func ( service * Service ) applyCoupons ( ctx context . Context , usages [ ] CouponUsage ) ( err error ) {
2019-11-26 17:58:51 +00:00
defer mon . Task ( ) ( & ctx ) ( & err )
2020-01-07 10:41:19 +00:00
for _ , usage := range usages {
if err = ctx . Err ( ) ; err != nil {
return err
2019-11-26 17:58:51 +00:00
}
2020-01-07 10:41:19 +00:00
coupon , err := service . db . Coupons ( ) . Get ( ctx , usage . CouponID )
2019-11-26 17:58:51 +00:00
if err != nil {
return err
}
2020-01-07 10:41:19 +00:00
customerID , err := service . db . Customers ( ) . GetCustomerID ( ctx , coupon . UserID )
2019-11-26 17:58:51 +00:00
if err != nil {
2020-01-07 10:41:19 +00:00
if err == ErrNoCustomer {
continue
}
2019-11-26 17:58:51 +00:00
return err
}
2020-01-07 10:41:19 +00:00
if err = service . createInvoiceCouponItems ( ctx , coupon , usage , customerID ) ; err != nil {
2019-11-26 17:58:51 +00:00
return err
}
}
return nil
}
2020-02-11 14:48:28 +00:00
// createInvoiceCouponItems consumes invoice project record and creates invoice line items for stripe customer.
2020-01-07 10:41:19 +00:00
func ( service * Service ) createInvoiceCouponItems ( ctx context . Context , coupon payments . Coupon , usage CouponUsage , customerID string ) ( err error ) {
2019-11-26 17:58:51 +00:00
defer mon . Task ( ) ( & ctx , customerID , coupon ) ( & err )
2020-01-07 10:41:19 +00:00
err = service . db . Coupons ( ) . ApplyUsage ( ctx , usage . CouponID , usage . Period )
if err != nil {
return err
}
totalUsage , err := service . db . Coupons ( ) . TotalUsage ( ctx , coupon . ID )
if err != nil {
return err
}
if totalUsage == coupon . Amount {
err = service . db . Coupons ( ) . Update ( ctx , coupon . ID , payments . CouponUsed )
if err != nil {
return err
}
}
2019-11-26 17:58:51 +00:00
projectItem := & stripe . InvoiceItemParams {
2020-01-07 10:41:19 +00:00
Amount : stripe . Int64 ( - usage . Amount ) ,
2019-11-26 17:58:51 +00:00
Currency : stripe . String ( string ( stripe . CurrencyUSD ) ) ,
Customer : stripe . String ( customerID ) ,
2020-05-08 17:26:33 +01:00
Description : stripe . String ( coupon . Description ) ,
2019-11-26 17:58:51 +00:00
Period : & stripe . InvoiceItemPeriodParams {
2020-01-07 10:41:19 +00:00
End : stripe . Int64 ( usage . Period . AddDate ( 0 , 1 , 0 ) . Unix ( ) ) ,
Start : stripe . Int64 ( usage . Period . Unix ( ) ) ,
2019-11-26 17:58:51 +00:00
} ,
}
projectItem . AddMetadata ( "projectID" , coupon . ProjectID . String ( ) )
projectItem . AddMetadata ( "couponID" , coupon . ID . String ( ) )
_ , err = service . stripeClient . InvoiceItems . New ( projectItem )
return err
}
2020-02-11 12:42:08 +00:00
// InvoiceApplyCredits iterates through credits with status false of project and creates invoice line items
// for stripe customer.
func ( service * Service ) InvoiceApplyCredits ( ctx context . Context ) ( err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
before := time . Now ( ) . UTC ( )
2020-03-16 19:34:15 +00:00
spendingsPage , err := service . db . Credits ( ) . ListCreditsSpendingsPaged ( ctx , int ( CreditsSpendingStatusUnapplied ) , 0 , fetchLimit , before )
2020-02-11 12:42:08 +00:00
if err != nil {
return Error . Wrap ( err )
}
if err = service . applySpendings ( ctx , spendingsPage . Spendings ) ; err != nil {
return Error . Wrap ( err )
}
for spendingsPage . Next {
if err = ctx . Err ( ) ; err != nil {
return Error . Wrap ( err )
}
2020-03-16 19:34:15 +00:00
spendingsPage , err = service . db . Credits ( ) . ListCreditsSpendingsPaged ( ctx , int ( CreditsSpendingStatusUnapplied ) , spendingsPage . NextOffset , fetchLimit , before )
2020-02-11 12:42:08 +00:00
if err != nil {
return Error . Wrap ( err )
}
if err = service . applySpendings ( ctx , spendingsPage . Spendings ) ; err != nil {
return Error . Wrap ( err )
}
}
return nil
}
// applyCredits applies concrete spending as invoice line item.
func ( service * Service ) applySpendings ( ctx context . Context , spendings [ ] CreditsSpending ) ( err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
for _ , spending := range spendings {
if err = ctx . Err ( ) ; err != nil {
return err
}
if err = service . createInvoiceCreditItem ( ctx , spending ) ; err != nil {
return err
}
}
return nil
}
2020-02-11 14:48:28 +00:00
// createInvoiceCreditItem consumes invoice project record and creates invoice line items for stripe customer.
2020-02-11 12:42:08 +00:00
func ( service * Service ) createInvoiceCreditItem ( ctx context . Context , spending CreditsSpending ) ( err error ) {
defer mon . Task ( ) ( & ctx , spending ) ( & err )
2020-02-11 14:48:28 +00:00
err = service . db . Credits ( ) . ApplyCreditsSpending ( ctx , spending . ID )
2020-02-11 12:42:08 +00:00
if err != nil {
return err
}
customerID , err := service . db . Customers ( ) . GetCustomerID ( ctx , spending . UserID )
projectItem := & stripe . InvoiceItemParams {
Amount : stripe . Int64 ( - spending . Amount ) ,
Currency : stripe . String ( string ( stripe . CurrencyUSD ) ) ,
Customer : stripe . String ( customerID ) ,
2020-05-08 17:26:33 +01:00
Description : stripe . String ( "Credits from STORJ deposit bonus" ) ,
2020-02-11 12:42:08 +00:00
Period : & stripe . InvoiceItemPeriodParams {
End : stripe . Int64 ( spending . Created . AddDate ( 0 , 1 , 0 ) . Unix ( ) ) ,
Start : stripe . Int64 ( spending . Created . Unix ( ) ) ,
} ,
}
projectItem . AddMetadata ( "projectID" , spending . ProjectID . String ( ) )
projectItem . AddMetadata ( "userID" , spending . UserID . String ( ) )
_ , err = service . stripeClient . InvoiceItems . New ( projectItem )
return err
}
2019-11-05 13:16:02 +00:00
// CreateInvoices lists through all customers and creates invoices.
func ( service * Service ) CreateInvoices ( ctx context . Context ) ( err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
before := time . Now ( )
2020-03-16 19:34:15 +00:00
cusPage , err := service . db . Customers ( ) . List ( ctx , 0 , fetchLimit , before )
2019-11-05 13:16:02 +00:00
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 )
}
2020-03-16 19:34:15 +00:00
cusPage , err = service . db . Customers ( ) . List ( ctx , cusPage . NextOffset , fetchLimit , before )
2019-11-05 13:16:02 +00:00
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 )
2020-03-13 16:07:39 +00:00
/ * var description string
// get the first invoice item's period
params := & stripe . InvoiceItemListParams { Customer : stripe . String ( cusID ) }
params . Filters . AddFilter ( "limit" , "" , "1" )
iter := service . stripeClient . InvoiceItems . List ( params )
for iter . Next ( ) {
//Add 12 hours to ensure we are in the correct billing month
start := time . Unix ( iter . InvoiceItem ( ) . Period . Start + 43200 , 0 )
year , month , _ := start . Date ( )
description = fmt . Sprintf ( "Billing Period %s %d" , month , year )
}
if iter . Err ( ) != nil {
return Error . Wrap ( iter . Err ( ) )
} * /
description := "Tardigrade Cloud Storage"
2019-11-05 13:16:02 +00:00
_ , err = service . stripeClient . Invoices . New (
& stripe . InvoiceParams {
Customer : stripe . String ( cusID ) ,
2020-03-13 16:07:39 +00:00
AutoAdvance : stripe . Bool ( service . AutoAdvance ) ,
Description : stripe . String ( description ) ,
2019-11-05 13:16:02 +00:00
} ,
)
if err != nil {
if stripeErr , ok := err . ( * stripe . Error ) ; ok {
switch stripeErr . Code {
case stripe . ErrorCodeInvoiceNoCustomerLineItems :
return nil
default :
return err
}
}
}
return nil
}
2020-01-28 23:36:54 +00:00
// projectUsagePrice represents pricing for project usage.
type projectUsagePrice struct {
Storage decimal . Decimal
Egress decimal . Decimal
Objects decimal . Decimal
}
// Total returns project usage price total.
func ( price projectUsagePrice ) Total ( ) decimal . Decimal {
return price . Storage . Add ( price . Egress ) . Add ( price . Objects )
}
// Total returns project usage price total.
func ( price projectUsagePrice ) TotalInt64 ( ) int64 {
return price . Storage . Add ( price . Egress ) . Add ( price . Objects ) . IntPart ( )
}
// calculateProjectUsagePrice calculate project usage price.
func ( service * Service ) calculateProjectUsagePrice ( egress int64 , storage , objects float64 ) projectUsagePrice {
return projectUsagePrice {
Storage : service . ByteHourCents . Mul ( decimal . NewFromFloat ( storage ) ) ,
Egress : service . EgressByteCents . Mul ( decimal . New ( egress , 0 ) ) ,
Objects : service . ObjectHourCents . Mul ( decimal . NewFromFloat ( objects ) ) ,
}
}