first commit

This commit is contained in:
Sas Andy
2024-12-09 09:51:19 +07:00
commit ecc5dfd9c0
69 changed files with 5365 additions and 0 deletions

View File

@@ -0,0 +1,71 @@
package auth
import (
"errors"
"fmt"
"net/http"
"time"
"github.com/go-playground/validator/v10"
"sismedika.com/sas/westone/types"
"sismedika.com/sas/westone/utils"
)
// Login login using westone account
//
// @Summary login using westone account
// @Description login using westone account
// @Tags Auth
// @Accept json
// @Param parameter body types.SignInPayload true "parameter"
// @Produce json
// @Success 200 {object} map[string]any
// @Failure 400 {object} map[string]any
// @Failure 500 {object} map[string]any
// @Router /westone/api/v1/auth/login [post]
func (h *Handler) handleLogin(w http.ResponseWriter, r *http.Request) {
var payload types.SignInPayload
fmt.Println("User Agent")
fmt.Println(r.UserAgent())
if err := utils.ParseJSON(r, &payload); err != nil {
utils.WriteError(w, http.StatusBadRequest, err)
h.store.LogSignIn(payload.Email, r.RemoteAddr, "FAILED", "LOGIN", "westone")
return
}
if err := utils.Validate.Struct(payload); err != nil {
errorz := err.(validator.ValidationErrors)
utils.WriteError(w, http.StatusBadRequest, fmt.Errorf("invalid payload: %v", errorz))
h.store.LogSignIn(payload.Email, r.RemoteAddr, "FAILED", "LOGIN", "westone")
return
}
hashedPassword := HashWithMD5(payload.Password)
response, err := h.store.SignInWestone(payload.Email, hashedPassword)
if err != nil {
var logError *utils.LogError
if errors.As(err, &logError) {
h.errorz.CreateErrorLog(*logError)
utils.WriteErrorLog(w, http.StatusBadRequest, *logError)
}
h.store.LogSignIn(payload.Email, r.RemoteAddr, "FAILED", "LOGIN", "westone")
return
}
timenow := time.Now()
token, err := CreateJWT(types.DataJWT{
User: *response,
Ip: r.RemoteAddr,
Agent: r.UserAgent(),
Version: "v1",
LastLogin: timenow.Format("2006-01-02 15:04:05"),
})
if err != nil {
utils.WriteError(w, http.StatusInternalServerError, err)
h.store.LogSignIn(payload.Email, r.RemoteAddr, "FAILED", "LOGIN", "westone")
return
}
utils.WriteJSONLogin(w, http.StatusOK, response, token, "westone")
}

View File

@@ -0,0 +1,77 @@
package auth
import (
"context"
"time"
"sismedika.com/sas/westone/types"
"sismedika.com/sas/westone/utils"
)
func (s *Store) SignInWestone(email string, password string) (*types.User, error) {
user := new(types.User)
qry := `
SELECT
M_UserID,
M_UserName,
M_UserGroupDashboard,
IFNULL(M_UserFullName, "") as M_StaffName,
10000000 as time_autologout
FROM m_user
JOIN m_usergroup ON M_UserM_UserGroupID = M_UserGroupID
LEFT JOIN m_staff on M_UserM_StaffID = M_StaffID
WHERE M_UserName = ? AND M_UserPassword = ? AND M_UserIsActive = 'Y'
`
if err := s.db.Get(user, qry, email, password); err != nil {
return nil, &utils.LogError{
Code: "151",
TraceID: utils.RandomTraceID(15),
Type: "AUTH",
Params: "email: " + email,
Query: "-",
Message: err.Error(),
TimeStamp: time.Now().Format("2006-01-02 15:04:05"),
}
}
user.Type_Akun = "westone"
return user, nil
}
func (s *Store) LogSignIn(email string, ip string, status string, tipe string, provider string) error {
tx, err := s.db.BeginTxx(context.Background(), nil)
if err != nil {
return err
}
defer func() {
if err != nil {
tx.Rollback()
}
}()
logval := types.LogLogin{
LogLoginMUserEmail: email,
LogLoginDateTime: time.Now().Format("2006-01-02 15:04:05"),
LogLoginIP: ip,
LogLoginType: tipe,
LogLoginStatus: status,
LogLoginLogin: provider,
}
qry := `INSERT INTO log_login
(Log_LoginM_UserEmail, Log_LoginDateTime, Log_LoginIP, Log_LoginType, Log_LoginStatus, Log_LoginLogin)
VALUES (:Log_LoginM_UserEmail, :Log_LoginDateTime, :Log_LoginIP, :Log_LoginType, :Log_LoginStatus, :Log_LoginLogin)`
_, err = tx.NamedExec(qry, logval)
if err != nil {
return err
}
if err = tx.Commit(); err != nil {
return err
}
return nil
}

134
services/auth/jwt.go Normal file
View File

@@ -0,0 +1,134 @@
package auth
import (
"context"
"fmt"
"log"
"net/http"
"github.com/golang-jwt/jwt/v5"
"sismedika.com/sas/westone/configs"
"sismedika.com/sas/westone/types"
"sismedika.com/sas/westone/utils"
)
type ContextKey string
const UserContextKey ContextKey = "mk1S2sKM12KASd02dp1"
// func WithJWTAuth(handlerFunc http.HandlerFunc, store types.UserStore) http.HandlerFunc {
// return func(w http.ResponseWriter, r *http.Request) {
// tokenString := utils.GetTokenFromRequest(r)
// token, err := validateJWT(tokenString)
// if err != nil {
// log.Printf("failed to validate token: %v", err)
// permissionDenied(w)
// return
// }
// if !token.Valid {
// log.Println("invalid token")
// permissionDenied(w)
// return
// }
// claims := token.Claims.(jwt.MapClaims)
// str := claims["userID"].(string)
// userID, err := strconv.Atoi(str)
// if err != nil {
// log.Printf("failed to convert userID to int: %v", err)
// permissionDenied(w)
// return
// }
// u, err := store.GetUserByID(userID)
// if err != nil {
// log.Printf("failed to get user by id: %v", err)
// permissionDenied(w)
// return
// }
// // Add the user to the context
// ctx := r.Context()
// ctx = context.WithValue(ctx, UserKey, u.MUserID)
// r = r.WithContext(ctx)
// // Call the function if the token is valid
// handlerFunc(w, r)
// }
// }
func CreateJWT(data types.DataJWT) (string, error) {
secret := []byte(configs.Envs.JWTSecret)
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"M_UserID": data.M_UserID,
"M_UserEmail": data.M_UserEmail,
"M_UserUsername": data.M_UserUsername,
"M_UserGroupDashboard": data.M_UserGroupDashboard,
"M_UserDefaultTSampleStationID": data.M_UserDefaultTSampleStationID,
"M_StaffName": data.M_StaffName,
"Is_Courier": data.Is_Courier,
"Time_Autologout": data.Time_Autologout,
"Type_Akun": data.Type_Akun,
"IP": data.Ip,
"Agent": data.Agent,
"Version": data.Version,
"LastLogin": data.LastLogin,
})
tokenString, err := token.SignedString(secret)
if err != nil {
return "", err
}
return tokenString, err
}
func validateJWT(tokenString string) (*jwt.Token, error) {
return jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(configs.Envs.JWTSecret), nil
})
}
func permissionDenied(w http.ResponseWriter) {
utils.WriteError(w, http.StatusForbidden, fmt.Errorf("PERMISSION DENIED"))
}
func GetUserIDFromContext(ctx context.Context) int {
userID, ok := ctx.Value(UserContextKey).(int)
if !ok {
return -1
}
return userID
}
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tokenstr := utils.GetTokenFromRequest(r)
token, err := validateJWT(tokenstr)
if err != nil {
log.Printf("[ERROR] Failed to validate jwt token: %v", err)
permissionDenied(w)
return
}
if !token.Valid {
log.Println("[ERROR] Invalid token")
permissionDenied(w)
return
}
claims := token.Claims.(jwt.MapClaims)
ctx := r.Context()
ctx = context.WithValue(ctx, UserContextKey, claims)
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}

18
services/auth/jwt_test.go Normal file
View File

@@ -0,0 +1,18 @@
package auth
import (
"testing"
"sismedika.com/sas/westone/types"
)
func TestCreateJWT(t *testing.T) {
token, err := CreateJWT(types.DataJWT{})
if err != nil {
t.Errorf("error creating JWT: %v", err)
}
if token == "" {
t.Error("expected token to be not empty")
}
}

73
services/auth/oauth.go Normal file
View File

@@ -0,0 +1,73 @@
package auth
import (
"log"
"net/http"
"github.com/gorilla/sessions"
"github.com/markbates/goth"
"github.com/markbates/goth/gothic"
"github.com/markbates/goth/providers/google"
"sismedika.com/sas/westone/configs"
)
var maxAge = 86400 * 1 // 30 days
var isProd = false // set to true if in production
var key = configs.Envs.SessionKey
var store = sessions.NewCookieStore([]byte(key))
var usersession = "user"
func NewOAuth() {
googleClientID := configs.Envs.GoogleClientID
googleClientSecret := configs.Envs.GoogleClientSecret
callback := configs.Envs.PublicHost + ":" + configs.Envs.Port + "/api/v1/auth/google/callback"
store.MaxAge(maxAge)
store.Options.Path = "/"
store.Options.HttpOnly = true
store.Options.Secure = isProd
store.Options.SameSite = http.SameSiteLaxMode
gothic.Store = store
goth.UseProviders(
google.New(googleClientID, googleClientSecret, callback),
)
}
func addUsertoSession(w http.ResponseWriter, r *http.Request, user goth.User) error {
user_session, err := store.Get(r, usersession)
if err != nil {
user_session, err = store.New(r, usersession)
if err != nil {
log.Println("[ERROR] get user to session, error: ", err)
return err
}
}
user.RawData = map[string]interface{}{}
user_session.Values["user"] = user
err = user_session.Save(r, w)
if err != nil {
log.Println("[ERROR] saving user to session, error: ", err)
return err
}
return nil
}
func removeUsertoSession(w http.ResponseWriter, r *http.Request) error {
user_session, err := store.Get(r, usersession)
if err != nil {
log.Println("[ERROR] get user to session, error: ", err)
return err
}
user_session.Values["user"] = goth.User{}
err = user_session.Save(r, w)
if err != nil {
log.Println("[ERROR] remove user to session, error: ", err)
return err
}
return nil
}

