From 5c1f02614c96f2f9a96166e1cc4123e931dcdc62 Mon Sep 17 00:00:00 2001 From: devzeeh <148837352+devzeeh@users.noreply.github.com> Date: Thu, 21 May 2026 18:59:12 +0800 Subject: [PATCH 1/8] feat: implement super admin merchant management system with database onboarding and UI template --- backend/cmd/app/main.go | 1 + backend/internal/admin/super_admin_pages.go | 182 +++++++++++++++++- .../templates/admin/merchant_management.html | 117 ++++++++++- 3 files changed, 295 insertions(+), 5 deletions(-) diff --git a/backend/cmd/app/main.go b/backend/cmd/app/main.go index 5beb96a..0e7f107 100644 --- a/backend/cmd/app/main.go +++ b/backend/cmd/app/main.go @@ -108,6 +108,7 @@ func main() { mux.HandleFunc("GET /admin/merchants", adminHanlder.MerchantManagementView) mux.HandleFunc("GET /admin/terminals", adminHanlder.TerminalRegistryView) mux.HandleFunc("GET /admin/settings", adminHanlder.SystemSettingsView) + mux.HandleFunc("POST /v1/admin/merchants/add", adminHanlder.AddMerchantHandler) mux.HandleFunc("GET /admin/dashboard", adminHanlder.DashboardView) mux.HandleFunc("GET /v1/admin/dashboard-data", adminHanlder.DashboardDataHandler) diff --git a/backend/internal/admin/super_admin_pages.go b/backend/internal/admin/super_admin_pages.go index b570c32..b74a0c6 100644 --- a/backend/internal/admin/super_admin_pages.go +++ b/backend/internal/admin/super_admin_pages.go @@ -1,16 +1,46 @@ 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"` +} + +var Validate = validator.New() + // PlatformOverviewView serves the new Super Admin Platform Overview func (h *Handler) PlatformOverviewView(w http.ResponseWriter, r *http.Request) { err := h.Tpl.ExecuteTemplate(w, "platform_overview.html", nil) if err != nil { log.Printf("Template execution error: %v", err) - http.Error(w, "Internal Server Error", http.StatusInternalServerError) + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ + Success: false, + Message: "Internal Server Error", + }) } } @@ -19,7 +49,10 @@ func (h *Handler) MerchantManagementView(w http.ResponseWriter, r *http.Request) err := h.Tpl.ExecuteTemplate(w, "merchant_management.html", nil) if err != nil { log.Printf("Template execution error: %v", err) - http.Error(w, "Internal Server Error", http.StatusInternalServerError) + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ + Success: false, + Message: "Internal Server Error", + }) } } @@ -28,7 +61,10 @@ func (h *Handler) TerminalRegistryView(w http.ResponseWriter, r *http.Request) { err := h.Tpl.ExecuteTemplate(w, "hardware_registry.html", nil) if err != nil { log.Printf("Template execution error: %v", err) - http.Error(w, "Internal Server Error", http.StatusInternalServerError) + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ + Success: false, + Message: "Internal Server Error", + }) } } @@ -37,6 +73,144 @@ func (h *Handler) SystemSettingsView(w http.ResponseWriter, r *http.Request) { err := h.Tpl.ExecuteTemplate(w, "system_settings.html", nil) if err != nil { log.Printf("Template execution error: %v", err) - http.Error(w, "Internal Server Error", http.StatusInternalServerError) + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ + Success: false, + Message: "Internal Server Error", + }) + } +} + +// AddMerchantHandler creates a new merchant and its corresponding owner user +func (h *Handler) AddMerchantHandler(w http.ResponseWriter, r *http.Request) { + var req AddMerchantRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ + Success: false, + Message: "Invalid JSON payload", + }) + return + } + + err := Validate.Struct(req) + if err != nil { + log.Printf("Validation error: %v", err) + + var validationErrs validator.ValidationErrors + 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", + } + + msg := "Validation failed on field: " + firstErr.Field() + if customMsg, ok := fieldMessages[firstErr.Field()]; ok { + msg = customMsg + } + + jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ + Success: false, + Message: msg, + }) + return + } + + jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ + Success: false, + Message: "Validation error", + }) + 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 { + log.Printf("Error hashing password: %v", err) + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ + Success: false, + Message: "Failed to secure user credentials", + }) + 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 + } + + // Insert User + userStmt := `INSERT INTO users (user_id, username, name, email, phone_number, password_hash, role, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?)` + // Using business email as username for simplicity, you could use a dedicated username field + username := req.BusinessEmail + _, err = tx.Exec(userStmt, userID, username, req.OwnerName, req.BusinessEmail, req.BusinessPhone, string(hashedPassword), "merchant_admin", "active") + if err != nil { + tx.Rollback() + log.Printf("Error creating user: %v", err) + jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ + Success: false, + Message: "Failed to create user account (email or phone might already exist)", + }) + return } + + // Insert Merchant + merchStmt := `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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` + + _, err = tx.Exec(merchStmt, + 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: %v", err) + jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ + Success: false, + Message: "Failed to create merchant profile (registration num or email might exist)", + }) + 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 creation", + }) + return + } + + jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.APIResponse{ + Success: true, + Message: "Merchant onboarded successfully", + }) } diff --git a/frontend/templates/admin/merchant_management.html b/frontend/templates/admin/merchant_management.html index 0a0946b..017a61f 100644 --- a/frontend/templates/admin/merchant_management.html +++ b/frontend/templates/admin/merchant_management.html @@ -118,7 +118,7 @@
View and manage all registered merchants operating on the platform.
-