diff --git a/README.md b/README.md index cf752eb..0f51881 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Jamming around orders with API endpoints 🎸 ## Environment Variables -**Required** environment variables +If you want to run this project locally you must set up these environment variables: CONTRACTUS_POSTGRES_URL CONTRACTUS_GOOGLE_CLIENT_ID @@ -20,6 +20,8 @@ Jamming around orders with API endpoints 🎸 CONTRACTUS_DOMAIN CONTRACTUS_JWT_SECRET_KEY +This could be made in the [docker-compose.yml](./docker-compose.yaml) + ## Get Started To start the service locally, you can type `make dev/start` and after that you can use the docker container IP to play around the routes, `make ip` @@ -64,7 +66,13 @@ Local: ## API documentation [API Docs](api/docs/) -## Attention points - - For a while, the integration-tests just ran locally not in CI, this increased the time to ship code 🚀 - - We don't have a way to paginate transactions. 😔 - - To publish images and new releases, for now, the only way is using the command line, isn't automate by CI yet 😔 +## It's also good to know +- You can log in to the service using your Google account. No bureaucracy to 🎸 +- Structured logs all the way 🥸 +- Deployed to the open sea through Heroku 🌊 (Check the repository details to access the link) +--- +- For a while, the integration-tests just ran locally not in CI 😔, this increased the time to 🚀 code; +- We don't have a way to paginate transactions yet; 😔 +- To publish images and new releases, for now, the only way is using the command line, isn't automate by CI yet; 😔 +- The infra isn't automated by the power of the IAC yet.😔 Button engineer only. 🔘✅ + diff --git a/api/api.go b/api/api.go index 7f3f8d0..0a885f9 100644 --- a/api/api.go +++ b/api/api.go @@ -73,7 +73,7 @@ func parseFile(r *http.Request) (content string, err error) { } // convert is an internal function responsible for converting the file content into transactions. -func convert(content string) (t []contractus.Transaction, err error) { +func convert(content, email string) (t []contractus.Transaction, err error) { content = strings.Replace(content, "\t", "", -1) var re = regexp.MustCompile(`(?m).*$\n`) for _, match := range re.FindAllString(content, -1) { @@ -84,7 +84,7 @@ func convert(content string) (t []contractus.Transaction, err error) { ProductPriceCents: match[56:66], SellerName: match[66:], } - transac, err := rawTransaction.Convert() + transac, err := rawTransaction.Convert(email) if err != nil { return nil, fmt.Errorf("failed to convert transaction: %v", err) } diff --git a/api/api_test.go b/api/api_test.go index fd3ad7d..4759016 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -34,13 +34,14 @@ func TestConvert(t *testing.T) { content := `12022-01-15T19:20:30-03:00CURSO DE BEM-ESTAR 0000012750JOSE CARLOS 12021-12-03T11:46:02-03:00DOMINANDO INVESTIMENTOS 0000050000MARIA CANDIDA ` - transac, err := convert(content) + transac, err := convert(content, "jj@example.com") if err != nil { t.Fatal(err) } want := []contractus.Transaction{ { + Email: "jj@example.com", Type: 1, Date: time.Date(2022, 01, 15, 22, 20, 30, 0, time.UTC), ProductDescription: "CURSO DE BEM-ESTAR", @@ -50,6 +51,7 @@ func TestConvert(t *testing.T) { Action: "venda produtor", }, { + Email: "jj@example.com", Type: 1, Date: time.Date(2021, 12, 03, 14, 46, 02, 0, time.UTC), ProductDescription: "DOMINANDO INVESTIMENTOS", @@ -61,6 +63,7 @@ func TestConvert(t *testing.T) { } if len(transac) == len(want) { + assert(t, transac[0].Email, want[0].Email) assert(t, transac[0].Type, want[0].Type) assert(t, transac[0].Date.Format(time.RFC3339), want[0].Date.Format(time.RFC3339)) assert(t, transac[0].ProductDescription, want[0].ProductDescription) @@ -69,6 +72,7 @@ func TestConvert(t *testing.T) { assert(t, transac[0].SellerType, want[0].SellerType) assert(t, transac[0].Action, want[0].Action) + assert(t, transac[1].Email, want[1].Email) assert(t, transac[1].Type, want[1].Type) assert(t, transac[1].Date.Format(time.RFC3339), want[1].Date.Format(time.RFC3339)) assert(t, transac[1].ProductDescription, want[1].ProductDescription) diff --git a/api/transactionhandler.go b/api/transactionhandler.go index c94a9a2..f7b6505 100644 --- a/api/transactionhandler.go +++ b/api/transactionhandler.go @@ -12,6 +12,7 @@ import ( "log/slog" "github.com/go-chi/chi/v5" + "github.com/go-chi/jwtauth/v5" "github.com/go-openapi/runtime/middleware" "github.com/perebaj/contractus" "github.com/perebaj/contractus/postgres" @@ -19,8 +20,8 @@ import ( type transactionStorage interface { SaveTransaction(ctx context.Context, t []contractus.Transaction) error - Balance(ctx context.Context, sellerType, sellerName string) (*contractus.BalanceResponse, error) - Transactions(ctx context.Context) (contractus.TransactionResponse, error) + Balance(ctx context.Context, sellerType, sellerName, email string) (*contractus.BalanceResponse, error) + Transactions(ctx context.Context, email string) (contractus.TransactionResponse, error) } type transactionHandler struct { @@ -62,8 +63,12 @@ func RegisterSwaggerHandler(r chi.Router) { } func (s transactionHandler) producerBalance(w http.ResponseWriter, r *http.Request) { - // _, claims, _ := jwtauth.FromContext(r.Context()) - // slog.Info("Claims", "claims", claims) + email, err := emailFromToken(r) + if err != nil { + sendErr(w, http.StatusBadRequest, Error{"email_required", "email is required"}) + return + } + query := r.URL.Query() name := query.Get("name") if name == "" { @@ -71,7 +76,7 @@ func (s transactionHandler) producerBalance(w http.ResponseWriter, r *http.Reque return } - b, err := s.storage.Balance(r.Context(), "producer", name) + b, err := s.storage.Balance(r.Context(), "producer", name, email) if err != nil { if err == postgres.ErrSellerNotFound { sendErr(w, http.StatusNotFound, Error{"seller_not_found", "seller not found"}) @@ -85,6 +90,11 @@ func (s transactionHandler) producerBalance(w http.ResponseWriter, r *http.Reque } func (s transactionHandler) affiliateBalance(w http.ResponseWriter, r *http.Request) { + email, err := emailFromToken(r) + if err != nil { + sendErr(w, http.StatusBadRequest, Error{"email_required", "email is required"}) + return + } query := r.URL.Query() name := query.Get("name") if name == "" { @@ -92,7 +102,7 @@ func (s transactionHandler) affiliateBalance(w http.ResponseWriter, r *http.Requ return } - b, err := s.storage.Balance(r.Context(), "affiliate", name) + b, err := s.storage.Balance(r.Context(), "affiliate", name, email) if err != nil { if err == postgres.ErrSellerNotFound { sendErr(w, http.StatusNotFound, Error{"seller_not_found", "seller not found"}) @@ -106,6 +116,12 @@ func (s transactionHandler) affiliateBalance(w http.ResponseWriter, r *http.Requ } func (s transactionHandler) upload(w http.ResponseWriter, r *http.Request) { + email, err := emailFromToken(r) + if err != nil { + sendErr(w, http.StatusBadRequest, Error{"email_required", "email is required"}) + return + } + content, err := parseFile(r) if err != nil { slog.Error("Failed to parse file", "error", err) @@ -113,7 +129,7 @@ func (s transactionHandler) upload(w http.ResponseWriter, r *http.Request) { return } - transactions, err := convert(content) + transactions, err := convert(content, email) if err != nil { slog.Error("Failed to convert transactions", "error", err, "content", content) sendErr(w, http.StatusBadRequest, Error{"invalid_file", "invalid file"}) @@ -130,7 +146,13 @@ func (s transactionHandler) upload(w http.ResponseWriter, r *http.Request) { } func (s transactionHandler) transactions(w http.ResponseWriter, r *http.Request) { - tResponse, err := s.storage.Transactions(r.Context()) + _, claims, _ := jwtauth.FromContext(r.Context()) + if claims["email"] == nil { + sendErr(w, http.StatusBadRequest, Error{"email_required", "email is required"}) + return + } + email := claims["email"].(string) + tResponse, err := s.storage.Transactions(r.Context(), email) if err != nil { sendErr(w, http.StatusInternalServerError, err) return @@ -150,7 +172,7 @@ type Transaction struct { // Convert transform the raw transaction to the business Transaction structure. // TODO(JOJO) Join errors in one, and return all the errors. -func (t *Transaction) Convert() (*contractus.Transaction, error) { +func (t *Transaction) Convert(email string) (*contractus.Transaction, error) { typeInt, err := strconv.Atoi(t.Type) if err != nil { return nil, fmt.Errorf("failed to convert type: %v", err) @@ -171,6 +193,7 @@ func (t *Transaction) Convert() (*contractus.Transaction, error) { sellerName := strings.Replace(t.SellerName, "\n", "", -1) transac := &contractus.Transaction{ + Email: email, Type: typeInt, Date: dateTime.UTC(), ProductDescription: prodDesc, @@ -193,3 +216,11 @@ func (t *Transaction) Convert() (*contractus.Transaction, error) { return transac, nil } + +func emailFromToken(r *http.Request) (string, error) { + _, claims, err := jwtauth.FromContext(r.Context()) + if err != nil { + return "", fmt.Errorf("failed to get claims from context: %v", err) + } + return claims["email"].(string), nil +} diff --git a/api/transactionhandler_test.go b/api/transactionhandler_test.go index 262ef60..e1a8fbf 100644 --- a/api/transactionhandler_test.go +++ b/api/transactionhandler_test.go @@ -10,6 +10,7 @@ import ( "testing" "github.com/go-chi/chi/v5" + "github.com/go-chi/jwtauth/v5" "github.com/perebaj/contractus" ) @@ -19,11 +20,11 @@ func (m *mockTransactionStorage) SaveTransaction(_ context.Context, _ []contract return nil } -func (m *mockTransactionStorage) Balance(_ context.Context, _ string, _ string) (*contractus.BalanceResponse, error) { +func (m *mockTransactionStorage) Balance(_ context.Context, _ string, _ string, _ string) (*contractus.BalanceResponse, error) { return nil, nil } -func (m *mockTransactionStorage) Transactions(_ context.Context) (contractus.TransactionResponse, error) { +func (m *mockTransactionStorage) Transactions(_ context.Context, _ string) (contractus.TransactionResponse, error) { return contractus.TransactionResponse{}, nil } @@ -48,13 +49,20 @@ func TestTransactionHandlerUpload(t *testing.T) { m := &mockTransactionStorage{} r := chi.NewRouter() - RegisterTransactionsHandler(r, m) + tokenAuth := jwtauth.New("HS256", []byte("secret"), nil) + _, token, _ := tokenAuth.Encode(map[string]interface{}{"email": "jj@example.com"}) + + r.Group(func(r chi.Router) { + r.Use(jwtauth.Verifier(tokenAuth)) + r.Use(jwtauth.Authenticator) + RegisterTransactionsHandler(r, m) + }) req := httptest.NewRequest(http.MethodPost, "/upload", &b) req.Header.Set("Content-Type", w.FormDataContentType()) req.AddCookie(&http.Cookie{ Name: "jwt", - Value: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InBlcmViYWpAZ21haWwuY29tIn0.7uJvLACFC_2470iO_G8_xLaa1ChFgxQxHBvS9nzOMDM", + Value: token, }) resp := httptest.NewRecorder() @@ -66,15 +74,21 @@ func TestTransactionHandlerUpload(t *testing.T) { } func TestTransactionHandlerBalanceProducer(t *testing.T) { + tokenAuth := jwtauth.New("HS256", []byte("secret"), nil) + _, token, _ := tokenAuth.Encode(map[string]interface{}{"email": "jj@example.com"}) + m := &mockTransactionStorage{} r := chi.NewRouter() - - RegisterTransactionsHandler(r, m) + r.Group(func(r chi.Router) { + r.Use(jwtauth.Verifier(tokenAuth)) + r.Use(jwtauth.Authenticator) + RegisterTransactionsHandler(r, m) + }) req := httptest.NewRequest(http.MethodGet, "/balance/producer?name=JOSE%20CARLOS", nil) req.AddCookie(&http.Cookie{ Name: "jwt", - Value: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InBlcmViYWpAZ21haWwuY29tIn0.7uJvLACFC_2470iO_G8_xLaa1ChFgxQxHBvS9nzOMDM", + Value: token, }) resp := httptest.NewRecorder() @@ -82,20 +96,26 @@ func TestTransactionHandlerBalanceProducer(t *testing.T) { r.ServeHTTP(resp, req) if resp.Code != http.StatusOK { - t.Fatalf("expected status code %d, got %d", http.StatusOK, resp.Code) + t.Fatalf("expected status code %d, got %d, Response Body %s", http.StatusOK, resp.Code, resp.Body.String()) } } func TestTransactionHandlerBalanceAffiliate(t *testing.T) { m := &mockTransactionStorage{} r := chi.NewRouter() + tokenAuth := jwtauth.New("HS256", []byte("secret"), nil) + _, token, _ := tokenAuth.Encode(map[string]interface{}{"email": "jj@example.com"}) - RegisterTransactionsHandler(r, m) + r.Group(func(r chi.Router) { + r.Use(jwtauth.Verifier(tokenAuth)) + r.Use(jwtauth.Authenticator) + RegisterTransactionsHandler(r, m) + }) req := httptest.NewRequest(http.MethodGet, "/balance/affiliate?name=JOSE%20CARLOS", nil) req.AddCookie(&http.Cookie{ Name: "jwt", - Value: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InBlcmViYWpAZ21haWwuY29tIn0.7uJvLACFC_2470iO_G8_xLaa1ChFgxQxHBvS9nzOMDM", + Value: token, }) resp := httptest.NewRecorder() @@ -110,13 +130,19 @@ func TestTransactionHandlerBalanceAffiliate(t *testing.T) { func TestTransactionHandlerTransactions(t *testing.T) { m := &mockTransactionStorage{} r := chi.NewRouter() + tokenAuth := jwtauth.New("HS256", []byte("secret"), nil) + _, token, _ := tokenAuth.Encode(map[string]interface{}{"email": "jj@example.com"}) - RegisterTransactionsHandler(r, m) + r.Group(func(r chi.Router) { + r.Use(jwtauth.Verifier(tokenAuth)) + r.Use(jwtauth.Authenticator) + RegisterTransactionsHandler(r, m) + }) req := httptest.NewRequest(http.MethodGet, "/transactions", nil) req.AddCookie(&http.Cookie{ Name: "jwt", - Value: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InBlcmViYWpAZ21haWwuY29tIn0.7uJvLACFC_2470iO_G8_xLaa1ChFgxQxHBvS9nzOMDM", + Value: token, }) resp := httptest.NewRecorder() diff --git a/postgres/migrations/000002_add_transactions_email.down.sql b/postgres/migrations/000002_add_transactions_email.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/postgres/migrations/000002_add_transactions_email.up.sql b/postgres/migrations/000002_add_transactions_email.up.sql new file mode 100644 index 0000000..51008c3 --- /dev/null +++ b/postgres/migrations/000002_add_transactions_email.up.sql @@ -0,0 +1,12 @@ +BEGIN; + +ALTER TABLE transactions ADD COLUMN email TEXT; + +UPDATE transactions SET email = 'jj@example.com' WHERE email IS NULL; + +ALTER TABLE transactions ALTER COLUMN email SET NOT NULL ; + +ALTER TABLE transactions ADD CONSTRAINT empty_email CHECK (email <> ''); + +COMMIT; + diff --git a/postgres/storage.go b/postgres/storage.go index df11735..3f19760 100644 --- a/postgres/storage.go +++ b/postgres/storage.go @@ -28,8 +28,8 @@ func NewStorage(db *sqlx.DB) *Storage { // SaveTransaction is responsible for saving a transaction into the database. func (s Storage) SaveTransaction(ctx context.Context, t []contractus.Transaction) error { _, err := s.db.NamedExecContext(ctx, ` - INSERT INTO transactions (type, date, product_description, product_price_cents, seller_name, seller_type, action) - VALUES (:type, :date, :product_description, :product_price_cents, :seller_name, :seller_type, :action) + INSERT INTO transactions (email, type, date, product_description, product_price_cents, seller_name, seller_type, action) + VALUES (:email, :type, :date, :product_description, :product_price_cents, :seller_name, :seller_type, :action) `, t) return err @@ -37,13 +37,13 @@ func (s Storage) SaveTransaction(ctx context.Context, t []contractus.Transaction // Transactions is responsible for returning all the transactions from the database. // TODO(JOJO): Have a way to paginate the transactions. -func (s Storage) Transactions(ctx context.Context) (contractus.TransactionResponse, error) { +func (s Storage) Transactions(ctx context.Context, email string) (contractus.TransactionResponse, error) { var transactions []contractus.Transaction err := s.db.SelectContext(ctx, &transactions, ` - SELECT type, date, product_description, product_price_cents, seller_name, seller_type, action - FROM transactions - `) + SELECT email, type, date, product_description, product_price_cents, seller_name, seller_type, action + FROM transactions WHERE email = $1 + `, email) if err != nil { return contractus.TransactionResponse{}, err } @@ -55,14 +55,14 @@ func (s Storage) Transactions(ctx context.Context) (contractus.TransactionRespon } // Balance is responsible for return the balance of a seller. -func (s Storage) Balance(ctx context.Context, sellerType, sellerName string) (*contractus.BalanceResponse, error) { +func (s Storage) Balance(ctx context.Context, sellerType, sellerName, email string) (*contractus.BalanceResponse, error) { var transactions []contractus.Transaction err := s.db.SelectContext(ctx, &transactions, ` SELECT type, date, product_description, product_price_cents, seller_name, seller_type FROM transactions - WHERE seller_type = $1 AND seller_name = $2 - `, sellerType, sellerName) + WHERE seller_type = $1 AND seller_name = $2 AND email = $3 + `, sellerType, sellerName, email) if len(transactions) == 0 { return nil, ErrSellerNotFound diff --git a/postgres/storage_test.go b/postgres/storage_test.go index a557e7b..90303d5 100644 --- a/postgres/storage_test.go +++ b/postgres/storage_test.go @@ -81,6 +81,7 @@ func TestStorageSaveTransaction(t *testing.T) { ctx := context.Background() want := []contractus.Transaction{ { + Email: "jj@example.com", Type: 1, Date: time.Now().UTC(), ProductDescription: "Product description", @@ -90,7 +91,6 @@ func TestStorageSaveTransaction(t *testing.T) { Action: "venda produtor", }, } - err := storage.SaveTransaction(ctx, want) if err != nil { t.Fatalf("error saving transaction: %v", err) @@ -102,6 +102,7 @@ func TestStorageSaveTransaction(t *testing.T) { t.Fatalf("error getting transaction: %v", err) } + assert(t, got.Email, want[0].Email) assert(t, got.Type, want[0].Type) assert(t, got.Date.Format(time.RFC3339), want[0].Date.Format(time.RFC3339)) assert(t, got.ProductDescription, want[0].ProductDescription) @@ -117,6 +118,7 @@ func TestStorageTransactions(t *testing.T) { want := []contractus.Transaction{ { + Email: "jj@example.com", Type: 1, Date: time.Now().UTC(), ProductDescription: "Product description", @@ -126,6 +128,7 @@ func TestStorageTransactions(t *testing.T) { Action: "venda produtor", }, { + Email: "jj@example.com", Type: 2, Date: time.Now().UTC(), ProductDescription: "Product description 2", @@ -141,7 +144,7 @@ func TestStorageTransactions(t *testing.T) { t.Fatalf("error saving transaction 1: %v", err) } - got, err := storage.Transactions(ctx) + got, err := storage.Transactions(ctx, "jj@example.com") if err != nil { t.Fatalf("error getting transactions: %v", err) } @@ -151,6 +154,7 @@ func TestStorageTransactions(t *testing.T) { // the order based on the insertion order. // Reference: https://dba.stackexchange.com/questions/95822/does-postgres-preserve-insertion-order-of-records if got.Total == 2 { + assert(t, got.Transactions[0].Email, want[0].Email) assert(t, got.Transactions[0].Type, want[0].Type) assert(t, got.Transactions[0].Date.Format(time.RFC3339), want[0].Date.Format(time.RFC3339)) assert(t, got.Transactions[0].ProductDescription, want[0].ProductDescription) @@ -159,6 +163,7 @@ func TestStorageTransactions(t *testing.T) { assert(t, got.Transactions[0].SellerType, want[0].SellerType) assert(t, got.Transactions[0].Action, want[0].Action) + assert(t, got.Transactions[1].Email, want[1].Email) assert(t, got.Transactions[1].Type, want[1].Type) assert(t, got.Transactions[1].Date.Format(time.RFC3339), want[1].Date.Format(time.RFC3339)) assert(t, got.Transactions[1].ProductDescription, want[1].ProductDescription) @@ -167,7 +172,7 @@ func TestStorageTransactions(t *testing.T) { assert(t, got.Transactions[1].SellerType, want[1].SellerType) assert(t, got.Transactions[1].Action, want[1].Action) } else { - t.Fatal("error getting transactions: expected 2 transactions") + t.Fatalf("error getting transactions: expected 2 transactions, got %d", got.Total) } } @@ -178,6 +183,7 @@ func TestStorageBalance(t *testing.T) { transactions1 := []contractus.Transaction{ { + Email: "jj@example.com", Type: 1, Date: time.Now().UTC(), ProductDescription: "Product description", @@ -187,6 +193,7 @@ func TestStorageBalance(t *testing.T) { Action: "venda produtor", }, { + Email: "jj@example.com", Type: 3, Date: time.Now().UTC(), ProductDescription: "Product description 2", @@ -196,6 +203,7 @@ func TestStorageBalance(t *testing.T) { Action: "comissao paga", }, { + Email: "jj@example.com", Type: 1, Date: time.Now().UTC(), ProductDescription: "Product description 3", @@ -211,7 +219,7 @@ func TestStorageBalance(t *testing.T) { t.Fatalf("error saving transactions 1: %v", err) } - got, err := storage.Balance(ctx, "producer", "JOSE CARLOS") + got, err := storage.Balance(ctx, "producer", "JOSE CARLOS", "jj@example.com") if err != nil { t.Fatalf("error getting balance: %v", err) } @@ -221,6 +229,7 @@ func TestStorageBalance(t *testing.T) { transactions2 := []contractus.Transaction{ { + Email: "jj@example.com", Type: 2, Date: time.Now().UTC(), ProductDescription: "Product description", @@ -230,6 +239,7 @@ func TestStorageBalance(t *testing.T) { Action: "venda afiliado", }, { + Email: "jj@example.com", Type: 4, Date: time.Now().UTC(), ProductDescription: "Product description 2", @@ -245,7 +255,7 @@ func TestStorageBalance(t *testing.T) { t.Fatalf("error saving transactions 2: %v", err) } - got, err = storage.Balance(ctx, "affiliate", "CARLOS BATISTA") + got, err = storage.Balance(ctx, "affiliate", "CARLOS BATISTA", "jj@example.com") if err != nil { t.Fatalf("error getting balance: %v", err) } @@ -261,6 +271,7 @@ func TestStorageBalance_NotFound(t *testing.T) { transactions1 := []contractus.Transaction{ { + Email: "jj@gmail.com", Type: 1, Date: time.Now().UTC(), ProductDescription: "Product description", @@ -276,7 +287,7 @@ func TestStorageBalance_NotFound(t *testing.T) { t.Fatalf("error saving transactions 1: %v", err) } - got, err := storage.Balance(ctx, "producer", "INVALID NAME") + got, err := storage.Balance(ctx, "producer", "INVALID NAME", "jj@example.com") if !errors.Is(err, postgres.ErrSellerNotFound) { t.Fatalf("error getting balance: %v", err) } diff --git a/transaction.go b/transaction.go index c720cbc..744da45 100644 --- a/transaction.go +++ b/transaction.go @@ -20,6 +20,7 @@ type BalanceResponse struct { // Transaction have the fields that represent a single transaction. type Transaction struct { + Email string `json:"email" db:"email"` Type int `json:"type" db:"type"` Date time.Time `json:"date" db:"date"` ProductDescription string `json:"product_description" db:"product_description"`