View File

@@ -0,0 +1,249 @@
package auth
import (
"fmt"
"log"
"net/http"
"time"
"github.com/go-playground/validator/v10"
"github.com/golang-jwt/jwt/v5"
"github.com/gorilla/mux"
"github.com/markbates/goth"
"github.com/markbates/goth/gothic"
"sismedika.com/sas/westone/configs"
"sismedika.com/sas/westone/types"
"sismedika.com/sas/westone/utils"
)
type Handler struct {
store types.OauthStore
errorz types.ErrorLogStore
usrStore types.UserStore
}
func NewHandler(store types.OauthStore, errorz types.ErrorLogStore, usrStore types.UserStore) *Handler {
return &Handler{
store: store,
errorz: errorz,
usrStore: usrStore,
}
}
func (h *Handler) RegisterRoutes(router *mux.Router) {
authroute := router.PathPrefix("/auth").Subrouter()
authroute.HandleFunc("/generateauthcode", h.handlerGenerateAuthCode).Methods(http.MethodPost)
// auth
authroute.HandleFunc("/login", h.handleLogin).Methods(http.MethodPost)
// oauth
authroute.HandleFunc("/{provider}/login", h.handleAuthLogin).Methods(http.MethodGet)
authroute.HandleFunc("/{provider}/logout", h.handleAuthLogout).Methods(http.MethodGet)
authroute.HandleFunc("/{provider}/callback", h.handleAuthCallback).Methods(http.MethodGet)
authroute.HandleFunc("/{provider}/linking", h.handleAuthLinkAccount).Methods(http.MethodPost)
authroute.HandleFunc("/{provider}/authcode", h.handlerRedirectCode).Methods(http.MethodGet)
protec := authroute.PathPrefix("/redirect").Subrouter()
protec.Use(AuthMiddleware)
protec.HandleFunc("/dashboard", h.handleRedirectDash).Methods(http.MethodGet)
}
func (h *Handler) handleAuthLogin(w http.ResponseWriter, r *http.Request) {
if gothUser, err := gothic.CompleteUserAuth(w, r); err == nil {
// log.Println(gothUser)
log.Printf("[INFO] account found: %v", gothUser.Email)
} else {
gothic.BeginAuthHandler(w, r)
}
}
func (h *Handler) handleAuthCallback(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
provider := vars["provider"]
akungoogle, err := gothic.CompleteUserAuth(w, r)
if err != nil {
utils.WriteError(w, http.StatusInternalServerError, err)
return
}
// add user to cookie session
err = addUsertoSession(w, r, akungoogle)
if err != nil {
utils.WriteError(w, http.StatusInternalServerError, err)
return
}
// check google is linked to server
userID, err := h.store.CheckGoogleAccountLinked(types.UserGoogle{M_UserGoogleEmail: akungoogle.Email, M_UserGoogleIdentifier: akungoogle.UserID})
if err != nil {
utils.WriteError(w, http.StatusInternalServerError, err)
return
}
if userID > 0 {
// generate jwt token
user, err := h.usrStore.GetUserByID(userID)
if err != nil {
utils.WriteError(w, http.StatusInternalServerError, err)
return
}
user.Type_Akun = provider
timenow := time.Now()
token, err := CreateJWT(types.DataJWT{
User: *user,
Ip: r.RemoteAddr,
Agent: r.UserAgent(),
Version: "v1",
LastLogin: timenow.Format("2006-01-02 15:04:05"),
})
if err != nil {
utils.WriteError(w, http.StatusInternalServerError, err)
return
}
// data := "Bearer " + token
// w.Header().Set("Authorization", data)
// http.Redirect(w, r, "/api/v1/auth/redirect/dashboard", http.StatusFound)
utils.WriteJSONLogin(w, http.StatusOK, user, token, provider)
} else {
log.Println("[INFO] redirect to auth-code")
// http.Redirect(w, r, "/api/v1/auth/"+provider+"/authcode", http.StatusFound)
http.Redirect(w, r, configs.Envs.PublicHost+"/auth-code", http.StatusFound)
}
}
func (h *Handler) handleRedirectDash(w http.ResponseWriter, r *http.Request) {
claims, ok := r.Context().Value(UserContextKey).(jwt.MapClaims)
if !ok {
utils.WriteError(w, http.StatusInternalServerError, fmt.Errorf("token not found in context"))
return
}
username := claims["Username"].(string)
temp := "WELCOME TO DASHBOARD, " + username
utils.WriteJSON(w, http.StatusOK, temp)
}
func (h *Handler) handlerRedirectCode(w http.ResponseWriter, r *http.Request) {
user_session, _ := store.Get(r, usersession)
user := user_session.Values["user"].(goth.User)
if user.IDToken == "" {
log.Println("[ERROR] token not found in context")
utils.WriteJSON(w, http.StatusInternalServerError, "not found")
return
}
temp := "INSERT AUTH CODE, " + user.Email
utils.WriteJSON(w, http.StatusOK, temp)
}
func (h *Handler) handleAuthLogout(w http.ResponseWriter, r *http.Request) {
err := gothic.Logout(w, r)
if err != nil {
utils.WriteError(w, http.StatusInternalServerError, err)
return
}
err = removeUsertoSession(w, r)
if err != nil {
utils.WriteError(w, http.StatusInternalServerError, err)
return
}
utils.WriteJSON(w, http.StatusOK, "success logout")
}
func (h *Handler) handlerGenerateAuthCode(w http.ResponseWriter, r *http.Request) {
var payload types.GenerateAuthCode
if err := utils.ParseJSON(r, &payload); err != nil {
utils.WriteError(w, http.StatusBadRequest, err)
return
}
if err := utils.Validate.Struct(payload); err != nil {
errorss := err.(validator.ValidationErrors)
utils.WriteError(w, http.StatusBadRequest, fmt.Errorf("invalid payload: %v", errorss))
return
}
err := h.store.GenerateAuthCode(payload.Email, payload.Types, payload.UserID)
if err != nil {
log.Println("[ERROR] failed generate auth code")
utils.WriteJSON(w, http.StatusInternalServerError, "failed generate auth code")
return
}
msg := "Auth Code Generated for " + payload.Email
utils.WriteJSON(w, http.StatusOK, msg)
}
func (h *Handler) handleAuthLinkAccount(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
provider := vars["provider"]
// var payload types.AuthCode
var payload types.AuthCodePayload
if err := utils.ParseJSON(r, &payload); err != nil {
utils.WriteError(w, http.StatusBadRequest, err)
return
}
if err := utils.Validate.Struct(payload); err != nil {
errors := err.(validator.ValidationErrors)
utils.WriteError(w, http.StatusBadRequest, fmt.Errorf("invalid payload: %v", errors))
return
}
user_session, _ := store.Get(r, usersession)
akunGoogle := user_session.Values["user"].(goth.User)
if akunGoogle.IDToken == "" {
log.Println("[ERROR] google token not found in context")
utils.WriteError(w, http.StatusInternalServerError, fmt.Errorf("token not found"))
return
}
userID, err := h.store.CompareAuthCode(payload.Code, akunGoogle, provider)
if err != nil {
log.Println("[ERROR] compare auth code")
utils.WriteError(w, http.StatusInternalServerError, err)
return
}
user, err := h.usrStore.GetUserByID(userID)
if err != nil {
log.Println("[ERROR] cant get m_user data")
utils.WriteError(w, http.StatusInternalServerError, err)
return
}
user.Type_Akun = provider
timenow := time.Now()
token, err := CreateJWT(types.DataJWT{
User: *user,
Ip: r.RemoteAddr,
Agent: r.UserAgent(),
Version: "v1",
LastLogin: timenow.Format("2006-01-02 15:04:05"),
})
if err != nil {
utils.WriteError(w, http.StatusInternalServerError, err)
return
}
err = h.store.AddGoolgeAccount(types.UserGoogle{
M_UserGoogleM_UserID: user.M_UserID,
M_UserGoogleEmail: akunGoogle.Email,
M_UserGoogleIdentifier: akunGoogle.UserID,
M_UserGoogleCode: payload.Code,
M_UserGoogleToken: token,
})
if err != nil {
log.Printf("[ERROR] failed insert google account to db, %v", err)
utils.WriteError(w, http.StatusInternalServerError, err)
return
}
utils.WriteJSONLogin(w, http.StatusOK, user, token, provider)
}

View File

