Skip to content
Merged
26 changes: 18 additions & 8 deletions backend/cmd/app/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"log"
"net/http"
"os"
"strings"
"unicard-go/backend/internal/admin"
authentication "unicard-go/backend/internal/auth"
"unicard-go/backend/internal/user"
Expand Down Expand Up @@ -89,12 +90,7 @@ func main() {
mux.HandleFunc("POST /v1/reset-password", authHandler.ResetPassword)
mux.HandleFunc("GET /{username}", userHandler.DashboardView)
mux.HandleFunc("GET /v1/user/{username}", userHandler.DashboardHandler)
//mux.HandleFunc("GET /transaction", userHandler.TransactionView)
//mux.HandleFunc("GET /topup", userHandler.TopupView)
//mux.HandleFunc("GET /profile", userHandler.ProfileView)
//mux.HandleFunc("GET /settings", userHandler.SettingsView)
//mux.HandleFunc("GET /card", userHandler.CardView)
//mux.HandleFunc("GET /v1/user/transactions", userHandler.TransactionsJSONHandler)
mux.HandleFunc("GET /v1/user/{username}/transactions", userHandler.TransactionsJSONHandler)
//mux.HandleFunc("GET /logout",)

// super admin endpoints
Expand Down Expand Up @@ -123,14 +119,28 @@ func main() {
mux.HandleFunc("POST /v1/admin/{username}/deactivatecardauth", adminHanlder.DeactivateCardHanlder)
mux.HandleFunc("POST /v1/admin/{username}/deletecardauth", adminHanlder.DeleteCardHandler)
mux.HandleFunc("GET /admin/{username}/delete-cards", adminHanlder.DeleteCardView)



// terminal simulation endpoints
mux.HandleFunc("GET /terminal-sim", adminHanlder.TerminalSimView)
mux.HandleFunc("POST /v1/terminal-sim/transact", adminHanlder.TerminalSimTransactionHandler)

// Wrap mux with custom handler for root redirect
customHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}

// Handle GET /{username}/transaction(s) manually to avoid ServeMux conflict with /assets/
parts := strings.Split(r.URL.Path, "/")
if len(parts) == 3 && (parts[2] == "transaction" || parts[2] == "transactions") && r.Method == http.MethodGet {
if parts[1] != "assets" && parts[1] != "storage" && parts[1] != "v1" && parts[1] != "admin" {
r.SetPathValue("username", parts[1])
userHandler.TransactionView(w, r)
return
}
}

mux.ServeHTTP(w, r)
})

Expand Down
210 changes: 210 additions & 0 deletions backend/internal/admin/terminal_sim.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
package admin

import (
"database/sql"
"encoding/json"
"fmt"
"net/http"
"time"

jsonwrite "unicard-go/backend/internal/pkg/handler"
)

// TerminalSimView renders the terminal simulation page
func (h *Handler) TerminalSimView(w http.ResponseWriter, r *http.Request) {
fmt.Println("Terminal Simulation view is running...")

type Merchant struct {
ID string
Name string
}
var merchants []Merchant

rows, err := h.DB.Query("SELECT user_id, business_name FROM merchants")
if err == nil {
defer rows.Close()
for rows.Next() {
var m Merchant
if err := rows.Scan(&m.ID, &m.Name); err == nil {
merchants = append(merchants, m)
}
}
}

data := struct {
Merchants []Merchant
}{
Merchants: merchants,
}

h.Tpl.ExecuteTemplate(w, "terminal_sim.html", data)
}

// TerminalSimTransactionHandler handles simulated terminal transactions
func (h *Handler) TerminalSimTransactionHandler(w http.ResponseWriter, r *http.Request) {
fmt.Println("TerminalSimTransactionHandler is running...")

type SimRequest struct {
CardNumber string `json:"card_number"`
Type string `json:"type"`
Amount float64 `json:"amount"`
MerchantID string `json:"merchant_id"`
}

var req SimRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{
Success: false,
Message: "Invalid request payload",
})
return
}

if req.CardNumber == "" || req.Amount <= 0 || req.Type == "" {
jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{
Success: false,
Message: "Missing required fields",
})
return
}

// 1. Check if card exists, is active, and linked to a user
var balance float64
var status string
var userID sql.NullString

err := h.DB.QueryRow(`
SELECT balance, status, user_id
FROM cards
WHERE card_number = ?
`, req.CardNumber).Scan(&balance, &status, &userID)

if err != nil {
if err == sql.ErrNoRows {
jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{
Success: false,
Message: "Card not found",
})
return
}
jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{
Success: false,
Message: "Database error",
})
return
}

if status != "active" {
jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{
Success: false,
Message: "Card is not active",
})
return
}

if !userID.Valid || userID.String == "" {
jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{
Success: false,
Message: "Card is not linked to any user",
})
return
}

// 2. Check balance
if req.Type != "Refund" && balance < req.Amount {
jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.APIResponse{
Success: false,
Message: fmt.Sprintf("Insufficient balance. Current balance: %.2f", balance),
})
return
}

// 2.5 Get merchant commission rate
var commissionRate float64
err = h.DB.QueryRow("SELECT commission_rate FROM merchants WHERE user_id = ?", req.MerchantID).Scan(&commissionRate)
if err != nil {
commissionRate = 2.00 // default fallback
}

serviceFee := req.Amount * (commissionRate / 100.0)
loyaltyPoints := req.Amount * 0.002 // 0.2% reward points

// 3. Process Transaction (Start TX)
tx, err := h.DB.Begin()
if err != nil {
jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{
Success: false,
Message: "Failed to start transaction",
})
return
}

