diff --git a/api/accomodation/gate.controllers.go b/api/accomodation/gate.controllers.go index e88f55c0..a60f38f0 100644 --- a/api/accomodation/gate.controllers.go +++ b/api/accomodation/gate.controllers.go @@ -356,3 +356,192 @@ func GateStatus(c *gin.Context) { }) pkg.Log.SuccessCtx(c) } + +// If No Accomodation, then day scholar +// - Everyday One CheckIn. +// +// If Accomodation +// - LifeTime One CheckIn +func GateCheckInStatus(c *gin.Context) { + hospId := c.Param("hospId") + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + conn, err := cmd.DBPool.Acquire(ctx) + if pkg.HandleDbAcquireErr(c, err, "GATE-CHECKIN-STATUS") { + return + } + defer conn.Release() + + q := db.New() + res, err := q.GateCheckStatusQuery(ctx, conn, pgtype.Text{ + String: hospId, + Valid: true, + }) + if err == pgx.ErrNoRows { + c.JSON(http.StatusNotFound, gin.H{ + "message": "Invalid hospitality ID", + "allow": false, + "reason": "Invalid hospitality ID provided.", + "name": res.Name, + "email": res.Email, + "college_name": res.CollegeName, + }) + pkg.Log.WarnCtx(c, "[GATE-CHECKIN-WARN]: Invalid hospitality ID") + return + } + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "message": "Oops! Something happened. Please try again later", + "allow": false, + "reason": "Server error.", + "name": res.Name, + "email": res.Email, + "college_name": res.CollegeName, + }) + pkg.Log.ErrorCtx(c, "[GATE-CHECKIN-ERROR]: Failed to check gate status", err) + return + } + + // hasAccomodation would be true if payment_status is not NULL or empty + hasAccomodation := res.AccomodationStatus.Valid && res.AccomodationStatus.String != "" + + if hasAccomodation { + // Rule: Cannot check-in if already inside + // A person is inside if they have a valid check-in and either no valid checkout or check-in is after checkout. + isAlreadyInside := res.LastCheckIn.Valid && !res.LastCheckOut.Valid + + if isAlreadyInside { + // Rule: Cannot check-in if already inside + // Already inside is defined as last_check_in > last_check_out + c.JSON(http.StatusOK, gin.H{ + "message": "Check-in status", + "allow": false, + "reason": "Already check-in.", + "name": res.Name, + "email": res.Email, + "college_name": res.CollegeName, + }) + pkg.Log.InfoCtx(c, "[GATE-CHECKIN-STATUS]: Denied check-in for accomodation holder (already inside)") + return + } + + hasAlreadyLeft := res.LastCheckIn.Valid && res.LastCheckOut.Valid + + if hasAlreadyLeft { + // Rule: Cannot check-in once left + c.JSON(http.StatusOK, gin.H{ + "message": "Check-in status", + "allow": false, + "reason": "Checked out once already.", + "name": res.Name, + "email": res.Email, + "college_name": res.CollegeName, + }) + pkg.Log.InfoCtx(c, "[GATE-CHECKIN-STATUS]: Denied check-in for accomodation holder (already inside)") + return + } + // If not inside then fall through to success case + } else { // does not have accomodation + // Rule: Day scholars can check-in once per day + if res.LastCheckIn.Valid { + now := time.Now() + lastCheckInTime := res.LastCheckIn.Time + + if lastCheckInTime.Year() == now.Year() && lastCheckInTime.YearDay() == now.YearDay() { + c.JSON(http.StatusOK, gin.H{ + "message": "Check-in status", + "allow": false, + "reason": "Already checked in today", + "name": res.Name, + "email": res.Email, + "college_name": res.CollegeName, + }) + pkg.Log.InfoCtx(c, "[GATE-CHECKIN-STATUS]: Denied check-in for day scholar (already checked in today)") + return + } + } + } + + c.JSON(http.StatusOK, gin.H{ + "message": "Checkin status sent successfully", + "allow": true, + "reason": "", + "name": res.Name, + "email": res.Email, + "college_name": res.CollegeName, + }) + pkg.Log.SuccessCtx(c) +} + +// If No Accomodation, then day scholar +// - Everyday One CheckOut +// +// If Accomodation +// - LifeTime One CheckOut +func GateCheckOutStatus(c *gin.Context) { + hospId := c.Param("hospId") + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + conn, err := cmd.DBPool.Acquire(ctx) + if pkg.HandleDbAcquireErr(c, err, "GATE-CHECKOUT-STATUS") { + return + } + defer conn.Release() + + q := db.New() + + res, err := q.GateCheckStatusQuery(ctx, conn, pgtype.Text{ + String: hospId, + Valid: true, + }) + if err == pgx.ErrNoRows { + c.JSON(http.StatusNotFound, gin.H{ + "message": "Invalid hospitality ID", + "allow": false, + "reason": "Invalid hospitality ID provided", + "name": res.Name, + "email": res.Email, + "college_name": res.CollegeName, + }) + pkg.Log.WarnCtx(c, "[GATE-CHECKOUT-WARN]: Invalid hospitality ID") + return + } + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "message": "Oops! Something happened. Please try again later", + }) + pkg.Log.ErrorCtx(c, "[GATE-CHECKOUT-ERROR]: Failed to check gate status", err) + return + } + + // A user can checkout only if they are currently checked in. A user is + // inside if their last checkin is more recent that their last checkout + isCurrentlyInside := res.LastCheckIn.Valid && (!res.LastCheckOut.Valid || res.LastCheckIn.Time.After(res.LastCheckOut.Time)) + + if !isCurrentlyInside { + c.JSON(http.StatusOK, gin.H{ + "message": "Checkout status", + "allow": false, + "reason": "Not currently checked in.", + "name": res.Name, + "email": res.Email, + "college_name": res.CollegeName, + }) + pkg.Log.InfoCtx(c, "[GATE-CHECKOUT-STATUS]: Denied checkout (not inside)") + return + } + + c.JSON(http.StatusOK, gin.H{ + "message": "Checkout status sent successfully", + "allow": true, + "reason": "", + "name": res.Name, + "email": res.Email, + "college_name": res.CollegeName, + }) + pkg.Log.SuccessCtx(c) +} diff --git a/api/accomodation/routes.go b/api/accomodation/routes.go index 488f8586..56e9d6d7 100644 --- a/api/accomodation/routes.go +++ b/api/accomodation/routes.go @@ -45,8 +45,9 @@ func GateRoutes(r *gin.RouterGroup) { r.GET("/app/:accId", mw.Auth, mw.CheckHospitality, mw.CheckGate, GetAccommodationById) r.PUT("/app/:accId", mw.Auth, mw.CheckHospitality, mw.CheckGate, UpdateAccommodationById) + r.GET("/app/gate/status/check-in/:hospId", mw.Auth, mw.CheckHospitality, mw.CheckGate, GateCheckInStatus) r.POST("/app/gate/check-in/:hospId", mw.Auth, mw.CheckHospitality, mw.CheckGate, GateCheckIn) - r.GET("/app/gate/status/:hospId", mw.Auth, mw.CheckHospitality, mw.CheckGate, GateStatus) + r.GET("/app/gate/status/check-out/:hospId", mw.Auth, mw.CheckHospitality, mw.CheckGate, GateCheckOutStatus) r.POST("/app/gate/check-out/:hospId", mw.Auth, mw.CheckHospitality, mw.CheckGate, GateCheckOut) r.POST("/app/hostel/check-in/:hospId", mw.Auth, mw.CheckHospitality, mw.CheckHostel, HostelCheckIn) diff --git a/bruno/hospitality-gate/checkIn.bru b/bruno/hospitality-gate/checkIn.bru new file mode 100644 index 00000000..27ba81a2 --- /dev/null +++ b/bruno/hospitality-gate/checkIn.bru @@ -0,0 +1,20 @@ +meta { + name: checkIn + type: http + seq: 2 +} + +post { + url: {{baseUrl}}/accommodation/app/gate/check-in/:hospId + body: none + auth: inherit +} + +params:path { + hospId: +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno/hospitality-gate/checkInStatus.bru b/bruno/hospitality-gate/checkInStatus.bru new file mode 100644 index 00000000..972160c4 --- /dev/null +++ b/bruno/hospitality-gate/checkInStatus.bru @@ -0,0 +1,20 @@ +meta { + name: checkInStatus + type: http + seq: 1 +} + +get { + url: {{baseUrl}}/accommodation/app/gate/status/check-in/:hospId + body: none + auth: inherit +} + +params:path { + hospId: +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno/hospitality-gate/checkOut.bru b/bruno/hospitality-gate/checkOut.bru new file mode 100644 index 00000000..ab5d368f --- /dev/null +++ b/bruno/hospitality-gate/checkOut.bru @@ -0,0 +1,20 @@ +meta { + name: checkOut + type: http + seq: 4 +} + +post { + url: {{baseUrl}}/accommodation/app/gate/check-out/:hospId + body: none + auth: inherit +} + +params:path { + hospId: +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno/hospitality-gate/checkOutStatus.bru b/bruno/hospitality-gate/checkOutStatus.bru new file mode 100644 index 00000000..2b475b84 --- /dev/null +++ b/bruno/hospitality-gate/checkOutStatus.bru @@ -0,0 +1,20 @@ +meta { + name: checkOutStatus + type: http + seq: 3 +} + +get { + url: {{baseUrl}}/accommodation/app/gate/status/check-out/:hospId + body: none + auth: inherit +} + +params:path { + hospId: +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno/hospitality-gate/folder.bru b/bruno/hospitality-gate/folder.bru new file mode 100644 index 00000000..5e337e29 --- /dev/null +++ b/bruno/hospitality-gate/folder.bru @@ -0,0 +1,8 @@ +meta { + name: hospitality-gate + seq: 18 +} + +auth { + mode: inherit +} diff --git a/db/gen/gate-management-for-app.sql.go b/db/gen/gate-management-for-app.sql.go index d5bcda89..1cd515ef 100644 --- a/db/gen/gate-management-for-app.sql.go +++ b/db/gen/gate-management-for-app.sql.go @@ -81,6 +81,59 @@ func (q *Queries) GateCheckInOutQuery(ctx context.Context, db DBTX, arg GateChec return direction, err } +const gateCheckStatusQuery = `-- name: GateCheckStatusQuery :one +SELECT + s.name, + s.college_name, + s.email, + ad.payment_status AS accomodation_status, + ( + SELECT logged_at + FROM gate_management + WHERE + student_id = s.id + AND direction = 'IN' + ORDER BY logged_at DESC + LIMIT 1 + ) AS last_check_in, + ( + SELECT logged_at + FROM gate_management + WHERE + student_id = s.id + AND direction = 'OUT' + ORDER BY logged_at DESC + LIMIT 1 + ) AS last_check_out +FROM student s +LEFT JOIN accomodation_details ad ON s.id = ad.student_id +WHERE + s.hospitality_id = $1 +` + +type GateCheckStatusQueryRow struct { + Name string `json:"name"` + CollegeName string `json:"college_name"` + Email string `json:"email"` + AccomodationStatus pgtype.Text `json:"accomodation_status"` + LastCheckIn pgtype.Timestamp `json:"last_check_in"` + LastCheckOut pgtype.Timestamp `json:"last_check_out"` +} + +func (q *Queries) GateCheckStatusQuery(ctx context.Context, db DBTX, hospitalityID pgtype.Text) (GateCheckStatusQueryRow, error) { + row := db.QueryRow(ctx, gateCheckStatusQuery, hospitalityID) + var i GateCheckStatusQueryRow + err := row.Scan( + &i.Name, + &i.CollegeName, + &i.Email, + &i.AccomodationStatus, + &i.LastCheckIn, + &i.LastCheckOut, + ) + return i, err +} + const hostelCheckInQuery = `-- name: HostelCheckInQuery :one WITH student_lookup AS ( SELECT id diff --git a/db/queries/gate-management-for-app.sql b/db/queries/gate-management-for-app.sql index 28961274..05f11f2b 100644 --- a/db/queries/gate-management-for-app.sql +++ b/db/queries/gate-management-for-app.sql @@ -89,3 +89,32 @@ SELECT (SELECT logged_at FROM last_check_in) AS last_check_in, (SELECT logged_at FROM last_check_out) AS last_check_out FROM student_info si; + +-- name: GateCheckStatusQuery :one +SELECT + s.name, + s.college_name, + s.email, + ad.payment_status AS accomodation_status, + ( + SELECT logged_at + FROM gate_management + WHERE + student_id = s.id + AND direction = 'IN' + ORDER BY logged_at DESC + LIMIT 1 + ) AS last_check_in, + ( + SELECT logged_at + FROM gate_management + WHERE + student_id = s.id + AND direction = 'OUT' + ORDER BY logged_at DESC + LIMIT 1 + ) AS last_check_out +FROM student s +LEFT JOIN accomodation_details ad ON s.id = ad.student_id +WHERE + s.hospitality_id = $1; diff --git a/middleware/rbac.go b/middleware/rbac.go index 01dfc826..286f6528 100644 --- a/middleware/rbac.go +++ b/middleware/rbac.go @@ -61,7 +61,7 @@ func CheckHospitality(c *gin.Context) { c.AbortWithStatusJSON(http.StatusForbidden, gin.H{ "message": "Hospitality access denied.", }) - pkg.Log.WarnCtx(c, "[RBAC-FAIL]: Does not have hospitality-panel role") + pkg.Log.WarnCtx(c, "[RBAC-FAIL]: Does not have hospitality role") } func CheckGate(c *gin.Context) {