@@ -0,0 +1,143 @@
package auth
import (
"context"
"database/sql"
"fmt"
"github.com/jmoiron/sqlx"
"github.com/markbates/goth"
"sismedika.com/sas/westone/types"
"sismedika.com/sas/westone/utils"
)
type Store struct {
db *sqlx.DB
}
func NewStore(db *sqlx.DB) *Store {
return &Store{db: db}
}
func (s *Store) AddGoolgeAccount(user types.UserGoogle) error {
tx, err := s.db.BeginTxx(context.Background(), nil)
if err != nil {
return err
}
defer func() {
if err != nil {
tx.Rollback()
}
}()
query := `INSERT INTO m_usergoogle (
M_UserGoogleM_UserID,
M_UserGoogleEmail,
M_UserGoogleIdentifier,
M_UserGoogleCode,
M_UserGoogleToken
) VALUES (:M_UserGoogleM_UserID, :M_UserGoogleEmail, :M_UserGoogleIdentifier, :M_UserGoogleCode, :M_UserGoogleToken)`
_, err = tx.NamedExec(query, user)
if err != nil {
return err
}
if err = tx.Commit(); err != nil {
return err
}
return nil
}
func (s *Store) CheckGoogleAccountLinked(user types.UserGoogle) (int, error) {
var userID int
qry := `SELECT
M_UserGoogleM_UserID
FROM m_usergoogle
WHERE M_UserGoogleEmail = ? AND M_UserGoogleIdentifier = ?`
if err := s.db.Get(&userID, qry, user.M_UserGoogleEmail, user.M_UserGoogleIdentifier); err != nil {
if err == sql.ErrNoRows {
return 0, nil
}
return 0, err
}
if userID < 1 {
return 0, nil
}
return userID, nil
}
func (s *Store) GenerateAuthCode(email string, jenis string, userid int) error {
payload := types.AuthCode{
AuthCodeMUserID: userid,
AuthCodeUser: email,
AuthCodeType: jenis,
AuthCodeCode: utils.RandomTraceID(6),
}
tx, err := s.db.BeginTxx(context.Background(), nil)
if err != nil {
return err
}
defer func() {
if err != nil {
tx.Rollback()
}
}()
sql := `INSERT INTO x_auth_code (
AuthCodeMUserID,
AuthCodeUser,
AuthCodeType,
AuthCodeCode
) VALUES (:AuthCodeMUserID, :AuthCodeUser, :AuthCodeType, :AuthCodeCode)`
_, err = tx.NamedExec(sql, payload)
if err != nil {
return err
}
if err = tx.Commit(); err != nil {
return err
}
return nil
}
func (s *Store) CompareAuthCode(authcode string, user goth.User, typez string) (int, error) {
var code types.AuthCode
qry := `SELECT
AuthCodeID,
AuthCodeMUserID,
AuthCodeUser,
AuthCodeType,
AuthCodeCode,
AuthCodeIsUsed
FROM x_auth_code
WHERE AuthCodeUser = ? AND AuthCodeType = ?
AND AuthCodeIsUsed = 'N' AND AuthCodeIsActive = 'Y'`
if err := s.db.Get(&code, qry, user.Email, typez); err != nil {
return 0, fmt.Errorf("auth code not found, %v", err)
}
if authcode != code.AuthCodeCode {
return 0, fmt.Errorf("auth code do not match")
} else {
inst := `
UPDATE x_auth_code
SET AuthCodeIsUsed = 'Y'
WHERE AuthCodeUser = ? AND AuthCodeType = ? AND AuthCodeCode = ?
`
if _, err := s.db.Exec(inst, user.Email, typez, authcode); err != nil {
return 0, err
}
return code.AuthCodeMUserID, nil
}
}

29
services/auth/password.go Normal file
View File

@@ -0,0 +1,29 @@
package auth
import (
"crypto/md5"
"encoding/hex"
"golang.org/x/crypto/bcrypt"
)
func HashPassword(password string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(hash), nil
}
func ComparePasswords(hashed string, plain []byte) bool {
err := bcrypt.CompareHashAndPassword([]byte(hashed), plain)
return err == nil
}
func HashWithMD5(password string) string {
salt := "545"
data := []byte(salt + password + salt)
hash := md5.Sum(data)
return hex.EncodeToString(hash[:])
}

View File

@@ -0,0 +1,34 @@
package auth
import (
"testing"
)
func TestHashPassword(t *testing.T) {
hash, err := HashPassword("password")
if err != nil {
t.Errorf("error hashing password: %v", err)
}
if hash == "" {
t.Error("expected hash to be not empty")
}
if hash == "password" {
t.Error("expected hash to be different from password")
}
}
func TestComparePasswords(t *testing.T) {
hash, err := HashPassword("password")
if err != nil {
t.Errorf("error hashing password: %v", err)
}
if !ComparePasswords(hash, []byte("password")) {
t.Errorf("expected password to match hash")
}
if ComparePasswords(hash, []byte("notpassword")) {
t.Errorf("expected password to not match hash")
}
}

77
services/cart/routes.go Normal file
View File

@@ -0,0 +1,77 @@
package cart
import (
"fmt"
"net/http"
"github.com/go-playground/validator/v10"
"github.com/gorilla/mux"
"sismedika.com/sas/westone/services/auth"
"sismedika.com/sas/westone/types"
"sismedika.com/sas/westone/utils"
)
type Handler struct {
store types.ProductStore
orderStore types.OrderStore
userStore types.UserStore
}
func NewHandler(
store types.ProductStore,
orderStore types.OrderStore,
userStore types.UserStore,
) *Handler {
return &Handler{
store: store,
orderStore: orderStore,
userStore: userStore,
}
}
func (h *Handler) RegisterRoutes(router *mux.Router) {
cart := router.PathPrefix("/cart").Subrouter()
cart.Use(auth.AuthMiddleware)
cart.HandleFunc("/checkout", h.handleCheckout).Methods(http.MethodPost)
// router.HandleFunc("/cart/checkout", auth.WithJWTAuth(h.handleCheckout)).Methods(http.MethodPost)
}
func (h *Handler) handleCheckout(w http.ResponseWriter, r *http.Request) {
userID := auth.GetUserIDFromContext(r.Context())
var cart types.CartCheckoutPayload
if err := utils.ParseJSON(r, &cart); err != nil {
utils.WriteError(w, http.StatusBadRequest, err)
return
}
if err := utils.Validate.Struct(cart); err != nil {
errors := err.(validator.ValidationErrors)
utils.WriteError(w, http.StatusBadRequest, fmt.Errorf("invalid payload: %v", errors))
return
}
productIds, err := getCartItemsIDs(cart.Items)
if err != nil {
utils.WriteError(w, http.StatusBadRequest, err)
return
}
// get products
products, err := h.store.GetProductsByID(productIds)
if err != nil {
utils.WriteError(w, http.StatusInternalServerError, err)
return
}
orderID, totalPrice, err := h.createOrder(products, cart.Items, userID)
if err != nil {
utils.WriteError(w, http.StatusBadRequest, err)
return
}
utils.WriteJSON(w, http.StatusOK, map[string]interface{}{
"total_price": totalPrice,
"order_id": orderID,
})
}

View File

