satellite/payments/stripecoinpayments: credits added to invoice calculations

Change-Id: I6d3f5244a46f8945d2703af39ced333940db34e9
This commit is contained in:
Qweder93 2020-02-11 16:48:28 +02:00 committed by Nikolai Siedov
parent 985c3ef897
commit dca6fcbe28
7 changed files with 92 additions and 29 deletions

View File

@ -33,7 +33,7 @@ type CreditsDB interface {
// ListCreditsSpendingsPaged returns all spending of specific user.
ListCreditsSpendingsPaged(ctx context.Context, status int, offset int64, limit int, before time.Time) (CreditsSpendingsPage, error)
// ApplyCreditsSpending updated spending's status.
ApplyCreditsSpending(ctx context.Context, spendingID uuid.UUID, status int) (err error)
ApplyCreditsSpending(ctx context.Context, spendingID uuid.UUID) (err error)
// Balance returns difference between all credits and creditsSpendings of specific user.
Balance(ctx context.Context, userID uuid.UUID) (int64, error)
@ -52,27 +52,27 @@ type credits struct {
// CreditsSpending is an entity that holds funds been used from Accounts bonus credit balance.
// Status shows if spending have been used to pay for invoice already or not.
type CreditsSpending struct {
ID uuid.UUID `json:"id"`
ProjectID uuid.UUID `json:"projectId"`
UserID uuid.UUID `json:"userId"`
Amount int64 `json:"amount"`
Status int `json:"status"`
Created time.Time `json:"created"`
ID uuid.UUID `json:"id"`
ProjectID uuid.UUID `json:"projectId"`
UserID uuid.UUID `json:"userId"`
Amount int64 `json:"amount"`
Status CreditsSpendingStatus `json:"status"`
Created time.Time `json:"created"`
}
// CreditsSpendingsPage holds set of spendings and indicates if
// there are more spendings to fetch.
// CreditsSpendingsPage holds set of creditsSpendings and indicates if
// there are more creditsSpendings to fetch.
type CreditsSpendingsPage struct {
Spendings []CreditsSpending
Next bool
NextOffset int64
}
// CreditsSpendingStatus indicates the state of the spending.
// CreditsSpendingStatus indicates the state of the creditsSpending.
type CreditsSpendingStatus int
const (
// CreditsSpendingStatusUnapplied is a default spending state.
// CreditsSpendingStatusUnapplied is a default creditsSpending state.
CreditsSpendingStatusUnapplied CreditsSpendingStatus = 0
// CreditsSpendingStatusApplied status indicates that spending was applied.
CreditsSpendingStatusApplied CreditsSpendingStatus = 1

View File

@ -35,7 +35,7 @@ func TestCreditsRepository(t *testing.T) {
ProjectID: testrand.UUID(),
UserID: userID,
Amount: 5,
Status: int(stripecoinpayments.CreditsSpendingStatusUnapplied),
Status: stripecoinpayments.CreditsSpendingStatusUnapplied,
}
t.Run("insert", func(t *testing.T) {
@ -53,6 +53,7 @@ func TestCreditsRepository(t *testing.T) {
spendings, err := creditsRepo.ListCreditsSpendings(ctx, userID)
assert.NoError(t, err)
assert.Equal(t, 1, len(spendings))
spending.ID = spendings[0].ID
})
t.Run("get credit by transactionID", func(t *testing.T) {
@ -62,12 +63,12 @@ func TestCreditsRepository(t *testing.T) {
})
t.Run("update spending", func(t *testing.T) {
err := creditsRepo.ApplyCreditsSpending(ctx, spending.ID, int(stripecoinpayments.CreditsSpendingStatusApplied))
err := creditsRepo.ApplyCreditsSpending(ctx, spending.ID)
assert.NoError(t, err)
spendings, err := creditsRepo.ListCreditsSpendings(ctx, userID)
require.NoError(t, err)
require.Equal(t, int(stripecoinpayments.CreditsSpendingStatusApplied), spendings[0].Status)
require.Equal(t, stripecoinpayments.CreditsSpendingStatusApplied, spendings[0].Status)
spending = spendings[0]
})

View File

@ -17,8 +17,8 @@ var ErrProjectRecordExists = Error.New("invoice project record already exists")
//
// architecture: Database
type ProjectRecordsDB interface {
// Create creates new invoice project record with coupon usages in the DB.
Create(ctx context.Context, records []CreateProjectRecord, couponUsages []CouponUsage, start, end time.Time) error
// Create creates new invoice project record with coupon usages and credits spendings in the DB.
Create(ctx context.Context, records []CreateProjectRecord, couponUsages []CouponUsage, creditsSpendings []CreditsSpending, start, end time.Time) error
// Check checks if invoice project record for specified project and billing period exists.
Check(ctx context.Context, projectID uuid.UUID, start, end time.Time) error
// Get returns record for specified project and billing period.

View File

@ -40,6 +40,7 @@ func TestProjectRecords(t *testing.T) {
},
},
[]stripecoinpayments.CouponUsage{},
[]stripecoinpayments.CreditsSpending{},
start, end,
)
require.NoError(t, err)
@ -93,7 +94,7 @@ func TestProjectRecordsList(t *testing.T) {
)
}
err := projectRecordsDB.Create(ctx, createProjectRecords, []stripecoinpayments.CouponUsage{}, start, end)
err := projectRecordsDB.Create(ctx, createProjectRecords, []stripecoinpayments.CouponUsage{}, []stripecoinpayments.CreditsSpending{}, start, end)
require.NoError(t, err)
page, err := projectRecordsDB.ListUnapplied(ctx, 0, limit, time.Now())

View File

@ -11,6 +11,7 @@ import (
"time"
"github.com/shopspring/decimal"
"github.com/skyrings/skyring-common/tools/uuid"
"github.com/spacemonkeygo/monkit/v3"
"github.com/stripe/stripe-go"
"github.com/stripe/stripe-go/client"
@ -403,6 +404,7 @@ func (service *Service) createProjectRecords(ctx context.Context, projects []con
var records []CreateProjectRecord
var usages []CouponUsage
var creditsSpendings []CreditsSpending
for _, project := range projects {
if err = ctx.Err(); err != nil {
return err
@ -438,15 +440,18 @@ func (service *Service) createProjectRecords(ctx context.Context, projects []con
currentUsagePrice := service.calculateProjectUsagePrice(usage.Egress, usage.Storage, usage.ObjectCount).TotalInt64()
amountToChargeFromCoupon := currentUsagePrice
amountToChargeFromCoupon := int64(0)
// TODO: only for 1 coupon per project
for _, coupon := range coupons {
amountToChargeFromCoupon = currentUsagePrice
if coupon.IsExpired() {
if err = service.db.Coupons().Update(ctx, coupon.ID, payments.CouponExpired); err != nil {
return err
}
amountToChargeFromCoupon = 0
continue
}
@ -467,9 +472,39 @@ func (service *Service) createProjectRecords(ctx context.Context, projects []con
CouponID: coupon.ID,
})
}
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{
ID: *creditSpendingID,
Amount: amountChargedFromBonuses,
UserID: project.OwnerID,
ProjectID: project.ID,
Status: CreditsSpendingStatusUnapplied,
})
}
}
return service.db.ProjectRecords().Create(ctx, records, usages, start, end)
return service.db.ProjectRecords().Create(ctx, records, usages, creditsSpendings, start, end)
}
// InvoiceApplyProjectRecords iterates through unapplied invoice project records and creates invoice line items
@ -631,7 +666,7 @@ func (service *Service) applyCoupons(ctx context.Context, usages []CouponUsage)
return nil
}
// createInvoiceItems consumes invoice project record and creates invoice line items for stripe customer.
// createInvoiceCouponItems consumes invoice project record and creates invoice line items for stripe customer.
func (service *Service) createInvoiceCouponItems(ctx context.Context, coupon payments.Coupon, usage CouponUsage, customerID string) (err error) {
defer mon.Task()(&ctx, customerID, coupon)(&err)
@ -722,11 +757,11 @@ func (service *Service) applySpendings(ctx context.Context, spendings []CreditsS
return nil
}
// createInvoiceItems consumes invoice project record and creates invoice line items for stripe customer.
// createInvoiceCreditItem consumes invoice project record and creates invoice line items for stripe customer.
func (service *Service) createInvoiceCreditItem(ctx context.Context, spending CreditsSpending) (err error) {
defer mon.Task()(&ctx, spending)(&err)
err = service.db.Credits().ApplyCreditsSpending(ctx, spending.ID, int(CreditsSpendingStatusApplied))
err = service.db.Credits().ApplyCreditsSpending(ctx, spending.ID)
if err != nil {
return err
}

View File

@ -14,7 +14,7 @@ import (
"storj.io/storj/satellite/payments"
"storj.io/storj/satellite/payments/coinpayments"
"storj.io/storj/satellite/payments/stripecoinpayments"
dbx "storj.io/storj/satellite/satellitedb/dbx"
"storj.io/storj/satellite/satellitedb/dbx"
)
// ensures that credit implements payments.CreditsDB.
@ -104,13 +104,18 @@ func (credits *credit) ListCreditsPaged(ctx context.Context, offset int64, limit
func (credits *credit) InsertCreditsSpending(ctx context.Context, spending stripecoinpayments.CreditsSpending) (err error) {
defer mon.Task()(&ctx, spending)(&err)
id, err := uuid.New()
if err != nil {
return err
}
_, err = credits.db.Create_CreditsSpending(
ctx,
dbx.CreditsSpending_Id(spending.ID[:]),
dbx.CreditsSpending_Id(id[:]),
dbx.CreditsSpending_UserId(spending.UserID[:]),
dbx.CreditsSpending_ProjectId(spending.ProjectID[:]),
dbx.CreditsSpending_Amount(spending.Amount),
dbx.CreditsSpending_Status(spending.Status),
dbx.CreditsSpending_Status(int(spending.Status)),
)
return err
@ -132,13 +137,13 @@ func (credits *credit) ListCreditsSpendings(ctx context.Context, userID uuid.UUI
}
// ApplyCreditsSpending applies spending and updates its status.
func (credits *credit) ApplyCreditsSpending(ctx context.Context, spendingID uuid.UUID, status int) (err error) {
func (credits *credit) ApplyCreditsSpending(ctx context.Context, spendingID uuid.UUID) (err error) {
defer mon.Task()(&ctx)(&err)
_, err = credits.db.Update_CreditsSpending_By_Id(
ctx,
dbx.CreditsSpending_Id(spendingID[:]),
dbx.CreditsSpending_Update_Fields{Status: dbx.CreditsSpending_Status(status)},
dbx.CreditsSpending_Update_Fields{Status: dbx.CreditsSpending_Status(int(stripecoinpayments.CreditsSpendingStatusApplied))},
)
return err
@ -248,9 +253,15 @@ func fromDBXSpending(dbxSpending *dbx.CreditsSpending) (spending stripecoinpayme
return stripecoinpayments.CreditsSpending{}, err
}
spending.Status = dbxSpending.Status
spending.Status = stripecoinpayments.CreditsSpendingStatus(dbxSpending.Status)
spending.Created = dbxSpending.CreatedAt
spending.Amount = dbxSpending.Amount
spendingID, err := dbutil.BytesToUUID(dbxSpending.Id)
if err != nil {
return stripecoinpayments.CreditsSpending{}, err
}
spending.ID = spendingID
return spending, nil
}

View File

@ -42,7 +42,7 @@ type invoiceProjectRecords struct {
}
// Create creates new invoice project record in the DB.
func (db *invoiceProjectRecords) Create(ctx context.Context, records []stripecoinpayments.CreateProjectRecord, couponUsages []stripecoinpayments.CouponUsage, start, end time.Time) (err error) {
func (db *invoiceProjectRecords) Create(ctx context.Context, records []stripecoinpayments.CreateProjectRecord, couponUsages []stripecoinpayments.CouponUsage, creditsSpendings []stripecoinpayments.CreditsSpending, start, end time.Time) (err error) {
defer mon.Task()(&ctx)(&err)
return db.db.WithTx(ctx, func(ctx context.Context, tx *dbx.Tx) error {
@ -79,6 +79,21 @@ func (db *invoiceProjectRecords) Create(ctx context.Context, records []stripecoi
return err
}
}
for _, creditsSpending := range creditsSpendings {
_, err = db.db.Create_CreditsSpending(
ctx,
dbx.CreditsSpending_Id(creditsSpending.ID[:]),
dbx.CreditsSpending_UserId(creditsSpending.UserID[:]),
dbx.CreditsSpending_ProjectId(creditsSpending.ProjectID[:]),
dbx.CreditsSpending_Amount(creditsSpending.Amount),
dbx.CreditsSpending_Status(int(creditsSpending.Status)),
)
if err != nil {
return err
}
}
return nil
})
}