// Deduct or add balance and adjust loyalty points based on type
if req.Type == "Refund" {
_, err = tx.Exec(`UPDATE cards SET balance = balance + ?, loyalty_points = loyalty_points - ? WHERE card_number = ?`, req.Amount, loyaltyPoints, req.CardNumber)
} else {
_, err = tx.Exec(`UPDATE cards SET balance = balance - ?, loyalty_points = loyalty_points + ? WHERE card_number = ?`, req.Amount, loyaltyPoints, req.CardNumber)
}
if err != nil {
tx.Rollback()
jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{
Success: false,
Message: "Failed to deduct balance",
})
return
}

// Insert transaction record
transactionID := fmt.Sprintf("TXN-SIM-%d", time.Now().UnixNano())

// Get a dummy terminal ID
var terminalID string
err = h.DB.QueryRow("SELECT terminal_id FROM terminals LIMIT 1").Scan(&terminalID)
if err != nil {
terminalID = "TRM-SIM-001" // Fallback if no terminals exist
}

// Get a dummy processed_by user ID
var processedBy string
err = h.DB.QueryRow("SELECT user_id FROM users LIMIT 1").Scan(&processedBy)
if err != nil {
processedBy = "USR-SIM-001" // Fallback if no users exist
}

// Determine transaction type
dbTransactionType := "payment"
if req.Type == "Refund" {
dbTransactionType = "refund"
}

_, err = tx.Exec(`
INSERT INTO transactions (transaction_id, card_number, merchant_id, terminal_id, transaction_type, amount, service_fee, processed_by)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`, transactionID, req.CardNumber, req.MerchantID, terminalID, dbTransactionType, req.Amount, serviceFee, processedBy)

if err != nil {
// If `merchant_id` is not nullable and causes error or id doesn't auto-increment
// let's try with dummy data if needed, but we rollback first
tx.Rollback()
fmt.Printf("Error inserting transaction: %v\n", err)
jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{
Success: false,
Message: "Failed to record transaction",
})
return
}

if err = tx.Commit(); err != nil {
jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{
Success: false,
Message: "Failed to commit transaction",
})
return
}

jsonwrite.WriteJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"message": "Transaction successful",
"service_fee": serviceFee,
})
}
52 changes: 41 additions & 11 deletions backend/internal/user/dashboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ import (
jsonwrite "unicard-go/backend/internal/pkg/handler"
)

// Transaction struct represents a user's transaction for the dashboard view
type Transaction struct {
Date string `json:"date" db:"date"`
Description string `json:"description" db:"description"`
Type string `json:"type" db:"transaction_type"`
Amount float64 `json:"amount" db:"transaction_amount"`
TransactionID string `json:"transaction_id"`
TerminalID string `json:"terminal_id"`
Date string `json:"date" db:"date"`
Time string `json:"time"`
Description string `json:"description" db:"description"`
Type string `json:"type" db:"transaction_type"`
Amount float64 `json:"amount" db:"transaction_amount"`
}

// DashboardUser info struct for the user dashboard view
Expand All @@ -38,9 +40,14 @@ type DashboardUser struct {
func (h *Handler) DashboardView(w http.ResponseWriter, r *http.Request) {
fmt.Println("Dashboard view is running...")

// Check if session cookie is present (Removed)
username := r.PathValue("username")
data := struct {
Username string
}{
Username: username,
}

h.Tpl.ExecuteTemplate(w, "dashboard.html", nil)
h.Tpl.ExecuteTemplate(w, "dashboard.html", data)
}

func (h *Handler) DashboardHandler(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -128,11 +135,12 @@ func (h *Handler) DashboardHandler(w http.ResponseWriter, r *http.Request) {

// Fetch recent transactions
txnQuery := `
SELECT t.created_at, m.business_name, t.transaction_type, t.amount
SELECT t.transaction_id, t.created_at, m.business_name, t.transaction_type, t.amount, t.terminal_id
FROM transactions t
JOIN cards c ON t.card_number = c.card_number
JOIN merchants m ON t.merchant_id = m.id
WHERE c.user_id = ?
JOIN users u ON c.user_id = u.user_id
LEFT JOIN merchants m ON t.merchant_id = m.user_id
WHERE u.username = ?
ORDER BY t.created_at DESC LIMIT 5
`
rows, err := h.DB.Query(txnQuery, userID)
Expand All @@ -142,8 +150,15 @@ func (h *Handler) DashboardHandler(w http.ResponseWriter, r *http.Request) {
for rows.Next() {
var t Transaction
var createdAt string
if err := rows.Scan(&createdAt, &t.Description, &t.Type, &t.Amount); err == nil {
var businessName sql.NullString
if err := rows.Scan(&t.TransactionID, &createdAt, &businessName, &t.Type, &t.Amount, &t.TerminalID); err == nil {
t.Date = formatDate(createdAt)
t.Time = formatTime(createdAt)
if businessName.Valid {
t.Description = businessName.String
} else {
t.Description = "Terminal Simulation"
}
transactions = append(transactions, t)
}
}
Expand Down Expand Up @@ -185,3 +200,18 @@ func formatDate(dbTime string) string {
}
return dbTime
}

func formatTime(dbTime string) string {
t, err := time.Parse("2006-01-02 15:04:05", dbTime)
if err == nil {
return t.Format("03:04 PM")
}
t2, err := time.Parse(time.RFC3339, dbTime)
if err == nil {
return t2.Format("03:04 PM")
}
if len(dbTime) > 10 {
return dbTime[11:16]
}
return ""
}
Loading