@@ -0,0 +1,214 @@
package cart
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gorilla/mux"
"sismedika.com/sas/westone/types"
)
var mockProducts = []types.Product{
{ID: 1, Name: "product 1", Price: 10, Quantity: 100},
{ID: 2, Name: "product 2", Price: 20, Quantity: 200},
{ID: 3, Name: "product 3", Price: 30, Quantity: 300},
{ID: 4, Name: "empty stock", Price: 30, Quantity: 0},
{ID: 5, Name: "almost stock", Price: 30, Quantity: 1},
}
func TestCartServiceHandler(t *testing.T) {
productStore := &mockProductStore{}
orderStore := &mockOrderStore{}
handler := NewHandler(productStore, orderStore, nil)
t.Run("should fail to checkout if the cart items do not exist", func(t *testing.T) {
payload := types.CartCheckoutPayload{
Items: []types.CartCheckoutItem{
{ProductID: 99, Quantity: 100},
},
}
marshalled, err := json.Marshal(payload)
if err != nil {
t.Fatal(err)
}
req, err := http.NewRequest(http.MethodPost, "/cart/checkout", bytes.NewBuffer(marshalled))
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
router := mux.NewRouter()
router.HandleFunc("/cart/checkout", handler.handleCheckout).Methods(http.MethodPost)
router.ServeHTTP(rr, req)
if rr.Code != http.StatusBadRequest {
t.Errorf("expected status code %d, got %d", http.StatusBadRequest, rr.Code)
}
})
t.Run("should fail to checkout if the cart has negative quantities", func(t *testing.T) {
payload := types.CartCheckoutPayload{
Items: []types.CartCheckoutItem{
{ProductID: 1, Quantity: 0}, // invalid quantity
},
}
marshalled, err := json.Marshal(payload)
if err != nil {
t.Fatal(err)
}
req, err := http.NewRequest(http.MethodPost, "/cart/checkout", bytes.NewBuffer(marshalled))
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
router := mux.NewRouter()
router.HandleFunc("/cart/checkout", handler.handleCheckout).Methods(http.MethodPost)
router.ServeHTTP(rr, req)
if rr.Code != http.StatusBadRequest {
t.Errorf("expected status code %d, got %d", http.StatusBadRequest, rr.Code)
}
})
t.Run("should fail to checkout if there is no stock for an item", func(t *testing.T) {
payload := types.CartCheckoutPayload{
Items: []types.CartCheckoutItem{
{ProductID: 4, Quantity: 2},
},
}
marshalled, err := json.Marshal(payload)
if err != nil {
t.Fatal(err)
}
req, err := http.NewRequest(http.MethodPost, "/cart/checkout", bytes.NewBuffer(marshalled))
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
router := mux.NewRouter()
router.HandleFunc("/cart/checkout", handler.handleCheckout).Methods(http.MethodPost)
router.ServeHTTP(rr, req)
if rr.Code != http.StatusBadRequest {
t.Errorf("expected status code %d, got %d", http.StatusBadRequest, rr.Code)
}
})
t.Run("should fail to checkout if there is not enough stock", func(t *testing.T) {
payload := types.CartCheckoutPayload{
Items: []types.CartCheckoutItem{
{ProductID: 5, Quantity: 2},
},
}
marshalled, err := json.Marshal(payload)
if err != nil {
t.Fatal(err)
}
req, err := http.NewRequest(http.MethodPost, "/cart/checkout", bytes.NewBuffer(marshalled))
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
router := mux.NewRouter()
router.HandleFunc("/cart/checkout", handler.handleCheckout).Methods(http.MethodPost)
router.ServeHTTP(rr, req)
if rr.Code != http.StatusBadRequest {
t.Errorf("expected status code %d, got %d", http.StatusBadRequest, rr.Code)
}
})
t.Run("should checkout and calculate the price correctly", func(t *testing.T) {
payload := types.CartCheckoutPayload{
Items: []types.CartCheckoutItem{
{ProductID: 1, Quantity: 10},
{ProductID: 2, Quantity: 20},
{ProductID: 5, Quantity: 1},
},
}
marshalled, err := json.Marshal(payload)
if err != nil {
t.Fatal(err)
}
req, err := http.NewRequest(http.MethodPost, "/cart/checkout", bytes.NewBuffer(marshalled))
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
router := mux.NewRouter()
router.HandleFunc("/cart/checkout", handler.handleCheckout).Methods(http.MethodPost)
router.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("expected status code %d, got %d", http.StatusOK, rr.Code)
}
var response map[string]interface{}
if err := json.NewDecoder(rr.Body).Decode(&response); err != nil {
t.Fatal(err)
}
if response["total_price"] != 530.0 {
t.Errorf("expected total price to be 530, got %f", response["total_price"])
}
})
}
type mockProductStore struct{}
func (m *mockProductStore) GetProductByID(productID int) (*types.Product, error) {
return &types.Product{}, nil
}
func (m *mockProductStore) GetProducts() ([]*types.Product, error) {
return []*types.Product{}, nil
}
func (m *mockProductStore) CreateProduct(product types.CreateProductPayload) error {
return nil
}
func (m *mockProductStore) GetProductsByID(ids []int) ([]types.Product, error) {
return mockProducts, nil
}
func (m *mockProductStore) UpdateProduct(product types.Product) error {
return nil
}
type mockOrderStore struct{}
func (m *mockOrderStore) CreateOrder(order types.Order) (int, error) {
return 0, nil
}
func (m *mockOrderStore) CreateOrderItem(orderItem types.OrderItem) error {
return nil
}

96
services/cart/service.go Normal file
View File

@@ -0,0 +1,96 @@
package cart
import (
"fmt"
"sismedika.com/sas/westone/types"
)
func getCartItemsIDs(items []types.CartCheckoutItem) ([]int, error) {
productIds := make([]int, len(items))
for i, item := range items {
if item.Quantity <= 0 {
return nil, fmt.Errorf("invalid quantity for product %d", item.ProductID)
}
productIds[i] = item.ProductID
}
return productIds, nil
}
func checkIfCartIsInStock(cartItems []types.CartCheckoutItem, products map[int]types.Product) error {
if len(cartItems) == 0 {
return fmt.Errorf("cart is empty")
}
for _, item := range cartItems {
product, ok := products[item.ProductID]
if !ok {
return fmt.Errorf("product %d is not available in the store, please refresh your cart", item.ProductID)
}
if product.Quantity < item.Quantity {
return fmt.Errorf("product %s is not available in the quantity requested", product.Name)
}
}
return nil
}
func calculateTotalPrice(cartItems []types.CartCheckoutItem, products map[int]types.Product) float64 {
var total float64
for _, item := range cartItems {
product := products[item.ProductID]
total += product.Price * float64(item.Quantity)
}
return total
}
func (h *Handler) createOrder(products []types.Product, cartItems []types.CartCheckoutItem, userID int) (int, float64, error) {
// create a map of products for easier access
productsMap := make(map[int]types.Product)
for _, product := range products {
productsMap[product.ID] = product
}
// check if all products are available
if err := checkIfCartIsInStock(cartItems, productsMap); err != nil {
return 0, 0, err
}
// calculate total price
totalPrice := calculateTotalPrice(cartItems, productsMap)
// reduce the quantity of products in the store
for _, item := range cartItems {
product := productsMap[item.ProductID]
product.Quantity -= item.Quantity
h.store.UpdateProduct(product)
}
// create order record
orderID, err := h.orderStore.CreateOrder(types.Order{
UserID: userID,
Total: totalPrice,
Status: "pending",
Address: "some address", // could fetch address from a user addresses table
})
if err != nil {
return 0, 0, err
}
// create order the items records
for _, item := range cartItems {
h.orderStore.CreateOrderItem(types.OrderItem{
OrderID: orderID,
ProductID: item.ProductID,
Quantity: item.Quantity,
Price: productsMap[item.ProductID].Price,
})
}
return orderID, totalPrice, nil
}

View File

@@ -0,0 +1,62 @@
package doctor
import (
"fmt"
"net/http"
"github.com/go-playground/validator/v10"
"github.com/gorilla/mux"
"sismedika.com/sas/westone/services/auth"
"sismedika.com/sas/westone/types"
"sismedika.com/sas/westone/utils"
)
type Handler struct {
store types.DoctorStore
}
func NewHandler(store types.DoctorStore) *Handler {
return &Handler{store: store}
}
func (h *Handler) RegisterRoutes(router *mux.Router) {
doctorroutes := router.PathPrefix("/mddoctor").Subrouter()
doctorroutes.Use(auth.AuthMiddleware)
doctorroutes.HandleFunc("/searchdoctor", h.handlerSearchDoctor).Methods(http.MethodPost)
}
// SearchDoctor searches doctor by name, NIK, Doctor Code, DOB, or phone number
//
// @Summary Search Doctors listing
// @Description Search Doctors by name, NIK, Doctor Code, DOB, or phone number
// @Tags Doctor
// @Accept json
// @Param authorization header string true "Authorization token"
// @Param parameter body types.SearchDoctorPayload true "parameter"
// @Produce json
// @Success 200 {object} map[string]any
// @Failure 400 {object} map[string]any
// @Failure 500 {object} map[string]any
// @Router /westone/api/v1/mddoctor/searchdoctor [post]
func (h *Handler) handlerSearchDoctor(w http.ResponseWriter, r *http.Request) {
var payload types.SearchDoctorPayload
if err := utils.ParseJSON(r, &payload); err != nil {
utils.WriteError(w, http.StatusBadRequest, err)
return
}
if err := utils.Validate.Struct(payload); err != nil {
erros := err.(validator.ValidationErrors)
utils.WriteError(w, http.StatusBadRequest, fmt.Errorf("invalid payload: %v", erros))
return
}
response, err := h.store.SearchDoctor(payload.Keyword, payload.Page, payload.Perpage)
if err != nil {
utils.WriteError(w, http.StatusInternalServerError, err)
return
}
utils.WriteJSON(w, http.StatusOK, response)
}

View File

@@ -0,0 +1,76 @@
package doctor
import (
"strings"
"github.com/jmoiron/sqlx"
"sismedika.com/sas/westone/types"
)
type Store struct {
db *sqlx.DB
}
func NewStore(db *sqlx.DB) *Store {
return &Store{db: db}
}
func (s *Store) SearchDoctor(keyword string, page int, perpage int) ([]types.DoctorResult, error) {
var list_doctor []types.DoctorResult
var sqlFilter []string
if keyword != "" {
params := strings.Split(keyword, "+")
if len(params) > 0 && params[0] != "" {
sqlFilter = append(sqlFilter, "PersonName LIKE '%"+params[0]+"%'")
}
if len(params) > 1 && params[1] != "" {
sqlFilter = append(sqlFilter, "PersonIdentifierValue = '"+params[1]+"'")
}
if len(params) > 2 && params[2] != "" {
sqlFilter = append(sqlFilter, "DoctorCode = '"+params[2]+"'")
}
if len(params) > 3 && params[3] != "" {
sqlFilter = append(sqlFilter, "PersonBirthDate = '"+params[3]+"'")
}
if len(params) > 4 && params[4] != "" {
sqlFilter = append(sqlFilter, "PersonTelecomValue = '"+params[4]+"'")
}
}
doctor_tb := `DoctorID, DoctorCode, DoctorIHSNumber, DoctorPersonID`
person_tb := `PersonID, PersonName, PersonSalutation, PersonPrefix, PersonSuffix, PersonGender, PersonBirthPlace, PersonBirthDate,
PersonReligionCode, PersonReligionSystem, PersonBloodTypeCode, PersonBloodTypeSystem, PersonRhesusCode, PersonRhesusSystem,
PersonEducationCode, PersonEducationSystem, PersonJobClassCode, PersonJobClassSystem, PersonEtnicityCode, PersonEtnicitySystem,
PersonMaritalBirth, PersonMaritalStatus`
person_address_tb := `PersonAddressID, PersonAddressPersonID, PersonAddressIsDefault, PersonAddressUse, PersonAddressType, PersonAddressText,
PersonAddressLine1, PersonAddressLine2, PersonAddressRegionalCd, PersonAddressRT, PersonAddressRW, PersonAddressCity,
PersonAddressDistrict, PersonAddressState, PersonAddressCountry`
person_identifier_tb := `PersonIdentifierID, PersonIdentifierPersonID, PersonIdentifierUse, PersonIdentifierTypeCode,
PersonIdentifierTypeSystem, PersonIdentifierCode, PersonIdentifierSystem, PersonIdentifierValue`
person_telecom_tb := `PersonTelecomID, PersonTelecomPersonID, PersonTelecomSystem, PersonTelecomValue, PersonTelecomUse`
offset := (page - 1) * perpage
sql := `SELECT ` + doctor_tb + `,` + person_tb + `,` + person_identifier_tb + `,` + person_address_tb + `,` + person_telecom_tb +
` FROM doctor
JOIN person ON PersonID = DoctorPersonID
JOIN person_identifier ON PersonIdentifierPersonID = PersonID
JOIN person_telecom ON PersonTelecomPersonID = PersonID
JOIN person_address ON PersonAddressPersonID = PersonID
WHERE DoctorIsActive = 'Y'`
if len(sqlFilter) > 0 {
sql += " AND " + strings.Join(sqlFilter, " AND ")
}
sql += " LIMIT ? OFFSET ?"
if err := s.db.Select(&list_doctor, sql, perpage, offset); err != nil {
return nil, err
}
return list_doctor, nil
}

