diff --git a/devops/migrations/20180523161835-create_subscribers_table.sql b/devops/migrations/20180523161835-create_subscribers_table.sql new file mode 100644 index 00000000..a43a3037 --- /dev/null +++ b/devops/migrations/20180523161835-create_subscribers_table.sql @@ -0,0 +1,13 @@ + +-- +migrate Up +CREATE TABLE subscriptions ( + id SERIAL PRIMARY KEY, + organization_id INTEGER NOT NULL REFERENCES organizations (id) ON UPDATE CASCADE ON DELETE RESTRICT, + name VARCHAR(100) NOT NULL, + email VARCHAR(255) NOT NULL, + phone VARCHAR(45) NOT NULL, + date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- +migrate Down +DROP TABLE subscriptions; diff --git a/server/cmd/serve.go b/server/cmd/serve.go index d99c03fb..d95536aa 100644 --- a/server/cmd/serve.go +++ b/server/cmd/serve.go @@ -49,6 +49,7 @@ func serveCmdFunc(cmd *cobra.Command, args []string) { oR := repo.NewOrganizationRepository(conn) nR := repo.NewNeedRepository(conn) + sR := repo.NewSubscriptionRepository(conn) needResponseRepo := repo.NewNeedResponseRepository(conn) @@ -83,6 +84,10 @@ func serveCmdFunc(cmd *cobra.Command, args []string) { negroni.WrapFunc(handlers.DeleteOrganizationImageHandler(oR)), )).Methods("DELETE") + v1.Path("/organization/{id:[0-9]+}/subscribe").HandlerFunc( + handlers.CreateSubscriptionHandler(sR), + ).Methods("POST") + v1.HandleFunc("/need/{id}", handlers.GetNeedHandler(nR, oR)).Methods("GET") v1.Path("/need").Handler(authMiddleware.With( diff --git a/server/db/repo/subscription.go b/server/db/repo/subscription.go new file mode 100644 index 00000000..730de97a --- /dev/null +++ b/server/db/repo/subscription.go @@ -0,0 +1,93 @@ +package repo + +import ( + "database/sql" + "errors" + "fmt" + "strings" + + "github.com/Coderockr/vitrine-social/server/model" + "github.com/jmoiron/sqlx" +) + +// SubscriptionRepository is a implementation for Postgres +type SubscriptionRepository struct { + db *sqlx.DB + orgRepo *OrganizationRepository +} + +// NewSubscriptionRepository creates a new repository +func NewSubscriptionRepository(db *sqlx.DB) *SubscriptionRepository { + return &SubscriptionRepository{ + db: db, + orgRepo: NewOrganizationRepository(db), + } +} + +// Create new subscription +func (r *SubscriptionRepository) Create(s model.Subscription) (model.Subscription, error) { + s, err := validate(r, s) + + if err != nil { + return s, err + } + + row := r.db.QueryRow( + `INSERT INTO subscriptions (organization_id, name, email, phone) + VALUES($1, $2, $3, $4) + RETURNING id + `, + s.OrganizationID, + s.Name, + s.Email, + s.Phone, + ) + + err = row.Scan(&s.ID) + + if err != nil { + return s, err + } + + return s, nil +} + +func validate(r *SubscriptionRepository, s model.Subscription) (model.Subscription, error) { + s.Name = strings.TrimSpace(s.Name) + if len(s.Name) == 0 { + return s, errors.New("Deve ser informado um nome para a Inscrição") + } + + s.Email = strings.TrimSpace(s.Email) + if len(s.Email) == 0 { + return s, errors.New("Deve ser informado um email para a Inscrição") + } + + s.Phone = strings.TrimSpace(s.Phone) + if len(s.Phone) == 0 { + return s, errors.New("Deve ser informado um telefone para a Inscrição") + } + + _, err := getBaseOrganization(r.db, s.OrganizationID) + switch { + case err == sql.ErrNoRows: + return s, fmt.Errorf("Não foi encontrada Organização com ID: %d", s.OrganizationID) + case err != nil: + return s, err + } + + var found int64 + err = r.db.QueryRow(` + SELECT COUNT(1) as found + FROM subscriptions + WHERE organization_id = $1 AND email LIKE $2`, + s.OrganizationID, + s.Email, + ).Scan(&found) + + if found > 0 { + return s, fmt.Errorf("Este email já está inscrito para a Organização %d", s.OrganizationID) + } + + return s, nil +} diff --git a/server/handlers/subscription.go b/server/handlers/subscription.go new file mode 100644 index 00000000..637a4c3c --- /dev/null +++ b/server/handlers/subscription.go @@ -0,0 +1,53 @@ +package handlers + +import ( + "fmt" + "net/http" + "strconv" + "time" + + "github.com/Coderockr/vitrine-social/server/model" + "github.com/gorilla/mux" +) + +type ( + // SubscriptionRepository represet operations for subscription repository. + SubscriptionRepository interface { + Create(model.Subscription) (model.Subscription, error) + } +) + +// CreateSubscriptionHandler create a new subscription +func CreateSubscriptionHandler(repo SubscriptionRepository) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + urlVars := mux.Vars(r) + id, err := strconv.ParseInt(urlVars["id"], 10, 64) + if err != nil { + HandleHTTPError(w, http.StatusBadRequest, fmt.Errorf("Não foi possível entender o número: %s", urlVars["id"])) + return + } + + var bodyVars map[string]string + err = requestToJSONObject(r, &bodyVars) + if err != nil { + HandleHTTPError(w, http.StatusBadRequest, err) + return + } + + now := time.Now() + s, err := repo.Create(model.Subscription{ + OrganizationID: id, + Email: bodyVars["email"], + Name: bodyVars["name"], + Phone: bodyVars["phone"], + Date: &now, + }) + + if err != nil { + HandleHTTPError(w, http.StatusBadRequest, err) + return + } + + HandleHTTPSuccess(w, map[string]int64{"id": s.ID}) + } +} diff --git a/server/handlers/subscription_test.go b/server/handlers/subscription_test.go new file mode 100644 index 00000000..5d84939e --- /dev/null +++ b/server/handlers/subscription_test.go @@ -0,0 +1,113 @@ +package handlers_test + +import ( + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/Coderockr/vitrine-social/server/handlers" + "github.com/Coderockr/vitrine-social/server/model" + "github.com/gorilla/mux" + "github.com/stretchr/testify/require" +) + +type ( + subscriptionRepositoryMock struct { + CreateFN func(model.Subscription) (model.Subscription, error) + } +) + +func TestCreateSubscriptionHandler(t *testing.T) { + type params struct { + organizationID string + repository handlers.SubscriptionRepository + } + + tests := map[string]struct { + body string + status int + response string + params params + }{ + "should fail beacuse trying to create without parameters": { + body: ``, + status: http.StatusBadRequest, + response: ``, + params: params{ + organizationID: "1", + repository: &subscriptionRepositoryMock{ + CreateFN: func(model.Subscription) (model.Subscription, error) { + s := model.Subscription{} + return s, errors.New("Deve ser informado um nome para a Inscrição") + }, + }, + }, + }, + "should fail beacuse trying to create with no valid organization": { + body: ``, + status: http.StatusBadRequest, + response: ``, + params: params{ + organizationID: "5", + repository: &subscriptionRepositoryMock{ + CreateFN: func(model.Subscription) (model.Subscription, error) { + s := model.Subscription{} + return s, fmt.Errorf("Não foi encontrada Organização com ID: 5") + }, + }, + }, + }, + "should success beacuse the right values were sent": { + body: `{ + "name": "Coderockr Test", + "email": "test@coderockr.com", + "phone": "(54) 99999-9999" + }`, + status: http.StatusOK, + response: `{ + "id": 1 + }`, + params: params{ + organizationID: "1", + repository: &subscriptionRepositoryMock{ + CreateFN: func(model.Subscription) (model.Subscription, error) { + s := model.Subscription{ + ID: 1, + Name: "Coderockr Test", + Email: "test@coderockr.com", + Phone: "(54) 99999-9999", + } + return s, nil + }, + }, + }, + }, + } + + for name, v := range tests { + t.Run(name, func(t *testing.T) { + r, _ := http.NewRequest("POST", "/v1/organization/"+v.params.organizationID+"/subscribe", strings.NewReader(v.body)) + r = mux.SetURLVars(r, map[string]string{"id": v.params.organizationID}) + + resp := httptest.NewRecorder() + + handlers.CreateSubscriptionHandler(v.params.repository)(resp, r) + + result := resp.Result() + body, _ := ioutil.ReadAll(result.Body) + + if len(v.response) > 0 { + require.JSONEq(t, v.response, string(body)) + } + require.Equal(t, v.status, resp.Code) + }) + } +} + +func (r *subscriptionRepositoryMock) Create(s model.Subscription) (model.Subscription, error) { + return r.CreateFN(s) +} diff --git a/server/model/model.go b/server/model/model.go index c408c51b..ed7c83da 100644 --- a/server/model/model.go +++ b/server/model/model.go @@ -93,6 +93,16 @@ type Category struct { Icon string `valid:"required" db:"icon"` } +// Subscription relacionada a uma organização +type Subscription struct { + ID int64 `valid:"required" db:"id"` + OrganizationID int64 `valid:"required" db:"organization_id"` + Name string `valid:"required" db:"name"` + Email string `valid:"required" db:"email"` + Phone string `valid:"required" db:"phone"` + Date *time.Time `db:"date"` +} + func (s *needStatus) Scan(src interface{}) error { var str string