Explorar o código

Add skeleton for referrals

Mauricio Araujo %!s(int64=2) %!d(string=hai) anos
pai
achega
a4050f5845

+ 15 - 3
api/server/handlers/user/create.go

@@ -70,16 +70,17 @@ func (u *UserCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 
 	user.Password = string(hashedPw)
 
+	// Generate referral code for user
+	user.ReferralCode = models.NewReferralCode()
+
 	// write the user to the db
 	user, err = u.Repo().User().CreateUser(user)
-
 	if err != nil {
 		u.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
 
 	err = addUserToDefaultProject(u.Config(), user)
-
 	if err != nil {
 		u.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
@@ -95,7 +96,19 @@ func (u *UserCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	// non-fatal send email verification
 	if !user.EmailVerified {
 		err = startEmailVerification(u.Config(), w, r, user)
+		if err != nil {
+			u.HandleAPIErrorNoWrite(w, r, apierrors.NewErrInternal(err))
+		}
+	}
 
+	// create referral if referred by another user
+	if request.ReferredBy != "" {
+		referral := &models.Referral{
+			Code:           request.ReferredBy,
+			ReferredUserID: user.ID,
+		}
+
+		_, err := u.Repo().Referral().CreateReferral(referral)
 		if err != nil {
 			u.HandleAPIErrorNoWrite(w, r, apierrors.NewErrInternal(err))
 		}
@@ -146,7 +159,6 @@ func addUserToDefaultProject(config *config.Config, user *models.User) error {
 					Kind:      types.RoleAdmin,
 				},
 			})
-
 			if err != nil {
 				return err
 			}

+ 3 - 3
api/server/handlers/user/github_callback.go

@@ -85,7 +85,6 @@ func (p *UserOAuthGithubCallbackHandler) ServeHTTP(w http.ResponseWriter, r *htt
 	// non-fatal send email verification
 	if !user.EmailVerified {
 		err = startEmailVerification(p.Config(), w, r, user)
-
 		if err != nil {
 			p.HandleAPIErrorNoWrite(w, r, apierrors.NewErrInternal(err))
 		}
@@ -147,14 +146,15 @@ func upsertUserFromToken(config *config.Config, tok *oauth2.Token) (*models.User
 				GithubUserID:  githubUser.GetID(),
 			}
 
-			user, err = config.Repo.User().CreateUser(user)
+			// Generate referral code for user
+			user.ReferralCode = models.NewReferralCode()
 
+			user, err = config.Repo.User().CreateUser(user)
 			if err != nil {
 				return nil, err
 			}
 
 			err = addUserToDefaultProject(config, user)
-
 			if err != nil {
 				return nil, err
 			}

+ 3 - 3
api/server/handlers/user/google_callback.go

@@ -88,7 +88,6 @@ func (p *UserOAuthGoogleCallbackHandler) ServeHTTP(w http.ResponseWriter, r *htt
 	// non-fatal send email verification
 	if !user.EmailVerified {
 		err = startEmailVerification(p.Config(), w, r, user)
-
 		if err != nil {
 			p.HandleAPIErrorNoWrite(w, r, apierrors.NewErrInternal(err))
 		}
@@ -133,14 +132,15 @@ func upsertGoogleUserFromToken(config *config.Config, tok *oauth2.Token) (*model
 				GoogleUserID:  gInfo.Sub,
 			}
 
-			user, err = config.Repo.User().CreateUser(user)
+			// Generate referral code for user
+			user.ReferralCode = models.NewReferralCode()
 
+			user, err = config.Repo.User().CreateUser(user)
 			if err != nil {
 				return nil, err
 			}
 
 			err = addUserToDefaultProject(config, user)
-
 			if err != nil {
 				return nil, err
 			}

+ 39 - 0
api/server/handlers/user/referrals.go

@@ -0,0 +1,39 @@
+package user
+
+import (
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
+)
+
+// ListUserReferralsHandler is a handler for getting a list of user referrals
+type ListUserReferralsHandler struct {
+	handlers.PorterHandlerWriter
+}
+
+// NewListUserReferralsHandler returns an instance of ListUserReferralsHandler
+func NewListUserReferralsHandler(
+	config *config.Config,
+	writer shared.ResultWriter,
+) *ListUserReferralsHandler {
+	return &ListUserReferralsHandler{
+		PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+	}
+}
+
+func (u *ListUserReferralsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	// ctx, span := telemetry.NewSpan(r.Context(), "serve-list-user-referrals")
+	// defer span.End()
+
+	// user, _ := ctx.Value(types.UserScope).(*models.User)
+
+	// referrals, err := u.Repo().Referral().ListReferralsByUserID(user.ID)
+	// if err != nil {
+	// 	u.HandleAPIError(w, r, err)
+	// 	return
+	// }
+
+	u.WriteResult(w, r, "")
+}

+ 24 - 0
api/server/router/user.go

@@ -472,5 +472,29 @@ func getUserRoutes(
 		Router:   r,
 	})
 
+	// GET /api/users/referrals -> user.NewListUserReferralsHandler
+	listReferralsEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: "/users/referrals",
+			},
+			Scopes: []types.PermissionScope{types.UserScope},
+		},
+	)
+
+	listReferralsHandler := user.NewListUserReferralsHandler(
+		config,
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: listReferralsEndpoint,
+		Handler:  listReferralsHandler,
+		Router:   r,
+	})
+
 	return routes
 }

+ 11 - 0
api/types/referral.go

@@ -0,0 +1,11 @@
+package types
+
+type Referral struct {
+	ID uint `json:"id"`
+	// Code is the referral code that is shared with the referred user
+	Code string `json:"referral_code"`
+	// ReferredUserID is the ID of the user who was referred
+	ReferredUserID uint `json:"referred_user_id"`
+	// Status is the status of the referral (pending, signed_up, etc.)
+	Status string `json:"status"`
+}

+ 5 - 0
api/types/user.go

@@ -7,6 +7,9 @@ type User struct {
 	FirstName     string `json:"first_name"`
 	LastName      string `json:"last_name"`
 	CompanyName   string `json:"company_name"`
+
+	// ReferralCode is a unique code that can be shared to referr other users to Porter
+	ReferralCode string `json:"referral_code"`
 }
 
 type CreateUserRequest struct {
@@ -16,6 +19,8 @@ type CreateUserRequest struct {
 	LastName       string `json:"last_name" form:"required,max=255"`
 	CompanyName    string `json:"company_name" form:"required,max=255"`
 	ReferralMethod string `json:"referral_method" form:"max=255"`
+	// ReferredBy is the referral code of the user who referred this user
+	ReferredBy string `json:"referred_by_code" form:"max=255"`
 }
 
 type CreateUserResponse User

+ 34 - 0
dashboard/src/lib/hooks/useStripe.tsx

@@ -367,3 +367,37 @@ export const useCustomerUsage = (
     usage: usageReq.data ?? null,
   };
 };
+
+export const useReferrals = (): TGetPlan => {
+  const { currentProject, user } = useContext(Context);
+
+  // Fetch current plan
+  const planReq = useQuery(
+    ["getReferrals", user?.id],
+    async (): Promise<Plan | null> => {
+      if (!currentProject?.billing_enabled) {
+        return null;
+      }
+
+      if (!user?.id) {
+        return null;
+      }
+
+      try {
+        const res = await api.getReferrals(
+          "<token>",
+          {},
+          { user_id: user.id }
+        );
+
+        const referrals = PlanValidator.parse(res.data);
+        return referrals;
+      } catch (error) {
+        return null
+      }
+    });
+
+  return {
+    plan: planReq.data ?? null,
+  };
+};

+ 20 - 1
dashboard/src/main/auth/Register.tsx

@@ -35,6 +35,9 @@ const Register: React.FC<Props> = ({ authenticate }) => {
   const [lastName, setLastName] = useState("");
   const [lastNameError, setLastNameError] = useState(false);
   const [companyName, setCompanyName] = useState("");
+  const [referralCode, setReferralCode] = useState("");
+  const [referralCodeError, setReferralCodeError] = useState(false);
+
   const [companyNameError, setCompanyNameError] = useState(false);
   const [email, setEmail] = useState("");
   const [emailError, setEmailError] = useState(false);
@@ -118,6 +121,7 @@ const Register: React.FC<Props> = ({ authenticate }) => {
               chosenReferralOption === "Other"
                 ? `Other: ${referralOtherText}`
                 : chosenReferralOption,
+            referred_by_code: referralCode,
           },
           {}
         )
@@ -178,7 +182,7 @@ const Register: React.FC<Props> = ({ authenticate }) => {
           if (res?.data?.redirect) {
             window.location.href = res.data.redirect;
           } else {
-            setUser(res?.data?.id, res?.data?.email);
+            setUser(res?.data?.id);
             authenticate();
 
             try {
@@ -400,6 +404,21 @@ const Register: React.FC<Props> = ({ authenticate }) => {
               setValue={setChosenReferralOption}
               value={chosenReferralOption}
             />
+            <Spacer y={0.5} />
+            <Input
+              placeholder="Code"
+              label="Referral code"
+              value={referralCode}
+              setValue={(x) => {
+                setReferralCode(x);
+                setReferralCodeError(false);
+              }}
+              width="100%"
+              height="40px"
+              error={referralCodeError && ""}
+            />
+            <Spacer y={0.5} />
+
             {chosenReferralOption === "Other" && (
               <>
                 <Spacer y={0.5} />

+ 8 - 0
dashboard/src/main/home/project-settings/ProjectSettings.tsx

@@ -26,6 +26,7 @@ import settingsGrad from "assets/settings-grad.svg";
 import DashboardHeader from "../cluster-dashboard/DashboardHeader";
 import APITokensSection from "./APITokensSection";
 import BillingPage from "./BillingPage";
+import ReferralsPage from "./ReferralsPage";
 import InvitePage from "./InviteList";
 import Metadata from "./Metadata";
 import ProjectDeleteConsent from "./ProjectDeleteConsent";
@@ -95,6 +96,11 @@ function ProjectSettings(props: any) {
         });
       }
 
+      tabOpts.push({
+        value: "referrals",
+        label: "Referrals",
+      });
+
       tabOpts.push({
         value: "additional-settings",
         label: "Additional settings",
@@ -172,6 +178,8 @@ function ProjectSettings(props: any) {
       return <APITokensSection />;
     } else if (currentTab === "billing") {
       return <BillingPage></BillingPage>;
+    } else if (currentTab === "referrals") {
+      return <ReferralsPage></ReferralsPage>
     } else {
       return (
         <>

+ 26 - 0
dashboard/src/main/home/project-settings/ReferralsPage.tsx

@@ -0,0 +1,26 @@
+import React, { useContext } from "react";
+import Spacer from "components/porter/Spacer";
+import Text from "components/porter/Text";
+import { Context } from "shared/Context";
+import Fieldset from "components/porter/Fieldset";
+
+function ReferralsPage(): JSX.Element {
+
+
+    return (
+        <>
+            <Text size={16}>Referrals</Text>
+            <Spacer y={1} />
+            <Text color="helper">
+                Refer people to Porter to earn credits.
+            </Text>
+            <Spacer y={1} />
+            <Text>
+                Your referral code is {user?.referralCode}
+            </Text>
+            <Spacer y={1} />
+        </>
+    )
+}
+
+export default ReferralsPage;

+ 1 - 1
dashboard/src/shared/Context.tsx

@@ -139,7 +139,7 @@ class ContextProvider extends Component<PropsType, StateType> {
     user: null,
     setUser: (userId: number, email: string) => {
       this.setState({
-        user: { userId, email, isPorterUser: email?.endsWith("@porter.run") },
+        user: { userId, email, isPorterUser: email?.endsWith("@porter.run"), referralCode: referralCode },
       });
       if (window.intercomSettings) {
         window.intercomSettings["Porter User ID"] = userId;

+ 50 - 50
dashboard/src/shared/api.tsx

@@ -386,9 +386,8 @@ const getFeedEvents = baseApi<
   }
 >("GET", (pathParams) => {
   const { project_id, cluster_id, stack_name, page } = pathParams;
-  return `/api/projects/${project_id}/clusters/${cluster_id}/applications/${stack_name}/events?page=${
-    page || 1
-  }`;
+  return `/api/projects/${project_id}/clusters/${cluster_id}/applications/${stack_name}/events?page=${page || 1
+    }`;
 });
 
 const createEnvironment = baseApi<
@@ -876,11 +875,9 @@ const detectBuildpack = baseApi<
     branch: string;
   }
 >("GET", (pathParams) => {
-  return `/api/projects/${pathParams.project_id}/gitrepos/${
-    pathParams.git_repo_id
-  }/repos/${pathParams.kind}/${pathParams.owner}/${
-    pathParams.name
-  }/${encodeURIComponent(pathParams.branch)}/buildpack/detect`;
+  return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id
+    }/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name
+    }/${encodeURIComponent(pathParams.branch)}/buildpack/detect`;
 });
 
 const detectGitlabBuildpack = baseApi<
@@ -911,11 +908,9 @@ const getBranchContents = baseApi<
     branch: string;
   }
 >("GET", (pathParams) => {
-  return `/api/projects/${pathParams.project_id}/gitrepos/${
-    pathParams.git_repo_id
-  }/repos/${pathParams.kind}/${pathParams.owner}/${
-    pathParams.name
-  }/${encodeURIComponent(pathParams.branch)}/contents`;
+  return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id
+    }/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name
+    }/${encodeURIComponent(pathParams.branch)}/contents`;
 });
 
 const getProcfileContents = baseApi<
@@ -931,11 +926,9 @@ const getProcfileContents = baseApi<
     branch: string;
   }
 >("GET", (pathParams) => {
-  return `/api/projects/${pathParams.project_id}/gitrepos/${
-    pathParams.git_repo_id
-  }/repos/${pathParams.kind}/${pathParams.owner}/${
-    pathParams.name
-  }/${encodeURIComponent(pathParams.branch)}/procfile`;
+  return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id
+    }/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name
+    }/${encodeURIComponent(pathParams.branch)}/procfile`;
 });
 
 const getPorterYamlContents = baseApi<
@@ -951,11 +944,9 @@ const getPorterYamlContents = baseApi<
     branch: string;
   }
 >("GET", (pathParams) => {
-  return `/api/projects/${pathParams.project_id}/gitrepos/${
-    pathParams.git_repo_id
-  }/repos/${pathParams.kind}/${pathParams.owner}/${
-    pathParams.name
-  }/${encodeURIComponent(pathParams.branch)}/porteryaml`;
+  return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id
+    }/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name
+    }/${encodeURIComponent(pathParams.branch)}/porteryaml`;
 });
 
 const parsePorterYaml = baseApi<
@@ -1015,32 +1006,30 @@ const getBranchHead = baseApi<
     branch: string;
   }
 >("GET", (pathParams) => {
-  return `/api/projects/${pathParams.project_id}/gitrepos/${
-    pathParams.git_repo_id
-  }/repos/${pathParams.kind}/${pathParams.owner}/${
-    pathParams.name
-  }/${encodeURIComponent(pathParams.branch)}/head`;
+  return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id
+    }/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name
+    }/${encodeURIComponent(pathParams.branch)}/head`;
 });
 
 const createApp = baseApi<
   | {
-      name: string;
-      deployment_target_id: string;
-      type: "github";
-      git_repo_id: number;
-      git_branch: string;
-      git_repo_name: string;
-      porter_yaml_path: string;
-    }
+    name: string;
+    deployment_target_id: string;
+    type: "github";
+    git_repo_id: number;
+    git_branch: string;
+    git_repo_name: string;
+    porter_yaml_path: string;
+  }
   | {
-      name: string;
-      deployment_target_id: string;
-      type: "docker-registry";
-      image: {
-        repository: string;
-        tag: string;
-      };
-    },
+    name: string;
+    deployment_target_id: string;
+    type: "docker-registry";
+    image: {
+      repository: string;
+      tag: string;
+    };
+  },
   {
     project_id: number;
     cluster_id: number;
@@ -2167,6 +2156,7 @@ const registerUser = baseApi<{
   last_name: string;
   company_name: string;
   referral_method?: string;
+  referred_by_code?: string;
 }>("POST", "/api/users");
 
 const rollbackChart = baseApi<
@@ -2308,11 +2298,9 @@ const getEnvGroup = baseApi<
     version?: number;
   }
 >("GET", (pathParams) => {
-  return `/api/projects/${pathParams.id}/clusters/${
-    pathParams.cluster_id
-  }/namespaces/${pathParams.namespace}/envgroup?name=${pathParams.name}${
-    pathParams.version ? "&version=" + pathParams.version : ""
-  }`;
+  return `/api/projects/${pathParams.id}/clusters/${pathParams.cluster_id
+    }/namespaces/${pathParams.namespace}/envgroup?name=${pathParams.name}${pathParams.version ? "&version=" + pathParams.version : ""
+    }`;
 });
 
 const getConfigMap = baseApi<
@@ -3589,7 +3577,18 @@ const deletePaymentMethod = baseApi<
     `/api/projects/${project_id}/billing/payment_method/${payment_method_id}`
 );
 
-const getGithubStatus = baseApi<{}, {}>("GET", ({}) => `/api/status/github`);
+const getReferrals = baseApi<
+  {},
+  {
+    user_id?: number;
+  }
+>(
+  "GET",
+  ({ user_id }) =>
+    `/api/users/${user_id}/referrals`
+);
+
+const getGithubStatus = baseApi<{}, {}>("GET", ({ }) => `/api/status/github`);
 
 const createSecretAndOpenGitHubPullRequest = baseApi<
   {
@@ -3982,6 +3981,7 @@ export default {
   addPaymentMethod,
   setDefaultPaymentMethod,
   deletePaymentMethod,
+  getReferrals,
 
   // STATUS
   getGithubStatus,

+ 1 - 0
go.mod

@@ -82,6 +82,7 @@ require (
 	github.com/honeycombio/otel-config-go v1.11.0
 	github.com/launchdarkly/go-sdk-common/v3 v3.0.1
 	github.com/launchdarkly/go-server-sdk/v6 v6.1.0
+	github.com/lithammer/shortuuid/v4 v4.0.0
 	github.com/matryer/is v1.4.0
 	github.com/nats-io/nats.go v1.24.0
 	github.com/open-policy-agent/opa v0.44.0

+ 2 - 0
go.sum

@@ -1249,6 +1249,8 @@ github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0=
 github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE=
 github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo=
+github.com/lithammer/shortuuid/v4 v4.0.0 h1:QRbbVkfgNippHOS8PXDkti4NaWeyYfcBTHtw7k08o4c=
+github.com/lithammer/shortuuid/v4 v4.0.0/go.mod h1:Zs8puNcrvf2rV9rTH51ZLLcj7ZXqQI3lv67aw4KiB1Y=
 github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
 github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=

+ 34 - 0
internal/models/referral.go

@@ -0,0 +1,34 @@
+package models
+
+import (
+	"github.com/lithammer/shortuuid/v4"
+	"github.com/porter-dev/porter/api/types"
+	"gorm.io/gorm"
+)
+
+// Referral type that extends gorm.Model
+type Referral struct {
+	gorm.Model
+
+	// Code is the referral code that is shared with the referred user
+	Code string
+	// UserID is the ID of the user who made the referral
+	UserID uint
+	// ReferredUserID is the ID of the user who was referred
+	ReferredUserID uint
+	// Status is the status of the referral (pending, signed_up, etc.)
+	Status string
+}
+
+func NewReferralCode() string {
+	return shortuuid.New()
+}
+
+// ToReferralType generates an external types.Referral to be shared over REST
+func (r *Referral) ToReferralType() *types.Referral {
+	return &types.Referral{
+		ID:             r.ID,
+		ReferredUserID: r.ReferredUserID,
+		Status:         r.Status,
+	}
+}

+ 6 - 0
internal/models/user.go

@@ -23,6 +23,11 @@ type User struct {
 	// The github user id used for login (optional)
 	GithubUserID int64
 	GoogleUserID string
+
+	// ReferralCode is a unique code that can be shared to referr other users to Porter
+	ReferralCode string
+
+	Referrals []Referral `json:"referrals"`
 }
 
 // ToUserType generates an external types.User to be shared over REST
@@ -34,5 +39,6 @@ func (u *User) ToUserType() *types.User {
 		FirstName:     u.FirstName,
 		LastName:      u.LastName,
 		CompanyName:   u.CompanyName,
+		ReferralCode:  u.ReferralCode,
 	}
 }

+ 1 - 0
internal/repository/gorm/migrate.go

@@ -88,5 +88,6 @@ func AutoMigrate(db *gorm.DB, debug bool) error {
 		&models.Ipam{},
 		&models.AppEventWebhooks{},
 		&models.ClusterHealthReport{},
+		&models.Referral{},
 	)
 }

+ 39 - 0
internal/repository/gorm/referrals.go

@@ -0,0 +1,39 @@
+package gorm
+
+import (
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
+	"gorm.io/gorm"
+)
+
+// ReferralRepository uses gorm.DB for querying the database
+type ReferralRepository struct {
+	db *gorm.DB
+}
+
+// NewReferralRepository returns a ReferralRepository which uses
+// gorm.DB for querying the database
+func NewReferralRepository(db *gorm.DB) repository.ReferralRepository {
+	return &ReferralRepository{db}
+}
+
+// CreateInvite creates a new invite
+func (repo *ReferralRepository) CreateReferral(referral *models.Referral) (*models.Referral, error) {
+	user := &models.User{}
+
+	if err := repo.db.Where("referral_code = ?", referral.Code).First(&user).Error; err != nil {
+		return nil, err
+	}
+
+	assoc := repo.db.Model(&user).Association("Referrals")
+
+	if assoc.Error != nil {
+		return nil, assoc.Error
+	}
+
+	if err := assoc.Append(referral); err != nil {
+		return nil, err
+	}
+
+	return referral, nil
+}

+ 6 - 0
internal/repository/gorm/repository.go

@@ -62,6 +62,7 @@ type GormRepository struct {
 	datastore                 repository.DatastoreRepository
 	appInstance               repository.AppInstanceRepository
 	ipam                      repository.IpamRepository
+	referral                  repository.ReferralRepository
 }
 
 func (t *GormRepository) User() repository.UserRepository {
@@ -293,6 +294,10 @@ func (t *GormRepository) Ipam() repository.IpamRepository {
 	return t.ipam
 }
 
+func (t *GormRepository) Referral() repository.ReferralRepository {
+	return t.referral
+}
+
 // NewRepository returns a Repository which persists users in memory
 // and accepts a parameter that can trigger read/write errors
 func NewRepository(db *gorm.DB, key *[32]byte, storageBackend credentials.CredentialStorage) repository.Repository {
@@ -352,5 +357,6 @@ func NewRepository(db *gorm.DB, key *[32]byte, storageBackend credentials.Creden
 		appInstance:               NewAppInstanceRepository(db),
 		ipam:                      NewIpamRepository(db),
 		appEventWebhook:           NewAppEventWebhookRepository(db),
+		referral:                  NewReferralRepository(db),
 	}
 }

+ 15 - 0
internal/repository/referral.go

@@ -0,0 +1,15 @@
+package repository
+
+import (
+	"github.com/porter-dev/porter/internal/models"
+)
+
+// ReferralRepository represents the set of queries on the Referral model
+type ReferralRepository interface {
+	CreateReferral(referral *models.Referral) (*models.Referral, error)
+	// ReadReferral(referralID uint) (*models.Referral, error)
+	// ReadReferralByUserID(userID, referralID string) (*models.Referral, error)
+	// ListReferralsByUserID(userID uint) ([]*models.Referral, error)
+	// UpdateReferral(referral *models.Referral) (*models.Referral, error)
+	// DeleteReferral(referralID uint) error
+}

+ 1 - 0
internal/repository/repository.go

@@ -55,4 +55,5 @@ type Repository interface {
 	GithubWebhook() GithubWebhookRepository
 	Datastore() DatastoreRepository
 	AppInstance() AppInstanceRepository
+	Referral() ReferralRepository
 }