satellite/main,stripe/{client,service}: stripe balance invoice item cmd
Add a new billing command that will convert stripe customer balances into invoice items so that the charges can be processed normally by the invoicing workflow. Change-Id: Iaa8350e7aca80a0f14e94eb8ef8b7d6ce0b5b3b8
This commit is contained in:
parent
5fc493b276
commit
efcae857ba
@ -208,6 +208,12 @@ var (
|
||||
Long: "Applies free tier coupon to Stripe customers without a coupon",
|
||||
RunE: cmdApplyFreeTierCoupons,
|
||||
}
|
||||
createCustomerBalanceInvoiceItems = &cobra.Command{
|
||||
Use: "create-balance-invoice-items",
|
||||
Short: "Creates stripe invoice line items for stripe customer balance",
|
||||
Long: "Creates stripe invoice line items for stripe customer balances obtained from past invoices and other miscellaneous charges.",
|
||||
RunE: cmdCreateCustomerBalanceInvoiceItems,
|
||||
}
|
||||
prepareCustomerInvoiceRecordsCmd = &cobra.Command{
|
||||
Use: "prepare-invoice-records [period]",
|
||||
Short: "Prepares invoice project records",
|
||||
@ -375,6 +381,7 @@ func init() {
|
||||
compensationCmd.AddCommand(recordPeriodCmd)
|
||||
compensationCmd.AddCommand(recordOneOffPaymentsCmd)
|
||||
billingCmd.AddCommand(applyFreeTierCouponsCmd)
|
||||
billingCmd.AddCommand(createCustomerBalanceInvoiceItems)
|
||||
billingCmd.AddCommand(prepareCustomerInvoiceRecordsCmd)
|
||||
billingCmd.AddCommand(createCustomerProjectInvoiceItemsCmd)
|
||||
billingCmd.AddCommand(createCustomerInvoicesCmd)
|
||||
@ -406,6 +413,7 @@ func init() {
|
||||
process.Bind(reportsVerifyGEReceiptCmd, &reportsVerifyGracefulExitReceiptCfg, defaults, cfgstruct.ConfDir(confDir), cfgstruct.IdentityDir(identityDir))
|
||||
process.Bind(partnerAttributionCmd, &partnerAttribtionCfg, defaults, cfgstruct.ConfDir(confDir), cfgstruct.IdentityDir(identityDir))
|
||||
process.Bind(applyFreeTierCouponsCmd, &runCfg, defaults, cfgstruct.ConfDir(confDir), cfgstruct.IdentityDir(identityDir))
|
||||
process.Bind(createCustomerBalanceInvoiceItems, &runCfg, defaults, cfgstruct.ConfDir(confDir), cfgstruct.IdentityDir(identityDir))
|
||||
process.Bind(prepareCustomerInvoiceRecordsCmd, &runCfg, defaults, cfgstruct.ConfDir(confDir), cfgstruct.IdentityDir(identityDir))
|
||||
process.Bind(createCustomerProjectInvoiceItemsCmd, &runCfg, defaults, cfgstruct.ConfDir(confDir), cfgstruct.IdentityDir(identityDir))
|
||||
process.Bind(createCustomerInvoicesCmd, &runCfg, defaults, cfgstruct.ConfDir(confDir), cfgstruct.IdentityDir(identityDir))
|
||||
@ -746,6 +754,14 @@ func cmdValueAttribution(cmd *cobra.Command, args []string) (err error) {
|
||||
return reports.GenerateAttributionCSV(ctx, partnerAttribtionCfg.Database, start, end, userAgents, file)
|
||||
}
|
||||
|
||||
func cmdCreateCustomerBalanceInvoiceItems(cmd *cobra.Command, _ []string) (err error) {
|
||||
ctx, _ := process.Ctx(cmd)
|
||||
|
||||
return runBillingCmd(ctx, func(ctx context.Context, payments *stripe.Service, _ satellite.DB) error {
|
||||
return payments.CreateBalanceInvoiceItems(ctx)
|
||||
})
|
||||
}
|
||||
|
||||
func cmdPrepareCustomerInvoiceRecords(cmd *cobra.Command, args []string) (err error) {
|
||||
ctx, _ := process.Ctx(cmd)
|
||||
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
"github.com/stripe/stripe-go/v72"
|
||||
"github.com/stripe/stripe-go/v72/charge"
|
||||
"github.com/stripe/stripe-go/v72/client"
|
||||
"github.com/stripe/stripe-go/v72/customer"
|
||||
"github.com/stripe/stripe-go/v72/customerbalancetransaction"
|
||||
"github.com/stripe/stripe-go/v72/form"
|
||||
"github.com/stripe/stripe-go/v72/invoice"
|
||||
@ -43,6 +44,7 @@ type Customers interface {
|
||||
New(params *stripe.CustomerParams) (*stripe.Customer, error)
|
||||
Get(id string, params *stripe.CustomerParams) (*stripe.Customer, error)
|
||||
Update(id string, params *stripe.CustomerParams) (*stripe.Customer, error)
|
||||
List(listParams *stripe.CustomerListParams) *customer.Iter
|
||||
}
|
||||
|
||||
// PaymentMethods Stripe PaymentMethods interface.
|
||||
|
@ -774,6 +774,60 @@ func (service *Service) createInvoices(ctx context.Context, customers []Customer
|
||||
return scheduled, draft, errGrp.Err()
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
// GenerateInvoices performs all tasks necessary to generate Stripe invoices.
|
||||
// This is equivalent to invoking ApplyFreeTierCoupons, PrepareInvoiceProjectRecords,
|
||||
// InvoiceApplyProjectRecords, and CreateInvoices in order.
|
||||
|
@ -35,6 +35,68 @@ import (
|
||||
stripe1 "storj.io/storj/satellite/payments/stripe"
|
||||
)
|
||||
|
||||
func TestService_BalanceInvoiceItems(t *testing.T) {
|
||||
testplanet.Run(t, testplanet.Config{
|
||||
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 0,
|
||||
Reconfigure: testplanet.Reconfigure{
|
||||
Satellite: func(log *zap.Logger, index int, config *satellite.Config) {
|
||||
config.Payments.StripeCoinPayments.ListingLimit = 4
|
||||
},
|
||||
},
|
||||
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
||||
satellite := planet.Satellites[0]
|
||||
payments := satellite.API.Payments
|
||||
|
||||
numberOfUsers := 10
|
||||
users := make([]*console.User, numberOfUsers)
|
||||
projects := make([]*console.Project, numberOfUsers)
|
||||
// create a bunch of users
|
||||
for i := 0; i < numberOfUsers; i++ {
|
||||
var err error
|
||||
|
||||
users[i], err = satellite.AddUser(ctx, console.CreateUser{
|
||||
FullName: "testuser" + strconv.Itoa(i),
|
||||
Email: "user@test" + strconv.Itoa(i),
|
||||
}, 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
projects[i], err = satellite.AddProject(ctx, users[i].ID, "testproject-"+strconv.Itoa(i))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// give one of the users a stripe balance
|
||||
cusID, err := satellite.DB.StripeCoinPayments().Customers().GetCustomerID(ctx, users[4].ID)
|
||||
require.NoError(t, err)
|
||||
_, err = payments.StripeClient.Customers().Update(cusID, &stripe.CustomerParams{
|
||||
Params: stripe.Params{
|
||||
Context: ctx,
|
||||
},
|
||||
Balance: stripe.Int64(1000),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// convert the stripe balance into an invoice item
|
||||
require.NoError(t, payments.StripeService.CreateBalanceInvoiceItems(ctx))
|
||||
|
||||
// check that the invoice item was created
|
||||
itr := payments.StripeClient.InvoiceItems().List(&stripe.InvoiceItemListParams{
|
||||
Customer: stripe.String(cusID),
|
||||
})
|
||||
require.True(t, itr.Next())
|
||||
require.NoError(t, itr.Err())
|
||||
require.Equal(t, int64(1000), itr.InvoiceItem().UnitAmount)
|
||||
|
||||
// check that the stripe balance was reset
|
||||
cus, err := payments.StripeClient.Customers().Get(cusID, &stripe.CustomerParams{
|
||||
Params: stripe.Params{
|
||||
Context: ctx,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(0), cus.Balance)
|
||||
})
|
||||
}
|
||||
|
||||
func TestService_InvoiceElementsProcessing(t *testing.T) {
|
||||
testplanet.Run(t, testplanet.Config{
|
||||
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 0,
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
|
||||
"github.com/stripe/stripe-go/v72"
|
||||
"github.com/stripe/stripe-go/v72/charge"
|
||||
"github.com/stripe/stripe-go/v72/customer"
|
||||
"github.com/stripe/stripe-go/v72/customerbalancetransaction"
|
||||
"github.com/stripe/stripe-go/v72/form"
|
||||
"github.com/stripe/stripe-go/v72/invoice"
|
||||
@ -194,6 +195,21 @@ type mockCustomers struct {
|
||||
coupons map[string]*stripe.Coupon
|
||||
}
|
||||
|
||||
func (m *mockCustomers) List(listParams *stripe.CustomerListParams) *customer.Iter {
|
||||
m.root.mu.Lock()
|
||||
defer m.root.mu.Unlock()
|
||||
|
||||
return &customer.Iter{
|
||||
Iter: stripe.GetIter(listParams, func(p *stripe.Params, vals *form.Values) ([]interface{}, stripe.ListContainer, error) {
|
||||
var customers []interface{}
|
||||
for _, cus := range m.state.customers {
|
||||
customers = append(customers, cus)
|
||||
}
|
||||
return customers, newListContainer(&stripe.ListMeta{}), nil
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
type mockCustomersState struct {
|
||||
customers []*stripe.Customer
|
||||
repopulated bool
|
||||
@ -332,7 +348,9 @@ func (m *mockCustomers) Update(id string, params *stripe.CustomerParams) (*strip
|
||||
}
|
||||
customer.Discount = &stripe.Discount{Coupon: &stripe.Coupon{ID: c.ID}}
|
||||
}
|
||||
|
||||
if params.Balance != nil {
|
||||
customer.Balance = *params.Balance
|
||||
}
|
||||
// TODO update customer with more params as necessary
|
||||
|
||||
return customer, nil
|
||||
@ -661,6 +679,9 @@ func (m *mockInvoiceItems) New(params *stripe.InvoiceItemParams) (*stripe.Invoic
|
||||
if params.UnitAmountDecimal != nil {
|
||||
item.UnitAmountDecimal = *params.UnitAmountDecimal
|
||||
}
|
||||
if params.UnitAmount != nil {
|
||||
item.UnitAmount = *params.UnitAmount
|
||||
}
|
||||
if params.Amount != nil {
|
||||
item.Amount = *params.Amount
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user