View File

@@ -0,0 +1,50 @@
package error_log
import (
"context"
"github.com/jmoiron/sqlx"
"sismedika.com/sas/westone/utils"
)
type Store struct {
db *sqlx.DB
}
func NewStore(db *sqlx.DB) *Store {
return &Store{db: db}
}
func (s *Store) CreateErrorLog(errval utils.LogError) error {
tx, err := s.db.BeginTxx(context.Background(), nil)
if err != nil {
return err
}
defer func() {
if err != nil {
tx.Rollback()
}
}()
qry := `INSERT INTO westone_log.error_log
(ErrorLogCode, ErrorTraceID, ErrorLogType, ErrorLogParams, ErrorLogQuery, ErrorMessage, ErrorLogTimestamp)
VALUES (:ErrorLogCode, :ErrorTraceID, :ErrorLogType, :ErrorLogParams, :ErrorLogQuery, :ErrorMessage, :ErrorLogTimestamp)`
_, err = tx.NamedExec(qry, errval)
if err != nil {
return err
}
delqry := `DELETE FROM error_log WHERE ErrorLogTimestamp < DATE_SUB(NOW(), INTERVAL 1 MONTH)`
_, err = tx.Exec(delqry)
if err != nil {
return err
}
if err = tx.Commit(); err != nil {
return err
}
return nil
}

82
services/order/store.go Normal file
View File

