a57ce18f58
Change-Id: Ic5d5a2fc116388647efe46896cfccc2038c77537
358 lines
9.7 KiB
Go
358 lines
9.7 KiB
Go
// Copyright (C) 2019 Storj Labs, Inc.
|
|
// See LICENSE for copying information.
|
|
|
|
package satellitedb
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"time"
|
|
|
|
"github.com/skyrings/skyring-common/tools/uuid"
|
|
"github.com/zeebo/errs"
|
|
|
|
"storj.io/storj/private/dbutil"
|
|
"storj.io/storj/satellite/payments"
|
|
"storj.io/storj/satellite/payments/coinpayments"
|
|
"storj.io/storj/satellite/payments/stripecoinpayments"
|
|
dbx "storj.io/storj/satellite/satellitedb/dbx"
|
|
)
|
|
|
|
// ensures that coupons implements payments.CouponsDB.
|
|
var _ stripecoinpayments.CouponsDB = (*coupons)(nil)
|
|
|
|
// coupons is an implementation of payments.CouponsDB.
|
|
//
|
|
// architecture: Database
|
|
type coupons struct {
|
|
db *satelliteDB
|
|
}
|
|
|
|
// Insert inserts a coupon into the database.
|
|
func (coupons *coupons) Insert(ctx context.Context, coupon payments.Coupon) (err error) {
|
|
defer mon.Task()(&ctx, coupon)(&err)
|
|
|
|
id, err := uuid.New()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = coupons.db.Create_Coupon(
|
|
ctx,
|
|
dbx.Coupon_Id(id[:]),
|
|
dbx.Coupon_ProjectId(coupon.ProjectID[:]),
|
|
dbx.Coupon_UserId(coupon.UserID[:]),
|
|
dbx.Coupon_Amount(coupon.Amount),
|
|
dbx.Coupon_Description(coupon.Description),
|
|
dbx.Coupon_Type(int(coupon.Type)),
|
|
dbx.Coupon_Status(int(coupon.Status)),
|
|
dbx.Coupon_Duration(int64(coupon.Duration)),
|
|
)
|
|
|
|
return err
|
|
}
|
|
|
|
// Update updates coupon in database.
|
|
func (coupons *coupons) Update(ctx context.Context, couponID uuid.UUID, status payments.CouponStatus) (err error) {
|
|
defer mon.Task()(&ctx, couponID)(&err)
|
|
|
|
_, err = coupons.db.Update_Coupon_By_Id(
|
|
ctx,
|
|
dbx.Coupon_Id(couponID[:]),
|
|
dbx.Coupon_Update_Fields{
|
|
Status: dbx.Coupon_Status(int(status)),
|
|
},
|
|
)
|
|
|
|
return err
|
|
}
|
|
|
|
// Get returns coupon by ID.
|
|
func (coupons *coupons) Get(ctx context.Context, couponID uuid.UUID) (_ payments.Coupon, err error) {
|
|
defer mon.Task()(&ctx, couponID)(&err)
|
|
|
|
dbxCoupon, err := coupons.db.Get_Coupon_By_Id(ctx, dbx.Coupon_Id(couponID[:]))
|
|
if err != nil {
|
|
return payments.Coupon{}, err
|
|
}
|
|
|
|
return fromDBXCoupon(dbxCoupon)
|
|
}
|
|
|
|
// List returns all coupons of specified user.
|
|
func (coupons *coupons) ListByUserID(ctx context.Context, userID uuid.UUID) (_ []payments.Coupon, err error) {
|
|
defer mon.Task()(&ctx, userID)(&err)
|
|
|
|
dbxCoupons, err := coupons.db.All_Coupon_By_UserId_OrderBy_Desc_CreatedAt(
|
|
ctx,
|
|
dbx.Coupon_UserId(userID[:]),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return couponsFromDbxSlice(dbxCoupons)
|
|
}
|
|
|
|
// ListByUserIDAndStatus returns all coupons of specified user and status.
|
|
func (coupons *coupons) ListByUserIDAndStatus(ctx context.Context, userID uuid.UUID, status payments.CouponStatus) (_ []payments.Coupon, err error) {
|
|
defer mon.Task()(&ctx, userID)(&err)
|
|
|
|
dbxCoupons, err := coupons.db.All_Coupon_By_UserId_And_Status_OrderBy_Desc_CreatedAt(
|
|
ctx,
|
|
dbx.Coupon_UserId(userID[:]),
|
|
dbx.Coupon_Status(int(status)),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return couponsFromDbxSlice(dbxCoupons)
|
|
}
|
|
|
|
// List returns all coupons with specified status.
|
|
func (coupons *coupons) List(ctx context.Context, status payments.CouponStatus) (_ []payments.Coupon, err error) {
|
|
defer mon.Task()(&ctx, status)(&err)
|
|
|
|
dbxCoupons, err := coupons.db.All_Coupon_By_Status_OrderBy_Desc_CreatedAt(
|
|
ctx,
|
|
dbx.Coupon_Status(int(status)),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return couponsFromDbxSlice(dbxCoupons)
|
|
}
|
|
|
|
// ListPending returns paginated list of pending transactions.
|
|
func (coupons *coupons) ListPaged(ctx context.Context, offset int64, limit int, before time.Time, status payments.CouponStatus) (_ payments.CouponsPage, err error) {
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
var page payments.CouponsPage
|
|
|
|
dbxCoupons, err := coupons.db.Limited_Coupon_By_CreatedAt_LessOrEqual_And_Status_OrderBy_Desc_CreatedAt(
|
|
ctx,
|
|
dbx.Coupon_CreatedAt(before.UTC()),
|
|
dbx.Coupon_Status(coinpayments.StatusPending.Int()),
|
|
limit+1,
|
|
offset,
|
|
)
|
|
if err != nil {
|
|
return payments.CouponsPage{}, err
|
|
}
|
|
|
|
if len(dbxCoupons) == limit+1 {
|
|
page.Next = true
|
|
page.NextOffset = offset + int64(limit) + 1
|
|
|
|
dbxCoupons = dbxCoupons[:len(dbxCoupons)-1]
|
|
}
|
|
|
|
page.Coupons, err = couponsFromDbxSlice(dbxCoupons)
|
|
if err != nil {
|
|
return payments.CouponsPage{}, nil
|
|
}
|
|
|
|
return page, nil
|
|
}
|
|
|
|
// ListByProjectID returns all active coupons for specified project.
|
|
func (coupons *coupons) ListByProjectID(ctx context.Context, projectID uuid.UUID) (_ []payments.Coupon, err error) {
|
|
defer mon.Task()(&ctx, projectID)(&err)
|
|
|
|
dbxCoupons, err := coupons.db.All_Coupon_By_ProjectId_And_Status_Equal_Number_OrderBy_Desc_CreatedAt(
|
|
ctx,
|
|
dbx.Coupon_ProjectId(projectID[:]),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return couponsFromDbxSlice(dbxCoupons)
|
|
}
|
|
|
|
// fromDBXCoupon converts *dbx.Coupon to *payments.Coupon.
|
|
func fromDBXCoupon(dbxCoupon *dbx.Coupon) (coupon payments.Coupon, err error) {
|
|
coupon.UserID, err = dbutil.BytesToUUID(dbxCoupon.UserId)
|
|
if err != nil {
|
|
return payments.Coupon{}, err
|
|
}
|
|
|
|
coupon.ProjectID, err = dbutil.BytesToUUID(dbxCoupon.ProjectId)
|
|
if err != nil {
|
|
return payments.Coupon{}, err
|
|
}
|
|
|
|
coupon.ID, err = dbutil.BytesToUUID(dbxCoupon.Id)
|
|
if err != nil {
|
|
return payments.Coupon{}, err
|
|
}
|
|
|
|
coupon.Duration = int(dbxCoupon.Duration)
|
|
coupon.Description = dbxCoupon.Description
|
|
coupon.Amount = dbxCoupon.Amount
|
|
coupon.Created = dbxCoupon.CreatedAt
|
|
coupon.Status = payments.CouponStatus(dbxCoupon.Status)
|
|
|
|
return coupon, nil
|
|
}
|
|
|
|
// AddUsage creates new coupon usage record in database.
|
|
func (coupons *coupons) AddUsage(ctx context.Context, usage stripecoinpayments.CouponUsage) (err error) {
|
|
defer mon.Task()(&ctx, usage)(&err)
|
|
|
|
_, err = coupons.db.Create_CouponUsage(
|
|
ctx,
|
|
dbx.CouponUsage_CouponId(usage.CouponID[:]),
|
|
dbx.CouponUsage_Amount(usage.Amount),
|
|
dbx.CouponUsage_Status(int(usage.Status)),
|
|
dbx.CouponUsage_Period(usage.Period),
|
|
)
|
|
|
|
return err
|
|
}
|
|
|
|
// TotalUsage gets sum of all usage records for specified coupon.
|
|
func (coupons *coupons) TotalUsage(ctx context.Context, couponID uuid.UUID) (_ int64, err error) {
|
|
defer mon.Task()(&ctx, couponID)(&err)
|
|
|
|
query := coupons.db.Rebind(
|
|
`SELECT COALESCE(SUM(amount), 0)
|
|
FROM coupon_usages
|
|
WHERE coupon_id = ?;`,
|
|
)
|
|
|
|
amountRow := coupons.db.QueryRowContext(ctx, query, couponID[:])
|
|
|
|
var amount int64
|
|
err = amountRow.Scan(&amount)
|
|
|
|
return amount, err
|
|
}
|
|
|
|
// TotalUsage gets sum of all usage records for specified coupon.
|
|
func (coupons *coupons) TotalUsageForPeriod(ctx context.Context, couponID uuid.UUID, period time.Time) (_ int64, err error) {
|
|
defer mon.Task()(&ctx, couponID)(&err)
|
|
|
|
query := coupons.db.Rebind(
|
|
`SELECT COALESCE(SUM(amount), 0)
|
|
FROM coupon_usages
|
|
WHERE coupon_id = ?;`,
|
|
)
|
|
|
|
amountRow := coupons.db.QueryRowContext(ctx, query, couponID[:])
|
|
|
|
var amount int64
|
|
err = amountRow.Scan(&amount)
|
|
|
|
return amount, err
|
|
}
|
|
|
|
// GetLatest return period_end of latest coupon charge.
|
|
func (coupons *coupons) GetLatest(ctx context.Context, couponID uuid.UUID) (_ time.Time, err error) {
|
|
defer mon.Task()(&ctx, couponID)(&err)
|
|
|
|
query := coupons.db.Rebind(
|
|
`SELECT period
|
|
FROM coupon_usages
|
|
WHERE coupon_id = ?
|
|
ORDER BY period DESC
|
|
LIMIT 1;`,
|
|
)
|
|
|
|
amountRow := coupons.db.QueryRowContext(ctx, query, couponID[:])
|
|
|
|
var created time.Time
|
|
err = amountRow.Scan(&created)
|
|
if err == sql.ErrNoRows {
|
|
return created, stripecoinpayments.ErrNoCouponUsages.Wrap(err)
|
|
}
|
|
|
|
return created, err
|
|
}
|
|
|
|
// ListUnapplied returns coupon usage page with unapplied coupon usages.
|
|
func (coupons *coupons) ListUnapplied(ctx context.Context, offset int64, limit int, before time.Time) (_ stripecoinpayments.CouponUsagePage, err error) {
|
|
defer mon.Task()(&ctx, offset, limit, before)(&err)
|
|
|
|
var page stripecoinpayments.CouponUsagePage
|
|
|
|
dbxRecords, err := coupons.db.Limited_CouponUsage_By_Period_LessOrEqual_And_Status_Equal_Number_OrderBy_Desc_Period(
|
|
ctx,
|
|
dbx.CouponUsage_Period(before),
|
|
limit+1,
|
|
offset,
|
|
)
|
|
if err != nil {
|
|
return stripecoinpayments.CouponUsagePage{}, err
|
|
}
|
|
|
|
if len(dbxRecords) == limit+1 {
|
|
page.Next = true
|
|
page.NextOffset = offset + int64(limit) + 1
|
|
|
|
dbxRecords = dbxRecords[:len(dbxRecords)-1]
|
|
}
|
|
|
|
for _, dbxRecord := range dbxRecords {
|
|
record, err := couponUsageFromDbxSlice(dbxRecord)
|
|
if err != nil {
|
|
return stripecoinpayments.CouponUsagePage{}, err
|
|
}
|
|
|
|
page.Usages = append(page.Usages, record)
|
|
}
|
|
|
|
return page, nil
|
|
}
|
|
|
|
// ApplyUsage applies coupon usage and updates its status.
|
|
func (coupons *coupons) ApplyUsage(ctx context.Context, couponID uuid.UUID, period time.Time) (err error) {
|
|
defer mon.Task()(&ctx, couponID, period)(&err)
|
|
|
|
_, err = coupons.db.Update_CouponUsage_By_CouponId_And_Period(
|
|
ctx,
|
|
dbx.CouponUsage_CouponId(couponID[:]),
|
|
dbx.CouponUsage_Period(period),
|
|
dbx.CouponUsage_Update_Fields{
|
|
Status: dbx.CouponUsage_Status(int(stripecoinpayments.CouponUsageStatusApplied)),
|
|
},
|
|
)
|
|
|
|
return err
|
|
}
|
|
|
|
// couponsFromDbxSlice is used for creating []payments.Coupon entities from autogenerated []dbx.Coupon struct.
|
|
func couponsFromDbxSlice(couponsDbx []*dbx.Coupon) (_ []payments.Coupon, err error) {
|
|
var coupons = make([]payments.Coupon, 0)
|
|
var errors []error
|
|
|
|
// Generating []dbo from []dbx and collecting all errors
|
|
for _, couponDbx := range couponsDbx {
|
|
coupon, err := fromDBXCoupon(couponDbx)
|
|
if err != nil {
|
|
errors = append(errors, err)
|
|
continue
|
|
}
|
|
|
|
coupons = append(coupons, coupon)
|
|
}
|
|
|
|
return coupons, errs.Combine(errors...)
|
|
}
|
|
|
|
// couponUsageFromDbxSlice is used for creating stripecoinpayments.CouponUsage entity from autogenerated dbx.CouponUsage struct.
|
|
func couponUsageFromDbxSlice(couponUsageDbx *dbx.CouponUsage) (usage stripecoinpayments.CouponUsage, err error) {
|
|
usage.Status = stripecoinpayments.CouponUsageStatus(couponUsageDbx.Status)
|
|
usage.Period = couponUsageDbx.Period
|
|
usage.Amount = couponUsageDbx.Amount
|
|
|
|
usage.CouponID, err = dbutil.BytesToUUID(couponUsageDbx.CouponId)
|
|
if err != nil {
|
|
return stripecoinpayments.CouponUsage{}, err
|
|
}
|
|
|
|
return usage, err
|
|
}
|