diff --git a/GUIDELINES.md b/GUIDELINES.md index edcbf05bb..841956b7f 100644 --- a/GUIDELINES.md +++ b/GUIDELINES.md @@ -6,7 +6,7 @@ - `app/models/dto.`: A simple object used for data transfer between various packages/services. E.g: `dto.NewUserInfo`; - `app/models/entity.`: An object that is mapped to a database table. E.g: `entity.User`; - `app/models/cmd.` something that must be done and potentially return some value. E.g.: `cmd.HttpRequest`, `cmd.LogDebug`, `cmd.SendMail`, `cmd.CreateNewUser`; -- `app/models/query.` get some information from somewhere. E.g.: `query.GetUserById`, `query.GetBillingStatus`; +- `app/models/query.` get some information from somewhere. E.g.: `query.GetUserById`, `query.GetAllPosts`; # UI Development diff --git a/app/actions/billing.go b/app/actions/billing.go deleted file mode 100644 index a1c756277..000000000 --- a/app/actions/billing.go +++ /dev/null @@ -1,134 +0,0 @@ -package actions - -import ( - "context" - "fmt" - - "github.com/getfider/fider/app" - - "github.com/getfider/fider/app/models/query" - "github.com/getfider/fider/app/pkg/bus" - "github.com/getfider/fider/app/pkg/errors" - "github.com/getfider/fider/app/pkg/log" - - "github.com/getfider/fider/app/models" - "github.com/getfider/fider/app/models/dto" - "github.com/getfider/fider/app/pkg/validate" - "github.com/goenning/vat" -) - -// CreateEditBillingPaymentInfo is used to create/edit billing payment info -type CreateEditBillingPaymentInfo struct { - Model *dto.CreateEditBillingPaymentInfo -} - -// Initialize the model -func (input *CreateEditBillingPaymentInfo) Initialize() interface{} { - input.Model = new(dto.CreateEditBillingPaymentInfo) - return input.Model -} - -// IsAuthorized returns true if current user is authorized to perform this action -func (input *CreateEditBillingPaymentInfo) IsAuthorized(ctx context.Context, user *models.User) bool { - return user != nil && user.IsAdministrator() -} - -// Validate if current model is valid -func (input *CreateEditBillingPaymentInfo) Validate(ctx context.Context, user *models.User) *validate.Result { - result := validate.Success() - - if input.Model.Name == "" { - result.AddFieldFailure("name", "Name is required.") - } - - if input.Model.Email == "" { - result.AddFieldFailure("email", "Email is required") - } else { - messages := validate.Email(input.Model.Email) - if len(messages) > 0 { - result.AddFieldFailure("email", messages...) - } - } - - if input.Model.AddressLine1 == "" { - result.AddFieldFailure("addressLine1", "Address Line 1 is required.") - } - - if input.Model.AddressLine2 == "" { - result.AddFieldFailure("addressLine2", "Address Line 2 is required.") - } - - if input.Model.AddressCity == "" { - result.AddFieldFailure("addressCity", "City is required.") - } - - if input.Model.AddressPostalCode == "" { - result.AddFieldFailure("addressPostalCode", "Postal Code is required.") - } - - getPaymentInfo := &query.GetPaymentInfo{} - err := bus.Dispatch(ctx, getPaymentInfo) - if err != nil { - return validate.Error(err) - } - - current := getPaymentInfo.Result - isNew := current == nil - isUpdate := current != nil && input.Model.Card == nil - isReplacing := current != nil && input.Model.Card != nil - - if (isNew || isReplacing) && (input.Model.Card == nil || input.Model.Card.Token == "") { - result.AddFieldFailure("card", "Card information is required.") - } - - if input.Model.AddressCountry == "" { - result.AddFieldFailure("addressCountry", "Country is required.") - } else { - err := bus.Dispatch(ctx, &query.GetCountryByCode{Code: input.Model.AddressCountry}) - if err != nil { - if err == app.ErrNotFound { - result.AddFieldFailure("addressCountry", fmt.Sprintf("'%s' is not a valid country code.", input.Model.AddressCountry)) - } else { - return validate.Error(err) - } - } - - if (isNew || isReplacing) && input.Model.Card != nil && input.Model.AddressCountry != input.Model.Card.Country { - result.AddFieldFailure("addressCountry", "Country doesn't match with card issue country.") - } else if isUpdate && input.Model.AddressCountry != current.CardCountry { - result.AddFieldFailure("addressCountry", "Country doesn't match with card issue country.") - } - - if isReplacing || isUpdate { - prevIsEU := vat.IsEU(current.AddressCountry) - nextIsEU := vat.IsEU(input.Model.AddressCountry) - if prevIsEU != nextIsEU { - result.AddFieldFailure("currency", "Billing currency cannot be changed.") - } - } - } - - if input.Model.VATNumber != "" && vat.IsEU(input.Model.AddressCountry) && (isNew || input.Model.VATNumber != current.VATNumber) { - valid, euCC := vat.ValidateNumberFormat(input.Model.VATNumber) - if !valid { - result.AddFieldFailure("vatNumber", "VAT Number is an invalid format.") - } else if euCC != input.Model.AddressCountry { - result.AddFieldFailure("vatNumber", "VAT Number doesn't match with selected country.") - } else { - resp, err := vat.Query(input.Model.VATNumber) - if err != nil { - if err == vat.ErrInvalidVATNumberFormat { - result.AddFieldFailure("vatNumber", "VAT Number is an invalid format.") - } else { - log.Error(ctx, errors.Wrap(err, "failed to validate VAT Number '%s'", input.Model.VATNumber)) - result.AddFieldFailure("vatNumber", "We couldn't validate your VAT Number right now, please try again soon.") - } - } - if !resp.IsValid { - result.AddFieldFailure("vatNumber", "VAT Number is invalid.") - } - } - } - - return result -} diff --git a/app/actions/billing_test.go b/app/actions/billing_test.go deleted file mode 100644 index 3a0babe05..000000000 --- a/app/actions/billing_test.go +++ /dev/null @@ -1,131 +0,0 @@ -package actions_test - -import ( - "context" - "testing" - - "github.com/getfider/fider/app/services/billing" - - "github.com/getfider/fider/app" - - "github.com/getfider/fider/app/actions" - "github.com/getfider/fider/app/models" - "github.com/getfider/fider/app/models/dto" - . "github.com/getfider/fider/app/pkg/assert" - "github.com/getfider/fider/app/pkg/bus" -) - -func TestCreateEditBillingPaymentInfo_InvalidInput(t *testing.T) { - RegisterT(t) - bus.Init(billing.Service{}) - - testCases := []struct { - expected []string - input *dto.CreateEditBillingPaymentInfo - }{ - { - expected: []string{"card", "name", "email", "addressLine1", "addressLine2", "addressCity", "addressPostalCode", "addressCountry"}, - input: &dto.CreateEditBillingPaymentInfo{}, - }, - { - expected: []string{"card", "email", "addressCity", "addressPostalCode", "addressCountry"}, - input: &dto.CreateEditBillingPaymentInfo{ - Name: "John", - AddressLine1: "Street 1", - AddressLine2: "Street 2", - Email: "jo@a", - AddressCountry: "PP", - }, - }, - { - expected: []string{"card", "email", "addressCity", "addressPostalCode", "addressCountry"}, - input: &dto.CreateEditBillingPaymentInfo{ - Name: "John", - AddressLine1: "Street 1", - AddressLine2: "Street 2", - Email: "jo@a", - AddressCountry: "US", - Card: &dto.CreateEditBillingPaymentInfoCard{ - Country: "IE", - }, - }, - }, - } - - for _, testCase := range testCases { - action := &actions.CreateEditBillingPaymentInfo{ - Model: testCase.input, - } - ctx := context.WithValue(context.Background(), app.TenantCtxKey, &models.Tenant{ID: 2}) - result := action.Validate(ctx, nil) - ExpectFailed(result, testCase.expected...) - } -} - -func TestCreateEditBillingPaymentInfo_ValidInput(t *testing.T) { - RegisterT(t) - bus.Init(billing.Service{}) - - action := &actions.CreateEditBillingPaymentInfo{ - Model: &dto.CreateEditBillingPaymentInfo{ - Name: "Jon Snow", - AddressLine1: "Street 1", - AddressLine2: "Street 2", - AddressCity: "New York", - AddressPostalCode: "12345", - AddressState: "NY", - Email: "jon.show@got.com", - AddressCountry: "US", - Card: &dto.CreateEditBillingPaymentInfoCard{ - Token: "tok_visa", - Country: "US", - }, - }, - } - ctx := context.WithValue(context.Background(), app.TenantCtxKey, &models.Tenant{ID: 2}) - result := action.Validate(ctx, nil) - ExpectSuccess(result) -} - -func TestCreateEditBillingPaymentInfo_VATNumber(t *testing.T) { - RegisterT(t) - bus.Init(billing.Service{}) - - ctx := context.WithValue(context.Background(), app.TenantCtxKey, &models.Tenant{ID: 2}) - - action := &actions.CreateEditBillingPaymentInfo{ - Model: &dto.CreateEditBillingPaymentInfo{ - Name: "Jon Snow", - AddressLine1: "Street 1", - AddressLine2: "Street 2", - AddressCity: "New York", - AddressPostalCode: "12345", - AddressState: "NY", - Email: "jon.show@got.com", - AddressCountry: "IE", - VATNumber: "IE0", - Card: &dto.CreateEditBillingPaymentInfoCard{ - Token: "tok_visa", - Country: "IE", - }, - }, - } - result := action.Validate(ctx, nil) - ExpectFailed(result, "vatNumber") - - action.Model.VATNumber = "GB270600730" - result = action.Validate(ctx, nil) - ExpectFailed(result, "vatNumber") - - action.Model.VATNumber = "IE6388047A" - result = action.Validate(ctx, nil) - ExpectFailed(result, "vatNumber") - - action.Model.VATNumber = "IE6388047V" - result = action.Validate(ctx, nil) - ExpectSuccess(result) - - action.Model.VATNumber = "" - result = action.Validate(ctx, nil) - ExpectSuccess(result) -} diff --git a/app/cmd/routes.go b/app/cmd/routes.go index 0d3c0abe5..9686c4231 100644 --- a/app/cmd/routes.go +++ b/app/cmd/routes.go @@ -159,14 +159,6 @@ func routes(r *web.Engine) *web.Engine { ui.Post("/_api/admin/roles/:role/users", handlers.ChangeUserRole()) ui.Put("/_api/admin/users/:userID/block", handlers.BlockUser()) ui.Delete("/_api/admin/users/:userID/block", handlers.UnblockUser()) - - ui.Use(middlewares.RequireBillingEnabled()) - - ui.Get("/admin/billing", handlers.BillingPage()) - ui.Get("/_api/admin/billing/plans/:countryCode", handlers.GetBillingPlans()) - ui.Post("/_api/admin/billing/paymentinfo", handlers.UpdatePaymentInfo()) - ui.Post("/_api/admin/billing/subscription/:planID", handlers.BillingSubscribe()) - ui.Delete("/_api/admin/billing/subscription/:planID", handlers.CancelBillingSubscription()) } api := r.Group() diff --git a/app/cmd/server.go b/app/cmd/server.go index d25be59c6..7a2c6df88 100644 --- a/app/cmd/server.go +++ b/app/cmd/server.go @@ -14,7 +14,6 @@ import ( "github.com/getfider/fider/app/pkg/log" "github.com/getfider/fider/app/pkg/web" - _ "github.com/getfider/fider/app/services/billing" _ "github.com/getfider/fider/app/services/blob/fs" _ "github.com/getfider/fider/app/services/blob/s3" _ "github.com/getfider/fider/app/services/blob/sql" diff --git a/app/handlers/billing.go b/app/handlers/billing.go deleted file mode 100644 index efedfe288..000000000 --- a/app/handlers/billing.go +++ /dev/null @@ -1,161 +0,0 @@ -package handlers - -import ( - "github.com/getfider/fider/app/actions" - "github.com/getfider/fider/app/models/cmd" - "github.com/getfider/fider/app/models/query" - "github.com/getfider/fider/app/pkg/bus" - "github.com/getfider/fider/app/pkg/web" -) - -// BillingPage is the billing settings page -func BillingPage() web.HandlerFunc { - return func(c *web.Context) error { - if c.Tenant().Billing.StripeCustomerID == "" { - if err := bus.Dispatch(c, &cmd.CreateBillingCustomer{}); err != nil { - return c.Failure(err) - } - - if err := bus.Dispatch(c, &cmd.UpdateTenantBillingSettings{ - Settings: c.Tenant().Billing, - }); err != nil { - return c.Failure(err) - } - } - - getUpcomingInvoiceQuery := &query.GetUpcomingInvoice{} - if c.Tenant().Billing.StripeSubscriptionID != "" { - err := bus.Dispatch(c, getUpcomingInvoiceQuery) - if err != nil { - return c.Failure(err) - } - } - - paymentInfo := &query.GetPaymentInfo{} - err := bus.Dispatch(c, paymentInfo) - if err != nil { - return c.Failure(err) - } - - listPlansQuery := &query.ListBillingPlans{} - if paymentInfo.Result != nil { - listPlansQuery.CountryCode = paymentInfo.Result.AddressCountry - err = bus.Dispatch(c, listPlansQuery) - if err != nil { - return c.Failure(err) - } - } - - countUsers := &query.CountUsers{} - allCountries := &query.GetAllCountries{} - if err := bus.Dispatch(c, countUsers); err != nil { - return c.Failure(err) - } - - return c.Page(web.Props{ - Title: "Billing · Site Settings", - ChunkName: "Billing.page", - Data: web.Map{ - "invoiceDue": getUpcomingInvoiceQuery.Result, - "tenantUserCount": countUsers.Result, - "plans": listPlansQuery.Result, - "paymentInfo": paymentInfo.Result, - "countries": allCountries.Result, - }, - }) - } -} - -// UpdatePaymentInfo on stripe based on given input -func UpdatePaymentInfo() web.HandlerFunc { - return func(c *web.Context) error { - input := new(actions.CreateEditBillingPaymentInfo) - if result := c.BindTo(input); !result.Ok { - return c.HandleValidation(result) - } - - if err := bus.Dispatch(c, &cmd.UpdatePaymentInfo{Input: input.Model}); err != nil { - return c.Failure(err) - } - - return c.Ok(web.Map{}) - } -} - -// GetBillingPlans returns a list of plans for given country code -func GetBillingPlans() web.HandlerFunc { - return func(c *web.Context) error { - countryCode := c.Param("countryCode") - listPlansQuery := &query.ListBillingPlans{CountryCode: countryCode} - err := bus.Dispatch(c, listPlansQuery) - if err != nil { - return c.Failure(err) - } - return c.Ok(listPlansQuery.Result) - } -} - -// BillingSubscribe subscribes current tenant to given plan on stripe -func BillingSubscribe() web.HandlerFunc { - return func(c *web.Context) error { - planID := c.Param("planID") - - paymentInfoQuery := &query.GetPaymentInfo{} - err := bus.Dispatch(c, paymentInfoQuery) - if err != nil { - return c.Failure(err) - } - - getPlanByIDQuery := &query.GetBillingPlanByID{ - PlanID: planID, - CountryCode: paymentInfoQuery.Result.AddressCountry, - } - err = bus.Dispatch(c, getPlanByIDQuery) - if err != nil { - return c.Failure(err) - } - plan := getPlanByIDQuery.Result - - countUsers := &query.CountUsers{} - err = bus.Dispatch(c, countUsers) - if err != nil { - return c.Failure(err) - } - - if plan.MaxUsers > 0 && countUsers.Result > plan.MaxUsers { - return c.Unauthorized() - } - - if err = bus.Dispatch(c, &cmd.CreateBillingSubscription{ - PlanID: plan.ID, - }); err != nil { - return c.Failure(err) - } - - updateBilling := &cmd.UpdateTenantBillingSettings{Settings: c.Tenant().Billing} - activateTenant := &cmd.ActivateTenant{TenantID: c.Tenant().ID} - if err := bus.Dispatch(c, updateBilling, activateTenant); err != nil { - return c.Failure(err) - } - - return c.Ok(web.Map{}) - } -} - -// CancelBillingSubscription cancels current subscription from current tenant -func CancelBillingSubscription() web.HandlerFunc { - return func(c *web.Context) error { - err := bus.Dispatch(c, &cmd.CancelBillingSubscription{}) - if err != nil { - return c.Failure(err) - } - - if err := bus.Dispatch(c, &cmd.UpdateTenantBillingSettings{ - Settings: c.Tenant().Billing, - }); err != nil { - return c.Failure(err) - } - - return c.Ok(web.Map{}) - } -} diff --git a/app/middlewares/common.go b/app/middlewares/common.go deleted file mode 100644 index b4bf54d35..000000000 --- a/app/middlewares/common.go +++ /dev/null @@ -1,18 +0,0 @@ -package middlewares - -import ( - "github.com/getfider/fider/app/pkg/env" - "github.com/getfider/fider/app/pkg/web" -) - -// RequireBillingEnabled returns 404 if billing is not enabled, otherwise it continues the chain -func RequireBillingEnabled() web.MiddlewareFunc { - return func(next web.HandlerFunc) web.HandlerFunc { - return func(c *web.Context) error { - if !env.IsBillingEnabled() || c.Tenant().Billing == nil { - return c.NotFound() - } - return next(c) - } - } -} diff --git a/app/middlewares/security_test.go b/app/middlewares/security_test.go index a786f70de..efd5c9fd7 100644 --- a/app/middlewares/security_test.go +++ b/app/middlewares/security_test.go @@ -23,7 +23,7 @@ func TestSecureWithoutCDN(t *testing.T) { return c.NoContent(http.StatusOK) }) - expectedPolicy := "base-uri 'self'; default-src 'self'; style-src 'self' 'nonce-" + ctxID + "' https://fonts.googleapis.com ; script-src 'self' 'nonce-" + ctxID + "' https://js.stripe.com https://www.google-analytics.com ; img-src 'self' https: data: ; font-src 'self' https://fonts.gstatic.com data: ; object-src 'none'; media-src 'none'; connect-src 'self' https://www.google-analytics.com https://ipinfo.io https://js.stripe.com ; frame-src 'self' https://js.stripe.com" + expectedPolicy := "base-uri 'self'; default-src 'self'; style-src 'self' 'nonce-" + ctxID + "' https://fonts.googleapis.com ; script-src 'self' 'nonce-" + ctxID + "' https://www.google-analytics.com ; img-src 'self' https: data: ; font-src 'self' https://fonts.gstatic.com data: ; object-src 'none'; media-src 'none'; connect-src 'self' https://www.google-analytics.com ; frame-src 'self'" Expect(status).Equals(http.StatusOK) Expect(response.Header().Get("Content-Security-Policy")).Equals(expectedPolicy) @@ -46,7 +46,7 @@ func TestSecureWithCDN(t *testing.T) { return c.NoContent(http.StatusOK) }) - expectedPolicy := "base-uri 'self'; default-src 'self'; style-src 'self' 'nonce-" + ctxID + "' https://fonts.googleapis.com *.test.fider.io; script-src 'self' 'nonce-" + ctxID + "' https://js.stripe.com https://www.google-analytics.com *.test.fider.io; img-src 'self' https: data: *.test.fider.io; font-src 'self' https://fonts.gstatic.com data: *.test.fider.io; object-src 'none'; media-src 'none'; connect-src 'self' https://www.google-analytics.com https://ipinfo.io https://js.stripe.com *.test.fider.io; frame-src 'self' https://js.stripe.com" + expectedPolicy := "base-uri 'self'; default-src 'self'; style-src 'self' 'nonce-" + ctxID + "' https://fonts.googleapis.com *.test.fider.io; script-src 'self' 'nonce-" + ctxID + "' https://www.google-analytics.com *.test.fider.io; img-src 'self' https: data: *.test.fider.io; font-src 'self' https://fonts.gstatic.com data: *.test.fider.io; object-src 'none'; media-src 'none'; connect-src 'self' https://www.google-analytics.com *.test.fider.io; frame-src 'self'" Expect(status).Equals(http.StatusOK) Expect(response.Header().Get("Content-Security-Policy")).Equals(expectedPolicy) @@ -69,7 +69,7 @@ func TestSecureWithCDN_SingleHost(t *testing.T) { return c.NoContent(http.StatusOK) }) - expectedPolicy := "base-uri 'self'; default-src 'self'; style-src 'self' 'nonce-" + ctxID + "' https://fonts.googleapis.com test.fider.io; script-src 'self' 'nonce-" + ctxID + "' https://js.stripe.com https://www.google-analytics.com test.fider.io; img-src 'self' https: data: test.fider.io; font-src 'self' https://fonts.gstatic.com data: test.fider.io; object-src 'none'; media-src 'none'; connect-src 'self' https://www.google-analytics.com https://ipinfo.io https://js.stripe.com test.fider.io; frame-src 'self' https://js.stripe.com" + expectedPolicy := "base-uri 'self'; default-src 'self'; style-src 'self' 'nonce-" + ctxID + "' https://fonts.googleapis.com test.fider.io; script-src 'self' 'nonce-" + ctxID + "' https://www.google-analytics.com test.fider.io; img-src 'self' https: data: test.fider.io; font-src 'self' https://fonts.gstatic.com data: test.fider.io; object-src 'none'; media-src 'none'; connect-src 'self' https://www.google-analytics.com test.fider.io; frame-src 'self'" Expect(status).Equals(http.StatusOK) Expect(response.Header().Get("Content-Security-Policy")).Equals(expectedPolicy) diff --git a/app/models/cmd/billing.go b/app/models/cmd/billing.go deleted file mode 100644 index 77fba7a23..000000000 --- a/app/models/cmd/billing.go +++ /dev/null @@ -1,23 +0,0 @@ -package cmd - -import "github.com/getfider/fider/app/models/dto" - -type CancelBillingSubscription struct { -} - -type CreateBillingSubscription struct { - PlanID string -} - -type CreateBillingCustomer struct { -} - -type DeleteBillingCustomer struct { -} - -type ClearPaymentInfo struct { -} - -type UpdatePaymentInfo struct { - Input *dto.CreateEditBillingPaymentInfo -} diff --git a/app/models/cmd/tenant.go b/app/models/cmd/tenant.go index 9dd031831..a726ca506 100644 --- a/app/models/cmd/tenant.go +++ b/app/models/cmd/tenant.go @@ -22,10 +22,6 @@ type UpdateTenantSettings struct { Settings *models.UpdateTenantSettings } -type UpdateTenantBillingSettings struct { - Settings *models.TenantBilling -} - type UpdateTenantAdvancedSettings struct { Settings *models.UpdateTenantAdvancedSettings } diff --git a/app/models/dto/billing.go b/app/models/dto/billing.go deleted file mode 100644 index 5a19563a7..000000000 --- a/app/models/dto/billing.go +++ /dev/null @@ -1,68 +0,0 @@ -package dto - -import "time" - -// Country is a valid country within Fider -type Country struct { - Code string `json:"code"` - Name string `json:"name"` - IsEU bool `json:"isEU"` -} - -// BillingPlan is the model for billing plan from Stripe -type BillingPlan struct { - ID string `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - Currency string `json:"currency"` - MaxUsers int `json:"maxUsers"` - Price int64 `json:"price"` - Interval string `json:"interval"` -} - -// UpcomingInvoice is the model for upcoming invoice from Stripe -type UpcomingInvoice struct { - Currency string `json:"currency"` - DueDate time.Time `json:"dueDate"` - AmountDue int64 `json:"amountDue"` -} - -// PaymentInfo is the model for billing payment info -type PaymentInfo struct { - StripeCardID string `json:"-"` - CardCountry string `json:"cardCountry"` - CardBrand string `json:"cardBrand"` - CardLast4 string `json:"cardLast4"` - CardExpMonth uint8 `json:"cardExpMonth"` - CardExpYear uint16 `json:"cardExpYear"` - AddressCity string `json:"addressCity"` - AddressCountry string `json:"addressCountry"` - Name string `json:"name"` - Email string `json:"email"` - AddressLine1 string `json:"addressLine1"` - AddressLine2 string `json:"addressLine2"` - AddressState string `json:"addressState"` - AddressPostalCode string `json:"addressPostalCode"` - VATNumber string `json:"vatNumber"` -} - -// CreateEditBillingPaymentInfo is the input model to create or edit billing payment info -type CreateEditBillingPaymentInfo struct { - Name string `json:"name"` - Email string `json:"email"` - Card *CreateEditBillingPaymentInfoCard `json:"card"` - AddressLine1 string `json:"addressLine1"` - AddressLine2 string `json:"addressLine2"` - AddressCity string `json:"addressCity"` - AddressState string `json:"addressState"` - AddressPostalCode string `json:"addressPostalCode"` - AddressCountry string `json:"addressCountry" format:"upper"` - VATNumber string `json:"vatNumber"` -} - -// CreateEditBillingPaymentInfoCard is the input model for a card during billing payment info update -type CreateEditBillingPaymentInfoCard struct { - Type string `json:"type"` - Token string `json:"token"` - Country string `json:"country"` -} diff --git a/app/models/identity.go b/app/models/identity.go index ce4d0d7f3..a8ae48f09 100644 --- a/app/models/identity.go +++ b/app/models/identity.go @@ -10,26 +10,16 @@ import ( //Tenant represents a tenant type Tenant struct { - ID int `json:"id"` - Name string `json:"name"` - Subdomain string `json:"subdomain"` - Invitation string `json:"invitation"` - WelcomeMessage string `json:"welcomeMessage"` - CNAME string `json:"cname"` - Status int `json:"status"` - IsPrivate bool `json:"isPrivate"` - LogoBlobKey string `json:"logoBlobKey"` - Billing *TenantBilling `json:"billing,omitempty"` - CustomCSS string `json:"-"` -} - -//TenantBilling has all the billing information of given tenant -type TenantBilling struct { - StripeCustomerID string - StripeSubscriptionID string - StripePlanID string `json:"stripePlanID"` - TrialEndsAt time.Time `json:"trialEndsAt"` - SubscriptionEndsAt *time.Time `json:"subscriptionEndsAt,omitempty"` + ID int `json:"id"` + Name string `json:"name"` + Subdomain string `json:"subdomain"` + Invitation string `json:"invitation"` + WelcomeMessage string `json:"welcomeMessage"` + CNAME string `json:"cname"` + Status int `json:"status"` + IsPrivate bool `json:"isPrivate"` + LogoBlobKey string `json:"logoBlobKey"` + CustomCSS string `json:"-"` } //Upload represents a file that has been uploaded to Fider diff --git a/app/models/query/billing.go b/app/models/query/billing.go deleted file mode 100644 index 78bd4c099..000000000 --- a/app/models/query/billing.go +++ /dev/null @@ -1,34 +0,0 @@ -package query - -import "github.com/getfider/fider/app/models/dto" - -type ListBillingPlans struct { - CountryCode string - - Result []*dto.BillingPlan -} - -type GetBillingPlanByID struct { - PlanID string - CountryCode string - - Result *dto.BillingPlan -} - -type GetUpcomingInvoice struct { - Result *dto.UpcomingInvoice -} - -type GetPaymentInfo struct { - Result *dto.PaymentInfo -} - -type GetAllCountries struct { - Result []*dto.Country -} - -type GetCountryByCode struct { - Code string - - Result *dto.Country -} diff --git a/app/pkg/env/env.go b/app/pkg/env/env.go index f1f751a8a..b777db800 100644 --- a/app/pkg/env/env.go +++ b/app/pkg/env/env.go @@ -31,10 +31,6 @@ type config struct { MaxIdleConns int `env:"DATABASE_MAX_IDLE_CONNS,default=2,strict"` MaxOpenConns int `env:"DATABASE_MAX_OPEN_CONNS,default=4,strict"` } - Stripe struct { - SecretKey string `env:"STRIPE_SECRET_KEY"` - PublicKey string `env:"STRIPE_PUBLIC_KEY"` - } CDN struct { Host string `env:"CDN_HOST"` } @@ -129,11 +125,6 @@ func mustBeSet(name string) { } } -// IsBillingEnabled returns true if billing is enabled -func IsBillingEnabled() bool { - return Config.Stripe.SecretKey != "" -} - // IsSingleHostMode returns true if host mode is set to single tenant func IsSingleHostMode() bool { return Config.HostMode == "single" diff --git a/app/pkg/env/env_test.go b/app/pkg/env/env_test.go index 5cc193797..ecebb07b7 100644 --- a/app/pkg/env/env_test.go +++ b/app/pkg/env/env_test.go @@ -50,16 +50,6 @@ func TestMultiTenantDomain(t *testing.T) { Expect(env.MultiTenantDomain()).Equals(".fider.io") } -func TestIsBillingEnbled(t *testing.T) { - RegisterT(t) - - env.Config.Stripe.SecretKey = "" - env.Config.Stripe.PublicKey = "pk_111" - Expect(env.IsBillingEnabled()).IsFalse() - env.Config.Stripe.SecretKey = "sk_1234" - Expect(env.IsBillingEnabled()).IsTrue() -} - func TestSubdomain(t *testing.T) { RegisterT(t) diff --git a/app/pkg/web/engine.go b/app/pkg/web/engine.go index b8a7eda8c..182fd113e 100644 --- a/app/pkg/web/engine.go +++ b/app/pkg/web/engine.go @@ -27,13 +27,13 @@ var ( cspBase = "base-uri 'self'" cspDefault = "default-src 'self'" cspStyle = "style-src 'self' 'nonce-%[1]s' https://fonts.googleapis.com %[2]s" - cspScript = "script-src 'self' 'nonce-%[1]s' https://js.stripe.com https://www.google-analytics.com %[2]s" + cspScript = "script-src 'self' 'nonce-%[1]s' https://www.google-analytics.com %[2]s" cspFont = "font-src 'self' https://fonts.gstatic.com data: %[2]s" cspImage = "img-src 'self' https: data: %[2]s" cspObject = "object-src 'none'" - cspFrame = "frame-src 'self' https://js.stripe.com" + cspFrame = "frame-src 'self'" cspMedia = "media-src 'none'" - cspConnect = "connect-src 'self' https://www.google-analytics.com https://ipinfo.io https://js.stripe.com %[2]s" + cspConnect = "connect-src 'self' https://www.google-analytics.com %[2]s" //CspPolicyTemplate is the template used to generate the policy CspPolicyTemplate = fmt.Sprintf("%s; %s; %s; %s; %s; %s; %s; %s; %s; %s", cspBase, cspDefault, cspStyle, cspScript, cspImage, cspFont, cspObject, cspMedia, cspConnect, cspFrame) diff --git a/app/pkg/web/renderer.go b/app/pkg/web/renderer.go index 9251ceac2..1ef067c82 100644 --- a/app/pkg/web/renderer.go +++ b/app/pkg/web/renderer.go @@ -222,7 +222,6 @@ func (r *Renderer) Render(w io.Writer, statusCode int, name string, props Props, "environment": r.settings.Environment, "compiler": r.settings.Compiler, "googleAnalytics": r.settings.GoogleAnalytics, - "stripePublicKey": env.Config.Stripe.PublicKey, "domain": r.settings.Domain, "hasLegal": r.settings.HasLegal, "baseURL": ctx.BaseURL(), diff --git a/app/pkg/web/testdata/basic.html b/app/pkg/web/testdata/basic.html index fb417ce12..dd5fa6984 100755 --- a/app/pkg/web/testdata/basic.html +++ b/app/pkg/web/testdata/basic.html @@ -37,7 +37,7 @@

