satellite/payments: promotional coupons generation functional added
Change-Id: Ie0df256503114ca377d81bf7c8b26cc90a1f5b26
This commit is contained in:
parent
a4026f97b8
commit
c4cbc6ff2f
@ -129,6 +129,7 @@ func NewServer(logger *zap.Logger, config Config, service *console.Service, mail
|
||||
|
||||
router.HandleFunc("/api/v0/graphql", server.grapqlHandler)
|
||||
router.HandleFunc("/registrationToken/", server.createRegistrationTokenHandler)
|
||||
router.HandleFunc("/populate-promotional-coupons", server.populatePromotionalCoupons).Methods(http.MethodPost)
|
||||
router.HandleFunc("/robots.txt", server.seoHandler)
|
||||
|
||||
router.Handle(
|
||||
@ -327,7 +328,7 @@ func (server *Server) bucketUsageReportHandler(w http.ResponseWriter, r *http.Re
|
||||
}
|
||||
}
|
||||
|
||||
// accountActivationHandler is web app http handler function
|
||||
// createRegistrationTokenHandler is web app http handler function.
|
||||
func (server *Server) createRegistrationTokenHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
defer mon.Task()(&ctx)(nil)
|
||||
@ -372,6 +373,40 @@ func (server *Server) createRegistrationTokenHandler(w http.ResponseWriter, r *h
|
||||
response.Secret = token.Secret.String()
|
||||
}
|
||||
|
||||
// populatePromotionalCoupons is web app http handler function for populating promotional coupons.
|
||||
func (server *Server) populatePromotionalCoupons(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
defer mon.Task()(&ctx)(nil)
|
||||
w.Header().Set(contentType, applicationJSON)
|
||||
|
||||
var response struct {
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err := json.NewEncoder(w).Encode(&response)
|
||||
if err != nil {
|
||||
server.log.Error("failed to write json error response", zap.Error(Error.Wrap(err)))
|
||||
}
|
||||
}()
|
||||
|
||||
equality := subtle.ConstantTimeCompare(
|
||||
[]byte(r.Header.Get("Authorization")),
|
||||
[]byte(server.config.AuthToken),
|
||||
)
|
||||
if equality != 1 {
|
||||
w.WriteHeader(401)
|
||||
response.Error = "unauthorized"
|
||||
return
|
||||
}
|
||||
|
||||
err := server.service.Payments().PopulatePromotionalCoupons(ctx)
|
||||
if err != nil {
|
||||
response.Error = err.Error()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// accountActivationHandler is web app http handler function
|
||||
func (server *Server) accountActivationHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
@ -17,6 +17,7 @@ import (
|
||||
"gopkg.in/spacemonkeygo/monkit.v2"
|
||||
|
||||
"storj.io/common/macaroon"
|
||||
"storj.io/common/memory"
|
||||
"storj.io/storj/pkg/auth"
|
||||
"storj.io/storj/satellite/accounting"
|
||||
"storj.io/storj/satellite/console/consoleauth"
|
||||
@ -203,7 +204,7 @@ func (payments PaymentsService) RemoveCreditCard(ctx context.Context, cardID str
|
||||
return payments.service.accounts.CreditCards().Remove(ctx, auth.User.ID, cardID)
|
||||
}
|
||||
|
||||
// BillingHistory returns a list of invoices, transactions and all others billing history items for payment account.
|
||||
// BillingHistory returns a list of billing history items for payment account.
|
||||
func (payments PaymentsService) BillingHistory(ctx context.Context) (billingHistory []*BillingHistoryItem, err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
@ -217,7 +218,6 @@ func (payments PaymentsService) BillingHistory(ctx context.Context) (billingHist
|
||||
return nil, Error.Wrap(err)
|
||||
}
|
||||
|
||||
// TODO: add transactions, etc in future
|
||||
for _, invoice := range invoices {
|
||||
billingHistory = append(billingHistory, &BillingHistoryItem{
|
||||
ID: invoice.ID,
|
||||
@ -275,9 +275,8 @@ func (payments PaymentsService) BillingHistory(ctx context.Context) (billingHist
|
||||
for _, coupon := range coupons {
|
||||
billingHistory = append(billingHistory,
|
||||
&BillingHistoryItem{
|
||||
ID: coupon.ID.String(),
|
||||
// TODO: update description in future, when there will be more coupon types.
|
||||
Description: fmt.Sprintf("Promotional credits (limited time - %d billing periods)", coupon.Duration),
|
||||
ID: coupon.ID.String(),
|
||||
Description: coupon.Description,
|
||||
Amount: coupon.Amount,
|
||||
Status: "Added to balance",
|
||||
Link: "",
|
||||
@ -309,6 +308,15 @@ func (payments PaymentsService) TokenDeposit(ctx context.Context, amount int64)
|
||||
return tx, errs.Wrap(err)
|
||||
}
|
||||
|
||||
// PopulatePromotionalCoupons is used to populate promotional coupons through all active users who already have
|
||||
// a project, payment method and do not have a promotional coupon yet.
|
||||
// And updates project limits to selected size.
|
||||
func (payments PaymentsService) PopulatePromotionalCoupons(ctx context.Context) (err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
return Error.Wrap(payments.service.accounts.PopulatePromotionalCoupons(ctx, 2, 5500, memory.TB))
|
||||
}
|
||||
|
||||
// CreateUser gets password hash value and creates new inactive User
|
||||
func (s *Service) CreateUser(ctx context.Context, user CreateUser, tokenSecret RegistrationSecret, refUserID string) (u *User, err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
@ -381,6 +389,7 @@ func (s *Service) CreateUser(ctx context.Context, user CreateUser, tokenSecret R
|
||||
FullName: user.FullName,
|
||||
ShortName: user.ShortName,
|
||||
PasswordHash: hash,
|
||||
Status: Inactive,
|
||||
}
|
||||
if user.PartnerID != "" {
|
||||
partnerID, err := uuid.Parse(user.PartnerID)
|
||||
|
@ -8,6 +8,8 @@ import (
|
||||
|
||||
"github.com/skyrings/skyring-common/tools/uuid"
|
||||
"github.com/zeebo/errs"
|
||||
|
||||
"storj.io/common/memory"
|
||||
)
|
||||
|
||||
// ErrAccountNotSetup is an error type which indicates that payment account is not created.
|
||||
@ -31,6 +33,11 @@ type Accounts interface {
|
||||
// Coupons return list of all coupons of specified payment account.
|
||||
Coupons(ctx context.Context, userID uuid.UUID) ([]Coupon, error)
|
||||
|
||||
// PopulatePromotionalCoupons is used to populate promotional coupons through all active users who already have
|
||||
// a project, payment method and do not have a promotional coupon yet.
|
||||
// And updates project limits to selected size.
|
||||
PopulatePromotionalCoupons(ctx context.Context, duration int, amount int64, projectLimit memory.Size) error
|
||||
|
||||
// CreditCards exposes all needed functionality to manage account credit cards.
|
||||
CreditCards() CreditCards
|
||||
|
||||
|
@ -8,8 +8,9 @@ import (
|
||||
|
||||
"github.com/skyrings/skyring-common/tools/uuid"
|
||||
"github.com/zeebo/errs"
|
||||
monkit "gopkg.in/spacemonkeygo/monkit.v2"
|
||||
"gopkg.in/spacemonkeygo/monkit.v2"
|
||||
|
||||
"storj.io/common/memory"
|
||||
"storj.io/storj/satellite/payments"
|
||||
)
|
||||
|
||||
@ -95,6 +96,15 @@ func (accounts *accounts) Coupons(ctx context.Context, userID uuid.UUID) (coupon
|
||||
return coupons, nil
|
||||
}
|
||||
|
||||
// PopulatePromotionalCoupons is used to populate promotional coupons through all active users who already have
|
||||
// a project, payment method and do not have a promotional coupon yet.
|
||||
// And updates project limits to selected size.
|
||||
func (accounts *accounts) PopulatePromotionalCoupons(ctx context.Context, duration int, amount int64, projectLimit memory.Size) (err error) {
|
||||
defer mon.Task()(&ctx, duration, amount, projectLimit)(&err)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// List returns a list of credit cards for a given payment account.
|
||||
func (creditCards *creditCards) List(ctx context.Context, userID uuid.UUID) (_ []payments.CreditCard, err error) {
|
||||
defer mon.Task()(&ctx, userID)(&err)
|
||||
|
@ -195,6 +195,77 @@ func (accounts *accounts) Coupons(ctx context.Context, userID uuid.UUID) (coupon
|
||||
return coupons, Error.Wrap(err)
|
||||
}
|
||||
|
||||
// PopulatePromotionalCoupons is used to populate promotional coupons through all active users who already have
|
||||
// a project, payment method and do not have a promotional coupon yet.
|
||||
// And updates project limits to selected size.
|
||||
func (accounts *accounts) PopulatePromotionalCoupons(ctx context.Context, duration int, amount int64, projectLimit memory.Size) (err error) {
|
||||
defer mon.Task()(&ctx, duration, amount, projectLimit)(&err)
|
||||
|
||||
const limit = 50
|
||||
before := time.Now()
|
||||
|
||||
cusPage, err := accounts.service.db.Customers().List(ctx, 0, limit, before)
|
||||
if err != nil {
|
||||
return Error.Wrap(err)
|
||||
}
|
||||
|
||||
// taking only users that attached a payment method.
|
||||
var usersIDs []uuid.UUID
|
||||
for _, cus := range cusPage.Customers {
|
||||
params := &stripe.PaymentMethodListParams{
|
||||
Type: stripe.String(string(stripe.PaymentMethodTypeCard)),
|
||||
Customer: stripe.String(cus.ID),
|
||||
}
|
||||
|
||||
paymentMethodsIterator := accounts.service.stripeClient.PaymentMethods.List(params)
|
||||
for paymentMethodsIterator.Next() {
|
||||
// if user has at least 1 payment method - break a loop.
|
||||
usersIDs = append(usersIDs, cus.UserID)
|
||||
break
|
||||
}
|
||||
|
||||
if err = paymentMethodsIterator.Err(); err != nil {
|
||||
return Error.Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
err = accounts.service.db.Coupons().PopulatePromotionalCoupons(ctx, usersIDs, duration, amount, projectLimit)
|
||||
if err != nil {
|
||||
return Error.Wrap(err)
|
||||
}
|
||||
|
||||
for cusPage.Next {
|
||||
// we have to wait before each iteration because
|
||||
// Stripe has rate limits - 100 read and 100 write operations per second per secret key.
|
||||
time.Sleep(time.Second)
|
||||
|
||||
var usersIDs []uuid.UUID
|
||||
for _, cus := range cusPage.Customers {
|
||||
params := &stripe.PaymentMethodListParams{
|
||||
Type: stripe.String(string(stripe.PaymentMethodTypeCard)),
|
||||
Customer: stripe.String(cus.ID),
|
||||
}
|
||||
|
||||
paymentMethodsIterator := accounts.service.stripeClient.PaymentMethods.List(params)
|
||||
for paymentMethodsIterator.Next() {
|
||||
usersIDs = append(usersIDs, cus.UserID)
|
||||
break
|
||||
}
|
||||
|
||||
if err = paymentMethodsIterator.Err(); err != nil {
|
||||
return Error.Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
err = accounts.service.db.Coupons().PopulatePromotionalCoupons(ctx, usersIDs, duration, amount, projectLimit)
|
||||
if err != nil {
|
||||
return Error.Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StorjTokens exposes all storj token related functionality.
|
||||
func (accounts *accounts) StorjTokens() payments.StorjTokens {
|
||||
return &storjTokens{service: accounts.service}
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/skyrings/skyring-common/tools/uuid"
|
||||
|
||||
"storj.io/common/memory"
|
||||
"storj.io/storj/satellite/payments"
|
||||
)
|
||||
|
||||
@ -43,6 +44,10 @@ type CouponsDB interface {
|
||||
ListUnapplied(ctx context.Context, offset int64, limit int, before time.Time) (CouponUsagePage, error)
|
||||
// ApplyUsage applies coupon usage and updates its status.
|
||||
ApplyUsage(ctx context.Context, couponID uuid.UUID, period time.Time) error
|
||||
|
||||
// PopulatePromotionalCoupons is used to populate promotional coupons through all active users who already have a project
|
||||
// and do not have a promotional coupon yet. And updates project limits to selected size.
|
||||
PopulatePromotionalCoupons(ctx context.Context, users []uuid.UUID, duration int, amount int64, projectLimit memory.Size) error
|
||||
}
|
||||
|
||||
// CouponUsage stores amount of money that should be charged from coupon for billing period.
|
||||
|
@ -7,11 +7,15 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/skyrings/skyring-common/tools/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"storj.io/common/memory"
|
||||
"storj.io/common/testcontext"
|
||||
"storj.io/common/testrand"
|
||||
"storj.io/storj/satellite"
|
||||
"storj.io/storj/satellite/console"
|
||||
"storj.io/storj/satellite/payments"
|
||||
"storj.io/storj/satellite/payments/stripecoinpayments"
|
||||
"storj.io/storj/satellite/satellitedb/satellitedbtest"
|
||||
@ -86,3 +90,282 @@ func TestCouponRepository(t *testing.T) {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// TestPopulatePromotionalCoupons is a test for PopulatePromotionalCoupons function
|
||||
// that creates coupons with predefined values for each of user (from arguments) that have a project
|
||||
// and that don't have a promotional coupon yet. Also it updates limits of selected projects to 1TB.
|
||||
// Because the coupon should be added to a project, we select the first project of the user.
|
||||
// In this test i have next test cases:
|
||||
// 1. Activated user, 2 projects, without coupon. For this case we should add new coupon to his first project.
|
||||
// 2. Activated user, 1 project, without coupon.
|
||||
// 3. Activated user without project. Coupon should not be added.
|
||||
// 4. User with inactive account. Coupon should not be added.
|
||||
// 5. Activated user with project and coupon. Coupon should not be added.
|
||||
// 6. Next step - is populating coupons for all 5 users. Only 2 coupons should be added.
|
||||
// 7. Creating new user with project.
|
||||
// 8. Populating coupons again. For 6 users above. Only 1 new coupon should be added.
|
||||
// Three new coupons total should be added by 2 runs of PopulatePromotionalCoupons method.
|
||||
func TestPopulatePromotionalCoupons(t *testing.T) {
|
||||
satellitedbtest.Run(t, func(t *testing.T, db satellite.DB) {
|
||||
ctx := testcontext.New(t)
|
||||
defer ctx.Cleanup()
|
||||
|
||||
usersRepo := db.Console().Users()
|
||||
projectsRepo := db.Console().Projects()
|
||||
couponsRepo := db.StripeCoinPayments().Coupons()
|
||||
usageRepo := db.ProjectAccounting()
|
||||
|
||||
// creating test users with different status.
|
||||
|
||||
// activated user with 2 project. New coupon should be added to the first project.
|
||||
user1, err := usersRepo.Insert(ctx, &console.User{
|
||||
ID: testrand.UUID(),
|
||||
FullName: "user1",
|
||||
ShortName: "",
|
||||
Email: "test1@example.com",
|
||||
PasswordHash: []byte("123qwe"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
user1.Status = console.Active
|
||||
|
||||
err = usersRepo.Update(ctx, user1)
|
||||
require.NoError(t, err)
|
||||
|
||||
// activated user with proj. New coupon should be added.
|
||||
user2, err := usersRepo.Insert(ctx, &console.User{
|
||||
ID: testrand.UUID(),
|
||||
FullName: "user2",
|
||||
ShortName: "",
|
||||
Email: "test2@example.com",
|
||||
PasswordHash: []byte("123qwe"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
user2.Status = console.Active
|
||||
|
||||
err = usersRepo.Update(ctx, user2)
|
||||
require.NoError(t, err)
|
||||
|
||||
// activated user without proj. New coupon should not be added.
|
||||
user3, err := usersRepo.Insert(ctx, &console.User{
|
||||
ID: testrand.UUID(),
|
||||
FullName: "user3",
|
||||
ShortName: "",
|
||||
Email: "test3@example.com",
|
||||
PasswordHash: []byte("123qwe"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
user3.Status = console.Active
|
||||
|
||||
err = usersRepo.Update(ctx, user3)
|
||||
require.NoError(t, err)
|
||||
|
||||
// inactive user. New coupon should not be added.
|
||||
user4, err := usersRepo.Insert(ctx, &console.User{
|
||||
ID: testrand.UUID(),
|
||||
FullName: "user4",
|
||||
ShortName: "",
|
||||
Email: "test4@example.com",
|
||||
PasswordHash: []byte("123qwe"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// activated user with proj and coupon. New coupon should not be added.
|
||||
user5, err := usersRepo.Insert(ctx, &console.User{
|
||||
ID: testrand.UUID(),
|
||||
FullName: "user5",
|
||||
ShortName: "",
|
||||
Email: "test5@example.com",
|
||||
PasswordHash: []byte("123qwe"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
user5.Status = console.Active
|
||||
|
||||
err = usersRepo.Update(ctx, user5)
|
||||
require.NoError(t, err)
|
||||
|
||||
// creating projects for users above.
|
||||
proj1, err := projectsRepo.Insert(ctx, &console.Project{
|
||||
ID: testrand.UUID(),
|
||||
Name: "proj 1 of user 1",
|
||||
Description: "descr 1",
|
||||
OwnerID: user1.ID,
|
||||
})
|
||||
if err != nil {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// should not be processed as we takes only first project of the user.
|
||||
proj2, err := projectsRepo.Insert(ctx, &console.Project{
|
||||
ID: testrand.UUID(),
|
||||
Name: "proj 2 of user 1",
|
||||
Description: "descr 2",
|
||||
OwnerID: user1.ID,
|
||||
})
|
||||
if err != nil {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
proj3, err := projectsRepo.Insert(ctx, &console.Project{
|
||||
ID: testrand.UUID(),
|
||||
Name: "proj 1 of user 2",
|
||||
Description: "descr 3",
|
||||
OwnerID: user2.ID,
|
||||
})
|
||||
if err != nil {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
proj4, err := projectsRepo.Insert(ctx, &console.Project{
|
||||
ID: testrand.UUID(),
|
||||
Name: "proj 1 of user 5",
|
||||
Description: "descr 4",
|
||||
OwnerID: user5.ID,
|
||||
})
|
||||
if err != nil {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
couponID := testrand.UUID()
|
||||
err = couponsRepo.Insert(ctx, payments.Coupon{
|
||||
ID: couponID,
|
||||
UserID: user5.ID,
|
||||
ProjectID: proj4.ID,
|
||||
Amount: 5500,
|
||||
Duration: 2,
|
||||
Description: "qw",
|
||||
Type: payments.CouponTypePromotional,
|
||||
Status: payments.CouponActive,
|
||||
})
|
||||
if err != nil {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// creating new users and projects to test that multiple execution of populate method wont generate extra coupons.
|
||||
user6, err := usersRepo.Insert(ctx, &console.User{
|
||||
ID: testrand.UUID(),
|
||||
FullName: "user6",
|
||||
ShortName: "",
|
||||
Email: "test6@example.com",
|
||||
PasswordHash: []byte("123qwe"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
user6.Status = console.Active
|
||||
|
||||
err = usersRepo.Update(ctx, user6)
|
||||
require.NoError(t, err)
|
||||
|
||||
proj5, err := projectsRepo.Insert(ctx, &console.Project{
|
||||
ID: testrand.UUID(),
|
||||
Name: "proj 1 of user 6",
|
||||
Description: "descr 6",
|
||||
OwnerID: user6.ID,
|
||||
})
|
||||
if err != nil {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
t.Run("first population", func(t *testing.T) {
|
||||
var usersIds = []uuid.UUID{
|
||||
user1.ID,
|
||||
user2.ID,
|
||||
user3.ID,
|
||||
user4.ID,
|
||||
user5.ID,
|
||||
}
|
||||
err := couponsRepo.PopulatePromotionalCoupons(ctx, usersIds, 2, 5500, memory.TB)
|
||||
assert.NoError(t, err)
|
||||
|
||||
user1Coupons, err := couponsRepo.ListByUserID(ctx, user1.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(user1Coupons))
|
||||
assert.Equal(t, proj1.ID, user1Coupons[0].ProjectID)
|
||||
|
||||
proj1Usage, err := usageRepo.GetProjectStorageLimit(ctx, proj1.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, memory.TB, proj1Usage)
|
||||
|
||||
proj2Usage, err := usageRepo.GetProjectStorageLimit(ctx, proj2.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, int(proj2Usage))
|
||||
|
||||
user2Coupons, err := couponsRepo.ListByUserID(ctx, user2.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(user2Coupons))
|
||||
|
||||
proj3Usage, err := usageRepo.GetProjectStorageLimit(ctx, proj3.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, memory.TB, proj3Usage)
|
||||
|
||||
user3Coupons, err := couponsRepo.ListByUserID(ctx, user3.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, len(user3Coupons))
|
||||
|
||||
user4Coupons, err := couponsRepo.ListByUserID(ctx, user4.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, len(user4Coupons))
|
||||
|
||||
user5Coupons, err := couponsRepo.ListByUserID(ctx, user5.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(user5Coupons))
|
||||
assert.Equal(t, "qw", user5Coupons[0].Description)
|
||||
})
|
||||
|
||||
t.Run("second population", func(t *testing.T) {
|
||||
var usersIds = []uuid.UUID{
|
||||
user1.ID,
|
||||
user2.ID,
|
||||
user3.ID,
|
||||
user4.ID,
|
||||
user5.ID,
|
||||
user6.ID,
|
||||
}
|
||||
err := couponsRepo.PopulatePromotionalCoupons(ctx, usersIds, 2, 5500, memory.TB)
|
||||
assert.NoError(t, err)
|
||||
|
||||
user1Coupons, err := couponsRepo.ListByUserID(ctx, user1.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(user1Coupons))
|
||||
assert.Equal(t, proj1.ID, user1Coupons[0].ProjectID)
|
||||
|
||||
proj1Usage, err := usageRepo.GetProjectStorageLimit(ctx, proj1.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, memory.TB, proj1Usage)
|
||||
|
||||
proj2Usage, err := usageRepo.GetProjectStorageLimit(ctx, proj2.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, int(proj2Usage))
|
||||
|
||||
user2Coupons, err := couponsRepo.ListByUserID(ctx, user2.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(user2Coupons))
|
||||
|
||||
proj3Usage, err := usageRepo.GetProjectStorageLimit(ctx, proj3.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, memory.TB, proj3Usage)
|
||||
|
||||
user3Coupons, err := couponsRepo.ListByUserID(ctx, user3.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, len(user3Coupons))
|
||||
|
||||
user4Coupons, err := couponsRepo.ListByUserID(ctx, user4.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, len(user4Coupons))
|
||||
|
||||
user5Coupons, err := couponsRepo.ListByUserID(ctx, user5.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(user5Coupons))
|
||||
assert.Equal(t, "qw", user5Coupons[0].Description)
|
||||
|
||||
user6Coupons, err := couponsRepo.ListByUserID(ctx, user6.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(user6Coupons))
|
||||
|
||||
proj5Usage, err := usageRepo.GetProjectStorageLimit(ctx, proj5.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, memory.TB, proj5Usage)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -6,12 +6,15 @@ package satellitedb
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/skyrings/skyring-common/tools/uuid"
|
||||
"github.com/zeebo/errs"
|
||||
|
||||
"storj.io/common/memory"
|
||||
"storj.io/storj/private/dbutil"
|
||||
"storj.io/storj/satellite/console"
|
||||
"storj.io/storj/satellite/payments"
|
||||
"storj.io/storj/satellite/payments/coinpayments"
|
||||
"storj.io/storj/satellite/payments/stripecoinpayments"
|
||||
@ -355,3 +358,115 @@ func couponUsageFromDbxSlice(couponUsageDbx *dbx.CouponUsage) (usage stripecoinp
|
||||
|
||||
return usage, err
|
||||
}
|
||||
|
||||
// PopulatePromotionalCoupons is used to populate promotional coupons through all active users who already have a project
|
||||
// and do not have a promotional coupon yet. And updates project limits to selected size.
|
||||
func (coupons *coupons) PopulatePromotionalCoupons(ctx context.Context, users []uuid.UUID, duration int, amount int64, projectLimit memory.Size) (err error) {
|
||||
defer mon.Task()(&ctx, users, duration, amount, projectLimit)(&err)
|
||||
|
||||
// converting to []interface{} instead of [][]byte
|
||||
// to pass it to QueryContext as usersIDs...
|
||||
var usersIDs []interface{}
|
||||
for i := range users {
|
||||
usersIDs = append(usersIDs, users[i][:])
|
||||
}
|
||||
|
||||
query := coupons.db.Rebind(fmt.Sprintf(
|
||||
`SELECT users_with_projects.id, users_with_projects.project_id FROM (
|
||||
SELECT selected_users.id, first_proj.id as project_id
|
||||
FROM (select id, status from users where id in `+generateArgumentsDBX(len(users))+`) as selected_users INNER JOIN
|
||||
(select distinct on (owner_id) owner_id, id from projects order by owner_id, created_at asc) as first_proj
|
||||
ON (selected_users.id = first_proj.owner_id)
|
||||
WHERE selected_users.status = %d) AS users_with_projects
|
||||
WHERE users_with_projects.id NOT IN (
|
||||
SELECT user_id FROM coupons WHERE type = %d
|
||||
);`, console.Active, payments.CouponTypePromotional),
|
||||
)
|
||||
|
||||
usersIDsRows, err := coupons.db.QueryContext(ctx, query, usersIDs...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { err = errs.Combine(err, usersIDsRows.Close()) }()
|
||||
|
||||
type UserAndProjectIDs struct {
|
||||
userID uuid.UUID
|
||||
projectID uuid.UUID
|
||||
}
|
||||
|
||||
var ids []UserAndProjectIDs
|
||||
|
||||
for usersIDsRows.Next() {
|
||||
var userID uuid.UUID
|
||||
var userIDBytes []byte
|
||||
var projectID uuid.UUID
|
||||
var projectIDBytes []byte
|
||||
|
||||
err = usersIDsRows.Scan(&userIDBytes, &projectIDBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userID, err = dbutil.BytesToUUID(userIDBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
projectID, err = dbutil.BytesToUUID(projectIDBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ids = append(ids, UserAndProjectIDs{userID: userID, projectID: projectID})
|
||||
}
|
||||
if err = usersIDsRows.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return coupons.db.WithTx(ctx, func(ctx context.Context, tx *dbx.Tx) error {
|
||||
for _, userAndProjectID := range ids {
|
||||
err = coupons.Insert(ctx, payments.Coupon{
|
||||
UserID: userAndProjectID.userID,
|
||||
ProjectID: userAndProjectID.projectID,
|
||||
Amount: amount,
|
||||
Duration: duration,
|
||||
Description: fmt.Sprintf("Promotional credits (limited time - %d billing periods)", duration),
|
||||
Type: payments.CouponTypePromotional,
|
||||
Status: payments.CouponActive,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = coupons.db.Update_Project_By_Id(ctx,
|
||||
dbx.Project_Id(userAndProjectID.projectID[:]),
|
||||
dbx.Project_Update_Fields{
|
||||
UsageLimit: dbx.Project_UsageLimit(projectLimit.Int64()),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// generates variadic number of '?' symbols in brackets depends on input parameter.
|
||||
func generateArgumentsDBX(length int) string {
|
||||
start := "("
|
||||
|
||||
if length >= 1 {
|
||||
start += "?"
|
||||
}
|
||||
|
||||
middle := ""
|
||||
for i := 1; i < length; i++ {
|
||||
middle += ", ?"
|
||||
}
|
||||
|
||||
end := ")"
|
||||
|
||||
return start + middle + end
|
||||
}
|
||||
|
28
satellite/satellitedb/coupons_test.go
Normal file
28
satellite/satellitedb/coupons_test.go
Normal file
@ -0,0 +1,28 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
package satellitedb
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGenerateArgumentsDBX(t *testing.T) {
|
||||
testCases := [...]struct {
|
||||
length int
|
||||
expectedResult string
|
||||
}{
|
||||
0: {0, "()"},
|
||||
1: {1, "(?)"},
|
||||
2: {2, "(?, ?)"},
|
||||
3: {3, "(?, ?, ?)"},
|
||||
4: {-1, "()"},
|
||||
5: {-2, "()"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
assert.Equal(t, tc.expectedResult, generateArgumentsDBX(tc.length))
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user