@@ -0,0 +1,82 @@
package order
import (
"context"
"github.com/jmoiron/sqlx"
"sismedika.com/sas/westone/types"
)
type Store struct {
db *sqlx.DB
}
func NewStore(db *sqlx.DB) *Store {
return &Store{db: db}
}
func (s *Store) CreateOrder(order types.Order) (int, error) {
// Start a new transaction
tx, err := s.db.BeginTxx(context.Background(), nil)
if err != nil {
return 0, err
}
// Defer rollback to ensure transaction is rolled back in case of an error
defer func() {
if err != nil {
tx.Rollback()
}
}()
query := `INSERT INTO orders (userId, total, status, address)
VALUES (:userId, :total, :status, :address)`
// Using tx.NamedExec to insert the order within the transaction
res, err := tx.NamedExec(query, order)
if err != nil {
return 0, err
}
// Retrieving the last inserted ID
id, err := res.LastInsertId()
if err != nil {
return 0, err
}
// Commit the transaction if everything succeeded
if err = tx.Commit(); err != nil {
return 0, err
}
return int(id), nil
}
func (s *Store) CreateOrderItem(orderItem types.OrderItem) error {
// Start a new transaction
tx, err := s.db.BeginTxx(context.Background(), nil)
if err != nil {
return err
}
// Defer rollback to ensure transaction is rolled back in case of an error
defer func() {
if err != nil {
tx.Rollback()
}
}()
query := `INSERT INTO order_items (orderId, productId, quantity, price)
VALUES (:orderId, :productId, :quantity, :price)`
// Using tx.NamedExec to insert the order item within the transaction
_, err = tx.NamedExec(query, orderItem)
if err != nil {
return err
}
// Commit the transaction if everything succeeded
if err = tx.Commit(); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,98 @@
package patient
import (
"fmt"
"net/http"
"github.com/go-playground/validator/v10"
"github.com/gorilla/mux"
"sismedika.com/sas/westone/services/auth"
"sismedika.com/sas/westone/types"
"sismedika.com/sas/westone/utils"
)
type Handler struct {
store types.PatientStore
}
func NewHandler(store types.PatientStore) *Handler {
return &Handler{store: store}
}
func (h *Handler) RegisterRoutes(router *mux.Router) {
patientroute := router.PathPrefix("/mdpatient").Subrouter()
patientroute.Use(auth.AuthMiddleware)
patientroute.HandleFunc("/searchpatient", h.handlerSearchPatient).Methods(http.MethodPost)
patientroute.HandleFunc("/searchpatientold", h.handlerSearchPatientOld).Methods(http.MethodPost)
}
// SearchPatient searches patients by name, NIK, registration number, DOB, or phone number
//
// @Summary Search patient listing
// @Description Search patients by name, NIK, registration number, DOB, or phone number
// @Tags Patient
// @Accept json
// @Param authorization header string true "Authorization token"
// @Param parameter body types.SearchPatientPayload true "parameter"
// @Produce json
// @Success 200 {object} map[string]any
// @Failure 400 {object} map[string]any
// @Failure 500 {object} map[string]any
// @Router /westone/api/v1/mdpatient/searchpatient [post]
func (h *Handler) handlerSearchPatient(w http.ResponseWriter, r *http.Request) {
var payload types.SearchPatientPayload
if err := utils.ParseJSON(r, &payload); err != nil {
utils.WriteError(w, http.StatusBadRequest, err)
return
}
if err := utils.Validate.Struct(payload); err != nil {
erros := err.(validator.ValidationErrors)
utils.WriteError(w, http.StatusBadRequest, fmt.Errorf("invalid payload: %v", erros))
return
}
response, err := h.store.SearchPatient(payload.Keyword, payload.Page, payload.Perpage)
if err != nil {
utils.WriteError(w, http.StatusInternalServerError, err)
return
}
utils.WriteJSON(w, http.StatusOK, response)
}
// SearchPatientOld searches patients by name, NIK, registration number, DOB, or phone number
//
// @Summary Search patient listing old methods
// @Description Search patients by name, NIK, registration number, DOB, or phone number
// @Tags Patient
// @Accept json
// @Param authorization header string true "Authorization token"
// @Param parameter body types.SearchPatientPayload true "parameter"
// @Produce json
// @Success 200 {object} map[string]any
// @Failure 400 {object} map[string]any
// @Failure 500 {object} map[string]any
// @Router /westone/api/v1/mdpatient/searchpatientold [post]
func (h *Handler) handlerSearchPatientOld(w http.ResponseWriter, r *http.Request) {
var payload types.SearchPatientPayload
if err := utils.ParseJSON(r, &payload); err != nil {
utils.WriteError(w, http.StatusBadRequest, err)
return
}
if err := utils.Validate.Struct(payload); err != nil {
erros := err.(validator.ValidationErrors)
utils.WriteError(w, http.StatusBadRequest, fmt.Errorf("invalid payload: %v", erros))
return
}
response, err := h.store.SearchPatientOld(payload.Keyword, payload.Page, payload.Perpage)
if err != nil {
utils.WriteError(w, http.StatusInternalServerError, err)
return
}
utils.WriteJSON(w, http.StatusOK, response)
}

View File

@@ -0,0 +1,161 @@
package patient
import (
"strings"
"github.com/jmoiron/sqlx"
"sismedika.com/sas/westone/types"
)
type Store struct {
db *sqlx.DB
}
func NewStore(db *sqlx.DB) *Store {
return &Store{db: db}
}
func (s *Store) GetPatientByID(id int) (*types.Patient, error) {
patient := new(types.Patient)
qry := `
SELECT
PatientID
PatientPersonID,
PatientManaginOrganizationID,
PatientHISNumber,
PatientRegNumber
FROM patient
WHERE PatientID = ?
`
if err := s.db.Get(patient, qry, id); err != nil {
return nil, err
}
return patient, nil
}
func (s *Store) SearchPatient(keyword string, page int, perpage int) ([]types.PatientResult, error) {
var list_patient []types.PatientResult
var sqlName, sqlNIK, sqlReg, sqlDob, sqlhp string
// params nama+nik+noreg+dob+nohp
if keyword != "" {
params := strings.Split(keyword, "+")
if len(params) > 0 && params[0] != "" {
// sqlName = " AND PersonName LIKE '%" + params[0] + "%' "
sqlName = " WHERE MATCH(PersonNameFulltext) AGAINST('" + params[0] + "' IN BOOLEAN MODE) "
}
if len(params) > 1 && params[1] != "" {
sqlNIK = " AND PersonIdentifierValue = '" + params[1] + "' "
}
if len(params) > 2 && params[2] != "" {
sqlReg = " AND PatientRegNumber = '" + params[2] + "' "
}
if len(params) > 3 && params[3] != "" {
sqlDob = " AND PersonBirthDate = '" + params[3] + "' "
}
if len(params) > 4 && params[4] != "" {
sqlhp = " AND PersonTelecomValue = '" + params[4] + "' "
}
}
patient_tb := `PatientID, PatientPersonID, PatientManaginOrganizationID, PatientHISNumber, PatientRegNumber`
person_tb := `PersonID, PersonName, PersonSalutation, PersonPrefix, PersonSuffix, PersonGender, PersonBirthPlace, PersonBirthDate,
PersonReligionCode, PersonReligionSystem, PersonBloodTypeCode, PersonBloodTypeSystem, PersonRhesusCode, PersonRhesusSystem,
PersonEducationCode, PersonEducationSystem, PersonJobClassCode, PersonJobClassSystem, PersonEtnicityCode, PersonEtnicitySystem,
PersonMaritalBirth, PersonMaritalStatus`
person_address_tb := `PersonAddressID, PersonAddressPersonID, PersonAddressIsDefault, PersonAddressUse, PersonAddressType, PersonAddressText,
PersonAddressLine1, PersonAddressLine2, PersonAddressRegionalCd, PersonAddressRT, PersonAddressRW, PersonAddressCity,
PersonAddressDistrict, PersonAddressState, PersonAddressCountry`
person_identifier_tb := `PersonIdentifierID, PersonIdentifierPersonID, PersonIdentifierUse, PersonIdentifierTypeCode,
PersonIdentifierTypeSystem, PersonIdentifierCode, PersonIdentifierSystem, PersonIdentifierValue`
person_telecom_tb := `PersonTelecomID, PersonTelecomPersonID, PersonTelecomSystem, PersonTelecomValue, PersonTelecomUse`
offset := (page - 1) * perpage
sql := `SELECT ` + patient_tb + `,` + person_tb + `,` + person_identifier_tb + `,` + person_address_tb + `,` + person_telecom_tb +
` FROM patient
JOIN person ON PersonID = PatientPersonID AND PatientIsActive = 'Y'` + sqlDob + sqlReg + `
JOIN person_identifier ON PersonIdentifierPersonID = PersonID` + sqlNIK + `
JOIN person_telecom ON PersonTelecomPersonID = PersonID` + sqlhp + `
JOIN person_address ON PersonAddressPersonID = PersonID
` + sqlName + `
LIMIT ? OFFSET ?`
if err := s.db.Select(&list_patient, sql, perpage, offset); err != nil {
return nil, err
}
return list_patient, nil
}
func (s *Store) SearchPatientOld(keyword string, page int, perpage int) ([]types.PatientResult, error) {
var list_patient []types.PatientResult
// var sqlFilter []string
var sqlName, sqlNIK, sqlReg, sqlDob, sqlhp string
// params nama+nik+noreg+dob+nohp
if keyword != "" {
params := strings.Split(keyword, "+")
if len(params) > 0 && params[0] != "" {
// sqlFilter = append(sqlFilter, "PersonName LIKE '%"+params[0]+"%'")
sqlName = " AND PersonName LIKE '%" + params[0] + "%' "
}
if len(params) > 1 && params[1] != "" {
// sqlFilter = append(sqlFilter, "PersonIdentifierValue = '"+params[1]+"'")
sqlNIK = " AND PersonIdentifierValue = '" + params[1] + "' "
}
if len(params) > 2 && params[2] != "" {
// sqlFilter = append(sqlFilter, "PatientRegNumber = '"+params[2]+"'")
sqlReg = " AND PatientRegNumber = '" + params[2] + "' "
}
if len(params) > 3 && params[3] != "" {
// sqlFilter = append(sqlFilter, "PersonBirthDate = '"+params[3]+"'")
sqlDob = " AND PersonBirthDate = '" + params[3] + "' "
}
if len(params) > 4 && params[4] != "" {
// sqlFilter = append(sqlFilter, "PersonTelecomValue = '"+params[4]+"'")
sqlhp = " AND PersonTelecomValue = '" + params[4] + "' "
}
}
patient_tb := `PatientID, PatientPersonID, PatientManaginOrganizationID, PatientHISNumber, PatientRegNumber`
person_tb := `PersonID, PersonName, PersonSalutation, PersonPrefix, PersonSuffix, PersonGender, PersonBirthPlace, PersonBirthDate,
PersonReligionCode, PersonReligionSystem, PersonBloodTypeCode, PersonBloodTypeSystem, PersonRhesusCode, PersonRhesusSystem,
PersonEducationCode, PersonEducationSystem, PersonJobClassCode, PersonJobClassSystem, PersonEtnicityCode, PersonEtnicitySystem,
PersonMaritalBirth, PersonMaritalStatus`
person_address_tb := `PersonAddressID, PersonAddressPersonID, PersonAddressIsDefault, PersonAddressUse, PersonAddressType, PersonAddressText,
PersonAddressLine1, PersonAddressLine2, PersonAddressRegionalCd, PersonAddressRT, PersonAddressRW, PersonAddressCity,
PersonAddressDistrict, PersonAddressState, PersonAddressCountry`
person_identifier_tb := `PersonIdentifierID, PersonIdentifierPersonID, PersonIdentifierUse, PersonIdentifierTypeCode,
PersonIdentifierTypeSystem, PersonIdentifierCode, PersonIdentifierSystem, PersonIdentifierValue`
person_telecom_tb := `PersonTelecomID, PersonTelecomPersonID, PersonTelecomSystem, PersonTelecomValue, PersonTelecomUse`
offset := (page - 1) * perpage
sql := `SELECT ` + patient_tb + `,` + person_tb + `,` + person_identifier_tb + `,` + person_address_tb + `,` + person_telecom_tb +
` FROM patient
JOIN person ON PersonID = PatientPersonID AND PatientIsActive = 'Y'` + sqlName + sqlDob + sqlReg + `
JOIN person_identifier ON PersonIdentifierPersonID = PersonID` + sqlNIK + `
JOIN person_telecom ON PersonTelecomPersonID = PersonID` + sqlhp + `
JOIN person_address ON PersonAddressPersonID = PersonID
LIMIT ? OFFSET ?`
// if len(sqlFilter) > 0 {
// sql += " AND " + strings.Join(sqlFilter, " AND ")
// }
// sql += " "
if err := s.db.Select(&list_patient, sql, perpage, offset); err != nil {
return nil, err
}
return list_patient, nil
}

View File

@@ -0,0 +1,57 @@
package person
import (
"fmt"
"net/http"
"strconv"
"github.com/gorilla/mux"
"sismedika.com/sas/westone/services/auth"
"sismedika.com/sas/westone/types"
"sismedika.com/sas/westone/utils"
)
type Handler struct {
store types.PersonStore
terminology types.TerminologyStore
}
func NewHandler(store types.PersonStore, terminology types.TerminologyStore) *Handler {
return &Handler{store: store, terminology: terminology}
}
func (h *Handler) RegisterRoutes(router *mux.Router) {
personroute := router.PathPrefix("/mdperson").Subrouter()
personroute.Use(auth.AuthMiddleware)
personroute.HandleFunc("/get/{personID}", h.handleGetPersonByID).Methods(http.MethodGet)
}
func (h *Handler) handleGetPersonByID(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
param, ok := vars["personID"]
if !ok {
utils.WriteError(w, http.StatusBadRequest, fmt.Errorf("missing params person id"))
return
}
personID, err := strconv.Atoi(param)
if err != nil {
utils.WriteError(w, http.StatusBadRequest, fmt.Errorf("invalid params person id"))
return
}
person, err := h.store.GetPersonByID(personID)
if err != nil {
utils.WriteError(w, http.StatusInternalServerError, err)
return
}
json, err := h.getDisplayTerminology(*person)
if err != nil {
utils.WriteError(w, http.StatusInternalServerError, err)
return
}
utils.WriteJSON(w, http.StatusOK, json)
}

View File

@@ -0,0 +1,66 @@
package person
import (
"fmt"
"sismedika.com/sas/westone/types"
"sismedika.com/sas/westone/utils"
)
func (h *Handler) getDisplayTerminology(person types.Person) (*types.PersonJSON, error) {
ret := new(types.PersonJSON)
err := utils.MatchStruct(person, ret)
if err != nil {
return nil, err
}
if person.PersonBloodTypeCode != "" {
blood, err := h.terminology.GetTerminologyDisplay(person.PersonBloodTypeCode, person.PersonBloodTypeSystem)
if err != nil {
return nil, fmt.Errorf("blood")
}
ret.PersonBlood = *blood
}
if person.PersonEducationCode != "" {
education, err := h.terminology.GetTerminologyDisplay(person.PersonEducationCode, person.PersonEducationSystem)
if err != nil {
return nil, fmt.Errorf("edu")
}
ret.PersonEducation = *education
}
if person.PersonEtnicityCode != "" {
etnicity, err := h.terminology.GetTerminologyDisplay(person.PersonEtnicityCode, person.PersonEtnicitySystem)
if err != nil {
return nil, fmt.Errorf("etni")
}
ret.PersonEtnicity = *etnicity
}
if person.PersonJobClassCode != "" {
job, err := h.terminology.GetTerminologyDisplay(person.PersonJobClassCode, person.PersonJobClassSystem)
if err != nil {
return nil, fmt.Errorf("job")
}
ret.PersonJobClass = *job
}
if person.PersonReligionCode != "" {
religion, err := h.terminology.GetTerminologyDisplay(person.PersonReligionCode, person.PersonReligionSystem)
if err != nil {
return nil, fmt.Errorf("reli")
}
ret.PersonReligion = *religion
}
if person.PersonRhesusCode != "" {
rhesus, err := h.terminology.GetTerminologyDisplay(person.PersonRhesusCode, person.PersonRhesusSystem)
if err != nil {
return nil, fmt.Errorf("rhes")
}
ret.PersonRhesus = *rhesus
}
return ret, nil
}

View File

@@ -0,0 +1,50 @@
package person
import (
"github.com/jmoiron/sqlx"
"sismedika.com/sas/westone/types"
)
type Store struct {
db *sqlx.DB
}
func NewStore(db *sqlx.DB) *Store {
return &Store{db: db}
}
func (s *Store) GetPersonByID(id int) (*types.Person, error) {
person := new(types.Person)
qry := `
SELECT
PersonID,
PersonName,
PersonSalutation,
PersonPrefix,
PersonSuffix,
PersonGender,
PersonBirthPlace,
PersonBirthDate,
PersonReligionCode,
PersonReligionSystem,
PersonBloodTypeCode,
PersonBloodTypeSystem,
PersonRhesusCode,
PersonRhesusSystem,
PersonEducationCode,
PersonEducationSystem,
PersonJobClassCode,
PersonJobClassSystem,
PersonEtnicityCode,
PersonEtnicitySystem,
PersonMaritalBirth,
PersonMaritalStatus
FROM person WHERE PersonID = ?
`
if err := s.db.Get(person, qry, id); err != nil {
return nil, err
}
return person, nil
}

View File

@@ -0,0 +1,84 @@
package product
import (
"fmt"
"net/http"
"strconv"
"github.com/go-playground/validator/v10"
"github.com/gorilla/mux"
"sismedika.com/sas/westone/types"
"sismedika.com/sas/westone/utils"
)
type Handler struct {
store types.ProductStore
userStore types.UserStore
}
func NewHandler(store types.ProductStore, userStore types.UserStore) *Handler {
return &Handler{store: store, userStore: userStore}
}
func (h *Handler) RegisterRoutes(router *mux.Router) {
router.HandleFunc("/products", h.handleGetProducts).Methods(http.MethodGet)
router.HandleFunc("/products/{productID}", h.handleGetProduct).Methods(http.MethodGet)
// admin routes
// router.HandleFunc("/products", auth.WithJWTAuth(h.handleCreateProduct)).Methods(http.MethodPost)
}
func (h *Handler) handleGetProducts(w http.ResponseWriter, r *http.Request) {
products, err := h.store.GetProducts()
if err != nil {
utils.WriteError(w, http.StatusInternalServerError, err)
return
}
utils.WriteJSON(w, http.StatusOK, products)
}
func (h *Handler) handleGetProduct(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
str, ok := vars["productID"]
if !ok {
utils.WriteError(w, http.StatusBadRequest, fmt.Errorf("missing product ID"))
return
}
productID, err := strconv.Atoi(str)
if err != nil {
utils.WriteError(w, http.StatusBadRequest, fmt.Errorf("invalid product ID"))
return
}
product, err := h.store.GetProductByID(productID)
if err != nil {
utils.WriteError(w, http.StatusInternalServerError, err)
return
}
utils.WriteJSON(w, http.StatusOK, product)
}
func (h *Handler) handleCreateProduct(w http.ResponseWriter, r *http.Request) {
var product types.CreateProductPayload
if err := utils.ParseJSON(r, &product); err != nil {
utils.WriteError(w, http.StatusBadRequest, err)
return
}
if err := utils.Validate.Struct(product); err != nil {
errors := err.(validator.ValidationErrors)
utils.WriteError(w, http.StatusBadRequest, fmt.Errorf("invalid payload: %v", errors))
return
}
err := h.store.CreateProduct(product)
if err != nil {
utils.WriteError(w, http.StatusInternalServerError, err)
return
}
utils.WriteJSON(w, http.StatusCreated, product)
}

View File

@@ -0,0 +1,157 @@
package product
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gorilla/mux"
"sismedika.com/sas/westone/types"
)
func TestProductServiceHandlers(t *testing.T) {
productStore := &mockProductStore{}
userStore := &mockUserStore{}
handler := NewHandler(productStore, userStore)
t.Run("should handle get products", func(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, "/products", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
router := mux.NewRouter()
router.HandleFunc("/products", handler.handleGetProducts).Methods(http.MethodGet)
router.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("expected status code %d, got %d", http.StatusOK, rr.Code)
}
})
t.Run("should fail if the product ID is not a number", func(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, "/products/abc", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
router := mux.NewRouter()
router.HandleFunc("/products/{productID}", handler.handleGetProduct).Methods(http.MethodGet)
router.ServeHTTP(rr, req)
if rr.Code != http.StatusBadRequest {
t.Errorf("expected status code %d, got %d", http.StatusBadRequest, rr.Code)
}
})
t.Run("should handle get product by ID", func(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, "/products/42", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
router := mux.NewRouter()
router.HandleFunc("/products/{productID}", handler.handleGetProduct).Methods(http.MethodGet)
router.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("expected status code %d, got %d", http.StatusOK, rr.Code)
}
})
t.Run("should fail creating a product if the payload is missing", func(t *testing.T) {
req, err := http.NewRequest(http.MethodPost, "/products", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
router := mux.NewRouter()
router.HandleFunc("/products", handler.handleCreateProduct).Methods(http.MethodPost)
router.ServeHTTP(rr, req)
if rr.Code != http.StatusBadRequest {
t.Errorf("expected status code %d, got %d", http.StatusBadRequest, rr.Code)
}
})
t.Run("should handle creating a product", func(t *testing.T) {
payload := types.CreateProductPayload{
Name: "test",
Price: 100,
Image: "test.jpg",
Description: "test description",
Quantity: 10,
}
marshalled, err := json.Marshal(payload)
if err != nil {
t.Fatal(err)
}
req, err := http.NewRequest(http.MethodPost, "/products", bytes.NewBuffer(marshalled))
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
router := mux.NewRouter()
router.HandleFunc("/products", handler.handleCreateProduct).Methods(http.MethodPost)
router.ServeHTTP(rr, req)
if rr.Code != http.StatusCreated {
t.Errorf("expected status code %d, got %d", http.StatusCreated, rr.Code)
}
})
}
type mockProductStore struct{}
func (m *mockProductStore) GetProductByID(productID int) (*types.Product, error) {
return &types.Product{}, nil
}
func (m *mockProductStore) GetProducts() ([]*types.Product, error) {
return []*types.Product{}, nil
}
func (m *mockProductStore) CreateProduct(product types.CreateProductPayload) error {
return nil
}
func (m *mockProductStore) UpdateProduct(product types.Product) error {
return nil
}
func (m *mockProductStore) GetProductsByID(ids []int) ([]types.Product, error) {
return []types.Product{}, nil
}
type mockUserStore struct{}
func (m *mockUserStore) GetUserByID(userID int) (*types.User, error) {
return &types.User{}, nil
}
func (m *mockUserStore) CreateUser(user types.User) error {
return nil
}
func (m *mockUserStore) SignIn(email string, password string) (*types.User, error) {
return &types.User{}, nil
}

