Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reads plans from stripe instead from local file #29

Merged
merged 4 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,12 @@ func TestMain(m *testing.M) {
// set reset db env var to true
_ = os.Setenv("VOCDONI_MONGO_RESET_DB", "true")
// create a new MongoDB connection with the test database
if testDB, err = db.New(mongoURI, test.RandomDatabaseName(), "subscriptions.json"); err != nil {
// create a new MongoDB connection with the test database
plans, err := db.ReadPlanJSON()
if err != nil {
panic(err)
}
if testDB, err = db.New(mongoURI, test.RandomDatabaseName(), plans); err != nil {
panic(err)
}
defer testDB.Close()
Expand Down
22 changes: 21 additions & 1 deletion api/docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,16 @@ This request can be made only by organization admins.
"smsNotification":false
}
},
"censusSizeTiers": [
{
"flatAmount":9900,
"upTo":100
},
{
"flatAmount":79900,
"upTo":1500
}
],
...
]
}
Expand Down Expand Up @@ -882,7 +892,17 @@ This request can be made only by organization admins.
"personalization":false,
"emailReminder":true,
"smsNotification":false
}
},
"censusSizeTiers": [
{
"flatAmount":9900,
"upTo":100
},
{
"flatAmount":79900,
"upTo":1500
}
],
}
```

Expand Down
21 changes: 13 additions & 8 deletions assets/subscriptions.json → assets/plans.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
[
{
"ID": 1,
"Name": "Basic",
"StripeID": "prod_R3LTVsjklmuQAL",
"Name": "Essential Annual Plan",
"StripeID": "price_1QBEmzDW6VLep8WGpkwjynXV",
"StartingPrice": 9900,
"Default": false,
"Organization": {
"Memberships": 5,
"SubOrgs": 1
"SubOrgs": 1,
"CensusSize": 0
},
"VotingTypes": {
"Approval": true,
Expand All @@ -21,12 +23,14 @@
},
{
"ID": 2,
"Name": "Pro",
"StripeID": "prod_R0kTryoMNl8I19",
"Name": "Premium Annual Plan",
"StripeID": "price_1Q8iyUDW6VLep8WGWXdjC78r",
"StartingPrice": 30000,
"Default": false,
"Organization": {
"Memberships": 10,
"SubOrgs": 5
"SubOrgs": 5,
"CensusSize": 0
},
"VotingTypes": {
"Approval": true,
Expand All @@ -41,9 +45,10 @@
},
{
"ID": 3,
"Name": "free",
"Name": "Free Plan",
"StripeID": "price_1QMtoJDW6VLep8WGC2vsJ2CV",
"StartingPrice": 0,
"Default": true,
"StripeID": "stripe_789",
"Organization": {
"Memberships": 10,
"SubOrgs": 5,
Expand Down
23 changes: 14 additions & 9 deletions cmd/service/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ func main() {
flag.StringP("vocdoniApi", "v", "https://api-dev.vocdoni.net/v2", "vocdoni node remote API URL")
flag.StringP("privateKey", "k", "", "private key for the Vocdoni account")
flag.BoolP("fullTransparentMode", "a", false, "allow all transactions and do not modify any of them")
flag.String("plansFile", "subscriptions.json", "JSON file that contains the subscriptions info")
flag.String("smtpServer", "", "SMTP server")
flag.Int("smtpPort", 587, "SMTP port")
flag.String("smtpUsername", "", "SMTP username")
Expand All @@ -55,7 +54,6 @@ func main() {
}
mongoURL := viper.GetString("mongoURL")
mongoDB := viper.GetString("mongoDB")
plansFile := viper.GetString("plansFile")
// email vars
smtpServer := viper.GetString("smtpServer")
smtpPort := viper.GetInt("smtpPort")
Expand All @@ -68,8 +66,20 @@ func main() {
stripeWebhookSecret := viper.GetString("stripeWebhookSecret")

log.Init("debug", "stdout", os.Stderr)
// create Stripe client and include it in the API configuration
var stripeClient *stripe.StripeClient
if stripeApiSecret != "" || stripeWebhookSecret != "" {
stripeClient = stripe.New(stripeApiSecret, stripeWebhookSecret)
} else {
log.Fatalf("stripeApiSecret and stripeWebhookSecret are required")
}
availablePlans, err := stripeClient.GetPlans()
if err != nil || len(availablePlans) == 0 {
log.Fatalf("could not get the available plans: %v", err)
}

// initialize the MongoDB database
database, err := db.New(mongoURL, mongoDB, plansFile)
database, err := db.New(mongoURL, mongoDB, availablePlans)
if err != nil {
log.Fatalf("could not create the MongoDB database: %v", err)
}
Expand Down Expand Up @@ -100,6 +110,7 @@ func main() {
Client: apiClient,
Account: acc,
FullTransparentMode: fullTransparentMode,
StripeClient: stripeClient,
}
// overwrite the email notifications service with the SMTP service if the
// required parameters are set and include it in the API configuration
Expand All @@ -120,12 +131,6 @@ func main() {
}
log.Infow("email service created", "from", fmt.Sprintf("%s <%s>", emailFromName, emailFromAddress))
}
// create Stripe client and include it in the API configuration
if stripeApiSecret != "" || stripeWebhookSecret != "" {
apiConf.StripeClient = stripe.New(stripeApiSecret, stripeWebhookSecret)
} else {
log.Fatalf("stripeApiSecret and stripeWebhookSecret are required")
}
subscriptions := subscriptions.New(&subscriptions.SubscriptionsConfig{
DB: database,
})
Expand Down
23 changes: 4 additions & 19 deletions db/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,6 @@ func (ms *MongoStorage) initCollections(database string) error {
return err
}
log.Infow("current collections", "collections", currentCollections)
log.Infow("reading plans from file %s", ms.plansFile)
loadedPlans, err := readPlanJSON(ms.plansFile)
if err != nil {
return err
}
// aux method to get a collection if it exists, or create it if it doesn't
getCollection := func(name string) (*mongo.Collection, error) {
alreadyCreated := false
Expand Down Expand Up @@ -70,11 +65,11 @@ func (ms *MongoStorage) initCollections(database string) error {
}
if name == "plans" {
var plans []interface{}
for _, plan := range loadedPlans {
for _, plan := range ms.stripePlans {
plans = append(plans, plan)
}
count, err := ms.client.Database(database).Collection(name).InsertMany(ctx, plans)
if err != nil || len(count.InsertedIDs) != len(loadedPlans) {
if err != nil || len(count.InsertedIDs) != len(ms.stripePlans) {
return nil, fmt.Errorf("failed to insert plans: %w", err)
}
}
Expand Down Expand Up @@ -224,21 +219,11 @@ func dynamicUpdateDocument(item interface{}, alwaysUpdateTags []string) (bson.M,

// readPlanJSON reads a JSON file with an array of subscritpions
// and return it as a Plan array
func readPlanJSON(plansFile string) ([]*Plan, error) {
log.Warnf("Reading subscriptions from %s", plansFile)
file, err := root.Assets.Open(fmt.Sprintf("assets/%s", plansFile))
func ReadPlanJSON() ([]*Plan, error) {
file, err := root.Assets.Open("assets/plans.json")
if err != nil {
return nil, err
}
// file, err := os.Open(plansFile)
// if err != nil {
// return nil, err
// }
// defer func() {
// if err := file.Close(); err != nil {
// log.Warnw("failed to close subscriptions file", "error", err)
// }
// }()

// Create a JSON decoder
decoder := json.NewDecoder(file)
Expand Down
12 changes: 6 additions & 6 deletions db/mongo.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ import (

// MongoStorage uses an external MongoDB service for stoting the user data and election details.
type MongoStorage struct {
database string
client *mongo.Client
keysLock sync.RWMutex
plansFile string
database string
client *mongo.Client
keysLock sync.RWMutex
stripePlans []*Plan

users *mongo.Collection
verifications *mongo.Collection
Expand All @@ -34,7 +34,7 @@ type Options struct {
Database string
}

func New(url, database, plansFile string) (*MongoStorage, error) {
func New(url, database string, plans []*Plan) (*MongoStorage, error) {
var err error
ms := &MongoStorage{}
if url == "" {
Expand Down Expand Up @@ -67,7 +67,7 @@ func New(url, database, plansFile string) (*MongoStorage, error) {
// init the database client
ms.client = client
ms.database = database
ms.plansFile = plansFile
ms.stripePlans = plans
// init the collections
if err := ms.initCollections(ms.database); err != nil {
return nil, err
Expand Down
6 changes: 5 additions & 1 deletion db/mongo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ func TestMain(m *testing.M) {
// set reset db env var to true
_ = os.Setenv("VOCDONI_MONGO_RESET_DB", "true")
// create a new MongoDB connection with the test database
db, err = New(mongoURI, test.RandomDatabaseName(), "subscriptions.json")
plans, err := ReadPlanJSON()
if err != nil {
panic(err)
}
db, err = New(mongoURI, test.RandomDatabaseName(), plans)
if err != nil {
panic(err)
}
Expand Down
21 changes: 14 additions & 7 deletions db/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,20 @@ type Features struct {
}

type Plan struct {
ID uint64 `json:"id" bson:"_id"`
Name string `json:"name" bson:"name"`
StripeID string `json:"stripeID" bson:"stripeID"`
Default bool `json:"default" bson:"default"`
Organization PlanLimits `json:"organization" bson:"organization"`
VotingTypes VotingTypes `json:"votingTypes" bson:"votingTypes"`
Features Features `json:"features" bson:"features"`
ID uint64 `json:"id" bson:"_id"`
Name string `json:"name" bson:"name"`
StripeID string `json:"stripeID" bson:"stripeID"`
StartingPrice int64 `json:"startingPrice" bson:"startingPrice"`
Default bool `json:"default" bson:"default"`
Organization PlanLimits `json:"organization" bson:"organization"`
VotingTypes VotingTypes `json:"votingTypes" bson:"votingTypes"`
Features Features `json:"features" bson:"features"`
CensusSizeTiers []PlanTier `json:"censusSizeTiers" bson:"censusSizeTiers"`
}

type PlanTier struct {
Amount int64 `json:"Amount" bson:"Amount"`
UpTo int64 `json:"upTo" bson:"upTo"`
}

type OrganizationSubscription struct {
Expand Down
94 changes: 94 additions & 0 deletions stripe/stripe.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,22 @@ package stripe

import (
"encoding/json"
"fmt"

"github.com/stripe/stripe-go/v81"
"github.com/stripe/stripe-go/v81/customer"
"github.com/stripe/stripe-go/v81/price"
"github.com/stripe/stripe-go/v81/webhook"
"github.com/vocdoni/saas-backend/db"
"go.vocdoni.io/dvote/log"
)

var PricesLookupKeys = []string{
"essential_annual_plan",
"premium_annual_plan",
"free_plan",
}

// StripeClient is a client for interacting with the Stripe API.
// It holds the necessary configuration such as the webhook secret.
type StripeClient struct {
Expand Down Expand Up @@ -57,3 +66,88 @@ func (s *StripeClient) GetInfoFromEvent(event stripe.Event) (*stripe.Customer, *
}
return customer, &subscription, nil
}

func (s *StripeClient) GetPriceByID(priceID string) *stripe.Price {
query := fmt.Sprintf("active:'true' AND lookup_key:'%s'", priceID)
params := &stripe.PriceSearchParams{
SearchParams: stripe.SearchParams{
Query: query,
},
}
emmdim marked this conversation as resolved.
Show resolved Hide resolved
params.AddExpand("data.tiers")
results := price.Search(params)
lucasmenendez marked this conversation as resolved.
Show resolved Hide resolved
lucasmenendez marked this conversation as resolved.
Show resolved Hide resolved
if results.Next() {
return results.Price()
}
return nil
}

func (s *StripeClient) GetPrices(priceIDs []string) []*stripe.Price {
var prices []*stripe.Price
for _, priceID := range priceIDs {
price := s.GetPriceByID(priceID)
if price != nil {
prices = append(prices, price)
}
emmdim marked this conversation as resolved.
Show resolved Hide resolved
}
return prices
}

func (s *StripeClient) GetPlans() ([]*db.Plan, error) {
var plans []*db.Plan
for i, priceID := range PricesLookupKeys {
price := s.GetPriceByID(priceID)
if price != nil {
emmdim marked this conversation as resolved.
Show resolved Hide resolved
var organizationData map[string]int
if err := json.Unmarshal([]byte(price.Metadata["Organization"]), &organizationData); err != nil {
return nil, fmt.Errorf("error parsing plan organization metadata JSON: %s\n", err.Error())
}
emmdim marked this conversation as resolved.
Show resolved Hide resolved
var votingTypesData map[string]bool
if err := json.Unmarshal([]byte(price.Metadata["VotingTypes"]), &votingTypesData); err != nil {
return nil, fmt.Errorf("error parsing plan voting types metadata JSON: %s\n", err.Error())
}
emmdim marked this conversation as resolved.
Show resolved Hide resolved
var featuresData map[string]bool
if err := json.Unmarshal([]byte(price.Metadata["Features"]), &featuresData); err != nil {
return nil, fmt.Errorf("error parsing plan features metadata JSON: %s\n", err.Error())
}
emmdim marked this conversation as resolved.
Show resolved Hide resolved
emmdim marked this conversation as resolved.
Show resolved Hide resolved
startingPrice := price.UnitAmount
if len(price.Tiers) > 0 {
startingPrice = price.Tiers[0].FlatAmount
}
var tiers []db.PlanTier
for _, tier := range price.Tiers {
if tier.UpTo == 0 {
continue
}
tiers = append(tiers, db.PlanTier{
Amount: tier.FlatAmount,
UpTo: tier.UpTo,
})
}
plans = append(plans, &db.Plan{
ID: uint64(i),
Name: price.Nickname,
StartingPrice: startingPrice,
StripeID: price.ID,
Default: price.Metadata["Default"] == "true",
Organization: db.PlanLimits{
Memberships: organizationData["Memberships"],
SubOrgs: organizationData["SubOrgs"],
CensusSize: organizationData["CensusSize"],
},
VotingTypes: db.VotingTypes{
Approval: votingTypesData["Approval"],
Ranked: votingTypesData["Ranked"],
Weighted: votingTypesData["Weighted"],
},
Features: db.Features{
Personalization: featuresData["Personalization"],
EmailReminder: featuresData["EmailReminder"],
SmsNotification: featuresData["SmsNotification"],
},
CensusSizeTiers: tiers,
})
}
}
return plans, nil
}
Loading