Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions backend/cmd/app/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ func main() {
mux.HandleFunc("GET /signup", authHandler.SignupView)
mux.HandleFunc("POST /v1/loginauth", authHandler.LoginAuthHandler) // Login authentication endpoint
mux.HandleFunc("POST /v1/signupauth", authHandler.SignupHandler)
mux.HandleFunc("GET /admin-signup", authHandler.AdminSignupView)
mux.HandleFunc("POST /v1/admin-signup", authHandler.AdminSignupHandler)
mux.HandleFunc("POST /v1/signup/check-details", authHandler.CheckDetailsHandler)
mux.HandleFunc("POST /v1/signup/check-card", authHandler.CheckCardHandler)
mux.HandleFunc("GET /forgot-password", authHandler.ForgotPasswordView)
Expand All @@ -95,6 +97,8 @@ func main() {
mux.HandleFunc("GET /admin/merchants", adminHanlder.MerchantManagementView)
mux.HandleFunc("GET /v1/admin/merchants-data", adminHanlder.MerchantManagementDataHandler)
mux.HandleFunc("GET /admin/terminals", adminHanlder.TerminalRegistryView)
mux.HandleFunc("GET /v1/admin/terminals-data", adminHanlder.TerminalRegistryDataHandler)
mux.HandleFunc("POST /v1/admin/terminals/add", adminHanlder.AddTerminalHandler)
mux.HandleFunc("GET /admin/settings", adminHanlder.SystemSettingsView)
mux.HandleFunc("POST /v1/admin/merchants/add", adminHanlder.AddMerchantHandler)
mux.HandleFunc("GET /admin/card-inventory", adminHanlder.CardInventoryView)
Expand Down
231 changes: 231 additions & 0 deletions backend/internal/admin/add_merchant.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
package admin

import (
"crypto/rand"
"encoding/json"
"errors"
"fmt"
"log"
"math/big"
"net/http"
"time"
jsonwrite "unicard-go/backend/internal/pkg/handler"

"github.com/go-playground/validator/v10"
"golang.org/x/crypto/bcrypt"
)

// AddMerchantRequest represents the payload for adding a new merchant
type AddMerchantRequest struct {
BusinessName string `json:"businessName" validate:"required" db:"business_name"`
BusinessType string `json:"businessType" validate:"required" db:"business_type"`
RegistrationNum string `json:"registrationNum" validate:"required" db:"registration_num"`
BusinessAddress string `json:"businessAddress" validate:"required" db:"business_address"`
OwnerName string `json:"ownerName" validate:"required" db:"owner_name"`
BusinessEmail string `json:"businessEmail" validate:"required,email" db:"business_email"`
BusinessPhone string `json:"businessPhone" validate:"required" db:"business_phone"`
CommissionRate string `json:"commissionRate" validate:"required" db:"commission_rate"`
SettlementName string `json:"settlementName" validate:"required" db:"settlement_name"`
SettlementAccount string `json:"settlementAccount" validate:"required" db:"settlement_account_number"`
SettlementBank string `json:"settlementBank" validate:"required" db:"settlement_bank_name"`
TerminalSN string `json:"terminalSn" validate:"required" db:"terminal_sn"`
DeviceName string `json:"deviceName" validate:"required" db:"device_name"`
}

// AddMerchantHandler creates new merchants and their corresponding owner users in bulk
func (h *Handler) AddMerchantHandler(w http.ResponseWriter, r *http.Request) {
var reqs []AddMerchantRequest
if err := json.NewDecoder(r.Body).Decode(&reqs); err != nil {
jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{
Success: false,
Message: "Invalid JSON payload format. Expected array of merchants.",
})
return
}

if len(reqs) == 0 {
jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{
Success: false,
Message: "No merchants provided.",
})
return
}

tx, err := h.DB.Begin()
if err != nil {
log.Printf("Error starting tx: %v", err)
jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{
Success: false,
Message: "Database error",
})
return
}

// Prepare statements
userStmt, err := tx.Prepare(`INSERT INTO users (user_id, username, name, email, phone_number, password_hash, role, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
if err != nil {
tx.Rollback()
log.Printf("Error preparing user stmt: %v", err)
jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{
Success: false,
Message: "Database error",
})
return
}
defer userStmt.Close()

merchStmt, err := tx.Prepare(`INSERT INTO merchants (
merchant_id, business_name, business_type, business_registration_number, business_address,
owner_user_id, owner_name, business_email, business_phone, commission_rate,
settlement_account_name, settlement_account_number, settlement_bank_name, status
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
if err != nil {
tx.Rollback()
log.Printf("Error preparing merchant stmt: %v", err)
jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{
Success: false,
Message: "Database error",
})
return
}
defer merchStmt.Close()

termStmt, err := tx.Prepare(`INSERT INTO terminals (
terminal_id, terminal_sn, merchant_id, device_name, status
) VALUES (?, ?, ?, ?, ?)`)
if err != nil {
tx.Rollback()
log.Printf("Error preparing terminal stmt: %v", err)
jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{
Success: false,
Message: "Database error",
})
return
}
defer termStmt.Close()