Please enable JavaScript

diff --git a/app/pkg/web/testdata/canonical.html b/app/pkg/web/testdata/canonical.html index 2dc415d13..175552dc0 100755 --- a/app/pkg/web/testdata/canonical.html +++ b/app/pkg/web/testdata/canonical.html @@ -39,7 +39,7 @@

Please enable JavaScript

diff --git a/app/pkg/web/testdata/chunk.html b/app/pkg/web/testdata/chunk.html index 15ca3765c..ad1035af7 100644 --- a/app/pkg/web/testdata/chunk.html +++ b/app/pkg/web/testdata/chunk.html @@ -44,7 +44,7 @@

Please enable JavaScript

diff --git a/app/pkg/web/testdata/oauth.html b/app/pkg/web/testdata/oauth.html index bea6b4f33..85710d90e 100755 --- a/app/pkg/web/testdata/oauth.html +++ b/app/pkg/web/testdata/oauth.html @@ -37,7 +37,7 @@

Please enable JavaScript

diff --git a/app/pkg/web/testdata/props.html b/app/pkg/web/testdata/props.html index 6a0c2f735..1c850da44 100755 --- a/app/pkg/web/testdata/props.html +++ b/app/pkg/web/testdata/props.html @@ -37,7 +37,7 @@

Please enable JavaScript

