2019-10-10 18:12:23 +01:00
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
2023-04-06 12:41:14 +01:00
package stripe
2019-10-10 18:12:23 +01:00
import (
2019-10-23 13:04:54 +01:00
"context"
2022-05-10 20:19:53 +01:00
"encoding/json"
2020-07-14 14:04:38 +01:00
"errors"
2019-11-05 13:16:02 +00:00
"fmt"
2023-04-07 10:57:54 +01:00
"math"
2023-02-23 16:27:37 +00:00
"sort"
2020-05-28 12:31:02 +01:00
"strconv"
2021-07-30 23:11:36 +01:00
"strings"
2023-03-23 15:38:07 +00:00
"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"
2021-06-22 01:09:56 +01:00
"github.com/stripe/stripe-go/v72"
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
2022-09-06 13:43:09 +01:00
"storj.io/common/currency"
2023-03-23 15:38:07 +00:00
"storj.io/common/sync2"
2022-05-10 20:19:53 +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"
2022-05-10 20:19:53 +01:00
"storj.io/storj/satellite/payments/billing"
"storj.io/storj/satellite/payments/storjscan"
2019-10-10 18:12:23 +01:00
)
2019-11-04 10:54:25 +00:00
var (
// Error defines stripecoinpayments service error.
2021-04-28 09:06:17 +01:00
Error = errs . Class ( "stripecoinpayments service" )
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-05-26 12:00:14 +01:00
// hoursPerMonth is the number of months in a billing month. For the purpose of billing, the billing month is always 30 days.
const hoursPerMonth = 24 * 30
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 {
2022-11-29 12:36:41 +00:00
StripeSecretKey string ` help:"stripe API secret key" default:"" `
StripePublicKey string ` help:"stripe API public key" default:"" `
StripeFreeTierCouponID string ` help:"stripe free tier coupon ID" default:"" `
2022-12-14 13:35:53 +00:00
AutoAdvance bool ` help:"toggle autoadvance feature for invoice creation" default:"false" `
2022-11-29 12:36:41 +00:00
ListingLimit int ` help:"sets the maximum amount of items before we start paging on requests" default:"100" hidden:"true" `
2022-12-14 13:35:53 +00:00
SkipEmptyInvoices bool ` help:"if set, skips the creation of empty invoices for customers with zero usage for the billing period" default:"true" `
2023-03-23 15:38:07 +00:00
MaxParallelCalls int ` help:"the maximum number of concurrent Stripe API calls in invoicing methods" default:"10" `
2023-03-03 08:20:28 +00:00
Retries RetryConfig
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 {
2022-05-10 20:19:53 +01:00
log * zap . Logger
db DB
walletsDB storjscan . WalletsDB
billingDB billing . TransactionsDB
2019-11-15 14:59:39 +00:00
projectsDB console . Projects
2023-01-30 22:11:12 +00:00
usersDB console . Users
2019-11-15 14:59:39 +00:00
usageDB accounting . ProjectAccounting
2023-04-06 12:41:14 +01:00
stripeClient Client
2019-11-15 14:59:39 +00:00
2023-01-12 03:41:14 +00:00
usagePrices payments . ProjectUsagePriceModel
usagePriceOverrides map [ string ] payments . ProjectUsagePriceModel
2023-01-30 22:11:12 +00:00
packagePlans map [ string ] payments . PackagePlan
2023-01-27 05:34:08 +00:00
partnerNames [ ] string
2020-01-24 13:38:53 +00:00
// BonusRate amount of percents
BonusRate int64
2020-03-16 19:34:15 +00:00
// Coupon Values
2021-05-10 18:12:05 +01:00
StripeFreeTierCouponID string
2019-11-15 14:59:39 +00:00
2020-10-13 13:47:55 +01:00
// Stripe Extended Features
2020-03-13 16:07:39 +00:00
AutoAdvance bool
2022-12-14 13:35:53 +00:00
listingLimit int
skipEmptyInvoices bool
2023-03-23 15:38:07 +00:00
maxParallelCalls int
2022-12-14 13:35:53 +00:00
nowFn func ( ) time . Time
2019-10-10 18:12:23 +01:00
}
2022-12-01 07:40:52 +00:00
// NewService creates a Service instance.
2023-04-06 12:41:14 +01:00
func NewService ( log * zap . Logger , stripeClient Client , config Config , db DB , walletsDB storjscan . WalletsDB , billingDB billing . TransactionsDB , projectsDB console . Projects , usersDB console . Users , usageDB accounting . ProjectAccounting , usagePrices payments . ProjectUsagePriceModel , usagePriceOverrides map [ string ] payments . ProjectUsagePriceModel , packagePlans map [ string ] payments . PackagePlan , bonusRate int64 ) ( * Service , error ) {
2023-01-27 05:34:08 +00:00
var partners [ ] string
for partner := range usagePriceOverrides {
partners = append ( partners , partner )
}
2020-01-28 23:36:54 +00:00
return & Service {
2022-12-01 07:40:52 +00:00
log : log ,
db : db ,
walletsDB : walletsDB ,
billingDB : billingDB ,
projectsDB : projectsDB ,
2023-01-30 22:11:12 +00:00
usersDB : usersDB ,
2022-12-01 07:40:52 +00:00
usageDB : usageDB ,
stripeClient : stripeClient ,
usagePrices : usagePrices ,
usagePriceOverrides : usagePriceOverrides ,
2023-01-30 22:11:12 +00:00
packagePlans : packagePlans ,
2023-01-27 05:34:08 +00:00
partnerNames : partners ,
2022-12-01 07:40:52 +00:00
BonusRate : bonusRate ,
StripeFreeTierCouponID : config . StripeFreeTierCouponID ,
AutoAdvance : config . AutoAdvance ,
listingLimit : config . ListingLimit ,
2022-12-14 13:35:53 +00:00
skipEmptyInvoices : config . SkipEmptyInvoices ,
2023-03-23 15:38:07 +00:00
maxParallelCalls : config . MaxParallelCalls ,
2022-12-01 07:40:52 +00:00
nowFn : time . Now ,
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
2021-08-27 01:51:26 +01:00
// PrepareInvoiceProjectRecords iterates through all projects and creates invoice records if none exist.
2019-11-05 13:16:02 +00:00
func ( service * Service ) PrepareInvoiceProjectRecords ( ctx context . Context , period time . Time ) ( err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
2020-05-19 08:42:07 +01:00
now := service . nowFn ( ) . UTC ( )
2019-11-05 13:16:02 +00:00
utc := period . UTC ( )
start := time . Date ( utc . Year ( ) , utc . Month ( ) , 1 , 0 , 0 , 0 , 0 , time . UTC )
2021-08-25 20:35:57 +01:00
end := time . Date ( utc . Year ( ) , utc . Month ( ) + 1 , 1 , 0 , 0 , 0 , 0 , time . UTC )
2019-11-05 13:16:02 +00:00
if end . After ( now ) {
2020-05-12 09:46:48 +01:00
return Error . New ( "allowed for past periods only" )
2019-11-05 13:16:02 +00:00
}
2021-08-27 01:51:26 +01:00
var numberOfCustomers , numberOfRecords int
2023-04-06 11:22:36 +01:00
customersPage := CustomersPage {
Next : true ,
2019-11-05 13:16:02 +00:00
}
2020-05-29 11:29:03 +01:00
for customersPage . Next {
2019-11-05 13:16:02 +00:00
if err = ctx . Err ( ) ; err != nil {
return Error . Wrap ( err )
}
2023-04-06 11:22:36 +01:00
customersPage , err = service . db . Customers ( ) . List ( ctx , customersPage . Cursor , service . listingLimit , end )
2019-11-05 13:16:02 +00:00
if err != nil {
return Error . Wrap ( err )
}
2022-12-14 13:35:53 +00:00
numberOfCustomers += len ( customersPage . Customers )
2019-11-05 13:16:02 +00:00
2021-08-27 01:51:26 +01:00
records , err := service . processCustomers ( ctx , customersPage . Customers , start , end )
2020-06-03 10:50:53 +01:00
if err != nil {
2019-11-05 13:16:02 +00:00
return Error . Wrap ( err )
}
2020-06-03 10:50:53 +01:00
numberOfRecords += records
2019-11-05 13:16:02 +00:00
}
2021-08-27 01:51:26 +01:00
service . log . Info ( "Number of processed entries." , zap . Int ( "Customers" , numberOfCustomers ) , zap . Int ( "Projects" , numberOfRecords ) )
2019-11-05 13:16:02 +00:00
return nil
}
2021-08-27 01:51:26 +01:00
func ( service * Service ) processCustomers ( ctx context . Context , customers [ ] Customer , start , end time . Time ) ( int , error ) {
2020-05-29 11:29:03 +01:00
var allRecords [ ] CreateProjectRecord
for _ , customer := range customers {
2020-06-03 18:01:54 +01:00
projects , err := service . projectsDB . GetOwn ( ctx , customer . UserID )
2020-01-07 10:41:19 +00:00
if err != nil {
2021-08-27 01:51:26 +01:00
return 0 , err
2020-01-07 10:41:19 +00:00
}
2021-08-27 01:51:26 +01:00
records , err := service . createProjectRecords ( ctx , customer . ID , projects , start , end )
2020-05-08 17:04:04 +01:00
if err != nil {
2021-08-27 01:51:26 +01:00
return 0 , err
2020-05-08 17:04:04 +01:00
}
2020-01-24 13:38:53 +00:00
2020-05-29 11:29:03 +01:00
allRecords = append ( allRecords , records ... )
2019-11-05 13:16:02 +00:00
}
2021-08-27 01:51:26 +01:00
return len ( allRecords ) , service . db . ProjectRecords ( ) . Create ( ctx , allRecords , start , end )
2020-05-29 11:29:03 +01:00
}
// createProjectRecords creates invoice project record if none exists.
2021-08-27 01:51:26 +01:00
func ( service * Service ) createProjectRecords ( ctx context . Context , customerID string , projects [ ] console . Project , start , end time . Time ) ( _ [ ] CreateProjectRecord , err error ) {
2020-05-29 11:29:03 +01:00
defer mon . Task ( ) ( & ctx ) ( & err )
var records [ ] CreateProjectRecord
for _ , project := range projects {
if err = ctx . Err ( ) ; err != nil {
2021-08-27 01:51:26 +01:00
return nil , err
2020-05-29 11:29:03 +01:00
}
if err = service . db . ProjectRecords ( ) . Check ( ctx , project . ID , start , end ) ; err != nil {
2020-07-14 14:04:38 +01:00
if errors . Is ( err , ErrProjectRecordExists ) {
2020-06-03 10:50:53 +01:00
service . log . Warn ( "Record for this project already exists." , zap . String ( "Customer ID" , customerID ) , zap . String ( "Project ID" , project . ID . String ( ) ) )
2020-05-29 11:29:03 +01:00
continue
}
2021-08-27 01:51:26 +01:00
return nil , err
2020-05-29 11:29:03 +01:00
}
usage , err := service . usageDB . GetProjectTotal ( ctx , project . ID , start , end )
if err != nil {
2021-08-27 01:51:26 +01:00
return nil , err
2020-05-29 11:29:03 +01:00
}
// TODO: account for usage data.
records = append ( records ,
CreateProjectRecord {
ProjectID : project . ID ,
Storage : usage . Storage ,
Egress : usage . Egress ,
2021-10-20 23:54:34 +01:00
Segments : usage . SegmentCount ,
2020-05-29 11:29:03 +01:00
} ,
)
}
2021-08-27 01:51:26 +01:00
return records , nil
2019-11-05 13:16:02 +00:00
}
// InvoiceApplyProjectRecords iterates through unapplied invoice project records and creates invoice line items
// for stripe customer.
2020-05-12 09:46:48 +01:00
func ( service * Service ) InvoiceApplyProjectRecords ( ctx context . Context , period time . Time ) ( err error ) {
2019-11-05 13:16:02 +00:00
defer mon . Task ( ) ( & ctx ) ( & err )
2020-05-19 08:42:07 +01:00
now := service . nowFn ( ) . UTC ( )
2020-05-12 09:46:48 +01:00
utc := period . UTC ( )
start := time . Date ( utc . Year ( ) , utc . Month ( ) , 1 , 0 , 0 , 0 , 0 , time . UTC )
2021-08-25 20:35:57 +01:00
end := time . Date ( utc . Year ( ) , utc . Month ( ) + 1 , 1 , 0 , 0 , 0 , 0 , time . UTC )
2019-11-05 13:16:02 +00:00
2020-05-12 09:46:48 +01:00
if end . After ( now ) {
return Error . New ( "allowed for past periods only" )
}
2022-12-14 13:35:53 +00:00
var totalRecords int
var totalSkipped int
2023-03-23 15:38:07 +00:00
for {
2019-11-05 13:16:02 +00:00
if err = ctx . Err ( ) ; err != nil {
return Error . Wrap ( err )
}
2020-05-18 14:01:26 +01:00
// we are always starting from offset 0 because applyProjectRecords is changing project record state to applied
2023-03-23 15:38:07 +00:00
recordsPage , err := service . db . ProjectRecords ( ) . ListUnapplied ( ctx , 0 , service . listingLimit , start , end )
2019-11-05 13:16:02 +00:00
if err != nil {
return Error . Wrap ( err )
}
2022-12-14 13:35:53 +00:00
totalRecords += len ( recordsPage . Records )
2019-11-05 13:16:02 +00:00
2022-12-14 13:35:53 +00:00
skipped , err := service . applyProjectRecords ( ctx , recordsPage . Records )
if err != nil {
2019-11-05 13:16:02 +00:00
return Error . Wrap ( err )
}
2022-12-14 13:35:53 +00:00
totalSkipped += skipped
2023-03-23 15:38:07 +00:00
if ! recordsPage . Next {
break
}
2019-11-05 13:16:02 +00:00
}
2022-12-14 13:35:53 +00:00
service . log . Info ( "Processed project records." ,
zap . Int ( "Total" , totalRecords ) ,
zap . Int ( "Skipped" , totalSkipped ) )
2019-11-05 13:16:02 +00:00
return nil
}
2022-09-28 18:41:41 +01:00
// InvoiceApplyTokenBalance iterates through customer storjscan wallets and creates invoice credit notes
// for stripe customers with invoices on or after the given date.
func ( service * Service ) InvoiceApplyTokenBalance ( ctx context . Context , createdOnAfter time . Time ) ( err error ) {
2022-05-10 20:19:53 +01:00
defer mon . Task ( ) ( & ctx ) ( & err )
// get all wallet entries
wallets , err := service . walletsDB . GetAll ( ctx )
if err != nil {
return Error . New ( "unable to get users in the wallets table" )
}
var errGrp errs . Group
for _ , wallet := range wallets {
// get the user token balance, if it's not > 0, don't bother with the rest
2022-08-27 00:08:03 +01:00
monetaryTokenBalance , err := service . billingDB . GetBalance ( ctx , wallet . UserID )
// truncate here since stripe only has cent level precision for invoices.
// The users account balance will still maintain the full precision monetary value!
2022-09-06 13:43:09 +01:00
tokenBalance := currency . AmountFromDecimal ( monetaryTokenBalance . AsDecimal ( ) . Truncate ( 2 ) , currency . USDollars )
2022-05-10 20:19:53 +01:00
if err != nil {
errGrp . Add ( Error . New ( "unable to compute balance for user ID %s" , wallet . UserID . String ( ) ) )
continue
}
2022-08-27 00:08:03 +01:00
if tokenBalance . BaseUnits ( ) <= 0 {
2022-05-10 20:19:53 +01:00
continue
}
// get the stripe customer invoice balance
cusID , err := service . db . Customers ( ) . GetCustomerID ( ctx , wallet . UserID )
if err != nil {
errGrp . Add ( Error . New ( "unable to get stripe customer ID for user ID %s" , wallet . UserID . String ( ) ) )
continue
}
2022-09-28 18:41:41 +01:00
invoices , err := service . getInvoices ( ctx , cusID , createdOnAfter )
2022-05-10 20:19:53 +01:00
if err != nil {
errGrp . Add ( Error . New ( "unable to get invoice balance for stripe customer ID %s" , cusID ) )
continue
}
for _ , invoice := range invoices {
// if no balance due, do nothing
2022-09-13 00:16:17 +01:00
if invoice . AmountRemaining <= 0 {
2022-05-10 20:19:53 +01:00
continue
}
var tokenCreditAmount int64
2022-09-13 00:16:17 +01:00
if invoice . AmountRemaining >= tokenBalance . BaseUnits ( ) {
tokenCreditAmount = tokenBalance . BaseUnits ( )
2022-05-10 20:19:53 +01:00
} else {
2022-09-13 00:16:17 +01:00
tokenCreditAmount = invoice . AmountRemaining
2022-05-10 20:19:53 +01:00
}
2022-09-13 00:16:17 +01:00
txID , err := service . createTokenPaymentBillingTransaction ( ctx , wallet . UserID , invoice . ID , wallet . Address . Hex ( ) , - tokenCreditAmount )
2022-05-10 20:19:53 +01:00
if err != nil {
errGrp . Add ( Error . New ( "unable to create token payment billing transaction for user %s" , wallet . UserID . String ( ) ) )
continue
}
2022-09-13 00:16:17 +01:00
creditNoteID , err := service . addCreditNoteToInvoice ( ctx , invoice . ID , cusID , wallet . Address . Hex ( ) , tokenCreditAmount , txID )
2022-05-10 20:19:53 +01:00
if err != nil {
2022-09-13 00:16:17 +01:00
errGrp . Add ( Error . New ( "unable to create token payment credit note for user %s" , wallet . UserID . String ( ) ) )
2022-05-10 20:19:53 +01:00
continue
}
metadata , err := json . Marshal ( map [ string ] interface { } {
2022-09-13 00:16:17 +01:00
"Credit Note ID" : creditNoteID ,
2022-05-10 20:19:53 +01:00
} )
if err != nil {
2022-09-13 00:16:17 +01:00
errGrp . Add ( Error . New ( "unable to marshall credit note ID %s" , creditNoteID ) )
2022-05-10 20:19:53 +01:00
continue
}
err = service . billingDB . UpdateMetadata ( ctx , txID , metadata )
if err != nil {
2022-09-13 00:16:17 +01:00
errGrp . Add ( Error . New ( "unable to add credit note ID to billing transaction for user %s" , wallet . UserID . String ( ) ) )
continue
}
err = service . billingDB . UpdateStatus ( ctx , txID , billing . TransactionStatusCompleted )
if err != nil {
errGrp . Add ( Error . New ( "unable to update status for billing transaction for user %s" , wallet . UserID . String ( ) ) )
2022-05-10 20:19:53 +01:00
continue
}
}
}
return errGrp . Err ( )
}
2022-09-28 18:41:41 +01:00
// getInvoices returns the stripe customer's open finalized invoices created on or after the given date.
func ( service * Service ) getInvoices ( ctx context . Context , cusID string , createdOnAfter time . Time ) ( _ [ ] stripe . Invoice , err error ) {
2022-05-10 20:19:53 +01:00
defer mon . Task ( ) ( & ctx ) ( & err )
params := & stripe . InvoiceListParams {
2023-03-14 02:59:24 +00:00
ListParams : stripe . ListParams { Context : ctx } ,
Customer : stripe . String ( cusID ) ,
Status : stripe . String ( string ( stripe . InvoiceStatusOpen ) ) ,
2022-05-10 20:19:53 +01:00
}
2022-09-28 18:41:41 +01:00
params . Filters . AddFilter ( "created" , "gte" , strconv . FormatInt ( createdOnAfter . Unix ( ) , 10 ) )
2022-05-10 20:19:53 +01:00
invoicesIterator := service . stripeClient . Invoices ( ) . List ( params )
var stripeInvoices [ ] stripe . Invoice
for invoicesIterator . Next ( ) {
stripeInvoice := invoicesIterator . Invoice ( )
if stripeInvoice != nil {
stripeInvoices = append ( stripeInvoices , * stripeInvoice )
}
}
return stripeInvoices , nil
}
2022-09-13 00:16:17 +01:00
// addCreditNoteToInvoice creates a credit note for the user token payment.
func ( service * Service ) addCreditNoteToInvoice ( ctx context . Context , invoiceID , cusID , wallet string , amount , txID int64 ) ( _ string , err error ) {
2022-05-10 20:19:53 +01:00
defer mon . Task ( ) ( & ctx ) ( & err )
2022-09-13 00:16:17 +01:00
var lineParams [ ] * stripe . CreditNoteLineParams
lineParam := stripe . CreditNoteLineParams {
Description : stripe . String ( "Storjscan Token payment" ) ,
Type : stripe . String ( "custom_line_item" ) ,
2022-05-10 20:19:53 +01:00
UnitAmount : stripe . Int64 ( amount ) ,
2022-09-13 00:16:17 +01:00
Quantity : stripe . Int64 ( 1 ) ,
2022-05-10 20:19:53 +01:00
}
2022-09-13 00:16:17 +01:00
lineParams = append ( lineParams , & lineParam )
params := & stripe . CreditNoteParams {
2023-03-14 02:59:24 +00:00
Params : stripe . Params { Context : ctx } ,
2022-09-13 00:16:17 +01:00
Invoice : stripe . String ( invoiceID ) ,
Lines : lineParams ,
2023-04-11 20:15:04 +01:00
Memo : stripe . String ( "Storjscan Token Payment - Wallet: " + wallet ) ,
2022-09-13 00:16:17 +01:00
}
2023-04-11 20:15:04 +01:00
params . AddMetadata ( "txID" , strconv . FormatInt ( txID , 10 ) )
2022-09-13 00:16:17 +01:00
params . AddMetadata ( "wallet address" , wallet )
creditNote , err := service . stripeClient . CreditNotes ( ) . New ( params )
2022-05-10 20:19:53 +01:00
if err != nil {
2022-09-13 00:16:17 +01:00
service . log . Warn ( "unable to add credit note for stripe customer" , zap . String ( "Customer ID" , cusID ) )
return "" , Error . Wrap ( err )
2022-05-10 20:19:53 +01:00
}
2022-09-13 00:16:17 +01:00
return creditNote . ID , nil
2022-05-10 20:19:53 +01:00
}
// createTokenPaymentBillingTransaction creates a billing DB entry for the user token payment.
func ( service * Service ) createTokenPaymentBillingTransaction ( ctx context . Context , userID uuid . UUID , invoiceID , wallet string , amount int64 ) ( _ int64 , err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
metadata , err := json . Marshal ( map [ string ] interface { } {
"InvoiceID" : invoiceID ,
"Wallet" : wallet ,
} )
transaction := billing . Transaction {
UserID : userID ,
2022-09-06 13:43:09 +01:00
Amount : currency . AmountFromBaseUnits ( amount , currency . USDollars ) ,
2022-05-10 20:19:53 +01:00
Description : "Paid Stripe Invoice" ,
2023-03-28 02:42:26 +01:00
Source : billing . StripeSource ,
2022-05-10 20:19:53 +01:00
Status : billing . TransactionStatusPending ,
Type : billing . TransactionTypeDebit ,
Metadata : metadata ,
Timestamp : time . Now ( ) ,
}
2023-03-24 12:08:40 +00:00
txIDs , err := service . billingDB . Insert ( ctx , transaction )
2022-05-10 20:19:53 +01:00
if err != nil {
service . log . Warn ( "unable to add transaction to billing DB for user" , zap . String ( "User ID" , userID . String ( ) ) )
return 0 , Error . Wrap ( err )
}
2023-03-24 12:08:40 +00:00
return txIDs [ 0 ] , nil
2022-05-10 20:19:53 +01:00
}
2019-11-05 13:16:02 +00:00
// applyProjectRecords applies invoice intents as invoice line items to stripe customer.
2022-12-14 13:35:53 +00:00
func ( service * Service ) applyProjectRecords ( ctx context . Context , records [ ] ProjectRecord ) ( skipCount int , err error ) {
2019-11-05 13:16:02 +00:00
defer mon . Task ( ) ( & ctx ) ( & err )
2023-03-23 15:38:07 +00:00
var mu sync . Mutex
var errGrp errs . Group
limiter := sync2 . NewLimiter ( service . maxParallelCalls )
ctx , cancel := context . WithCancel ( ctx )
defer func ( ) {
cancel ( )
limiter . Wait ( )
} ( )
2019-11-05 13:16:02 +00:00
for _ , record := range records {
if err = ctx . Err ( ) ; err != nil {
2022-12-14 13:35:53 +00:00
return 0 , errs . Wrap ( err )
2019-11-05 13:16:02 +00:00
}
proj , err := service . projectsDB . Get ( ctx , record . ProjectID )
if err != nil {
2022-02-04 17:31:24 +00:00
// This should never happen, but be sure to log info to further troubleshoot before exiting.
service . log . Error ( "project ID for corresponding project record not found" , zap . Stringer ( "Record ID" , record . ID ) , zap . Stringer ( "Project ID" , record . ProjectID ) )
2022-12-14 13:35:53 +00:00
return 0 , errs . Wrap ( err )
2019-11-05 13:16:02 +00:00
}
cusID , err := service . db . Customers ( ) . GetCustomerID ( ctx , proj . OwnerID )
if err != nil {
2020-07-14 14:04:38 +01:00
if errors . Is ( err , ErrNoCustomer ) {
2020-06-03 10:50:53 +01:00
service . log . Warn ( "Stripe customer does not exist for project owner." , zap . Stringer ( "Owner ID" , proj . OwnerID ) , zap . Stringer ( "Project ID" , proj . ID ) )
2019-11-05 13:16:02 +00:00
continue
}
2022-12-14 13:35:53 +00:00
return 0 , errs . Wrap ( err )
2019-11-05 13:16:02 +00:00
}
2023-03-23 15:38:07 +00:00
record := record
limiter . Go ( ctx , func ( ) {
skipped , err := service . createInvoiceItems ( ctx , cusID , proj . Name , record )
if err != nil {
mu . Lock ( )
errGrp . Add ( errs . Wrap ( err ) )
mu . Unlock ( )
return
}
if skipped {
mu . Lock ( )
skipCount ++
mu . Unlock ( )
}
} )
2019-11-05 13:16:02 +00:00
}
2023-03-23 15:38:07 +00:00
limiter . Wait ( )
return skipCount , errGrp . Err ( )
2019-11-05 13:16:02 +00:00
}
2022-12-14 13:35:53 +00:00
// createInvoiceItems creates invoice line items for stripe customer.
2023-01-27 05:34:08 +00:00
func ( service * Service ) createInvoiceItems ( ctx context . Context , cusID , projName string , record ProjectRecord ) ( skipped bool , err error ) {
2019-11-05 13:16:02 +00:00
defer mon . Task ( ) ( & ctx ) ( & err )
if err = service . db . ProjectRecords ( ) . Consume ( ctx , record . ID ) ; err != nil {
2022-12-14 13:35:53 +00:00
return false , err
}
if service . skipEmptyInvoices && doesProjectRecordHaveNoUsage ( record ) {
return true , nil
2019-11-05 13:16:02 +00:00
}
2023-02-23 16:27:37 +00:00
usages , err := service . usageDB . GetProjectTotalByPartner ( ctx , record . ProjectID , service . partnerNames , record . PeriodStart , record . PeriodEnd )
if err != nil {
return false , err
}
items := service . InvoiceItemsFromProjectUsage ( projName , usages )
2020-05-27 13:08:37 +01:00
for _ , item := range items {
2023-03-14 02:59:24 +00:00
item . Params = stripe . Params { Context : ctx }
2020-05-27 13:08:37 +01:00
item . Currency = stripe . String ( string ( stripe . CurrencyUSD ) )
item . Customer = stripe . String ( cusID )
item . AddMetadata ( "projectID" , record . ProjectID . String ( ) )
_ , err = service . stripeClient . InvoiceItems ( ) . New ( item )
if err != nil {
2022-12-14 13:35:53 +00:00
return false , err
2020-05-27 13:08:37 +01:00
}
2019-11-05 13:16:02 +00:00
}
2022-12-14 13:35:53 +00:00
return false , nil
2020-05-27 13:08:37 +01:00
}
2023-02-23 16:27:37 +00:00
// InvoiceItemsFromProjectUsage calculates Stripe invoice item from project usage.
func ( service * Service ) InvoiceItemsFromProjectUsage ( projName string , partnerUsages map [ string ] accounting . ProjectUsage ) ( result [ ] * stripe . InvoiceItemParams ) {
var partners [ ] string
if len ( partnerUsages ) == 0 {
partners = [ ] string { "" }
partnerUsages = map [ string ] accounting . ProjectUsage { "" : { } }
} else {
for partner := range partnerUsages {
partners = append ( partners , partner )
}
sort . Strings ( partners )
}
for _ , partner := range partners {
priceModel := service . Accounts ( ) . GetProjectUsagePriceModel ( partner )
2023-04-07 10:57:54 +01:00
usage := partnerUsages [ partner ]
2023-04-04 10:10:25 +01:00
usage . Egress = applyEgressDiscount ( usage , priceModel )
2023-04-07 10:57:54 +01:00
2023-02-23 16:27:37 +00:00
prefix := "Project " + projName
if partner != "" {
prefix += " (" + partner + ")"
}
projectItem := & stripe . InvoiceItemParams { }
projectItem . Description = stripe . String ( prefix + " - Segment Storage (MB-Month)" )
projectItem . Quantity = stripe . Int64 ( storageMBMonthDecimal ( usage . Storage ) . IntPart ( ) )
storagePrice , _ := priceModel . StorageMBMonthCents . Float64 ( )
projectItem . UnitAmountDecimal = stripe . Float64 ( storagePrice )
result = append ( result , projectItem )
projectItem = & stripe . InvoiceItemParams { }
projectItem . Description = stripe . String ( prefix + " - Egress Bandwidth (MB)" )
projectItem . Quantity = stripe . Int64 ( egressMBDecimal ( usage . Egress ) . IntPart ( ) )
egressPrice , _ := priceModel . EgressMBCents . Float64 ( )
projectItem . UnitAmountDecimal = stripe . Float64 ( egressPrice )
result = append ( result , projectItem )
projectItem = & stripe . InvoiceItemParams { }
projectItem . Description = stripe . String ( prefix + " - Segment Fee (Segment-Month)" )
projectItem . Quantity = stripe . Int64 ( segmentMonthDecimal ( usage . SegmentCount ) . IntPart ( ) )
segmentPrice , _ := priceModel . SegmentMonthCents . Float64 ( )
projectItem . UnitAmountDecimal = stripe . Float64 ( segmentPrice )
result = append ( result , projectItem )
}
2021-10-20 23:54:34 +01:00
service . log . Info ( "invoice items" , zap . Any ( "result" , result ) )
2020-05-27 13:08:37 +01:00
return result
2019-11-05 13:16:02 +00:00
}
2021-07-30 23:11:36 +01:00
// ApplyFreeTierCoupons iterates through all customers in Stripe. For each customer,
// if that customer does not currently have a Stripe coupon, the free tier Stripe coupon
// is applied.
func ( service * Service ) ApplyFreeTierCoupons ( ctx context . Context ) ( err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
customers := service . db . Customers ( )
2023-03-23 15:38:07 +00:00
limiter := sync2 . NewLimiter ( service . maxParallelCalls )
ctx , cancel := context . WithCancel ( ctx )
defer func ( ) {
cancel ( )
limiter . Wait ( )
} ( )
var mu sync . Mutex
var appliedCoupons int
2021-07-30 23:11:36 +01:00
failedUsers := [ ] string { }
morePages := true
2023-04-06 11:22:36 +01:00
var nextCursor uuid . UUID
2021-07-30 23:11:36 +01:00
listingLimit := 100
end := time . Now ( )
for morePages {
2023-04-06 11:22:36 +01:00
customersPage , err := customers . List ( ctx , nextCursor , listingLimit , end )
2021-07-30 23:11:36 +01:00
if err != nil {
return err
}
morePages = customersPage . Next
2023-04-06 11:22:36 +01:00
nextCursor = customersPage . Cursor
2021-07-30 23:11:36 +01:00
for _ , c := range customersPage . Customers {
2023-03-23 15:38:07 +00:00
cusID := c . ID
limiter . Go ( ctx , func ( ) {
applied , err := service . applyFreeTierCoupon ( ctx , cusID )
2021-07-30 23:11:36 +01:00
if err != nil {
2023-03-23 15:38:07 +00:00
mu . Lock ( )
failedUsers = append ( failedUsers , cusID )
mu . Unlock ( )
return
2021-07-30 23:11:36 +01:00
}
2023-03-23 15:38:07 +00:00
if applied {
mu . Lock ( )
appliedCoupons ++
mu . Unlock ( )
}
} )
2021-07-30 23:11:36 +01:00
}
}
2023-03-23 15:38:07 +00:00
limiter . Wait ( )
2021-07-30 23:11:36 +01:00
if len ( failedUsers ) > 0 {
service . log . Warn ( "Failed to get or apply free tier coupon to some customers:" , zap . String ( "idlist" , strings . Join ( failedUsers , ", " ) ) )
}
service . log . Info ( "Finished" , zap . Int ( "number of coupons applied" , appliedCoupons ) )
return nil
}
2023-03-23 15:38:07 +00:00
// applyFreeTierCoupon applies the free tier Stripe coupon to a customer if it doesn't already have a coupon.
func ( service * Service ) applyFreeTierCoupon ( ctx context . Context , cusID string ) ( applied bool , err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
params := & stripe . CustomerParams { Params : stripe . Params { Context : ctx } }
stripeCust , err := service . stripeClient . Customers ( ) . Get ( cusID , params )
if err != nil {
service . log . Error ( "Failed to get customer" , zap . Error ( err ) )
return false , err
}
// if customer has a coupon, don't apply the free tier coupon
if stripeCust . Discount != nil && stripeCust . Discount . Coupon != nil {
return false , nil
}
params = & stripe . CustomerParams {
Params : stripe . Params { Context : ctx } ,
Coupon : stripe . String ( service . StripeFreeTierCouponID ) ,
}
_ , err = service . stripeClient . Customers ( ) . Update ( cusID , params )
if err != nil {
service . log . Error ( "Failed to update customer with free tier coupon" , zap . Error ( err ) )
return false , err
}
return true , nil
}
2019-11-05 13:16:02 +00:00
// CreateInvoices lists through all customers and creates invoices.
2020-05-12 09:46:48 +01:00
func ( service * Service ) CreateInvoices ( ctx context . Context , period time . Time ) ( err error ) {
2019-11-05 13:16:02 +00:00
defer mon . Task ( ) ( & ctx ) ( & err )
2020-05-19 08:42:07 +01:00
now := service . nowFn ( ) . UTC ( )
2020-05-12 09:46:48 +01:00
utc := period . UTC ( )
start := time . Date ( utc . Year ( ) , utc . Month ( ) , 1 , 0 , 0 , 0 , 0 , time . UTC )
2021-08-25 20:35:57 +01:00
end := time . Date ( utc . Year ( ) , utc . Month ( ) + 1 , 1 , 0 , 0 , 0 , 0 , time . UTC )
2020-05-12 09:46:48 +01:00
if end . After ( now ) {
return Error . New ( "allowed for past periods only" )
}
2019-11-05 13:16:02 +00:00
2023-04-06 11:22:36 +01:00
var nextCursor uuid . UUID
2023-03-23 15:38:07 +00:00
var totalDraft , totalScheduled int
2022-09-26 19:00:07 +01:00
for {
2023-04-06 11:22:36 +01:00
cusPage , err := service . db . Customers ( ) . List ( ctx , nextCursor , service . listingLimit , end )
2019-11-05 13:16:02 +00:00
if err != nil {
return Error . Wrap ( err )
}
2023-03-23 15:38:07 +00:00
scheduled , draft , err := service . createInvoices ( ctx , cusPage . Customers , start )
if err != nil {
return Error . Wrap ( err )
2019-11-05 13:16:02 +00:00
}
2023-03-23 15:38:07 +00:00
totalScheduled += scheduled
totalDraft += draft
2020-06-09 14:07:06 +01:00
2022-09-26 19:00:07 +01:00
if ! cusPage . Next {
break
}
2023-04-06 11:22:36 +01:00
nextCursor = cusPage . Cursor
2019-11-05 13:16:02 +00:00
}
2023-03-23 15:38:07 +00:00
service . log . Info ( "Number of created invoices" , zap . Int ( "Draft" , totalDraft ) , zap . Int ( "Scheduled" , totalScheduled ) )
2019-11-05 13:16:02 +00:00
return nil
}
2023-03-23 15:38:07 +00:00
// createInvoice creates invoice for Stripe customer.
2022-09-26 19:00:07 +01:00
func ( service * Service ) createInvoice ( ctx context . Context , cusID string , period time . Time ) ( stripeInvoice * stripe . Invoice , err error ) {
2019-11-05 13:16:02 +00:00
defer mon . Task ( ) ( & ctx ) ( & err )
2021-04-15 14:57:34 +01:00
description := fmt . Sprintf ( "Storj DCS Cloud Storage for %s %d" , period . Month ( ) , period . Year ( ) )
2022-09-26 19:00:07 +01:00
stripeInvoice , err = service . stripeClient . Invoices ( ) . New (
2019-11-05 13:16:02 +00:00
& stripe . InvoiceParams {
2023-03-14 02:59:24 +00:00
Params : stripe . Params { Context : ctx } ,
2019-11-05 13:16:02 +00:00
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 {
2021-05-14 16:05:42 +01:00
var stripErr * stripe . Error
if errors . As ( err , & stripErr ) {
if stripErr . Code == stripe . ErrorCodeInvoiceNoCustomerLineItems {
2023-03-23 15:38:07 +00:00
return stripeInvoice , nil
2019-11-05 13:16:02 +00:00
}
}
2022-09-26 19:00:07 +01:00
return nil , err
2019-11-05 13:16:02 +00:00
}
2022-09-26 19:00:07 +01:00
// auto advance the invoice if nothing is due from the customer
if ! stripeInvoice . AutoAdvance && stripeInvoice . AmountDue == 0 {
2023-03-14 02:59:24 +00:00
params := & stripe . InvoiceParams {
Params : stripe . Params { Context : ctx } ,
AutoAdvance : stripe . Bool ( true ) ,
}
stripeInvoice , err = service . stripeClient . Invoices ( ) . Update ( stripeInvoice . ID , params )
2022-09-26 19:00:07 +01:00
if err != nil {
return nil , err
}
}
return stripeInvoice , nil
2019-11-05 13:16:02 +00:00
}
2020-01-28 23:36:54 +00:00
2023-03-23 15:38:07 +00:00
// createInvoices creates invoices for Stripe customers.
func ( service * Service ) createInvoices ( ctx context . Context , customers [ ] Customer , period time . Time ) ( scheduled , draft int , err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
limiter := sync2 . NewLimiter ( service . maxParallelCalls )
var errGrp errs . Group
var mu sync . Mutex
for _ , cus := range customers {
cusID := cus . ID
limiter . Go ( ctx , func ( ) {
inv , err := service . createInvoice ( ctx , cusID , period )
if err != nil {
mu . Lock ( )
errGrp . Add ( err )
mu . Unlock ( )
return
}
if inv != nil {
mu . Lock ( )
if inv . AutoAdvance {
scheduled ++
} else {
draft ++
}
mu . Unlock ( )
}
} )
}
limiter . Wait ( )
return scheduled , draft , errGrp . Err ( )
}
2023-04-20 21:51:19 +01:00
// CreateBalanceInvoiceItems will find users with a stripe balance, create an invoice
// item with the charges due, and zero out the stripe balance.
func ( service * Service ) CreateBalanceInvoiceItems ( ctx context . Context ) ( err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
custListParams := & stripe . CustomerListParams {
ListParams : stripe . ListParams {
Context : ctx ,
Limit : stripe . Int64 ( 100 ) ,
} ,
}
var errGrp errs . Group
itr := service . stripeClient . Customers ( ) . List ( custListParams )
for itr . Next ( ) {
if itr . Customer ( ) . Balance <= 0 {
continue
}
service . log . Info ( "Creating invoice item for customer prior balance" , zap . String ( "CustomerID" , itr . Customer ( ) . ID ) )
itemParams := & stripe . InvoiceItemParams {
Params : stripe . Params {
Context : ctx ,
} ,
Currency : stripe . String ( string ( stripe . CurrencyUSD ) ) ,
Customer : stripe . String ( itr . Customer ( ) . ID ) ,
Description : stripe . String ( "Prior Stripe Customer Balance" ) ,
Quantity : stripe . Int64 ( 1 ) ,
UnitAmount : stripe . Int64 ( itr . Customer ( ) . Balance ) ,
}
invoiceItem , err := service . stripeClient . InvoiceItems ( ) . New ( itemParams )
if err != nil {
service . log . Error ( "Failed to add invoice item for customer prior balance" , zap . Error ( err ) )
errGrp . Add ( err )
continue
}
service . log . Info ( "Updating customer balance to 0" , zap . String ( "CustomerID" , itr . Customer ( ) . ID ) )
custParams := & stripe . CustomerParams {
Params : stripe . Params {
Context : ctx ,
} ,
Balance : stripe . Int64 ( 0 ) ,
Description : stripe . String ( "Customer balance adjusted to 0 after adding invoice item " + invoiceItem . ID ) ,
}
_ , err = service . stripeClient . Customers ( ) . Update ( itr . Customer ( ) . ID , custParams )
if err != nil {
service . log . Error ( "Failed to update customer balance to 0 after adding invoice item" , zap . Error ( err ) )
errGrp . Add ( err )
continue
}
service . log . Info ( "Customer successfully updated" , zap . String ( "CustomerID" , itr . Customer ( ) . ID ) , zap . Int64 ( "Prior Balance" , itr . Customer ( ) . Balance ) , zap . Int64 ( "New Balance" , 0 ) , zap . String ( "InvoiceItemID" , invoiceItem . ID ) )
}
return errGrp . Err ( )
}
2023-04-25 16:48:17 +01:00
// GenerateInvoices performs tasks necessary to generate Stripe invoices.
// This is equivalent to invoking PrepareInvoiceProjectRecords, InvoiceApplyProjectRecords,
// and CreateInvoices in order.
2022-09-27 09:48:38 +01:00
func ( service * Service ) GenerateInvoices ( ctx context . Context , period time . Time ) ( err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
for _ , subFn := range [ ] struct {
Description string
Exec func ( context . Context , time . Time ) error
} {
{ "Preparing invoice project records" , service . PrepareInvoiceProjectRecords } ,
{ "Applying invoice project records" , service . InvoiceApplyProjectRecords } ,
{ "Creating invoices" , service . CreateInvoices } ,
} {
service . log . Info ( subFn . Description )
if err := subFn . Exec ( ctx , period ) ; err != nil {
return err
}
}
return nil
}
2022-09-13 00:16:17 +01:00
// FinalizeInvoices transitions all draft invoices to open finalized invoices in stripe. No payment is to be collected yet.
2020-06-09 16:18:36 +01:00
func ( service * Service ) FinalizeInvoices ( ctx context . Context ) ( err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
params := & stripe . InvoiceListParams {
2023-03-14 02:59:24 +00:00
ListParams : stripe . ListParams { Context : ctx } ,
Status : stripe . String ( "draft" ) ,
2020-06-09 16:18:36 +01:00
}
invoicesIterator := service . stripeClient . Invoices ( ) . List ( params )
for invoicesIterator . Next ( ) {
stripeInvoice := invoicesIterator . Invoice ( )
2022-09-26 19:00:07 +01:00
if stripeInvoice . AutoAdvance {
continue
}
2020-06-09 16:18:36 +01:00
err := service . finalizeInvoice ( ctx , stripeInvoice . ID )
if err != nil {
return Error . Wrap ( err )
}
}
return Error . Wrap ( invoicesIterator . Err ( ) )
}
func ( service * Service ) finalizeInvoice ( ctx context . Context , invoiceID string ) ( err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
2023-03-14 02:59:24 +00:00
params := & stripe . InvoiceFinalizeParams {
Params : stripe . Params { Context : ctx } ,
AutoAdvance : stripe . Bool ( false ) ,
}
2020-06-09 16:18:36 +01:00
_ , err = service . stripeClient . Invoices ( ) . FinalizeInvoice ( invoiceID , params )
return err
}
2022-09-28 18:41:41 +01:00
// PayInvoices attempts to transition all open finalized invoices created on or after a certain time to "paid"
// by charging the customer according to subscriptions settings.
func ( service * Service ) PayInvoices ( ctx context . Context , createdOnAfter time . Time ) ( err error ) {
2022-09-13 00:16:17 +01:00
defer mon . Task ( ) ( & ctx ) ( & err )
params := & stripe . InvoiceListParams {
2023-03-14 02:59:24 +00:00
ListParams : stripe . ListParams { Context : ctx } ,
Status : stripe . String ( "open" ) ,
2022-09-13 00:16:17 +01:00
}
2022-09-28 18:41:41 +01:00
params . Filters . AddFilter ( "created" , "gte" , strconv . FormatInt ( createdOnAfter . Unix ( ) , 10 ) )
2022-09-13 00:16:17 +01:00
var errGrp errs . Group
invoicesIterator := service . stripeClient . Invoices ( ) . List ( params )
for invoicesIterator . Next ( ) {
stripeInvoice := invoicesIterator . Invoice ( )
2023-01-20 20:56:12 +00:00
if stripeInvoice . DueDate > 0 {
service . log . Info ( "Skipping invoice marked for manual payment" ,
zap . String ( "id" , stripeInvoice . ID ) ,
zap . String ( "number" , stripeInvoice . Number ) ,
zap . String ( "customer" , stripeInvoice . Customer . ID ) )
continue
}
2022-09-13 00:16:17 +01:00
2023-03-14 02:59:24 +00:00
params := & stripe . InvoicePayParams { Params : stripe . Params { Context : ctx } }
2022-09-13 00:16:17 +01:00
_ , err = service . stripeClient . Invoices ( ) . Pay ( stripeInvoice . ID , params )
if err != nil {
errGrp . Add ( Error . New ( "unable to pay invoice %s" , stripeInvoice . ID ) )
continue
}
}
return errGrp . Err ( )
}
2020-01-28 23:36:54 +00:00
// projectUsagePrice represents pricing for project usage.
type projectUsagePrice struct {
2021-10-20 23:54:34 +01:00
Storage decimal . Decimal
Egress decimal . Decimal
Segments decimal . Decimal
2020-01-28 23:36:54 +00:00
}
// Total returns project usage price total.
func ( price projectUsagePrice ) Total ( ) decimal . Decimal {
2021-10-20 23:54:34 +01:00
return price . Storage . Add ( price . Egress ) . Add ( price . Segments )
2020-01-28 23:36:54 +00:00
}
// Total returns project usage price total.
func ( price projectUsagePrice ) TotalInt64 ( ) int64 {
2021-10-20 23:54:34 +01:00
return price . Storage . Add ( price . Egress ) . Add ( price . Segments ) . IntPart ( )
2020-01-28 23:36:54 +00:00
}
// calculateProjectUsagePrice calculate project usage price.
2023-04-04 10:10:25 +01:00
func ( service * Service ) calculateProjectUsagePrice ( usage accounting . ProjectUsage , pricing payments . ProjectUsagePriceModel ) projectUsagePrice {
2020-01-28 23:36:54 +00:00
return projectUsagePrice {
2023-04-04 10:10:25 +01:00
Storage : pricing . StorageMBMonthCents . Mul ( storageMBMonthDecimal ( usage . Storage ) ) . Round ( 0 ) ,
Egress : pricing . EgressMBCents . Mul ( egressMBDecimal ( usage . Egress ) ) . Round ( 0 ) ,
Segments : pricing . SegmentMonthCents . Mul ( segmentMonthDecimal ( usage . SegmentCount ) ) . Round ( 0 ) ,
2020-01-28 23:36:54 +00:00
}
}
2020-05-08 17:04:04 +01:00
2020-05-19 08:42:07 +01:00
// SetNow allows tests to have the Service act as if the current time is whatever
// they want. This avoids races and sleeping, making tests more reliable and efficient.
func ( service * Service ) SetNow ( now func ( ) time . Time ) {
service . nowFn = now
}
2020-05-26 12:00:14 +01:00
// storageMBMonthDecimal converts storage usage from Byte-Hours to Megabyte-Months.
// The result is rounded to the nearest whole number, but returned as Decimal for convenience.
func storageMBMonthDecimal ( storage float64 ) decimal . Decimal {
return decimal . NewFromFloat ( storage ) . Shift ( - 6 ) . Div ( decimal . NewFromInt ( hoursPerMonth ) ) . Round ( 0 )
}
// egressMBDecimal converts egress usage from bytes to Megabytes
// The result is rounded to the nearest whole number, but returned as Decimal for convenience.
func egressMBDecimal ( egress int64 ) decimal . Decimal {
return decimal . NewFromInt ( egress ) . Shift ( - 6 ) . Round ( 0 )
}
2021-10-20 23:54:34 +01:00
// segmentMonthDecimal converts segments usage from Segment-Hours to Segment-Months.
2020-05-26 12:00:14 +01:00
// The result is rounded to the nearest whole number, but returned as Decimal for convenience.
2021-10-20 23:54:34 +01:00
func segmentMonthDecimal ( segments float64 ) decimal . Decimal {
return decimal . NewFromFloat ( segments ) . Div ( decimal . NewFromInt ( hoursPerMonth ) ) . Round ( 0 )
2020-05-26 12:00:14 +01:00
}
2022-12-14 13:35:53 +00:00
// doesProjectRecordHaveNoUsage returns true if the given project record
// represents a billing cycle where there was no usage.
func doesProjectRecordHaveNoUsage ( record ProjectRecord ) bool {
return record . Storage == 0 && record . Egress == 0 && record . Segments == 0
}
2023-04-04 10:10:25 +01:00
// applyEgressDiscount returns the amount of egress that we should charge for by subtracting
// the discounted amount.
func applyEgressDiscount ( usage accounting . ProjectUsage , model payments . ProjectUsagePriceModel ) int64 {
egress := usage . Egress - int64 ( math . Round ( usage . Storage / hoursPerMonth * model . EgressDiscountRatio ) )
if egress < 0 {
egress = 0
}
return egress
}