satellite/console: purchase package grants credit
Instead of granting a coupon when purchasing a package, grant credit. This changes paymentsconfig.PackagePlan to use credit amount rather than coupon ID. Add additional check to see if a paid invoice with the description exists. If so, don't create and pay another invoice. Change-Id: I81df24984c519c773db5fc8e9070bd7797070ec2
This commit is contained in:
parent
a2e3247471
commit
c2cd213c4f
@ -499,12 +499,6 @@ func (p *Payments) PurchasePackage(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = p.service.Payments().ApplyCoupon(ctx, pkg.CouponID)
|
|
||||||
if err != nil {
|
|
||||||
p.serveJSONError(w, http.StatusInternalServerError, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
card, err := p.service.Payments().AddCreditCard(ctx, token)
|
card, err := p.service.Payments().AddCreditCard(ctx, token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
@ -516,8 +510,22 @@ func (p *Payments) PurchasePackage(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = p.service.Payments().Purchase(ctx, pkg.Price, fmt.Sprintf("%s package plan", string(u.UserAgent)), card.ID)
|
description := fmt.Sprintf("%s package plan", string(u.UserAgent))
|
||||||
|
err = p.service.Payments().UpdatePackage(ctx, description, time.Now())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if !console.ErrAlreadyHasPackage.Has(err) {
|
||||||
|
p.serveJSONError(w, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = p.service.Payments().Purchase(ctx, pkg.Price, description, card.ID)
|
||||||
|
if err != nil {
|
||||||
|
p.serveJSONError(w, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = p.service.Payments().ApplyCredit(ctx, pkg.Credit, description); err != nil {
|
||||||
p.serveJSONError(w, http.StatusInternalServerError, err)
|
p.serveJSONError(w, http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,6 @@ import (
|
|||||||
|
|
||||||
func Test_PurchasePackage(t *testing.T) {
|
func Test_PurchasePackage(t *testing.T) {
|
||||||
partner := "partner1"
|
partner := "partner1"
|
||||||
partner2 := "partner2"
|
|
||||||
|
|
||||||
testplanet.Run(t, testplanet.Config{
|
testplanet.Run(t, testplanet.Config{
|
||||||
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 1,
|
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 1,
|
||||||
@ -35,8 +34,7 @@ func Test_PurchasePackage(t *testing.T) {
|
|||||||
config.Console.RateLimit.Burst = 10
|
config.Console.RateLimit.Burst = 10
|
||||||
config.Payments.StripeCoinPayments.StripeFreeTierCouponID = stripecoinpayments.MockCouponID1
|
config.Payments.StripeCoinPayments.StripeFreeTierCouponID = stripecoinpayments.MockCouponID1
|
||||||
config.Payments.PackagePlans.Packages = map[string]payments.PackagePlan{
|
config.Payments.PackagePlans.Packages = map[string]payments.PackagePlan{
|
||||||
partner: {CouponID: stripecoinpayments.MockCouponID2, Price: 1000},
|
partner: {Credit: 2000, Price: 1000},
|
||||||
partner2: {CouponID: "invalidCouponID", Price: 1000},
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -53,12 +51,6 @@ func Test_PurchasePackage(t *testing.T) {
|
|||||||
"No matching package plan for partner", validCardToken, "unknownPartner",
|
"No matching package plan for partner", validCardToken, "unknownPartner",
|
||||||
http.StatusNotFound,
|
http.StatusNotFound,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
// partner2's coupon ID configured above in Reconfigure does not exist in underlying
|
|
||||||
// stipe mock coupons list.
|
|
||||||
"Coupon doesn't exist", validCardToken, partner2,
|
|
||||||
http.StatusInternalServerError,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"Add credit card fails", stripecoinpayments.TestPaymentMethodsNewFailure, partner,
|
"Add credit card fails", stripecoinpayments.TestPaymentMethodsNewFailure, partner,
|
||||||
http.StatusInternalServerError,
|
http.StatusInternalServerError,
|
||||||
@ -71,6 +63,10 @@ func Test_PurchasePackage(t *testing.T) {
|
|||||||
"Success", validCardToken, partner,
|
"Success", validCardToken, partner,
|
||||||
http.StatusOK,
|
http.StatusOK,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"Subsequent request succeeds", validCardToken, partner,
|
||||||
|
http.StatusOK,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for i, tt := range tests {
|
for i, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
@ -88,7 +84,7 @@ func Test_PurchasePackage(t *testing.T) {
|
|||||||
tokenInfo, err := sat.API.Console.Service.Token(ctx, console.AuthUser{Email: user.Email, Password: user.FullName})
|
tokenInfo, err := sat.API.Console.Service.Token(ctx, console.AuthUser{Email: user.Email, Password: user.FullName})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(userCtx, "POST", "http://"+planet.Satellites[0].API.Console.Listener.Addr().String()+"/api/v0/payments/purchase-package", strings.NewReader(tt.cardToken))
|
req, err := http.NewRequestWithContext(userCtx, "POST", "http://"+sat.API.Console.Listener.Addr().String()+"/api/v0/payments/purchase-package", strings.NewReader(tt.cardToken))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
expire := time.Now().AddDate(0, 0, 1)
|
expire := time.Now().AddDate(0, 0, 1)
|
||||||
@ -119,7 +115,7 @@ func Test_PackageAvailable(t *testing.T) {
|
|||||||
Reconfigure: testplanet.Reconfigure{
|
Reconfigure: testplanet.Reconfigure{
|
||||||
Satellite: func(log *zap.Logger, index int, config *satellite.Config) {
|
Satellite: func(log *zap.Logger, index int, config *satellite.Config) {
|
||||||
config.Payments.PackagePlans.Packages = map[string]payments.PackagePlan{
|
config.Payments.PackagePlans.Packages = map[string]payments.PackagePlan{
|
||||||
pkgPartner: {CouponID: stripecoinpayments.MockCouponID1, Price: 1000},
|
pkgPartner: {Credit: 2000, Price: 1000},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -3109,6 +3109,8 @@ func (payment Payments) WalletPayments(ctx context.Context) (_ WalletPayments, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Purchase makes a purchase of `price` amount with description of `desc` and payment method with id of `paymentMethodID`.
|
// Purchase makes a purchase of `price` amount with description of `desc` and payment method with id of `paymentMethodID`.
|
||||||
|
// If a paid invoice with the same description exists, then we assume this is a retried request and don't create and pay
|
||||||
|
// another invoice.
|
||||||
func (payment Payments) Purchase(ctx context.Context, price int64, desc string, paymentMethodID string) (err error) {
|
func (payment Payments) Purchase(ctx context.Context, price int64, desc string, paymentMethodID string) (err error) {
|
||||||
defer mon.Task()(&ctx)(&err)
|
defer mon.Task()(&ctx)(&err)
|
||||||
|
|
||||||
@ -3127,8 +3129,12 @@ func (payment Payments) Purchase(ctx context.Context, price int64, desc string,
|
|||||||
|
|
||||||
// check for any previously created unpaid invoice with the same description.
|
// check for any previously created unpaid invoice with the same description.
|
||||||
// If draft, delete it and create new and pay. If open, pay it and don't create new.
|
// If draft, delete it and create new and pay. If open, pay it and don't create new.
|
||||||
|
// If paid, skip.
|
||||||
for _, inv := range invoices {
|
for _, inv := range invoices {
|
||||||
if inv.Description == desc {
|
if inv.Description == desc {
|
||||||
|
if inv.Status == payments.InvoiceStatusPaid {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if inv.Status == payments.InvoiceStatusDraft {
|
if inv.Status == payments.InvoiceStatusDraft {
|
||||||
_, err := payment.service.accounts.Invoices().Delete(ctx, inv.ID)
|
_, err := payment.service.accounts.Invoices().Delete(ctx, inv.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -1656,7 +1656,7 @@ func TestPaymentsPurchasePreexistingInvoice(t *testing.T) {
|
|||||||
userCtx, err := sat.UserContext(ctx, user.ID)
|
userCtx, err := sat.UserContext(ctx, user.ID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
testDesc := "testDescription"
|
draftInvDesc := "testDraftDescription"
|
||||||
testPaymentMethod := "testPaymentMethod"
|
testPaymentMethod := "testPaymentMethod"
|
||||||
|
|
||||||
invs, err := sat.API.Payments.StripeService.Accounts().Invoices().List(ctx, user.ID)
|
invs, err := sat.API.Payments.StripeService.Accounts().Invoices().List(ctx, user.ID)
|
||||||
@ -1664,7 +1664,7 @@ func TestPaymentsPurchasePreexistingInvoice(t *testing.T) {
|
|||||||
require.Len(t, invs, 0)
|
require.Len(t, invs, 0)
|
||||||
|
|
||||||
// test purchase with draft invoice
|
// test purchase with draft invoice
|
||||||
inv, err := sat.API.Payments.StripeService.Accounts().Invoices().Create(ctx, user.ID, 1000, testDesc)
|
inv, err := sat.API.Payments.StripeService.Accounts().Invoices().Create(ctx, user.ID, 1000, draftInvDesc)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, payments.InvoiceStatusDraft, inv.Status)
|
require.Equal(t, payments.InvoiceStatusDraft, inv.Status)
|
||||||
|
|
||||||
@ -1675,7 +1675,7 @@ func TestPaymentsPurchasePreexistingInvoice(t *testing.T) {
|
|||||||
require.Len(t, invs, 1)
|
require.Len(t, invs, 1)
|
||||||
require.Equal(t, draftInv, invs[0].ID)
|
require.Equal(t, draftInv, invs[0].ID)
|
||||||
|
|
||||||
require.NoError(t, p.Purchase(userCtx, 1000, testDesc, testPaymentMethod))
|
require.NoError(t, p.Purchase(userCtx, 1000, draftInvDesc, testPaymentMethod))
|
||||||
|
|
||||||
invs, err = sat.API.Payments.StripeService.Accounts().Invoices().List(ctx, user.ID)
|
invs, err = sat.API.Payments.StripeService.Accounts().Invoices().List(ctx, user.ID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -1684,7 +1684,8 @@ func TestPaymentsPurchasePreexistingInvoice(t *testing.T) {
|
|||||||
require.Equal(t, payments.InvoiceStatusPaid, invs[0].Status)
|
require.Equal(t, payments.InvoiceStatusPaid, invs[0].Status)
|
||||||
|
|
||||||
// test purchase with open invoice
|
// test purchase with open invoice
|
||||||
inv, err = sat.API.Payments.StripeService.Accounts().Invoices().Create(ctx, user.ID, 1000, testDesc)
|
openInvDesc := "testOpenDescription"
|
||||||
|
inv, err = sat.API.Payments.StripeService.Accounts().Invoices().Create(ctx, user.ID, 1000, openInvDesc)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
openInv := inv.ID
|
openInv := inv.ID
|
||||||
@ -1705,7 +1706,7 @@ func TestPaymentsPurchasePreexistingInvoice(t *testing.T) {
|
|||||||
}
|
}
|
||||||
require.True(t, foundInv)
|
require.True(t, foundInv)
|
||||||
|
|
||||||
require.NoError(t, p.Purchase(userCtx, 1000, testDesc, testPaymentMethod))
|
require.NoError(t, p.Purchase(userCtx, 1000, openInvDesc, testPaymentMethod))
|
||||||
|
|
||||||
invs, err = sat.API.Payments.StripeService.Accounts().Invoices().List(ctx, user.ID)
|
invs, err = sat.API.Payments.StripeService.Accounts().Invoices().List(ctx, user.ID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -1718,6 +1719,13 @@ func TestPaymentsPurchasePreexistingInvoice(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
require.True(t, foundInv)
|
require.True(t, foundInv)
|
||||||
|
|
||||||
|
// purchase with paid invoice skips creating and or paying invoice
|
||||||
|
require.NoError(t, p.Purchase(userCtx, 1000, openInvDesc, testPaymentMethod))
|
||||||
|
|
||||||
|
invs, err = sat.API.Payments.StripeService.Accounts().Invoices().List(ctx, user.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, invs, 2)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,3 +40,10 @@ type BalanceTransaction struct {
|
|||||||
Amount int64
|
Amount int64
|
||||||
Description string
|
Description string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PackagePlan is an amount to charge a user one time in exchange for credit of greater value.
|
||||||
|
// Price and Credit are in cents USD.
|
||||||
|
type PackagePlan struct {
|
||||||
|
Price int64
|
||||||
|
Credit int64
|
||||||
|
}
|
||||||
|
@ -68,10 +68,3 @@ const (
|
|||||||
// SignupCoupon represents a valid promo code coupon.
|
// SignupCoupon represents a valid promo code coupon.
|
||||||
SignupCoupon = "signupCoupon"
|
SignupCoupon = "signupCoupon"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PackagePlan is an amount to charge a user one time in exchange for a coupon of greater value.
|
|
||||||
// Price is in cents USD.
|
|
||||||
type PackagePlan struct {
|
|
||||||
CouponID string
|
|
||||||
Price int64
|
|
||||||
}
|
|
||||||
|
@ -37,7 +37,7 @@ type Config struct {
|
|||||||
NodeAuditBandwidthPrice int64 `help:"price node receive for storing TB of audit in cents" default:"1000"`
|
NodeAuditBandwidthPrice int64 `help:"price node receive for storing TB of audit in cents" default:"1000"`
|
||||||
NodeDiskSpacePrice int64 `help:"price node receive for storing disk space in cents/TB" default:"150"`
|
NodeDiskSpacePrice int64 `help:"price node receive for storing disk space in cents/TB" default:"150"`
|
||||||
UsagePriceOverrides ProjectUsagePriceOverrides `help:"semicolon-separated usage price overrides in the format partner:storage,egress,segment"`
|
UsagePriceOverrides ProjectUsagePriceOverrides `help:"semicolon-separated usage price overrides in the format partner:storage,egress,segment"`
|
||||||
PackagePlans PackagePlans `help:"semicolon-separated partner package plans in the format partner:couponID,price. Price is in cents USD."`
|
PackagePlans PackagePlans `help:"semicolon-separated partner package plans in the format partner:price,credit. Price and credit are in cents USD."`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProjectUsagePrice holds the configuration for the satellite's project usage price model.
|
// ProjectUsagePrice holds the configuration for the satellite's project usage price model.
|
||||||
@ -172,7 +172,7 @@ func (p *PackagePlans) String() string {
|
|||||||
var s strings.Builder
|
var s strings.Builder
|
||||||
left := len(p.Packages)
|
left := len(p.Packages)
|
||||||
for partner, pkg := range p.Packages {
|
for partner, pkg := range p.Packages {
|
||||||
s.WriteString(fmt.Sprintf("%s:%s,%d", partner, pkg.CouponID, pkg.Price))
|
s.WriteString(fmt.Sprintf("%s:%d,%d", partner, pkg.Price, pkg.Credit))
|
||||||
left--
|
left--
|
||||||
if left > 0 {
|
if left > 0 {
|
||||||
s.WriteRune(';')
|
s.WriteRune(';')
|
||||||
@ -191,7 +191,7 @@ func (p *PackagePlans) Set(s string) error {
|
|||||||
|
|
||||||
info := strings.Split(packagePlansStr, ":")
|
info := strings.Split(packagePlansStr, ":")
|
||||||
if len(info) != 2 {
|
if len(info) != 2 {
|
||||||
return Error.New("Invalid package plan (expected format partner:couponID,price got %s)", packagePlansStr)
|
return Error.New("Invalid package plan (expected format partner:price,credit got %s)", packagePlansStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
partner := strings.TrimSpace(info[0])
|
partner := strings.TrimSpace(info[0])
|
||||||
@ -202,21 +202,26 @@ func (p *PackagePlans) Set(s string) error {
|
|||||||
packageStr := info[1]
|
packageStr := info[1]
|
||||||
pkg := strings.Split(packageStr, ",")
|
pkg := strings.Split(packageStr, ",")
|
||||||
if len(pkg) != 2 || pkg[0] == "" {
|
if len(pkg) != 2 || pkg[0] == "" {
|
||||||
return Error.New("Invalid package (expected format couponID,price got %s)", packageStr)
|
return Error.New("Invalid package (expected format price,credit got %s)", packageStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := decimal.NewFromString(pkg[1]); err != nil {
|
if _, err := decimal.NewFromString(pkg[1]); err != nil {
|
||||||
return Error.New("Invalid price (%s)", err)
|
return Error.New("Invalid price (%s)", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cents, err := strconv.Atoi(pkg[1])
|
priceCents, err := strconv.Atoi(pkg[0])
|
||||||
|
if err != nil {
|
||||||
|
return Error.Wrap(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
creditCents, err := strconv.Atoi(pkg[1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Error.Wrap(err)
|
return Error.Wrap(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
packages[info[0]] = payments.PackagePlan{
|
packages[info[0]] = payments.PackagePlan{
|
||||||
CouponID: pkg[0],
|
Price: int64(priceCents),
|
||||||
Price: int64(cents),
|
Credit: int64(creditCents),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
p.Packages = packages
|
p.Packages = packages
|
||||||
|
@ -120,43 +120,43 @@ func TestPackagePlans(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
testID: "missing partner",
|
testID: "missing partner",
|
||||||
configValue: ":abc123,100",
|
configValue: ":100,100",
|
||||||
}, {
|
|
||||||
testID: "empty coupon ID",
|
|
||||||
configValue: "partner:,1",
|
|
||||||
}, {
|
}, {
|
||||||
testID: "empty price",
|
testID: "empty price",
|
||||||
configValue: "partner:abc123,",
|
configValue: "partner:,100",
|
||||||
|
}, {
|
||||||
|
testID: "empty credit",
|
||||||
|
configValue: "partner:100,",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
testID: "too few values",
|
testID: "too few values",
|
||||||
configValue: "partner:abc123",
|
configValue: "partner:100",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
testID: "too many values",
|
testID: "too many values",
|
||||||
configValue: "partner:abc123,100,200",
|
configValue: "partner:100,100,200",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
testID: "single package plan",
|
testID: "single package plan",
|
||||||
configValue: "partner1:abc123,100",
|
configValue: "partner1:100,200",
|
||||||
expectedPackagePlans: packages{
|
expectedPackagePlans: packages{
|
||||||
"partner1": payments.PackagePlan{
|
"partner1": payments.PackagePlan{
|
||||||
CouponID: "abc123",
|
Price: 100,
|
||||||
Price: 100,
|
Credit: 200,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
testID: "multiple package plans",
|
testID: "multiple package plans",
|
||||||
configValue: "partner1:abc123,100;partner2:321bca,200",
|
configValue: "partner1:100,200;partner2:200,300",
|
||||||
expectedPackagePlans: packages{
|
expectedPackagePlans: packages{
|
||||||
"partner1": payments.PackagePlan{
|
"partner1": payments.PackagePlan{
|
||||||
CouponID: "abc123",
|
Price: 100,
|
||||||
Price: 100,
|
Credit: 200,
|
||||||
},
|
},
|
||||||
"partner2": payments.PackagePlan{
|
"partner2": payments.PackagePlan{
|
||||||
CouponID: "321bca",
|
Price: 200,
|
||||||
Price: 200,
|
Credit: 300,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -188,9 +188,9 @@ func TestPackagePlans(t *testing.T) {
|
|||||||
|
|
||||||
func TestPackagePlansGet(t *testing.T) {
|
func TestPackagePlansGet(t *testing.T) {
|
||||||
partner := "partnerName1"
|
partner := "partnerName1"
|
||||||
coupon := "abc123"
|
credit := int64(200)
|
||||||
price := int64(100)
|
price := int64(100)
|
||||||
configStr := fmt.Sprintf("%s:%s,%d", partner, coupon, price)
|
configStr := fmt.Sprintf("%s:%d,%d", partner, price, credit)
|
||||||
|
|
||||||
packagePlans := paymentsconfig.PackagePlans{}
|
packagePlans := paymentsconfig.PackagePlans{}
|
||||||
require.NoError(t, packagePlans.Set(configStr))
|
require.NoError(t, packagePlans.Set(configStr))
|
||||||
@ -231,7 +231,7 @@ func TestPackagePlansGet(t *testing.T) {
|
|||||||
p, err := packagePlans.Get(c.userAgent)
|
p, err := packagePlans.Get(c.userAgent)
|
||||||
if c.shouldPass {
|
if c.shouldPass {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, coupon, p.CouponID)
|
require.Equal(t, credit, p.Credit)
|
||||||
require.Equal(t, price, p.Price)
|
require.Equal(t, price, p.Price)
|
||||||
} else {
|
} else {
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
|
@ -66,24 +66,6 @@ func (coupons *coupons) ApplyCoupon(ctx context.Context, userID uuid.UUID, coupo
|
|||||||
func (coupons *coupons) ApplyCouponCode(ctx context.Context, userID uuid.UUID, couponCode string) (_ *payments.Coupon, err error) {
|
func (coupons *coupons) ApplyCouponCode(ctx context.Context, userID uuid.UUID, couponCode string) (_ *payments.Coupon, err error) {
|
||||||
defer mon.Task()(&ctx, userID, couponCode)(&err)
|
defer mon.Task()(&ctx, userID, couponCode)(&err)
|
||||||
|
|
||||||
user, err := coupons.service.usersDB.Get(ctx, userID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, Error.Wrap(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if user.UserAgent != nil {
|
|
||||||
partner := string(user.UserAgent)
|
|
||||||
if plan, ok := coupons.service.packagePlans[partner]; ok {
|
|
||||||
coupon, err := coupons.GetByUserID(ctx, userID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if coupon != nil && coupon.ID == plan.CouponID {
|
|
||||||
return nil, payments.ErrCouponConflict.New("coupon for partner '%s' should not be replaced", partner)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
promoCodeIter := coupons.service.stripeClient.PromoCodes().List(&stripe.PromotionCodeListParams{
|
promoCodeIter := coupons.service.stripeClient.PromoCodes().List(&stripe.PromotionCodeListParams{
|
||||||
ListParams: stripe.ListParams{Context: ctx},
|
ListParams: stripe.ListParams{Context: ctx},
|
||||||
Code: stripe.String(couponCode),
|
Code: stripe.String(couponCode),
|
||||||
|
@ -13,64 +13,9 @@ import (
|
|||||||
"storj.io/common/testrand"
|
"storj.io/common/testrand"
|
||||||
"storj.io/storj/private/testplanet"
|
"storj.io/storj/private/testplanet"
|
||||||
"storj.io/storj/satellite"
|
"storj.io/storj/satellite"
|
||||||
"storj.io/storj/satellite/console"
|
|
||||||
"storj.io/storj/satellite/payments"
|
|
||||||
"storj.io/storj/satellite/payments/stripecoinpayments"
|
"storj.io/storj/satellite/payments/stripecoinpayments"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCouponConflict(t *testing.T) {
|
|
||||||
const (
|
|
||||||
partnerName = "partner"
|
|
||||||
partnerCode = "promo1"
|
|
||||||
standardCode = "promo2"
|
|
||||||
)
|
|
||||||
testplanet.Run(t, testplanet.Config{
|
|
||||||
SatelliteCount: 1,
|
|
||||||
Reconfigure: testplanet.Reconfigure{
|
|
||||||
Satellite: func(log *zap.Logger, index int, config *satellite.Config) {
|
|
||||||
config.Payments.PackagePlans.Packages = map[string]payments.PackagePlan{
|
|
||||||
partnerName: {CouponID: "c1"},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
|
||||||
sat := planet.Satellites[0]
|
|
||||||
coupons := sat.Core.Payments.Accounts.Coupons()
|
|
||||||
|
|
||||||
t.Run("standard user can replace partner coupon", func(t *testing.T) {
|
|
||||||
user, err := sat.AddUser(ctx, console.CreateUser{
|
|
||||||
FullName: "Test User",
|
|
||||||
Email: "user@mail.test",
|
|
||||||
}, 2)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
_, err = coupons.ApplyCouponCode(ctx, user.ID, partnerCode)
|
|
||||||
require.NoError(t, err)
|
|
||||||
_, err = coupons.ApplyCouponCode(ctx, user.ID, standardCode)
|
|
||||||
require.NoError(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
partneredUser, err := sat.AddUser(ctx, console.CreateUser{
|
|
||||||
FullName: "Test User",
|
|
||||||
Email: "user2@mail.test",
|
|
||||||
UserAgent: []byte(partnerName),
|
|
||||||
}, 2)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
t.Run("partnered user can replace standard coupon", func(t *testing.T) {
|
|
||||||
_, err = coupons.ApplyCouponCode(ctx, partneredUser.ID, standardCode)
|
|
||||||
require.NoError(t, err)
|
|
||||||
_, err = coupons.ApplyCouponCode(ctx, partneredUser.ID, partnerCode)
|
|
||||||
require.NoError(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("partnered user cannot replace partner coupon", func(t *testing.T) {
|
|
||||||
_, err = coupons.ApplyCouponCode(ctx, partneredUser.ID, standardCode)
|
|
||||||
require.True(t, payments.ErrCouponConflict.Has(err))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCoupons(t *testing.T) {
|
func TestCoupons(t *testing.T) {
|
||||||
testplanet.Run(t, testplanet.Config{
|
testplanet.Run(t, testplanet.Config{
|
||||||
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 1,
|
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 1,
|
||||||
|
2
scripts/testdata/satellite-config.yaml.lock
vendored
2
scripts/testdata/satellite-config.yaml.lock
vendored
@ -826,7 +826,7 @@ identity.key-path: /root/.local/share/storj/identity/satellite/identity.key
|
|||||||
# price node receive for storing TB of repair in cents
|
# price node receive for storing TB of repair in cents
|
||||||
# payments.node-repair-bandwidth-price: 1000
|
# payments.node-repair-bandwidth-price: 1000
|
||||||
|
|
||||||
# semicolon-separated partner package plans in the format partner:couponID,price. Price is in cents USD.
|
# semicolon-separated partner package plans in the format partner:price,credit. Price and credit are in cents USD.
|
||||||
# payments.package-plans: ""
|
# payments.package-plans: ""
|
||||||
|
|
||||||
# payments provider to use
|
# payments provider to use
|
||||||
|
Loading…
Reference in New Issue
Block a user