diff --git a/app/pkg/web/testdata/tenant.html b/app/pkg/web/testdata/tenant.html index 94c24161f..30aedb21b 100755 --- a/app/pkg/web/testdata/tenant.html +++ b/app/pkg/web/testdata/tenant.html @@ -37,7 +37,7 @@

Please enable JavaScript

diff --git a/app/pkg/web/testdata/user.html b/app/pkg/web/testdata/user.html index 6a2a955a8..36040225c 100755 --- a/app/pkg/web/testdata/user.html +++ b/app/pkg/web/testdata/user.html @@ -37,7 +37,7 @@

Please enable JavaScript

diff --git a/app/services/billing/billing.go b/app/services/billing/billing.go deleted file mode 100644 index a2ddbaf85..000000000 --- a/app/services/billing/billing.go +++ /dev/null @@ -1,56 +0,0 @@ -package billing - -import ( - "context" - - "github.com/getfider/fider/app" - "github.com/getfider/fider/app/models" - "github.com/getfider/fider/app/pkg/bus" - "github.com/getfider/fider/app/pkg/env" - "github.com/stripe/stripe-go" - "github.com/stripe/stripe-go/client" -) - -var stripeClient *client.API - -func init() { - bus.Register(Service{}) -} - -type Service struct{} - -func (s Service) Name() string { - return "Stripe" -} - -func (s Service) Category() string { - return "billing" -} - -func (s Service) Enabled() bool { - return env.IsBillingEnabled() -} - -func (s Service) Init() { - stripe.DefaultLeveledLogger = &stripe.LeveledLogger{Level: 0} - stripeClient = &client.API{} - stripeClient.Init(env.Config.Stripe.SecretKey, nil) - - bus.AddHandler(listPlans) - bus.AddHandler(getPlanByID) - bus.AddHandler(cancelSubscription) - bus.AddHandler(subscribe) - bus.AddHandler(getUpcomingInvoice) - bus.AddHandler(createCustomer) - bus.AddHandler(deleteCustomer) - bus.AddHandler(getPaymentInfo) - bus.AddHandler(clearPaymentInfo) - bus.AddHandler(updatePaymentInfo) - bus.AddHandler(getAllCountries) - bus.AddHandler(getCountryByCode) -} - -func using(ctx context.Context, handler func(tenant *models.Tenant) error) error { - tenant, _ := ctx.Value(app.TenantCtxKey).(*models.Tenant) - return handler(tenant) -} diff --git a/app/services/billing/billing_test.go b/app/services/billing/billing_test.go deleted file mode 100644 index 55b30b781..000000000 --- a/app/services/billing/billing_test.go +++ /dev/null @@ -1,243 +0,0 @@ -package billing_test - -import ( - "context" - "testing" - "time" - - "github.com/getfider/fider/app" - - "github.com/getfider/fider/app/pkg/bus" - "github.com/getfider/fider/app/pkg/env" - - "github.com/getfider/fider/app/models" - "github.com/getfider/fider/app/models/cmd" - "github.com/getfider/fider/app/models/dto" - "github.com/getfider/fider/app/models/query" - . "github.com/getfider/fider/app/pkg/assert" - "github.com/getfider/fider/app/services/billing" -) - -func TestCreateCustomer_WithSubscription(t *testing.T) { - RegisterT(t) - if !env.IsBillingEnabled() { - return - } - - bus.Init(billing.Service{}) - - tenant := &models.Tenant{ - ID: 2, - Name: "Game Inc.", - Subdomain: "gameinc", - Billing: &models.TenantBilling{}, - } - ctx := context.WithValue(context.Background(), app.TenantCtxKey, tenant) - - err := bus.Dispatch(ctx, &cmd.CreateBillingCustomer{}) - Expect(err).IsNil() - Expect(tenant.Billing.StripeCustomerID).IsNotEmpty() - - err = bus.Dispatch(ctx, &cmd.UpdatePaymentInfo{ - Input: &dto.CreateEditBillingPaymentInfo{ - Email: "jon.snow@got.com", - Card: &dto.CreateEditBillingPaymentInfoCard{ - Token: "tok_visa", - }, - }, - }) - Expect(err).IsNil() - - createSubscription := &cmd.CreateBillingSubscription{PlanID: "plan_EKTT1YWe1Zmrtp"} - err = bus.Dispatch(ctx, createSubscription) - Expect(err).IsNil() - Expect(tenant.Billing.StripeSubscriptionID).IsNotEmpty() - Expect(tenant.Billing.StripePlanID).Equals("plan_EKTT1YWe1Zmrtp") - - invoiceQuery := &query.GetUpcomingInvoice{} - err = bus.Dispatch(ctx, invoiceQuery) - Expect(err).IsNil() - Expect(int(invoiceQuery.Result.AmountDue)).Equals(900) - Expect(invoiceQuery.Result.Currency).Equals("USD") - - err = bus.Dispatch(ctx, &cmd.CancelBillingSubscription{}) - Expect(err).IsNil() - Expect(tenant.Billing.SubscriptionEndsAt).IsNotNil() - - err = bus.Dispatch(ctx, &cmd.DeleteBillingCustomer{}) - Expect(err).IsNil() -} - -var forUnitTestingTenant = &models.Tenant{ - ID: 5, - Name: "For Unit Testing (DO NOT DELETE)", - Subdomain: "unittesting", - Billing: &models.TenantBilling{ - StripeCustomerID: "cus_EICBuXBIkhI2EV", - }, -} - -func TestUpdatePaymentInfo(t *testing.T) { - RegisterT(t) - if !env.IsBillingEnabled() { - return - } - - bus.Init(billing.Service{}) - - var firstCardID string - - ctx := context.WithValue(context.Background(), app.TenantCtxKey, forUnitTestingTenant) - - err := bus.Dispatch(ctx, &cmd.ClearPaymentInfo{}) - Expect(err).IsNil() - - //Creating a new card - err = bus.Dispatch(ctx, &cmd.UpdatePaymentInfo{ - Input: &dto.CreateEditBillingPaymentInfo{ - Email: "jon.snow@got.com", - VATNumber: "IE1234", - Card: &dto.CreateEditBillingPaymentInfoCard{ - Token: "tok_visa", - }, - AddressCountry: "IE", - }, - }) - Expect(err).IsNil() - - paymentInfoQuery := &query.GetPaymentInfo{} - err = bus.Dispatch(ctx, paymentInfoQuery) - Expect(err).IsNil() - info := paymentInfoQuery.Result - - firstCardID = info.StripeCardID - - Expect(info.StripeCardID).IsNotEmpty() - Expect(info.Email).Equals("jon.snow@got.com") - Expect(info.VATNumber).Equals("IE1234") - Expect(info.CardBrand).Equals("Visa") - Expect(info.CardCountry).Equals("US") - Expect(info.CardLast4).Equals("4242") - Expect(int(info.CardExpMonth)).Equals(int(time.Now().Month())) - Expect(int(info.CardExpYear)).Equals(time.Now().Year() + 1) - Expect(info.Name).Equals("") - Expect(info.AddressLine1).Equals("") - Expect(info.AddressLine2).Equals("") - Expect(info.AddressCity).Equals("") - Expect(info.AddressState).Equals("") - Expect(info.AddressPostalCode).Equals("") - Expect(info.AddressCountry).Equals("") - - //Update existing card - err = bus.Dispatch(ctx, &cmd.UpdatePaymentInfo{ - Input: &dto.CreateEditBillingPaymentInfo{ - Email: "jon.snow@got.com", - Name: "Jon Snow", - AddressLine1: "Street 1", - AddressLine2: "Av. ABC", - AddressCity: "New York", - AddressState: "NYC", - AddressPostalCode: "12098", - AddressCountry: "US", - }, - }) - Expect(err).IsNil() - - paymentInfoQuery = &query.GetPaymentInfo{} - err = bus.Dispatch(ctx, paymentInfoQuery) - Expect(err).IsNil() - info = paymentInfoQuery.Result - - Expect(info.Name).Equals("Jon Snow") - Expect(info.VATNumber).Equals("") - Expect(info.CardLast4).Equals("4242") - Expect(info.AddressLine1).Equals("Street 1") - Expect(info.AddressLine2).Equals("Av. ABC") - Expect(info.AddressCity).Equals("New York") - Expect(info.AddressState).Equals("NYC") - Expect(info.AddressPostalCode).Equals("12098") - Expect(info.AddressCountry).Equals("US") - - //Replace card - err = bus.Dispatch(ctx, &cmd.UpdatePaymentInfo{ - Input: &dto.CreateEditBillingPaymentInfo{ - Email: "jon.snow@got.com", - Card: &dto.CreateEditBillingPaymentInfoCard{ - Token: "tok_br", - }, - }, - }) - Expect(err).IsNil() - - paymentInfoQuery = &query.GetPaymentInfo{} - err = bus.Dispatch(ctx, paymentInfoQuery) - Expect(err).IsNil() - info = paymentInfoQuery.Result - - Expect(info.StripeCardID).IsNotEmpty() - Expect(info.StripeCardID).NotEquals(firstCardID) - Expect(info.Email).Equals("jon.snow@got.com") - Expect(info.VATNumber).Equals("") - Expect(info.CardBrand).Equals("Visa") - Expect(info.CardCountry).Equals("BR") - Expect(info.CardLast4).Equals("0002") - Expect(int(info.CardExpMonth)).Equals(int(time.Now().Month())) - Expect(int(info.CardExpYear)).Equals(time.Now().Year() + 1) - Expect(info.Name).Equals("") - Expect(info.AddressLine1).Equals("") - Expect(info.AddressLine2).Equals("") - Expect(info.AddressCity).Equals("") - Expect(info.AddressState).Equals("") - Expect(info.AddressPostalCode).Equals("") - Expect(info.AddressCountry).Equals("") -} - -func TestListPlans(t *testing.T) { - RegisterT(t) - if !env.IsBillingEnabled() { - return - } - - bus.Init(billing.Service{}) - ctx := context.Background() - - q := &query.ListBillingPlans{CountryCode: "US"} - err := bus.Dispatch(ctx, q) - Expect(err).IsNil() - Expect(q.Result).HasLen(3) - - Expect(q.Result[0].ID).Equals("plan_EKTT1YWe1Zmrtp") - Expect(q.Result[0].Name).Equals("Starter") - Expect(q.Result[0].Currency).Equals("USD") - Expect(q.Result[0].MaxUsers).Equals(200) - - Expect(q.Result[1].ID).Equals("plan_DoK187GZcnFpKY") - Expect(q.Result[1].Name).Equals("Business (monthly)") - Expect(q.Result[1].Currency).Equals("USD") - Expect(q.Result[1].MaxUsers).Equals(0) - - Expect(q.Result[2].ID).Equals("plan_DpN9SkJMjNTvLd") - Expect(q.Result[2].Name).Equals("Business (yearly)") - Expect(q.Result[2].Currency).Equals("USD") - Expect(q.Result[2].MaxUsers).Equals(0) - - q = &query.ListBillingPlans{CountryCode: "DE"} - err = bus.Dispatch(ctx, q) - Expect(err).IsNil() - Expect(q.Result).HasLen(3) - - Expect(q.Result[0].ID).Equals("plan_EKTSnrGmj5BuKI") - Expect(q.Result[0].Name).Equals("Starter") - Expect(q.Result[0].Currency).Equals("EUR") - - Expect(q.Result[0].MaxUsers).Equals(200) - Expect(q.Result[1].ID).Equals("plan_EKPnahGhiTEnCc") - Expect(q.Result[1].Name).Equals("Business (monthly)") - Expect(q.Result[1].Currency).Equals("EUR") - Expect(q.Result[1].MaxUsers).Equals(0) - - Expect(q.Result[2].ID).Equals("plan_EKTU4xD7LNI9dO") - Expect(q.Result[2].Name).Equals("Business (yearly)") - Expect(q.Result[2].Currency).Equals("EUR") - Expect(q.Result[2].MaxUsers).Equals(0) -} diff --git a/app/services/billing/country.go b/app/services/billing/country.go deleted file mode 100644 index 7f456421e..000000000 --- a/app/services/billing/country.go +++ /dev/null @@ -1,278 +0,0 @@ -package billing - -import ( - "context" - - "github.com/getfider/fider/app" - - "github.com/getfider/fider/app/models/dto" - "github.com/getfider/fider/app/models/query" - "github.com/goenning/vat" -) - -func getAllCountries(ctx context.Context, q *query.GetAllCountries) error { - q.Result = allCountries - return nil -} - -func getCountryByCode(ctx context.Context, q *query.GetCountryByCode) error { - for _, c := range allCountries { - if c.Code == q.Code { - q.Result = c - return nil - } - } - return app.ErrNotFound -} - -var allCountries = []*dto.Country{ - &dto.Country{Code: "AF", Name: "Afghanistan", IsEU: vat.IsEU("AF")}, - &dto.Country{Code: "AX", Name: "Åland Islands", IsEU: vat.IsEU("AX")}, - &dto.Country{Code: "AL", Name: "Albania", IsEU: vat.IsEU("AL")}, - &dto.Country{Code: "DZ", Name: "Algeria", IsEU: vat.IsEU("DZ")}, - &dto.Country{Code: "AS", Name: "American Samoa", IsEU: vat.IsEU("AS")}, - &dto.Country{Code: "AD", Name: "Andorra", IsEU: vat.IsEU("AD")}, - &dto.Country{Code: "AO", Name: "Angola", IsEU: vat.IsEU("AO")}, - &dto.Country{Code: "AI", Name: "Anguilla", IsEU: vat.IsEU("AI")}, - &dto.Country{Code: "AQ", Name: "Antarctica", IsEU: vat.IsEU("AQ")}, - &dto.Country{Code: "AG", Name: "Antigua and Barbuda", IsEU: vat.IsEU("AG")}, - &dto.Country{Code: "AR", Name: "Argentina", IsEU: vat.IsEU("AR")}, - &dto.Country{Code: "AM", Name: "Armenia", IsEU: vat.IsEU("AM")}, - &dto.Country{Code: "AW", Name: "Aruba", IsEU: vat.IsEU("AW")}, - &dto.Country{Code: "AU", Name: "Australia", IsEU: vat.IsEU("AU")}, - &dto.Country{Code: "AT", Name: "Austria", IsEU: vat.IsEU("AT")}, - &dto.Country{Code: "AZ", Name: "Azerbaijan", IsEU: vat.IsEU("AZ")}, - &dto.Country{Code: "BS", Name: "Bahamas", IsEU: vat.IsEU("BS")}, - &dto.Country{Code: "BH", Name: "Bahrain", IsEU: vat.IsEU("BH")}, - &dto.Country{Code: "BD", Name: "Bangladesh", IsEU: vat.IsEU("BD")}, - &dto.Country{Code: "BB", Name: "Barbados", IsEU: vat.IsEU("BB")}, - &dto.Country{Code: "BY", Name: "Belarus", IsEU: vat.IsEU("BY")}, - &dto.Country{Code: "BE", Name: "Belgium", IsEU: vat.IsEU("BE")}, - &dto.Country{Code: "BZ", Name: "Belize", IsEU: vat.IsEU("BZ")}, - &dto.Country{Code: "BJ", Name: "Benin", IsEU: vat.IsEU("BJ")}, - &dto.Country{Code: "BM", Name: "Bermuda", IsEU: vat.IsEU("BM")}, - &dto.Country{Code: "BT", Name: "Bhutan", IsEU: vat.IsEU("BT")}, - &dto.Country{Code: "BO", Name: "Bolivia, Plurinational State of", IsEU: vat.IsEU("BO")}, - &dto.Country{Code: "BQ", Name: "Bonaire, Sint Eustatius and Saba", IsEU: vat.IsEU("BQ")}, - &dto.Country{Code: "BA", Name: "Bosnia and Herzegovina", IsEU: vat.IsEU("BA")}, - &dto.Country{Code: "BW", Name: "Botswana", IsEU: vat.IsEU("BW")}, - &dto.Country{Code: "BV", Name: "Bouvet Island", IsEU: vat.IsEU("BV")}, - &dto.Country{Code: "BR", Name: "Brazil", IsEU: vat.IsEU("BR")}, - &dto.Country{Code: "IO", Name: "British Indian Ocean Territory", IsEU: vat.IsEU("IO")}, - &dto.Country{Code: "BN", Name: "Brunei Darussalam", IsEU: vat.IsEU("BN")}, - &dto.Country{Code: "BG", Name: "Bulgaria", IsEU: vat.IsEU("BG")}, - &dto.Country{Code: "BF", Name: "Burkina Faso", IsEU: vat.IsEU("BF")}, - &dto.Country{Code: "BI", Name: "Burundi", IsEU: vat.IsEU("BI")}, - &dto.Country{Code: "KH", Name: "Cambodia", IsEU: vat.IsEU("KH")}, - &dto.Country{Code: "CM", Name: "Cameroon", IsEU: vat.IsEU("CM")}, - &dto.Country{Code: "CA", Name: "Canada", IsEU: vat.IsEU("CA")}, - &dto.Country{Code: "CV", Name: "Cape Verde", IsEU: vat.IsEU("CV")}, - &dto.Country{Code: "KY", Name: "Cayman Islands", IsEU: vat.IsEU("KY")}, - &dto.Country{Code: "CF", Name: "Central African Republic", IsEU: vat.IsEU("CF")}, - &dto.Country{Code: "TD", Name: "Chad", IsEU: vat.IsEU("TD")}, - &dto.Country{Code: "CL", Name: "Chile", IsEU: vat.IsEU("CL")}, - &dto.Country{Code: "CN", Name: "China", IsEU: vat.IsEU("CN")}, - &dto.Country{Code: "CX", Name: "Christmas Island", IsEU: vat.IsEU("CX")}, - &dto.Country{Code: "CC", Name: "Cocos (Keeling) Islands", IsEU: vat.IsEU("CC")}, - &dto.Country{Code: "CO", Name: "Colombia", IsEU: vat.IsEU("CO")}, - &dto.Country{Code: "KM", Name: "Comoros", IsEU: vat.IsEU("KM")}, - &dto.Country{Code: "CG", Name: "Congo", IsEU: vat.IsEU("CG")}, - &dto.Country{Code: "CD", Name: "Congo, the Democratic Republic of the", IsEU: vat.IsEU("CD")}, - &dto.Country{Code: "CK", Name: "Cook Islands", IsEU: vat.IsEU("CK")}, - &dto.Country{Code: "CR", Name: "Costa Rica", IsEU: vat.IsEU("CR")}, - &dto.Country{Code: "CI", Name: "Côte d'Ivoire", IsEU: vat.IsEU("CI")}, - &dto.Country{Code: "HR", Name: "Croatia", IsEU: vat.IsEU("HR")}, - &dto.Country{Code: "CU", Name: "Cuba", IsEU: vat.IsEU("CU")}, - &dto.Country{Code: "CW", Name: "Curaçao", IsEU: vat.IsEU("CW")}, - &dto.Country{Code: "CY", Name: "Cyprus", IsEU: vat.IsEU("CY")}, - &dto.Country{Code: "CZ", Name: "Czech Republic", IsEU: vat.IsEU("CZ")}, - &dto.Country{Code: "DK", Name: "Denmark", IsEU: vat.IsEU("DK")}, - &dto.Country{Code: "DJ", Name: "Djibouti", IsEU: vat.IsEU("DJ")}, - &dto.Country{Code: "DM", Name: "Dominica", IsEU: vat.IsEU("DM")}, - &dto.Country{Code: "DO", Name: "Dominican Republic", IsEU: vat.IsEU("DO")}, - &dto.Country{Code: "EC", Name: "Ecuador", IsEU: vat.IsEU("EC")}, - &dto.Country{Code: "EG", Name: "Egypt", IsEU: vat.IsEU("EG")}, - &dto.Country{Code: "SV", Name: "El Salvador", IsEU: vat.IsEU("SV")}, - &dto.Country{Code: "GQ", Name: "Equatorial Guinea", IsEU: vat.IsEU("GQ")}, - &dto.Country{Code: "ER", Name: "Eritrea", IsEU: vat.IsEU("ER")}, - &dto.Country{Code: "EE", Name: "Estonia", IsEU: vat.IsEU("EE")}, - &dto.Country{Code: "ET", Name: "Ethiopia", IsEU: vat.IsEU("ET")}, - &dto.Country{Code: "FK", Name: "Falkland Islands (Malvinas)", IsEU: vat.IsEU("FK")}, - &dto.Country{Code: "FO", Name: "Faroe Islands", IsEU: vat.IsEU("FO")}, - &dto.Country{Code: "FJ", Name: "Fiji", IsEU: vat.IsEU("FJ")}, - &dto.Country{Code: "FI", Name: "Finland", IsEU: vat.IsEU("FI")}, - &dto.Country{Code: "FR", Name: "France", IsEU: vat.IsEU("FR")}, - &dto.Country{Code: "GF", Name: "French Guiana", IsEU: vat.IsEU("GF")}, - &dto.Country{Code: "PF", Name: "French Polynesia", IsEU: vat.IsEU("PF")}, - &dto.Country{Code: "TF", Name: "French Southern Territories", IsEU: vat.IsEU("TF")}, - &dto.Country{Code: "GA", Name: "Gabon", IsEU: vat.IsEU("GA")}, - &dto.Country{Code: "GM", Name: "Gambia", IsEU: vat.IsEU("GM")}, - &dto.Country{Code: "GE", Name: "Georgia", IsEU: vat.IsEU("GE")}, - &dto.Country{Code: "DE", Name: "Germany", IsEU: vat.IsEU("DE")}, - &dto.Country{Code: "GH", Name: "Ghana", IsEU: vat.IsEU("GH")}, - &dto.Country{Code: "GI", Name: "Gibraltar", IsEU: vat.IsEU("GI")}, - &dto.Country{Code: "GR", Name: "Greece", IsEU: vat.IsEU("GR")}, - &dto.Country{Code: "GL", Name: "Greenland", IsEU: vat.IsEU("GL")}, - &dto.Country{Code: "GD", Name: "Grenada", IsEU: vat.IsEU("GD")}, - &dto.Country{Code: "GP", Name: "Guadeloupe", IsEU: vat.IsEU("GP")}, - &dto.Country{Code: "GU", Name: "Guam", IsEU: vat.IsEU("GU")}, - &dto.Country{Code: "GT", Name: "Guatemala", IsEU: vat.IsEU("GT")}, - &dto.Country{Code: "GG", Name: "Guernsey", IsEU: vat.IsEU("GG")}, - &dto.Country{Code: "GN", Name: "Guinea", IsEU: vat.IsEU("GN")}, - &dto.Country{Code: "GW", Name: "Guinea-Bissau", IsEU: vat.IsEU("GW")}, - &dto.Country{Code: "GY", Name: "Guyana", IsEU: vat.IsEU("GY")}, - &dto.Country{Code: "HT", Name: "Haiti", IsEU: vat.IsEU("HT")}, - &dto.Country{Code: "HM", Name: "Heard Island and McDonald Islands", IsEU: vat.IsEU("HM")}, - &dto.Country{Code: "VA", Name: "Holy See (Vatican City State)", IsEU: vat.IsEU("VA")}, - &dto.Country{Code: "HN", Name: "Honduras", IsEU: vat.IsEU("HN")}, - &dto.Country{Code: "HK", Name: "Hong Kong", IsEU: vat.IsEU("HK")}, - &dto.Country{Code: "HU", Name: "Hungary", IsEU: vat.IsEU("HU")}, - &dto.Country{Code: "IS", Name: "Iceland", IsEU: vat.IsEU("IS")}, - &dto.Country{Code: "IN", Name: "India", IsEU: vat.IsEU("IN")}, - &dto.Country{Code: "ID", Name: "Indonesia", IsEU: vat.IsEU("ID")}, - &dto.Country{Code: "IR", Name: "Iran, Islamic Republic of", IsEU: vat.IsEU("IR")}, - &dto.Country{Code: "IQ", Name: "Iraq", IsEU: vat.IsEU("IQ")}, - &dto.Country{Code: "IE", Name: "Ireland", IsEU: vat.IsEU("IE")}, - &dto.Country{Code: "IM", Name: "Isle of Man", IsEU: vat.IsEU("IM")}, - &dto.Country{Code: "IL", Name: "Israel", IsEU: vat.IsEU("IL")}, - &dto.Country{Code: "IT", Name: "Italy", IsEU: vat.IsEU("IT")}, - &dto.Country{Code: "JM", Name: "Jamaica", IsEU: vat.IsEU("JM")}, - &dto.Country{Code: "JP", Name: "Japan", IsEU: vat.IsEU("JP")}, - &dto.Country{Code: "JE", Name: "Jersey", IsEU: vat.IsEU("JE")}, - &dto.Country{Code: "JO", Name: "Jordan", IsEU: vat.IsEU("JO")}, - &dto.Country{Code: "KZ", Name: "Kazakhstan", IsEU: vat.IsEU("KZ")}, - &dto.Country{Code: "KE", Name: "Kenya", IsEU: vat.IsEU("KE")}, - &dto.Country{Code: "KI", Name: "Kiribati", IsEU: vat.IsEU("KI")}, - &dto.Country{Code: "KP", Name: "Korea, Democratic People's Republic of", IsEU: vat.IsEU("KP")}, - &dto.Country{Code: "KR", Name: "Korea, Republic of", IsEU: vat.IsEU("KR")}, - &dto.Country{Code: "KW", Name: "Kuwait", IsEU: vat.IsEU("KW")}, - &dto.Country{Code: "KG", Name: "Kyrgyzstan", IsEU: vat.IsEU("KG")}, - &dto.Country{Code: "LA", Name: "Lao People's Democratic Republic", IsEU: vat.IsEU("LA")}, - &dto.Country{Code: "LV", Name: "Latvia", IsEU: vat.IsEU("LV")}, - &dto.Country{Code: "LB", Name: "Lebanon", IsEU: vat.IsEU("LB")}, - &dto.Country{Code: "LS", Name: "Lesotho", IsEU: vat.IsEU("LS")}, - &dto.Country{Code: "LR", Name: "Liberia", IsEU: vat.IsEU("LR")}, - &dto.Country{Code: "LY", Name: "Libya", IsEU: vat.IsEU("LY")}, - &dto.Country{Code: "LI", Name: "Liechtenstein", IsEU: vat.IsEU("LI")}, - &dto.Country{Code: "LT", Name: "Lithuania", IsEU: vat.IsEU("LT")}, - &dto.Country{Code: "LU", Name: "Luxembourg", IsEU: vat.IsEU("LU")}, - &dto.Country{Code: "MO", Name: "Macao", IsEU: vat.IsEU("MO")}, - &dto.Country{Code: "MK", Name: "Macedonia, the former Yugoslav Republic of", IsEU: vat.IsEU("MK")}, - &dto.Country{Code: "MG", Name: "Madagascar", IsEU: vat.IsEU("MG")}, - &dto.Country{Code: "MW", Name: "Malawi", IsEU: vat.IsEU("MW")}, - &dto.Country{Code: "MY", Name: "Malaysia", IsEU: vat.IsEU("MY")}, - &dto.Country{Code: "MV", Name: "Maldives", IsEU: vat.IsEU("MV")}, - &dto.Country{Code: "ML", Name: "Mali", IsEU: vat.IsEU("ML")}, - &dto.Country{Code: "MT", Name: "Malta", IsEU: vat.IsEU("MT")}, - &dto.Country{Code: "MH", Name: "Marshall Islands", IsEU: vat.IsEU("MH")}, - &dto.Country{Code: "MQ", Name: "Martinique", IsEU: vat.IsEU("MQ")}, - &dto.Country{Code: "MR", Name: "Mauritania", IsEU: vat.IsEU("MR")}, - &dto.Country{Code: "MU", Name: "Mauritius", IsEU: vat.IsEU("MU")}, - &dto.Country{Code: "YT", Name: "Mayotte", IsEU: vat.IsEU("YT")}, - &dto.Country{Code: "MX", Name: "Mexico", IsEU: vat.IsEU("MX")}, - &dto.Country{Code: "FM", Name: "Micronesia, Federated States of", IsEU: vat.IsEU("FM")}, - &dto.Country{Code: "MD", Name: "Moldova, Republic of", IsEU: vat.IsEU("MD")}, - &dto.Country{Code: "MC", Name: "Monaco", IsEU: vat.IsEU("MC")}, - &dto.Country{Code: "MN", Name: "Mongolia", IsEU: vat.IsEU("MN")}, - &dto.Country{Code: "ME", Name: "Montenegro", IsEU: vat.IsEU("ME")}, - &dto.Country{Code: "MS", Name: "Montserrat", IsEU: vat.IsEU("MS")}, - &dto.Country{Code: "MA", Name: "Morocco", IsEU: vat.IsEU("MA")}, - &dto.Country{Code: "MZ", Name: "Mozambique", IsEU: vat.IsEU("MZ")}, - &dto.Country{Code: "MM", Name: "Myanmar", IsEU: vat.IsEU("MM")}, - &dto.Country{Code: "NA", Name: "Namibia", IsEU: vat.IsEU("NA")}, - &dto.Country{Code: "NR", Name: "Nauru", IsEU: vat.IsEU("NR")}, - &dto.Country{Code: "NP", Name: "Nepal", IsEU: vat.IsEU("NP")}, - &dto.Country{Code: "NL", Name: "Netherlands", IsEU: vat.IsEU("NL")}, - &dto.Country{Code: "NC", Name: "New Caledonia", IsEU: vat.IsEU("NC")}, - &dto.Country{Code: "NZ", Name: "New Zealand", IsEU: vat.IsEU("NZ")}, - &dto.Country{Code: "NI", Name: "Nicaragua", IsEU: vat.IsEU("NI")}, - &dto.Country{Code: "NE", Name: "Niger", IsEU: vat.IsEU("NE")}, - &dto.Country{Code: "NG", Name: "Nigeria", IsEU: vat.IsEU("NG")}, - &dto.Country{Code: "NU", Name: "Niue", IsEU: vat.IsEU("NU")}, - &dto.Country{Code: "NF", Name: "Norfolk Island", IsEU: vat.IsEU("NF")}, - &dto.Country{Code: "MP", Name: "Northern Mariana Islands", IsEU: vat.IsEU("MP")}, - &dto.Country{Code: "NO", Name: "Norway", IsEU: vat.IsEU("NO")}, - &dto.Country{Code: "OM", Name: "Oman", IsEU: vat.IsEU("OM")}, - &dto.Country{Code: "PK", Name: "Pakistan", IsEU: vat.IsEU("PK")}, - &dto.Country{Code: "PW", Name: "Palau", IsEU: vat.IsEU("PW")}, - &dto.Country{Code: "PS", Name: "Palestinian Territory, Occupied", IsEU: vat.IsEU("PS")}, - &dto.Country{Code: "PA", Name: "Panama", IsEU: vat.IsEU("PA")}, - &dto.Country{Code: "PG", Name: "Papua New Guinea", IsEU: vat.IsEU("PG")}, - &dto.Country{Code: "PY", Name: "Paraguay", IsEU: vat.IsEU("PY")}, - &dto.Country{Code: "PE", Name: "Peru", IsEU: vat.IsEU("PE")}, - &dto.Country{Code: "PH", Name: "Philippines", IsEU: vat.IsEU("PH")}, - &dto.Country{Code: "PN", Name: "Pitcairn", IsEU: vat.IsEU("PN")}, - &dto.Country{Code: "PL", Name: "Poland", IsEU: vat.IsEU("PL")}, - &dto.Country{Code: "PT", Name: "Portugal", IsEU: vat.IsEU("PT")}, - &dto.Country{Code: "PR", Name: "Puerto Rico", IsEU: vat.IsEU("PR")}, - &dto.Country{Code: "QA", Name: "Qatar", IsEU: vat.IsEU("QA")}, - &dto.Country{Code: "RE", Name: "Réunion", IsEU: vat.IsEU("RE")}, - &dto.Country{Code: "RO", Name: "Romania", IsEU: vat.IsEU("RO")}, - &dto.Country{Code: "RU", Name: "Russian Federation", IsEU: vat.IsEU("RU")}, - &dto.Country{Code: "RW", Name: "Rwanda", IsEU: vat.IsEU("RW")}, - &dto.Country{Code: "BL", Name: "Saint Barthélemy", IsEU: vat.IsEU("BL")}, - &dto.Country{Code: "SH", Name: "Saint Helena, Ascension and Tristan da Cunha", IsEU: vat.IsEU("SH")}, - &dto.Country{Code: "KN", Name: "Saint Kitts and Nevis", IsEU: vat.IsEU("KN")}, - &dto.Country{Code: "LC", Name: "Saint Lucia", IsEU: vat.IsEU("LC")}, - &dto.Country{Code: "MF", Name: "Saint Martin (French part)", IsEU: vat.IsEU("MF")}, - &dto.Country{Code: "PM", Name: "Saint Pierre and Miquelon", IsEU: vat.IsEU("PM")}, - &dto.Country{Code: "VC", Name: "Saint Vincent and the Grenadines", IsEU: vat.IsEU("VC")}, - &dto.Country{Code: "WS", Name: "Samoa", IsEU: vat.IsEU("WS")}, - &dto.Country{Code: "SM", Name: "San Marino", IsEU: vat.IsEU("SM")}, - &dto.Country{Code: "ST", Name: "Sao Tome and Principe", IsEU: vat.IsEU("ST")}, - &dto.Country{Code: "SA", Name: "Saudi Arabia", IsEU: vat.IsEU("SA")}, - &dto.Country{Code: "SN", Name: "Senegal", IsEU: vat.IsEU("SN")}, - &dto.Country{Code: "RS", Name: "Serbia", IsEU: vat.IsEU("RS")}, - &dto.Country{Code: "SC", Name: "Seychelles", IsEU: vat.IsEU("SC")}, - &dto.Country{Code: "SL", Name: "Sierra Leone", IsEU: vat.IsEU("SL")}, - &dto.Country{Code: "SG", Name: "Singapore", IsEU: vat.IsEU("SG")}, - &dto.Country{Code: "SX", Name: "Sint Maarten (Dutch part)", IsEU: vat.IsEU("SX")}, - &dto.Country{Code: "SK", Name: "Slovakia", IsEU: vat.IsEU("SK")}, - &dto.Country{Code: "SI", Name: "Slovenia", IsEU: vat.IsEU("SI")}, - &dto.Country{Code: "SB", Name: "Solomon Islands", IsEU: vat.IsEU("SB")}, - &dto.Country{Code: "SO", Name: "Somalia", IsEU: vat.IsEU("SO")}, - &dto.Country{Code: "ZA", Name: "South Africa", IsEU: vat.IsEU("ZA")}, - &dto.Country{Code: "GS", Name: "South Georgia and the South Sandwich Islands", IsEU: vat.IsEU("GS")}, - &dto.Country{Code: "SS", Name: "South Sudan", IsEU: vat.IsEU("SS")}, - &dto.Country{Code: "ES", Name: "Spain", IsEU: vat.IsEU("ES")}, - &dto.Country{Code: "LK", Name: "Sri Lanka", IsEU: vat.IsEU("LK")}, - &dto.Country{Code: "SD", Name: "Sudan", IsEU: vat.IsEU("SD")}, - &dto.Country{Code: "SR", Name: "Suriname", IsEU: vat.IsEU("SR")}, - &dto.Country{Code: "SJ", Name: "Svalbard and Jan Mayen", IsEU: vat.IsEU("SJ")}, - &dto.Country{Code: "SZ", Name: "Swaziland", IsEU: vat.IsEU("SZ")}, - &dto.Country{Code: "SE", Name: "Sweden", IsEU: vat.IsEU("SE")}, - &dto.Country{Code: "CH", Name: "Switzerland", IsEU: vat.IsEU("CH")}, - &dto.Country{Code: "SY", Name: "Syrian Arab Republic", IsEU: vat.IsEU("SY")}, - &dto.Country{Code: "TW", Name: "Taiwan, Province of China", IsEU: vat.IsEU("TW")}, - &dto.Country{Code: "TJ", Name: "Tajikistan", IsEU: vat.IsEU("TJ")}, - &dto.Country{Code: "TZ", Name: "Tanzania, United Republic of", IsEU: vat.IsEU("TZ")}, - &dto.Country{Code: "TH", Name: "Thailand", IsEU: vat.IsEU("TH")}, - &dto.Country{Code: "TL", Name: "Timor-Leste", IsEU: vat.IsEU("TL")}, - &dto.Country{Code: "TG", Name: "Togo", IsEU: vat.IsEU("TG")}, - &dto.Country{Code: "TK", Name: "Tokelau", IsEU: vat.IsEU("TK")}, - &dto.Country{Code: "TO", Name: "Tonga", IsEU: vat.IsEU("TO")}, - &dto.Country{Code: "TT", Name: "Trinidad and Tobago", IsEU: vat.IsEU("TT")}, - &dto.Country{Code: "TN", Name: "Tunisia", IsEU: vat.IsEU("TN")}, - &dto.Country{Code: "TR", Name: "Turkey", IsEU: vat.IsEU("TR")}, - &dto.Country{Code: "TM", Name: "Turkmenistan", IsEU: vat.IsEU("TM")}, - &dto.Country{Code: "TC", Name: "Turks and Caicos Islands", IsEU: vat.IsEU("TC")}, - &dto.Country{Code: "TV", Name: "Tuvalu", IsEU: vat.IsEU("TV")}, - &dto.Country{Code: "UG", Name: "Uganda", IsEU: vat.IsEU("UG")}, - &dto.Country{Code: "UA", Name: "Ukraine", IsEU: vat.IsEU("UA")}, - &dto.Country{Code: "AE", Name: "United Arab Emirates", IsEU: vat.IsEU("AE")}, - &dto.Country{Code: "GB", Name: "United Kingdom", IsEU: vat.IsEU("GB")}, - &dto.Country{Code: "US", Name: "United States", IsEU: vat.IsEU("US")}, - &dto.Country{Code: "UM", Name: "United States Minor Outlying Islands", IsEU: vat.IsEU("UM")}, - &dto.Country{Code: "UY", Name: "Uruguay", IsEU: vat.IsEU("UY")}, - &dto.Country{Code: "UZ", Name: "Uzbekistan", IsEU: vat.IsEU("UZ")}, - &dto.Country{Code: "VU", Name: "Vanuatu", IsEU: vat.IsEU("VU")}, - &dto.Country{Code: "VE", Name: "Venezuela, Bolivarian Republic of", IsEU: vat.IsEU("VE")}, - &dto.Country{Code: "VN", Name: "Viet Nam", IsEU: vat.IsEU("VN")}, - &dto.Country{Code: "VG", Name: "Virgin Islands, British", IsEU: vat.IsEU("VG")}, - &dto.Country{Code: "VI", Name: "Virgin Islands, U.S.", IsEU: vat.IsEU("VI")}, - &dto.Country{Code: "WF", Name: "Wallis and Futuna", IsEU: vat.IsEU("WF")}, - &dto.Country{Code: "EH", Name: "Western Sahara", IsEU: vat.IsEU("EH")}, - &dto.Country{Code: "YE", Name: "Yemen", IsEU: vat.IsEU("YE")}, - &dto.Country{Code: "ZM", Name: "Zambia", IsEU: vat.IsEU("ZM")}, - &dto.Country{Code: "ZW", Name: "Zimbabwe", IsEU: vat.IsEU("ZW")}, -} diff --git a/app/services/billing/customer.go b/app/services/billing/customer.go deleted file mode 100644 index 5fb3dd9f7..000000000 --- a/app/services/billing/customer.go +++ /dev/null @@ -1,234 +0,0 @@ -package billing - -import ( - "context" - "fmt" - "strconv" - - "github.com/getfider/fider/app/models" - "github.com/getfider/fider/app/models/cmd" - "github.com/getfider/fider/app/models/dto" - "github.com/getfider/fider/app/models/query" - "github.com/getfider/fider/app/pkg/env" - "github.com/getfider/fider/app/pkg/errors" - "github.com/goenning/vat" - "github.com/stripe/stripe-go" -) - -func createCustomer(ctx context.Context, c *cmd.CreateBillingCustomer) error { - return using(ctx, func(tenant *models.Tenant) error { - if tenant.Billing == nil { - return errors.New("Tenant doesn't have a billing record") - } - - if tenant.Billing.StripeCustomerID == "" { - params := &stripe.CustomerParams{ - Description: stripe.String(tenant.Name), - } - params.AddMetadata("tenant_id", strconv.Itoa(tenant.ID)) - params.AddMetadata("tenant_subdomain", tenant.Subdomain) - customer, err := stripeClient.Customers.New(params) - if err != nil { - return errors.Wrap(err, "failed to create Stripe customer") - } - - tenant.Billing.StripeCustomerID = customer.ID - return nil - } - - return nil - }) -} - -func deleteCustomer(ctx context.Context, c *cmd.DeleteBillingCustomer) error { - return using(ctx, func(tenant *models.Tenant) error { - if !env.IsTest() { - return errors.New("Stripe customer can only be deleted on test mode") - } - - _, err := stripeClient.Customers.Del(tenant.Billing.StripeCustomerID, &stripe.CustomerParams{}) - if err != nil { - return errors.Wrap(err, "failed to delete Stripe customer") - } - return nil - }) -} - -func getPaymentInfo(ctx context.Context, q *query.GetPaymentInfo) error { - return using(ctx, func(tenant *models.Tenant) error { - if tenant.Billing == nil || tenant.Billing.StripeCustomerID == "" { - return nil - } - - customerID := tenant.Billing.StripeCustomerID - - customer, err := stripeClient.Customers.Get(customerID, &stripe.CustomerParams{}) - if err != nil { - return errors.Wrap(err, "failed to get customer") - } - - if customer.Metadata["tenant_id"] != strconv.Itoa(tenant.ID) { - panic(fmt.Sprintf("Stripe TenantID (%s) doesn't match current Tenant ID (%s). Aborting.", customer.Metadata["tenant_id"], strconv.Itoa(tenant.ID))) - } - - if customer.DefaultSource == nil { - return nil - } - - card, err := stripeClient.Cards.Get(customer.DefaultSource.ID, &stripe.CardParams{ - Customer: stripe.String(customerID), - }) - if err != nil { - return errors.Wrap(err, "failed to get customer's card") - } - - q.Result = &dto.PaymentInfo{ - Email: customer.Email, - Name: card.Name, - StripeCardID: card.ID, - CardCountry: card.Country, - CardBrand: string(card.Brand), - CardLast4: card.Last4, - CardExpMonth: card.ExpMonth, - CardExpYear: card.ExpYear, - AddressCity: card.AddressCity, - AddressCountry: card.AddressCountry, - AddressLine1: card.AddressLine1, - AddressLine2: card.AddressLine2, - AddressState: card.AddressState, - AddressPostalCode: card.AddressZip, - } - - for _, taxId := range customer.TaxIDs.Data { - q.Result.VATNumber = taxId.ID - } - - return nil - }) -} - -func clearPaymentInfo(ctx context.Context, c *cmd.ClearPaymentInfo) error { - return using(ctx, func(tenant *models.Tenant) error { - currentInfo := &query.GetPaymentInfo{} - if err := getPaymentInfo(ctx, currentInfo); err != nil { - return err - } - - if currentInfo.Result != nil { - customerID := tenant.Billing.StripeCustomerID - _, err := stripeClient.Customers.Update(customerID, &stripe.CustomerParams{ - Description: stripe.String(tenant.Name), - Email: stripe.String(""), - TaxIDData: []*stripe.CustomerTaxIDDataParams{ - { - Type: stripe.String(string(stripe.TaxIDTypeEUVAT)), - Value: stripe.String(""), - }, - }, - }) - if err != nil { - return errors.Wrap(err, "failed to delete customer billing email") - } - if currentInfo.Result.StripeCardID != "" { - _, err = stripeClient.Cards.Del(currentInfo.Result.StripeCardID, &stripe.CardParams{ - Customer: stripe.String(customerID), - }) - if err != nil { - return errors.Wrap(err, "failed to delete customer card") - } - } - } - - return nil - }) -} - -func updatePaymentInfo(ctx context.Context, c *cmd.UpdatePaymentInfo) error { - return using(ctx, func(tenant *models.Tenant) error { - customerID := tenant.Billing.StripeCustomerID - - currentInfo := &query.GetPaymentInfo{} - if err := getPaymentInfo(ctx, currentInfo); err != nil { - return err - } - - if !vat.IsEU(c.Input.AddressCountry) { - c.Input.VATNumber = "" - } - - // update customer info - params := &stripe.CustomerParams{ - Email: stripe.String(c.Input.Email), - Description: stripe.String(c.Input.Name), - Shipping: &stripe.CustomerShippingDetailsParams{ - Name: stripe.String(c.Input.Name), - Address: &stripe.AddressParams{ - City: stripe.String(c.Input.AddressCity), - Country: stripe.String(c.Input.AddressCountry), - Line1: stripe.String(c.Input.AddressLine1), - Line2: stripe.String(c.Input.AddressLine2), - PostalCode: stripe.String(c.Input.AddressPostalCode), - State: stripe.String(c.Input.AddressState), - }, - }, - TaxIDData: []*stripe.CustomerTaxIDDataParams{ - { - Type: stripe.String(string(stripe.TaxIDTypeEUVAT)), - Value: stripe.String(c.Input.VATNumber), - }, - }, - } - _, err := stripeClient.Customers.Update(customerID, params) - if err != nil { - return errors.Wrap(err, "failed to update customer billing email") - } - - // new card, just create it - if currentInfo.Result == nil || currentInfo.Result.StripeCardID == "" { - _, err = stripeClient.Cards.New(&stripe.CardParams{ - Customer: stripe.String(customerID), - Token: stripe.String(c.Input.Card.Token), - }) - if err != nil { - return errors.Wrap(err, "failed to create stripe card") - } - return nil - } - - // replacing card, create new and delete old - if c.Input.Card != nil && c.Input.Card.Token != "" { - _, err = stripeClient.Cards.New(&stripe.CardParams{ - Customer: stripe.String(customerID), - Token: stripe.String(c.Input.Card.Token), - }) - if err != nil { - return errors.Wrap(err, "failed to create new stripe card") - } - - _, err = stripeClient.Cards.Del(currentInfo.Result.StripeCardID, &stripe.CardParams{ - Customer: stripe.String(customerID), - Token: stripe.String(c.Input.Card.Token), - }) - if err != nil { - return errors.Wrap(err, "failed to delete old stripe card") - } - return nil - } - - // updating card, just update current card - _, err = stripeClient.Cards.Update(currentInfo.Result.StripeCardID, &stripe.CardParams{ - Customer: stripe.String(customerID), - Name: stripe.String(c.Input.Name), - AddressCity: stripe.String(c.Input.AddressCity), - AddressCountry: stripe.String(c.Input.AddressCountry), - AddressLine1: stripe.String(c.Input.AddressLine1), - AddressLine2: stripe.String(c.Input.AddressLine2), - AddressState: stripe.String(c.Input.AddressState), - AddressZip: stripe.String(c.Input.AddressPostalCode), - }) - if err != nil { - return errors.Wrap(err, "failed to update stripe card") - } - return nil - }) -} diff --git a/app/services/billing/plans.go b/app/services/billing/plans.go deleted file mode 100644 index 9a9c1f291..000000000 --- a/app/services/billing/plans.go +++ /dev/null @@ -1,92 +0,0 @@ -package billing - -import ( - "context" - "sort" - "strconv" - "strings" - "sync" - - "github.com/getfider/fider/app/models/dto" - "github.com/getfider/fider/app/models/query" - "github.com/getfider/fider/app/pkg/errors" - "github.com/goenning/vat" - "github.com/stripe/stripe-go" -) - -var plansMutex sync.RWMutex -var allPlans []*dto.BillingPlan - -func listPlans(ctx context.Context, q *query.ListBillingPlans) error { - if allPlans != nil { - q.Result = filterPlansByCountryCode(allPlans, q.CountryCode) - return nil - } - - plansMutex.Lock() - defer plansMutex.Unlock() - - if allPlans == nil { - allPlans = make([]*dto.BillingPlan, 0) - it := stripeClient.Plans.List(&stripe.PlanListParams{ - Active: stripe.Bool(true), - }) - for it.Next() { - plan := it.Plan() - name, ok := plan.Metadata["friendly_name"] - if !ok { - name = plan.Nickname - } - maxUsers, _ := strconv.Atoi(plan.Metadata["max_users"]) - allPlans = append(allPlans, &dto.BillingPlan{ - ID: plan.ID, - Name: name, - Description: plan.Metadata["description"], - MaxUsers: maxUsers, - Price: plan.Amount, - Currency: strings.ToUpper(string(plan.Currency)), - Interval: string(plan.Interval), - }) - } - if err := it.Err(); err != nil { - return err - } - sort.Slice(allPlans, func(i, j int) bool { - return allPlans[i].Price < allPlans[j].Price - }) - } - - q.Result = filterPlansByCountryCode(allPlans, q.CountryCode) - return nil -} - -func getPlanByID(ctx context.Context, q *query.GetBillingPlanByID) error { - listPlansQuery := &query.ListBillingPlans{CountryCode: q.CountryCode} - err := listPlans(ctx, listPlansQuery) - if err != nil { - return err - } - - for _, plan := range listPlansQuery.Result { - if plan.ID == q.PlanID { - q.Result = plan - return nil - } - } - return errors.New("failed to get plan by id '%s'", q.PlanID) -} - -func filterPlansByCountryCode(plans []*dto.BillingPlan, countryCode string) []*dto.BillingPlan { - currency := "USD" - if vat.IsEU(countryCode) { - currency = "EUR" - } - - filteredPlans := make([]*dto.BillingPlan, 0) - for _, p := range plans { - if p.Currency == currency { - filteredPlans = append(filteredPlans, p) - } - } - return filteredPlans -} diff --git a/app/services/billing/subscription.go b/app/services/billing/subscription.go deleted file mode 100644 index 0ca0599ab..000000000 --- a/app/services/billing/subscription.go +++ /dev/null @@ -1,98 +0,0 @@ -package billing - -import ( - "context" - "strings" - "time" - - "github.com/getfider/fider/app/models" - "github.com/getfider/fider/app/models/cmd" - "github.com/getfider/fider/app/models/dto" - "github.com/getfider/fider/app/models/query" - "github.com/getfider/fider/app/pkg/errors" - "github.com/stripe/stripe-go" -) - -func cancelSubscription(ctx context.Context, c *cmd.CancelBillingSubscription) error { - return using(ctx, func(tenant *models.Tenant) error { - sub, err := stripeClient.Subscriptions.Update(tenant.Billing.StripeSubscriptionID, &stripe.SubscriptionParams{ - CancelAtPeriodEnd: stripe.Bool(true), - }) - if err != nil { - return errors.Wrap(err, "failed to cancel stripe subscription") - } - endDate := time.Unix(sub.CurrentPeriodEnd, 0) - tenant.Billing.SubscriptionEndsAt = &endDate - return nil - }) -} - -func subscribe(ctx context.Context, c *cmd.CreateBillingSubscription) error { - return using(ctx, func(tenant *models.Tenant) error { - customerID := tenant.Billing.StripeCustomerID - if tenant.Billing.StripeSubscriptionID != "" { - sub, err := stripeClient.Subscriptions.Get(tenant.Billing.StripeSubscriptionID, nil) - if err != nil { - return errors.Wrap(err, "failed to get stripe subscription") - } - _, err = stripeClient.Subscriptions.Update(tenant.Billing.StripeSubscriptionID, &stripe.SubscriptionParams{ - CancelAtPeriodEnd: stripe.Bool(false), - Items: []*stripe.SubscriptionItemsParams{ - { - ID: stripe.String(sub.Items.Data[0].ID), - Plan: stripe.String(c.PlanID), - }, - }, - }) - - if err != nil { - return errors.Wrap(err, "failed to update stripe subscription") - } - - tenant.Billing.SubscriptionEndsAt = nil - } else { - sub, err := stripeClient.Subscriptions.New(&stripe.SubscriptionParams{ - Customer: stripe.String(customerID), - Items: []*stripe.SubscriptionItemsParams{ - { - Plan: stripe.String(c.PlanID), - }, - }, - }) - - if err != nil { - return errors.Wrap(err, "failed to create stripe subscription") - } - - tenant.Billing.StripeSubscriptionID = sub.ID - } - - tenant.Billing.StripePlanID = c.PlanID - return nil - }) -} - -func getUpcomingInvoice(ctx context.Context, q *query.GetUpcomingInvoice) error { - return using(ctx, func(tenant *models.Tenant) error { - inv, err := stripeClient.Invoices.GetNext(&stripe.InvoiceParams{ - Customer: stripe.String(tenant.Billing.StripeCustomerID), - Subscription: stripe.String(tenant.Billing.StripeSubscriptionID), - }) - if err != nil { - if stripeErr, ok := err.(*stripe.Error); ok { - if stripeErr.HTTPStatusCode == 404 { - return nil - } - } - return errors.Wrap(err, "failed to get upcoming invoice") - } - - dueDate := time.Unix(inv.DueDate, 0) - q.Result = &dto.UpcomingInvoice{ - Currency: strings.ToUpper(string(inv.Currency)), - AmountDue: inv.AmountDue, - DueDate: dueDate, - } - return nil - }) -} diff --git a/app/services/sqlstore/postgres/postgres.go b/app/services/sqlstore/postgres/postgres.go index 12b553cdb..3e72c5c02 100644 --- a/app/services/sqlstore/postgres/postgres.go +++ b/app/services/sqlstore/postgres/postgres.go @@ -107,7 +107,6 @@ func (s Service) Init() { bus.AddHandler(updateTenantSettings) bus.AddHandler(updateTenantPrivacySettings) bus.AddHandler(updateTenantAdvancedSettings) - bus.AddHandler(updateTenantBillingSettings) bus.AddHandler(getVerificationByKey) bus.AddHandler(saveVerificationKey) diff --git a/app/services/sqlstore/postgres/tenant.go b/app/services/sqlstore/postgres/tenant.go index 0ea501cad..5b1023c89 100644 --- a/app/services/sqlstore/postgres/tenant.go +++ b/app/services/sqlstore/postgres/tenant.go @@ -17,17 +17,16 @@ import ( ) type dbTenant struct { - ID int `db:"id"` - Name string `db:"name"` - Subdomain string `db:"subdomain"` - CNAME string `db:"cname"` - Invitation string `db:"invitation"` - WelcomeMessage string `db:"welcome_message"` - Status int `db:"status"` - IsPrivate bool `db:"is_private"` - LogoBlobKey string `db:"logo_bkey"` - CustomCSS string `db:"custom_css"` - Billing *dbTenantBilling `db:"billing"` + ID int `db:"id"` + Name string `db:"name"` + Subdomain string `db:"subdomain"` + CNAME string `db:"cname"` + Invitation string `db:"invitation"` + WelcomeMessage string `db:"welcome_message"` + Status int `db:"status"` + IsPrivate bool `db:"is_private"` + LogoBlobKey string `db:"logo_bkey"` + CustomCSS string `db:"custom_css"` } func (t *dbTenant) toModel() *models.Tenant { @@ -48,29 +47,9 @@ func (t *dbTenant) toModel() *models.Tenant { CustomCSS: t.CustomCSS, } - if t.Billing != nil && t.Billing.TrialEndsAt.Valid { - tenant.Billing = &models.TenantBilling{ - TrialEndsAt: t.Billing.TrialEndsAt.Time, - StripeCustomerID: t.Billing.StripeCustomerID.String, - StripeSubscriptionID: t.Billing.StripeSubscriptionID.String, - StripePlanID: t.Billing.StripePlanID.String, - } - if t.Billing.SubscriptionEndsAt.Valid { - tenant.Billing.SubscriptionEndsAt = &t.Billing.SubscriptionEndsAt.Time - } - } - return tenant } -type dbTenantBilling struct { - StripeCustomerID dbx.NullString `db:"stripe_customer_id"` - StripeSubscriptionID dbx.NullString `db:"stripe_subscription_id"` - StripePlanID dbx.NullString `db:"stripe_plan_id"` - TrialEndsAt dbx.NullTime `db:"trial_ends_at"` - SubscriptionEndsAt dbx.NullTime `db:"subscription_ends_at"` -} - type dbEmailVerification struct { ID int `db:"id"` Name string `db:"name"` @@ -160,29 +139,6 @@ func updateTenantSettings(ctx context.Context, c *cmd.UpdateTenantSettings) erro }) } -func updateTenantBillingSettings(ctx context.Context, c *cmd.UpdateTenantBillingSettings) error { - return using(ctx, func(trx *dbx.Trx, tenant *models.Tenant, user *models.User) error { - _, err := trx.Execute(` - UPDATE tenants_billing - SET stripe_customer_id = $1, - stripe_plan_id = $2, - stripe_subscription_id = $3, - subscription_ends_at = $4 - WHERE tenant_id = $5 - `, - c.Settings.StripeCustomerID, - c.Settings.StripePlanID, - c.Settings.StripeSubscriptionID, - c.Settings.SubscriptionEndsAt, - tenant.ID, - ) - if err != nil { - return errors.Wrap(err, "failed update tenant billing settings") - } - return nil - }) -} - func updateTenantAdvancedSettings(ctx context.Context, c *cmd.UpdateTenantAdvancedSettings) error { return using(ctx, func(trx *dbx.Trx, tenant *models.Tenant, user *models.User) error { query := "UPDATE tenants SET custom_css = $1 WHERE id = $2" @@ -262,16 +218,6 @@ func createTenant(ctx context.Context, c *cmd.CreateTenant) error { return err } - if env.IsBillingEnabled() { - _, err = trx.Execute( - `INSERT INTO tenants_billing (tenant_id, trial_ends_at) VALUES ($1, $2)`, - id, now.Add(30*24*time.Hour), - ) - if err != nil { - return err - } - } - byDomain := &query.GetTenantByDomain{Domain: c.Subdomain} err = bus.Dispatch(ctx, byDomain) c.Result = byDomain.Result @@ -284,16 +230,9 @@ func getFirstTenant(ctx context.Context, q *query.GetFirstTenant) error { tenant := dbTenant{} err := trx.Get(&tenant, ` - SELECT t.id, t.name, t.subdomain, t.cname, t.invitation, t.welcome_message, t.status, t.is_private, t.logo_bkey, t.custom_css, - tb.trial_ends_at AS billing_trial_ends_at, - tb.subscription_ends_at AS billing_subscription_ends_at, - tb.stripe_customer_id AS billing_stripe_customer_id, - tb.stripe_plan_id AS billing_stripe_plan_id, - tb.stripe_subscription_id AS billing_stripe_subscription_id - FROM tenants t - LEFT JOIN tenants_billing tb - ON tb.tenant_id = t.id - ORDER BY t.id LIMIT 1 + SELECT id, name, subdomain, cname, invitation, welcome_message, status, is_private, logo_bkey, custom_css + FROM tenants + ORDER BY id LIMIT 1 `) if err != nil { @@ -310,17 +249,10 @@ func getTenantByDomain(ctx context.Context, q *query.GetTenantByDomain) error { tenant := dbTenant{} err := trx.Get(&tenant, ` - SELECT t.id, t.name, t.subdomain, t.cname, t.invitation, t.welcome_message, t.status, t.is_private, t.logo_bkey, t.custom_css, - tb.trial_ends_at AS billing_trial_ends_at, - tb.subscription_ends_at AS billing_subscription_ends_at, - tb.stripe_customer_id AS billing_stripe_customer_id, - tb.stripe_plan_id AS billing_stripe_plan_id, - tb.stripe_subscription_id AS billing_stripe_subscription_id + SELECT id, name, subdomain, cname, invitation, welcome_message, status, is_private, logo_bkey, custom_css FROM tenants t - LEFT JOIN tenants_billing tb - ON tb.tenant_id = t.id - WHERE t.subdomain = $1 OR t.subdomain = $2 OR t.cname = $3 - ORDER BY t.cname DESC + WHERE subdomain = $1 OR subdomain = $2 OR cname = $3 + ORDER BY cname DESC `, env.Subdomain(q.Domain), q.Domain, q.Domain) if err != nil { return errors.Wrap(err, "failed to get tenant with domain '%s'", q.Domain) diff --git a/app/services/sqlstore/postgres/tenant_test.go b/app/services/sqlstore/postgres/tenant_test.go index 2b0374e1c..5d6ddbf80 100644 --- a/app/services/sqlstore/postgres/tenant_test.go +++ b/app/services/sqlstore/postgres/tenant_test.go @@ -21,7 +21,6 @@ func TestTenantStorage_Add_Activate(t *testing.T) { ctx := SetupDatabaseTest(t) defer TeardownDatabaseTest() - env.Config.Stripe.SecretKey = "" createTenant := &cmd.CreateTenant{ Name: "My Domain Inc.", Subdomain: "mydomain", @@ -49,31 +48,6 @@ func TestTenantStorage_Add_Activate(t *testing.T) { Expect(getByDomain.Result.Subdomain).Equals("mydomain") Expect(getByDomain.Result.Status).Equals(enum.TenantActive) Expect(getByDomain.Result.IsPrivate).IsFalse() - Expect(getByDomain.Result.Billing).IsNil() -} - -func TestTenantStorage_Add_WithBillingEnabled(t *testing.T) { - ctx := SetupDatabaseTest(t) - defer TeardownDatabaseTest() - - env.Config.Stripe.SecretKey = "sk_1" - err := bus.Dispatch(ctx, &cmd.CreateTenant{ - Name: "My Domain Inc.", - Subdomain: "mydomain", - Status: enum.TenantPending, - }) - Expect(err).IsNil() - - getByDomain := &query.GetTenantByDomain{Domain: "mydomain"} - err = bus.Dispatch(ctx, getByDomain) - Expect(err).IsNil() - - Expect(getByDomain.Result.Name).Equals("My Domain Inc.") - Expect(getByDomain.Result.Subdomain).Equals("mydomain") - Expect(getByDomain.Result.Status).Equals(enum.TenantPending) - Expect(getByDomain.Result.IsPrivate).IsFalse() - Expect(getByDomain.Result.Billing).IsNotNil() - Expect(getByDomain.Result.Billing.TrialEndsAt).TemporarilySimilar(time.Now().Add(30*24*time.Hour), 5*time.Second) } func TestTenantStorage_SingleTenant_Add(t *testing.T) { diff --git a/go.mod b/go.mod index 2b3cbb59f..023728317 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,6 @@ require ( github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/goenning/imagic v0.0.1 github.com/goenning/letteravatar v0.0.0-20180605200324-553181ed4055 - github.com/goenning/vat v0.1.0 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golangci/golangci-lint v1.38.0 github.com/gosimple/slug v1.9.0 @@ -21,7 +20,6 @@ require ( github.com/magefile/mage v1.11.0 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/russross/blackfriday v1.6.0 - github.com/stripe/stripe-go v70.15.0+incompatible golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93 ) diff --git a/go.sum b/go.sum index 6105819bb..53bd5e6fc 100644 --- a/go.sum +++ b/go.sum @@ -153,8 +153,6 @@ github.com/goenning/imagic v0.0.1 h1:JJ1AhdRugiPucmIHCB6GmvigCAfMYV7rTYhus4ROZ50 github.com/goenning/imagic v0.0.1/go.mod h1:uCYR1hKybppzS6QEa7KnPPa3Q69SjcL95eDR5MW1j38= github.com/goenning/letteravatar v0.0.0-20180605200324-553181ed4055 h1:xwq8JGLwF1UughJdViZyiUo6FcTv7mN6L532hBNbnfQ= github.com/goenning/letteravatar v0.0.0-20180605200324-553181ed4055/go.mod h1:3lA285vlcUfyyBfimSIsT200bJb1ScVZEN2KKoW8TDY= -github.com/goenning/vat v0.1.0 h1:Th8EhqCZdPVQgYlNwgp19MmfgmlZcVX4TTnmlAGnjIc= -github.com/goenning/vat v0.1.0/go.mod h1:jIdg4o7ZwGAVzBbQ5tPFMj6ypfcYkChoumAWZ2Li2ME= github.com/gofrs/flock v0.8.0 h1:MSdYClljsF3PbENUUEx85nkWfJSGfzYI9yEBZOJz6CY= github.com/gofrs/flock v0.8.0/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -505,8 +503,6 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stripe/stripe-go v70.15.0+incompatible h1:hNML7M1zx8RgtepEMlxyu/FpVPrP7KZm1gPFQquJQvM= -github.com/stripe/stripe-go v70.15.0+incompatible/go.mod h1:A1dQZmO/QypXmsL0T8axYZkSN/uA/T/A64pfKdBAMiY= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tdakkota/asciicheck v0.0.0-20200416200610-e657995f937b h1:HxLVTlqcHhFAz3nWUcuvpH7WuOMv8LQoCWmruLfFH2U= diff --git a/package-lock.json b/package-lock.json index 89f2a5d19..af47c3f0c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,6 @@ "react": "17.0.1", "react-dom": "17.0.1", "react-icons": "4.2.0", - "react-stripe-elements": "6.1.2", "react-textarea-autosize": "8.3.2", "react-toastify": "7.0.3", "tslib": "2.1.0" @@ -27,7 +26,6 @@ "@types/puppeteer": "5.4.3", "@types/react": "17.0.2", "@types/react-dom": "17.0.1", - "@types/react-stripe-elements": "6.0.4", "@types/react-textarea-autosize": "4.3.5", "@wojtekmaj/enzyme-adapter-react-17": "0.4.1", "css-loader": "5.1.1", @@ -52,6 +50,10 @@ "webpack": "5.24.3", "webpack-bundle-analyzer": "4.4.0", "webpack-cli": "4.5.0" + }, + "engines": { + "node": ">=14", + "npm": ">=7" } }, "node_modules/@babel/code-frame": { @@ -1121,16 +1123,6 @@ "@types/react": "*" } }, - "node_modules/@types/react-stripe-elements": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/@types/react-stripe-elements/-/react-stripe-elements-6.0.4.tgz", - "integrity": "sha512-EzEeBiOwQJ0fPzZhtg9zqyy5GJbEbi0sFNFCowFXmRwraRcMRyZ4p2UK8nylzfCR2KtMPZ2fgUVH9xD/VL/StQ==", - "dev": true, - "dependencies": { - "@types/react": "*", - "@types/stripe-v3": "*" - } - }, "node_modules/@types/react-textarea-autosize": { "version": "4.3.5", "resolved": "https://registry.npmjs.org/@types/react-textarea-autosize/-/react-textarea-autosize-4.3.5.tgz", @@ -1146,12 +1138,6 @@ "integrity": "sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw==", "dev": true }, - "node_modules/@types/stripe-v3": { - "version": "3.1.23", - "resolved": "https://registry.npmjs.org/@types/stripe-v3/-/stripe-v3-3.1.23.tgz", - "integrity": "sha512-fqCnai832M6o6qH6A+Dqk1I/SXAzx629bknbrDigyvkuAPGZGrjkLCMxfnrtWswHXNPjj/TtuGf4lQnDXc815w==", - "dev": true - }, "node_modules/@types/tough-cookie": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.0.tgz", @@ -7366,6 +7352,7 @@ "version": "15.7.2", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "dev": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -7386,7 +7373,8 @@ "node_modules/prop-types/node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true }, "node_modules/proxy-from-env": { "version": "1.1.0", @@ -7547,18 +7535,6 @@ "react": "^16.0.0 || ^17.0.0" } }, - "node_modules/react-stripe-elements": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/react-stripe-elements/-/react-stripe-elements-6.1.2.tgz", - "integrity": "sha512-gYbYhVVJm3Woc84TgmuiqUj44rI/BZGUVUTTmS0U6kZyZa5fYMPlKKIsVZdQZufQ7Ab4BXLO2LSxBlGY0s2jew==", - "dependencies": { - "prop-types": "15.7.2" - }, - "peerDependencies": { - "react": "^15.5.4 || ^16.0.0-0", - "react-dom": "^15.5.4 || ^16.0.0-0" - } - }, "node_modules/react-test-renderer": { "version": "17.0.1", "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-17.0.1.tgz", @@ -11827,16 +11803,6 @@ "@types/react": "*" } }, - "@types/react-stripe-elements": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/@types/react-stripe-elements/-/react-stripe-elements-6.0.4.tgz", - "integrity": "sha512-EzEeBiOwQJ0fPzZhtg9zqyy5GJbEbi0sFNFCowFXmRwraRcMRyZ4p2UK8nylzfCR2KtMPZ2fgUVH9xD/VL/StQ==", - "dev": true, - "requires": { - "@types/react": "*", - "@types/stripe-v3": "*" - } - }, "@types/react-textarea-autosize": { "version": "4.3.5", "resolved": "https://registry.npmjs.org/@types/react-textarea-autosize/-/react-textarea-autosize-4.3.5.tgz", @@ -11852,12 +11818,6 @@ "integrity": "sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw==", "dev": true }, - "@types/stripe-v3": { - "version": "3.1.23", - "resolved": "https://registry.npmjs.org/@types/stripe-v3/-/stripe-v3-3.1.23.tgz", - "integrity": "sha512-fqCnai832M6o6qH6A+Dqk1I/SXAzx629bknbrDigyvkuAPGZGrjkLCMxfnrtWswHXNPjj/TtuGf4lQnDXc815w==", - "dev": true - }, "@types/tough-cookie": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.0.tgz", @@ -16618,6 +16578,7 @@ "version": "15.7.2", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "dev": true, "requires": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -16627,7 +16588,8 @@ "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true } } }, @@ -16777,14 +16739,6 @@ "react-is": "^16.12.0 || ^17.0.0" } }, - "react-stripe-elements": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/react-stripe-elements/-/react-stripe-elements-6.1.2.tgz", - "integrity": "sha512-gYbYhVVJm3Woc84TgmuiqUj44rI/BZGUVUTTmS0U6kZyZa5fYMPlKKIsVZdQZufQ7Ab4BXLO2LSxBlGY0s2jew==", - "requires": { - "prop-types": "15.7.2" - } - }, "react-test-renderer": { "version": "17.0.1", "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-17.0.1.tgz", diff --git a/package.json b/package.json index 78a089136..89053a867 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,6 @@ "react": "17.0.1", "react-dom": "17.0.1", "react-icons": "4.2.0", - "react-stripe-elements": "6.1.2", "react-textarea-autosize": "8.3.2", "react-toastify": "7.0.3", "tslib": "2.1.0" @@ -24,7 +23,6 @@ "@types/puppeteer": "5.4.3", "@types/react": "17.0.2", "@types/react-dom": "17.0.1", - "@types/react-stripe-elements": "6.0.4", "@types/react-textarea-autosize": "4.3.5", "@wojtekmaj/enzyme-adapter-react-17": "0.4.1", "css-loader": "5.1.1", diff --git a/public/AsyncPages.tsx b/public/AsyncPages.tsx index d2bd07232..a235d9515 100644 --- a/public/AsyncPages.tsx +++ b/public/AsyncPages.tsx @@ -143,14 +143,6 @@ export const AsyncMySettingsPage = load( ) ); -export const AsyncBillingPage = load( - () => - import( - /* webpackChunkName: "Billing.page" */ - "@fider/pages/Administration/pages/Billing.page" - ) -); - export const AsyncOAuthEchoPage = load( () => import( diff --git a/public/assets/images/card-americanexpress.svg b/public/assets/images/card-americanexpress.svg deleted file mode 100644 index 52b26e831..000000000 --- a/public/assets/images/card-americanexpress.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - \ No newline at end of file diff --git a/public/assets/images/card-diners.svg b/public/assets/images/card-diners.svg deleted file mode 100644 index 427801323..000000000 --- a/public/assets/images/card-diners.svg +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/public/assets/images/card-discover.svg b/public/assets/images/card-discover.svg deleted file mode 100644 index 8aa7e300a..000000000 --- a/public/assets/images/card-discover.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/public/assets/images/card-jcb.svg b/public/assets/images/card-jcb.svg deleted file mode 100644 index 954410927..000000000 --- a/public/assets/images/card-jcb.svg +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/public/assets/images/card-mastercard.svg b/public/assets/images/card-mastercard.svg deleted file mode 100644 index a4cf75a00..000000000 --- a/public/assets/images/card-mastercard.svg +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/public/assets/images/card-unknown.svg b/public/assets/images/card-unknown.svg deleted file mode 100644 index a8ca58d69..000000000 --- a/public/assets/images/card-unknown.svg +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/public/assets/images/card-visa.svg b/public/assets/images/card-visa.svg deleted file mode 100644 index 4fded8808..000000000 --- a/public/assets/images/card-visa.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - \ No newline at end of file diff --git a/public/components/common/CardInfo.scss b/public/components/common/CardInfo.scss deleted file mode 100644 index 4f3e35703..000000000 --- a/public/components/common/CardInfo.scss +++ /dev/null @@ -1,18 +0,0 @@ -@import "~@fider/assets/styles/variables.scss"; - -.c-card-info { - span, - img { - vertical-align: middle; - margin-right: 5px; - } - img { - max-width: 40px; - max-height: 40px; - } - .c-card-info-exp { - margin-left: 5px; - font-size: $font-size-small; - color: $gray-darker; - } -} diff --git a/public/components/common/CardInfo.tsx b/public/components/common/CardInfo.tsx deleted file mode 100644 index 60a9413c1..000000000 --- a/public/components/common/CardInfo.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import React from "react"; -import "./CardInfo.scss"; -import { useFider } from "@fider/hooks"; - -const visa = require("@fider/assets/images/card-visa.svg"); -const diners = require("@fider/assets/images/card-diners.svg"); -const americanExpress = require("@fider/assets/images/card-americanexpress.svg"); -const discover = require("@fider/assets/images/card-discover.svg"); -const jcb = require("@fider/assets/images/card-jcb.svg"); -const unknown = require("@fider/assets/images/card-unknown.svg"); -const masterCard = require("@fider/assets/images/card-mastercard.svg"); - -interface CardBrandProps { - brand: string; - last4: string; - expMonth: number; - expYear: number; -} - -export const CardInfo = (props: CardBrandProps) => { - const fider = useFider(); - - return ( -

- {props.brand} - - **** **** **** {props.last4}{" "} - - Exp. {props.expMonth}/{props.expYear} - - -

- ); -}; - -const brandImage = (brand: string) => { - switch (brand) { - case "Visa": - return visa; - case "American Express": - return americanExpress; - case "MasterCard": - return masterCard; - case "Discover": - return discover; - case "JCB": - return jcb; - case "Diners Club": - return diners; - } - return unknown; -}; diff --git a/public/components/common/FiderVersion.tsx b/public/components/common/FiderVersion.tsx index 1e6539093..6aa3c2d96 100644 --- a/public/components/common/FiderVersion.tsx +++ b/public/components/common/FiderVersion.tsx @@ -6,15 +6,11 @@ export const FiderVersion = () => { return (

- {!fider.isBillingEnabled() && ( - <> - Support our{" "} - - OpenCollective - -
- - )} + Support our{" "} + + OpenCollective + +
Fider v{fider.settings.version}

); diff --git a/public/components/common/Header.tsx b/public/components/common/Header.tsx index 54f2fccd2..d7ce3e03b 100644 --- a/public/components/common/Header.tsx +++ b/public/components/common/Header.tsx @@ -1,7 +1,7 @@ import "./Header.scss"; import React, { useState, useEffect } from "react"; -import { SignInModal, EnvironmentInfo, Avatar, TenantLogo, TenantStatusInfo } from "@fider/components"; +import { SignInModal, EnvironmentInfo, Avatar, TenantLogo } from "@fider/components"; import { actions } from "@fider/services"; import { FaUser, FaCog, FaCaretDown } from "react-icons/fa"; import { useFider } from "@fider/hooks"; @@ -79,7 +79,6 @@ export const Header = () => { )} - ); }; diff --git a/public/components/common/TenantStatusInfo.tsx b/public/components/common/TenantStatusInfo.tsx deleted file mode 100644 index b68eb6450..000000000 --- a/public/components/common/TenantStatusInfo.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from "react"; -import { TenantStatus } from "@fider/models"; -import { Message } from "./Message"; -import { useFider } from "@fider/hooks"; - -export const TenantStatusInfo = () => { - const fider = useFider(); - - if (!fider.isBillingEnabled() || fider.session.tenant.status !== TenantStatus.Locked) { - return null; - } - - return ( -
- - This site is locked due to lack of a subscription. Visit the Billing settings to update it. - -
- ); -}; diff --git a/public/components/common/form/Form.scss b/public/components/common/form/Form.scss index de8a73e39..488abf568 100644 --- a/public/components/common/form/Form.scss +++ b/public/components/common/form/Form.scss @@ -48,8 +48,7 @@ input[type="text"], textarea, - select, - .StripeElement { + select { background-color: $white; -webkit-appearance: none; appearance: none; diff --git a/public/components/common/form/TextArea.tsx b/public/components/common/form/TextArea.tsx index 43374509c..813a9c683 100644 --- a/public/components/common/form/TextArea.tsx +++ b/public/components/common/form/TextArea.tsx @@ -42,7 +42,7 @@ export const TextArea: React.FunctionComponent = (props) => { value={props.value} minRows={props.minRows || 3} placeholder={props.placeholder} - inputRef={props.inputRef} + ref={props.inputRef} onFocus={props.onFocus} /> diff --git a/public/components/common/index.tsx b/public/components/common/index.tsx index 0fc48dd6a..791467873 100644 --- a/public/components/common/index.tsx +++ b/public/components/common/index.tsx @@ -1,5 +1,4 @@ export * from "./Button"; -export * from "./CardInfo"; export * from "./form/Form"; export * from "./form/Input"; export * from "./form/ImageUploader"; @@ -13,7 +12,6 @@ export * from "./form/Checkbox"; export * from "./form/ImageViewer"; export * from "./MultiLineText"; export * from "./EnvironmentInfo"; -export * from "./TenantStatusInfo"; export * from "./Avatar"; export * from "./Message"; export * from "./Hint"; diff --git a/public/models/billing.ts b/public/models/billing.ts deleted file mode 100644 index 12ec38f97..000000000 --- a/public/models/billing.ts +++ /dev/null @@ -1,37 +0,0 @@ -export interface PaymentInfo { - cardBrand: string; - cardLast4: string; - cardExpMonth: number; - cardExpYear: number; - addressCity: string; - addressCountry: string; - name: string; - email: string; - addressLine1: string; - addressLine2: string; - addressState: string; - addressPostalCode: string; - vatNumber: string; -} - -export interface InvoiceDue { - currency: string; - amountDue: number; - dueDate: string; -} - -export interface BillingPlan { - id: string; - name: string; - description: string; - currency: string; - maxUsers: number; - price: number; - interval: "month" | "year"; -} - -export interface Country { - code: string; - name: string; - isEU: boolean; -} diff --git a/public/models/identity.ts b/public/models/identity.ts index 18c3e3bd9..530c3fa20 100644 --- a/public/models/identity.ts +++ b/public/models/identity.ts @@ -8,11 +8,6 @@ export interface Tenant { status: TenantStatus; isPrivate: boolean; logoBlobKey: string; - billing?: { - stripePlanID: string; - subscriptionEndsAt: string; - trialEndsAt: string; - }; } export enum TenantStatus { diff --git a/public/models/index.ts b/public/models/index.ts index 317710ccb..be6347622 100644 --- a/public/models/index.ts +++ b/public/models/index.ts @@ -1,5 +1,4 @@ export * from "./post"; export * from "./identity"; -export * from "./billing"; export * from "./settings"; export * from "./notification"; diff --git a/public/models/settings.ts b/public/models/settings.ts index 7d38e87b3..d5e7478fd 100644 --- a/public/models/settings.ts +++ b/public/models/settings.ts @@ -18,7 +18,6 @@ export interface SystemSettings { domain: string; hasLegal: boolean; baseURL: string; - stripePublicKey?: string; tenantAssetsURL: string; globalAssetsURL: string; oauth: OAuthProviderOption[]; diff --git a/public/pages/Administration/components/BillingPlanPanel.tsx b/public/pages/Administration/components/BillingPlanPanel.tsx deleted file mode 100644 index 295b99bb3..000000000 --- a/public/pages/Administration/components/BillingPlanPanel.tsx +++ /dev/null @@ -1,234 +0,0 @@ -import React from "react"; -import { Segment, Button, Moment, Modal, ButtonClickEvent } from "@fider/components"; -import { BillingPlan, InvoiceDue } from "@fider/models"; -import { Fider, actions, classSet, currencySymbol } from "@fider/services"; -import { useFider } from "@fider/hooks"; - -interface BillingPlanOptionProps { - tenantUserCount: number; - disabled: boolean; - plan: BillingPlan; - currentPlan?: BillingPlan; - onSubscribe: (plan: BillingPlan) => Promise; - onCancel: (plan: BillingPlan) => Promise; -} - -const BillingPlanOption = (props: BillingPlanOptionProps) => { - const fider = useFider(); - - const billing = fider.session.tenant.billing!; - const isSelected = billing.stripePlanID === props.plan.id && !billing.subscriptionEndsAt; - const className = classSet({ "l-plan": true, selected: isSelected }); - - return ( -
- -

{props.plan.name}

-

{props.plan.description}

-

- {currencySymbol(props.plan.currency)} - {props.plan.price / 100} - /{props.plan.interval} -

- {isSelected && ( - <> -

- -

- - )} - {!isSelected && ( - <> -

- -

- - )} -
-
- ); -}; - -interface BillingPlanPanelProps { - invoiceDue?: InvoiceDue; - tenantUserCount: number; - disabled: boolean; - plans: BillingPlan[]; -} - -interface BillingPlanPanelState { - confirmPlan?: BillingPlan; - action?: "" | "subscribe" | "cancel"; -} - -export class BillingPlanPanel extends React.Component { - constructor(props: BillingPlanPanelProps) { - super(props); - this.state = {}; - } - - private onSubscribe = async (plan: BillingPlan) => { - this.setState({ - confirmPlan: plan, - action: "subscribe", - }); - }; - - private onCancel = async (plan: BillingPlan) => { - this.setState({ - confirmPlan: plan, - action: "cancel", - }); - }; - - private confirm = async (e: ButtonClickEvent) => { - e.preventEnable(); - - if (this.state.action && this.state.confirmPlan) { - const action = this.state.action === "subscribe" ? actions.billingSubscribe : actions.cancelBillingSubscription; - const result = await action(this.state.confirmPlan.id); - if (result.ok) { - location.reload(); - } - } - }; - - private closeModal = async () => { - this.setState({ - action: "", - confirmPlan: undefined, - }); - }; - - private getCurrentPlan(): BillingPlan | undefined { - const filtered = this.props.plans.filter((x) => x.id === Fider.session.tenant.billing!.stripePlanID); - if (filtered.length > 0) { - return filtered[0]; - } - } - - public render() { - const billing = Fider.session.tenant.billing!; - const currentPlan = this.getCurrentPlan(); - const trialExpired = new Date(billing.trialEndsAt) <= new Date(); - - return ( - <> - - {this.state.action === "subscribe" && Subscribe} - {this.state.action === "cancel" && Cancel Subscription} - - {this.state.action === "subscribe" && ( - <> -

- You'll be billed a total of{" "} - - {currencySymbol(this.state.confirmPlan!.currency)} - {this.state.confirmPlan!.price / 100} per {this.state.confirmPlan!.interval} - {" "} - on your card. -

-
    -
  • You can cancel it at any time.
  • -
  • You can upgrade/downgrade it at any time.
  • -
- - )} - {this.state.action === "cancel" && ( - <> -

You're about to cancel your subscription. Please review the following before continuing.

-
    -
  • Canceling the subscription will pause any further billing on your card.
  • -
  • You'll be able to use the service until the end of current period.
  • -
  • You can re-subscribe at any time.
  • -
  • No refunds will be given.
  • -
- Are you sure? - - )} -
- - - - -
- - -

Plans

- {currentPlan && ( -

- {billing.subscriptionEndsAt ? ( - <> - Your {currentPlan.name} subscription ends at{" "} - - - - . Subscribe to a new plan and avoid a service interruption. - - ) : this.props.invoiceDue ? ( - <> - Your upcoming invoice of{" "} - - {currencySymbol(this.props.invoiceDue.currency)} - {this.props.invoiceDue.amountDue / 100} - {" "} - is due on{" "} - - - - . - - ) : ( - <> - )} -

- )} - {!billing.stripePlanID && ( -

- You don't have any active subscription. - {!trialExpired && ( - <> - Your trial period ends at{" "} - - - - . Subscribe to a plan and avoid a service interruption. - - )} -

- )} -
- {this.props.plans.map((x) => ( - - ))} -
-
-
-

- You have {this.props.tenantUserCount} tracked users. -

-
-
-
- - ); - } -} diff --git a/public/pages/Administration/components/PaymentInfoModal.tsx b/public/pages/Administration/components/PaymentInfoModal.tsx deleted file mode 100644 index da009643a..000000000 --- a/public/pages/Administration/components/PaymentInfoModal.tsx +++ /dev/null @@ -1,255 +0,0 @@ -import React from "react"; -import { injectStripe, CardElement, ReactStripeElements } from "react-stripe-elements"; -import { Input, Field, Button, Form, Select, SelectOption, Modal, CardInfo } from "@fider/components"; -import { Failure, actions } from "@fider/services"; -import { PaymentInfo, Country } from "@fider/models"; - -interface PaymentInfoModalProps extends ReactStripeElements.InjectedStripeProps { - paymentInfo?: PaymentInfo; - countries: Country[]; - onClose: () => void; -} - -interface PaymentInfoModalState { - changingCard: boolean; - name: string; - email: string; - addressLine1: string; - addressLine2: string; - addressCity: string; - addressState: string; - addressPostalCode: string; - addressCountry: string; - vatNumber: string; - stripe: stripe.Stripe | null; - error?: Failure; -} - -class PaymentInfoModal extends React.Component { - constructor(props: PaymentInfoModalProps) { - super(props); - this.state = { - stripe: null, - changingCard: false, - name: this.props.paymentInfo ? this.props.paymentInfo.name : "", - email: this.props.paymentInfo ? this.props.paymentInfo.email : "", - addressLine1: this.props.paymentInfo ? this.props.paymentInfo.addressLine1 : "", - addressLine2: this.props.paymentInfo ? this.props.paymentInfo.addressLine2 : "", - addressCity: this.props.paymentInfo ? this.props.paymentInfo.addressCity : "", - addressState: this.props.paymentInfo ? this.props.paymentInfo.addressState : "", - addressPostalCode: this.props.paymentInfo ? this.props.paymentInfo.addressPostalCode : "", - addressCountry: this.props.paymentInfo ? this.props.paymentInfo.addressCountry : "", - vatNumber: this.props.paymentInfo ? this.props.paymentInfo.vatNumber : "", - }; - } - - public handleSubmit = async () => { - if (this.props.paymentInfo && !this.state.changingCard) { - const response = await actions.updatePaymentInfo({ - ...this.state, - }); - - if (response.ok) { - location.reload(); - } else { - this.setState({ - error: response.error, - }); - } - - return; - } - - if (this.props.stripe) { - const result = await this.props.stripe.createToken({ - name: this.state.name, - address_line1: this.state.addressLine1, - address_line2: this.state.addressLine2, - address_city: this.state.addressCity, - address_state: this.state.addressState, - address_zip: this.state.addressPostalCode, - address_country: this.state.addressCountry, - }); - - if (result.token) { - const response = await actions.updatePaymentInfo({ - ...this.state, - card: { - type: result.token.type, - token: result.token.id, - country: result.token.card ? result.token.card.country : "", - }, - }); - - if (response.ok) { - location.reload(); - } else { - this.setState({ - error: response.error, - }); - } - } else if (result.error) { - this.setState({ - error: { - errors: [ - { - field: "card", - message: result.error.message!, - }, - ], - }, - }); - } - } - }; - - private setName = (name: string) => { - this.setState({ name }); - }; - - private setEmail = (email: string) => { - this.setState({ email }); - }; - - private setAddressLine1 = (addressLine1: string) => { - this.setState({ addressLine1 }); - }; - - private setAddressLine2 = (addressLine2: string) => { - this.setState({ addressLine2 }); - }; - - private setAddressCity = (addressCity: string) => { - this.setState({ addressCity }); - }; - - private setAddressState = (addressState: string) => { - this.setState({ addressState }); - }; - - private setAddressPostalCode = (addressPostalCode: string) => { - this.setState({ addressPostalCode }); - }; - - private setVATNumber = (vatNumber: string) => { - this.setState({ vatNumber }); - }; - - private setAddressCountry = (option: SelectOption | undefined) => { - if (option) { - this.setState({ addressCountry: option.value }); - } - }; - - private closeModal = async () => { - this.props.onClose(); - }; - - private changeCard = () => { - this.setState({ changingCard: true }); - }; - - private isEUCountry(): boolean { - if (this.state.addressCountry) { - const filtered = this.props.countries.filter((x) => x.code === this.state.addressCountry); - if (filtered && filtered.length > 0) { - return filtered[0].isEU; - } - } - return false; - } - - public render() { - return ( - - -
-
- {(!this.props.paymentInfo || this.state.changingCard) && ( -
- - -

We neither store nor see your card information. We integrate directly with Stripe.

-
-
- )} - {this.props.paymentInfo && !this.state.changingCard && ( -
- - change - - } - > - - -
- )} -
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
- )} - {!!this.state.addressCountry && ( -
- -

Based on your Billing Address, your subscription will be charged in {this.isEUCountry() ? "Euro (EUR)" : "US Dollar (USD)"}.

-
-
- )} -
-
-
- - - - - -
- ); - } -} - -export default injectStripe(PaymentInfoModal); diff --git a/public/pages/Administration/components/SideMenu.tsx b/public/pages/Administration/components/SideMenu.tsx index bbc390eb9..e87c33076 100644 --- a/public/pages/Administration/components/SideMenu.tsx +++ b/public/pages/Administration/components/SideMenu.tsx @@ -56,7 +56,6 @@ export const SideMenu = (props: SiteMenuProps) => { {fider.session.user.isAdministrator && ( <> - {fider.isBillingEnabled() && !!fider.session.tenant.billing && } )} diff --git a/public/pages/Administration/pages/Billing.page.scss b/public/pages/Administration/pages/Billing.page.scss deleted file mode 100644 index a40393de3..000000000 --- a/public/pages/Administration/pages/Billing.page.scss +++ /dev/null @@ -1,37 +0,0 @@ -@import "~@fider/assets/styles/variables.scss"; - -#p-admin-billing { - .l-plan { - padding: 10px; - border: 1px solid transparent; - - &.selected { - border: 1px solid $main-color; - } - - .l-title { - color: $main-color; - font-size: $font-size-large; - font-weight: 600; - } - - .l-description { - min-height: 40px; - } - - span.l-currency { - color: $gray-darker; - font-size: $font-size-small; - vertical-align: top; - } - - span.l-price { - font-size: $font-size-big; - } - - span.l-interval { - color: $gray-darker; - font-size: $font-size-small; - } - } -} diff --git a/public/pages/Administration/pages/Billing.page.tsx b/public/pages/Administration/pages/Billing.page.tsx deleted file mode 100644 index ebf7ef50e..000000000 --- a/public/pages/Administration/pages/Billing.page.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import "./Billing.page.scss"; - -import React from "react"; - -import { FaFileInvoice } from "react-icons/fa"; -import { AdminBasePage } from "../components/AdminBasePage"; -import { Segment, Button, CardInfo, Message } from "@fider/components"; -import { PaymentInfo, BillingPlan, Country, InvoiceDue } from "@fider/models"; -import { Fider, actions, navigator } from "@fider/services"; -import PaymentInfoModal from "../components/PaymentInfoModal"; -import { StripeProvider, Elements } from "react-stripe-elements"; -import { BillingPlanPanel } from "../components/BillingPlanPanel"; - -interface BillingPageProps { - invoiceDue?: InvoiceDue; - plans?: BillingPlan[]; - tenantUserCount: number; - paymentInfo?: PaymentInfo; - countries: Country[]; -} - -interface BillingPageState { - plans?: BillingPlan[]; - stripe: stripe.Stripe | null; - showModal: boolean; -} - -export default class BillingPage extends AdminBasePage { - public id = "p-admin-billing"; - public name = "billing"; - public icon = FaFileInvoice; - public title = "Billing"; - public subtitle = "Manage your subscription"; - - constructor(props: BillingPageProps) { - super(props); - this.state = { - stripe: null, - showModal: false, - plans: this.props.plans, - }; - } - - private openModal = async () => { - if (!this.state.stripe) { - const script = document.createElement("script"); - script.src = "https://js.stripe.com/v3/"; - script.onload = () => { - this.setState({ - stripe: Stripe(Fider.settings.stripePublicKey!), - showModal: true, - }); - }; - document.body.appendChild(script); - } else { - this.setState({ - showModal: true, - }); - } - }; - - private closeModal = async () => { - this.setState({ - showModal: false, - }); - }; - - public componentDidMount() { - if (!this.props.paymentInfo) { - navigator.getCountryCode().then(this.fetchPlans); - } - } - - private fetchPlans = (countryCode: string) => { - actions.listBillingPlans(countryCode).then((res) => { - if (res.ok) { - this.setState({ plans: res.data }); - } - }); - }; - - public content() { - return ( - <> - {this.state.showModal && ( - - - - - - )} -
-
- -

Payment Info

- {this.props.paymentInfo && ( - <> - - - - )} - {!this.props.paymentInfo && ( - <> - You don't have any payment method set up. Start by adding one. - - - )} -
-
- {this.state.plans && ( -
- -
- )} -
- - ); - } -} diff --git a/public/pages/Administration/pages/ManageMembers.page.tsx b/public/pages/Administration/pages/ManageMembers.page.tsx index 5ec27c3f7..be3c837a7 100644 --- a/public/pages/Administration/pages/ManageMembers.page.tsx +++ b/public/pages/Administration/pages/ManageMembers.page.tsx @@ -185,7 +185,7 @@ export default class ManageMembersPage extends AdminBasePage
  • - · Administratorshave full access to edit and manage content, permissions and all site settings{Fider.isBillingEnabled() ? ", including billing." : "."} + · Administratorshave full access to edit and manage content, permissions and all site settings.
  • · Collaborators can edit and manage content, but not permissions and settings. diff --git a/public/router.tsx b/public/router.tsx index 19c32bee4..8ed4bf1f5 100644 --- a/public/router.tsx +++ b/public/router.tsx @@ -19,7 +19,6 @@ const pathRegex = [ route("/admin/members", Pages.AsyncManageMembersPage), route("/admin/tags", Pages.AsyncManageTagsPage), route("/admin/privacy", Pages.AsyncPrivacySettingsPage), - route("/admin/billing", Pages.AsyncBillingPage), route("/admin/export", Pages.AsyncExportPage), route("/admin/invitations", Pages.AsyncInvitationsPage), route("/admin/authentication", Pages.AsyncManageAuthenticationPage), diff --git a/public/services/actions/billing.ts b/public/services/actions/billing.ts deleted file mode 100644 index b2f44b3c8..000000000 --- a/public/services/actions/billing.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { http, Result } from "@fider/services"; -import { BillingPlan } from "@fider/models"; - -interface UpdatePaymentInfoRequest { - name: string; - card?: { - type: string; - token: string; - country: string; - }; - addressLine1: string; - addressLine2: string; - addressCity: string; - addressState: string; - addressPostalCode: string; - addressCountry: string; - vatNumber: string; -} - -export const listBillingPlans = async (countryCode: string): Promise> => { - return http.get(`/_api/admin/billing/plans/${countryCode}`); -}; - -export const updatePaymentInfo = async (request: UpdatePaymentInfoRequest): Promise => { - return http.post("/_api/admin/billing/paymentinfo", request).then(http.event("billing", "updatepaymentinfo")); -}; - -export const billingSubscribe = async (planID: string): Promise => { - return http.post(`/_api/admin/billing/subscription/${planID}`).then(http.event("billing", "billingsubscribe")); -}; - -export const cancelBillingSubscription = async (planID: string): Promise => { - return http.delete(`/_api/admin/billing/subscription/${planID}`).then(http.event("billing", "cancelbillingsubscription")); -}; diff --git a/public/services/actions/index.ts b/public/services/actions/index.ts index 89511ab7d..ade45197f 100644 --- a/public/services/actions/index.ts +++ b/public/services/actions/index.ts @@ -2,7 +2,6 @@ export * from "./user"; export * from "./tag"; export * from "./post"; export * from "./tenant"; -export * from "./billing"; export * from "./notification"; export * from "./invite"; export * from "./infra"; diff --git a/public/services/fider.ts b/public/services/fider.ts index eea4c8c11..3d53f4b39 100644 --- a/public/services/fider.ts +++ b/public/services/fider.ts @@ -55,10 +55,6 @@ export class FiderImpl { return this.pSettings; } - public isBillingEnabled(): boolean { - return !!this.pSettings.stripePublicKey; - } - public isProduction(): boolean { return this.pSettings.environment === "production"; } diff --git a/public/services/http.ts b/public/services/http.ts index 896d8a2fc..6d21fe601 100644 --- a/public/services/http.ts +++ b/public/services/http.ts @@ -29,8 +29,6 @@ async function toResult(response: Response): Promise> { notify.error("An unexpected error occurred while processing your request."); } else if (response.status === 403) { notify.error("You are not authorized to perform this operation."); - } else if (response.status === 423) { - notify.error("This operation is not allowed. Update your billing settings to unlock it."); } return { diff --git a/public/services/navigator.ts b/public/services/navigator.ts index ed07d48a6..1981aaf51 100644 --- a/public/services/navigator.ts +++ b/public/services/navigator.ts @@ -1,5 +1,4 @@ -import { Fider, http } from "@fider/services"; -import { cache } from "./cache"; +import { Fider } from "@fider/services"; const navigator = { url: () => { @@ -20,19 +19,6 @@ const navigator = { window.history.replaceState({ path: newURL }, "", newURL); } }, - getCountryCode: (): Promise => { - const countryCode = cache.session.get("geolocation_countrycode"); - if (countryCode) { - return Promise.resolve(countryCode); - } - - return http.get("https://ipinfo.io/geo").then((res) => { - if (res.ok) { - cache.session.set("geolocation_countrycode", res.data.country); - return res.data.country; - } - }); - }, }; export default navigator;