101
services/product/store.go Normal file
View File

@@ -0,0 +1,101 @@
package product
import (
"context"
"github.com/jmoiron/sqlx"
"sismedika.com/sas/westone/types"
)
type Store struct {
db *sqlx.DB
}
func NewStore(db *sqlx.DB) *Store {
return &Store{db: db}
}
func (s *Store) GetProductByID(productID int) (*types.Product, error) {
product := new(types.Product)
query := "SELECT * FROM products WHERE id = ?"
// sqlx.Get is used for fetching a single record and scanning it into the struct
if err := s.db.Get(product, query, productID); err != nil {
return nil, err
}
return product, nil
}
func (s *Store) GetProductsByID(productIDs []int) ([]types.Product, error) {
query, args, err := sqlx.In("SELECT * FROM products WHERE id IN (?)", productIDs)
if err != nil {
return nil, err
}
// Rebind the query for the specific database driver (e.g., mysql uses "?", postgres uses "$1", etc.)
query = s.db.Rebind(query)
var products []types.Product
err = s.db.Select(&products, query, args...)
if err != nil {
return nil, err
}
return products, nil
}
func (s *Store) GetProducts() ([]*types.Product, error) {
var products []*types.Product
query := "SELECT * FROM products"
// sqlx.Select is used to fetch multiple rows and map them into a slice of structs
if err := s.db.Select(&products, query); err != nil {
return nil, err
}
return products, nil
}
func (s *Store) CreateProduct(product types.CreateProductPayload) error {
query := `INSERT INTO products (name, price, image, description, quantity)
VALUES (:name, :price, :image, :description, :quantity)`
// Using sqlx.NamedExec to map struct fields to named parameters in the query
_, err := s.db.NamedExec(query, product)
if err != nil {
return err
}
return nil
}
func (s *Store) UpdateProduct(product types.Product) error {
// Start a new transaction
tx, err := s.db.BeginTxx(context.Background(), nil)
if err != nil {
return err
}
// Defer rollback to ensure transaction is rolled back in case of an error
defer func() {
if err != nil {
tx.Rollback()
}
}()
query := `UPDATE products SET name = :name, price = :price, image = :image,
description = :description, quantity = :quantity WHERE id = :id`
// Using tx.NamedExec to execute the update within the transaction
_, err = tx.NamedExec(query, product)
if err != nil {
return err
}
// Commit the transaction if everything succeeded
if err = tx.Commit(); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,54 @@
package terminology
import (
"fmt"
"net/http"
"github.com/go-playground/validator/v10"
"github.com/gorilla/mux"
"sismedika.com/sas/westone/services/auth"
"sismedika.com/sas/westone/types"
"sismedika.com/sas/westone/utils"
)
type Handler struct {
store types.TerminologyStore
}
func NewHandler(store types.TerminologyStore) *Handler {
return &Handler{store: store}
}
func (h *Handler) RegisterRoutes(router *mux.Router) {
router.HandleFunc("/termicheck", h.checkAPI).Methods(http.MethodGet)
termi := router.PathPrefix("/terminology").Subrouter()
termi.Use(auth.AuthMiddleware)
termi.HandleFunc("/getdisplay", h.handleTerminologyDisplay).Methods(http.MethodGet)
}
func (h *Handler) checkAPI(w http.ResponseWriter, _ *http.Request) {
utils.WriteJSON(w, http.StatusOK, "Connected")
}
func (h *Handler) handleTerminologyDisplay(w http.ResponseWriter, r *http.Request) {
var payload types.GetTerminologyDisplayPayload
if err := utils.ParseJSON(r, &payload); err != nil {
utils.WriteError(w, http.StatusBadRequest, err)
return
}
if err := utils.Validate.Struct(payload); err != nil {
erros := err.(validator.ValidationErrors)
utils.WriteError(w, http.StatusBadRequest, fmt.Errorf("invalid payload: %v", erros))
return
}
response, err := h.store.GetTerminologyDisplay(payload.Code, payload.CodeSystem)
if err != nil {
utils.WriteError(w, http.StatusInternalServerError, err)
return
}
utils.WriteJSON(w, http.StatusOK, response)
}

View File

@@ -0,0 +1,29 @@
package terminology
import (
"github.com/jmoiron/sqlx"
"sismedika.com/sas/westone/types"
)
type Store struct {
db *sqlx.DB
}
func NewStore(db *sqlx.DB) *Store {
return &Store{db: db}
}
func (s *Store) GetTerminologyDisplay(code string, system string) (*types.CodeableTerminology, error) {
term := new(types.CodeableTerminology)
qry := `
SELECT code, display, code_system
FROM terminology
WHERE code = ? AND code_system = ?
`
if err := s.db.Get(term, qry, code, system); err != nil {
return nil, err
}
return term, nil
}

View File

@@ -0,0 +1,123 @@
package user
import (
"fmt"
"net/http"
"github.com/golang-jwt/jwt/v5"
"github.com/gorilla/mux"
"sismedika.com/sas/westone/services/auth"
"sismedika.com/sas/westone/types"
"sismedika.com/sas/westone/utils"
)
type Handler struct {
store types.UserStore
errorStore types.ErrorLogStore
}
func NewHandler(store types.UserStore, errorStore types.ErrorLogStore) *Handler {
return &Handler{
store: store,
errorStore: errorStore,
}
}
func (h *Handler) RegisterRoutes(router *mux.Router) {
router.HandleFunc("/login", h.handleLogin).Methods("POST")
router.HandleFunc("/register", h.handleRegister).Methods("POST")
// admin routes
// router.HandleFunc("/users/{userID}", auth.WithJWTAuth(h.handleGetUser)).Methods(http.MethodGet)
}
func (h *Handler) handleLogin(w http.ResponseWriter, r *http.Request) {
// var payload types.SignInPayload
// if err := utils.ParseJSON(r, &payload); err != nil {
// utils.WriteError(w, http.StatusBadRequest, err)
// return
// }
// if err := utils.Validate.Struct(payload); err != nil {
// errors := err.(validator.ValidationErrors)
// utils.WriteError(w, http.StatusBadRequest, fmt.Errorf("invalid payload: %v", errors))
// return
// }
// hashedPassword := auth.HashWithMD5(payload.Password)
// response, err := h.store.SignIn(payload.Email, hashedPassword)
// if err != nil {
// var logError *utils.LogError
// if errors.As(err, &logError) {
// h.errorStore.CreateErrorLog(*logError)
// utils.WriteErrorLog(w, http.StatusBadRequest, *logError)
// }
// return
// }
// // remoteAddr := r.RemoteAddr
// // userAgent := r.UserAgent()
// // response.IP = remoteAddr
// // response.Agent = userAgent
// secret := []byte(configs.Envs.JWTSecret)
// token, err := auth.CreateJWT(secret, *response)
// if err != nil {
// utils.WriteError(w, http.StatusInternalServerError, err)
// return
// }
// utils.WriteJSONLogin(w, http.StatusOK, response, token, "westone")
}
func (h *Handler) handleRegister(w http.ResponseWriter, r *http.Request) {
// var user types.RegisterUserPayload
// if err := utils.ParseJSON(r, &user); err != nil {
// utils.WriteError(w, http.StatusBadRequest, err)
// return
// }
// if err := utils.Validate.Struct(user); err != nil {
// errors := err.(validator.ValidationErrors)
// utils.WriteError(w, http.StatusBadRequest, fmt.Errorf("invalid payload: %v", errors))
// return
// }
// // check if user exists
// _, err := h.store.GetUserByEmail(user.Email)
// if err == nil {
// utils.WriteError(w, http.StatusBadRequest, fmt.Errorf("user with email %s already exists", user.Email))
// return
// }
// // hash password
// hashedPassword, err := auth.HashPassword(user.Password)
// if err != nil {
// utils.WriteError(w, http.StatusInternalServerError, err)
// return
// }
// err = h.store.CreateUser(types.User{
// FirstName: user.FirstName,
// LastName: user.LastName,
// Email: user.Email,
// Password: hashedPassword,
// })
// if err != nil {
// utils.WriteError(w, http.StatusInternalServerError, err)
// return
// }
// utils.WriteJSON(w, http.StatusCreated, nil)
}
func (h *Handler) handleGetUser(w http.ResponseWriter, r *http.Request) {
claims, ok := r.Context().Value(auth.UserContextKey).(jwt.MapClaims)
if !ok {
utils.WriteError(w, http.StatusInternalServerError, fmt.Errorf("token not found in context"))
return
}
email := claims["M_UserEmail"]
utils.WriteJSON(w, http.StatusOK, email)
}

View File

@@ -0,0 +1,77 @@
package user
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gorilla/mux"
"sismedika.com/sas/westone/types"
"sismedika.com/sas/westone/utils"
)
func TestUserServiceHandlers(t *testing.T) {
userStore := &mockUserStore{}
errorStore := &mockErrorLogStore{}
handler := NewHandler(userStore, errorStore)
t.Run("should fail if the user ID is not a number", func(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, "/user/abc", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
router := mux.NewRouter()
router.HandleFunc("/user/{userID}", handler.handleGetUser).Methods(http.MethodGet)
router.ServeHTTP(rr, req)
if rr.Code != http.StatusBadRequest {
t.Errorf("expected status code %d, got %d", http.StatusBadRequest, rr.Code)
}
})
t.Run("should handle get user by ID", func(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, "/user/42", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
router := mux.NewRouter()
router.HandleFunc("/user/{userID}", handler.handleGetUser).Methods(http.MethodGet)
router.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("expected status code %d, got %d", http.StatusOK, rr.Code)
}
})
}
type mockUserStore struct{}
func (m *mockUserStore) UpdateUser(u types.User) error {
return nil
}
func (m *mockUserStore) SignIn(email string, password string) (*types.User, error) {
return &types.User{}, nil
}
func (m *mockUserStore) CreateUser(u types.User) error {
return nil
}
func (m *mockUserStore) GetUserByID(id int) (*types.User, error) {
return &types.User{}, nil
}
type mockErrorLogStore struct{}
func (n *mockErrorLogStore) CreateErrorLog(errval utils.LogError) error {
return nil
}