for i, req := range reqs {
err := Validate.Struct(req)
if err != nil {
tx.Rollback()
log.Printf("Validation error on merchant %d: %v", i+1, err)
var validationErrs validator.ValidationErrors
msg := fmt.Sprintf("Validation failed on merchant #%d", i+1)
if errors.As(err, &validationErrs) {
firstErr := validationErrs[0]
fieldMessages := map[string]string{
"BusinessName": "Business name is required",
"BusinessType": "Business type is required",
"RegistrationNum": "Registration number is required",
"BusinessAddress": "Business address is required",
"OwnerName": "Owner name is required",
"BusinessEmail": "A valid business email is required",
"BusinessPhone": "Business phone number is required",
"CommissionRate": "Commission rate is required",
"SettlementName": "Settlement name is required",
"SettlementAccount": "Settlement account number is required",
"SettlementBank": "Settlement bank name is required",
"TerminalSN": "Terminal serial number is required",
"DeviceName": "Device name is required",
}
if customMsg, ok := fieldMessages[firstErr.Field()]; ok {
msg = fmt.Sprintf("Merchant #%d: %s", i+1, customMsg)
}
}
jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{
Success: false,
Message: msg,
})
return
}

// Generate IDs (Format: YYMMminsecxxxxx where xxxxx is 5 random numbers)
timestamp := time.Now().Format("06010405") // YYMMDDHH

nUser, _ := rand.Int(rand.Reader, big.NewInt(100000))
userID := fmt.Sprintf("UNI-%s%04d", timestamp, nUser.Int64())

nMerchant, _ := rand.Int(rand.Reader, big.NewInt(100000))
merchantID := fmt.Sprintf("MCH-%s%04d", timestamp, nMerchant.Int64())

// Create user for the merchant owner
hashedPassword, err := bcrypt.GenerateFromPassword([]byte("TempPass123!"), bcrypt.DefaultCost)
if err != nil {
tx.Rollback()
log.Printf("Error hashing password: %v", err)
jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{
Success: false,
Message: "Failed to secure user credentials",
})
return
}

// Using business email as username for simplicity
username := req.BusinessEmail
_, err = userStmt.Exec(userID, username, req.OwnerName, req.BusinessEmail, req.BusinessPhone, string(hashedPassword), "merchant_admin", "active")
if err != nil {
tx.Rollback()
log.Printf("Error creating user %d: %v", i+1, err)
jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{
Success: false,
Message: fmt.Sprintf("Failed to create user account for Merchant #%d (email or phone might already exist)", i+1),
})
return
}

res, err := merchStmt.Exec(
merchantID, req.BusinessName, req.BusinessType, req.RegistrationNum, req.BusinessAddress,
userID, req.OwnerName, req.BusinessEmail, req.BusinessPhone, req.CommissionRate,
req.SettlementName, req.SettlementAccount, req.SettlementBank, "active",
)

if err != nil {
tx.Rollback()
log.Printf("Error creating merchant %d: %v", i+1, err)
jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{
Success: false,
Message: fmt.Sprintf("Failed to create profile for Merchant #%d (registration num or email might exist)", i+1),
})
return
}

internalMerchantID, err := res.LastInsertId()
if err != nil {
tx.Rollback()
log.Printf("Error getting last insert ID for merchant %d: %v", i+1, err)
jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{
Success: false,
Message: "Database error",
})
return
}

nTerminal, _ := rand.Int(rand.Reader, big.NewInt(100000))
terminalID := fmt.Sprintf("TRM-%s%04d", timestamp, nTerminal.Int64())

_, err = termStmt.Exec(terminalID, req.TerminalSN, internalMerchantID, req.DeviceName, "active")
if err != nil {
tx.Rollback()
log.Printf("Error creating terminal %d: %v", i+1, err)
jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{
Success: false,
Message: fmt.Sprintf("Failed to register terminal for Merchant #%d (serial number might exist)", i+1),
})
return
}
}

if err := tx.Commit(); err != nil {
log.Printf("Error committing tx: %v", err)
jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{
Success: false,
Message: "Failed to finalize batch creation",
})
return
}

jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.APIResponse{
Success: true,
Message: fmt.Sprintf("Successfully onboarded %d merchant(s)", len(reqs)),
})
}
4 changes: 2 additions & 2 deletions backend/internal/admin/admin_dashboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,8 @@ func (h *Handler) AdminDashboardDataHandler(w http.ResponseWriter, r *http.Reque
}
log.Println("Total terminals row:", totalTerminals)

// Fetch all merchants for the table
merchantQuery := "SELECT merchant_id, business_name, business_type, owner_name, business_email, business_phone, status, created_at FROM merchants"
// Fetch recent merchants for the table (limit 5)
merchantQuery := "SELECT merchant_id, business_name, business_type, owner_name, business_email, business_phone, status, created_at FROM merchants ORDER BY created_at DESC LIMIT 5"
rows, err := h.DB.Query(merchantQuery)
if err != nil {
log.Println("Error querying merchants:", err)
Expand Down
Loading