View File

@@ -0,0 +1,70 @@
package user
import (
"github.com/jmoiron/sqlx"
"sismedika.com/sas/westone/types"
)
type Store struct {
db *sqlx.DB
}
func NewStore(db *sqlx.DB) *Store {
return &Store{db: db}
}
func (s *Store) CreateUser(user types.User) error {
// Start a new transaction
// tx, err := s.db.BeginTxx(context.Background(), nil)
// if err != nil {
// return err
// }
// // Defer rollback to ensure the transaction is rolled back in case of an error
// defer func() {
// if err != nil {
// tx.Rollback()
// }
// }()
// query := `INSERT INTO users (firstName, lastName, email, password)
// VALUES (:firstName, :lastName, :email, :password)`
// // Using tx.NamedExec to insert the user within the transaction
// _, err = tx.NamedExec(query, user)
// if err != nil {
// return err
// }
// // Commit the transaction if everything succeeded
// if err = tx.Commit(); err != nil {
// return err
// }
return nil
}
func (s *Store) GetUserByID(id int) (*types.User, error) {
user := new(types.User)
qryget := `
SELECT
M_UserID,
M_UserEmail,
M_UserEmail as M_UserUsername,
M_UserGroupDashboard,
1 as M_UserDefaultT_SampleStationID,
IFNULL(M_UserFullName, "") as M_StaffName,
IFNULL(M_StaffIsCourier, "N") as is_courier,
IFNULL(S_SystemsAutoLogoutTime,0) as time_autologout
FROM m_user
JOIN m_usergroup ON M_UserM_UserGroupID = M_UserGroupID
LEFT JOIN m_staff on M_UserM_StaffID = M_StaffID
JOIN conf_systems ON S_SystemsIsActive = 'Y'
WHERE M_UserID = ?
`
if err := s.db.Get(user, qryget, id); err != nil {
return nil, err
}
return user, nil
}