From 00ff02f789a73474406de2304e0497577089eb36 Mon Sep 17 00:00:00 2001 From: devzeeh <148837352+devzeeh@users.noreply.github.com> Date: Mon, 22 Jun 2026 18:11:35 +0800 Subject: [PATCH 1/4] feat: add database schema for core identity, merchant tenancy, and hardware nodes while scaffolding auth repository and password recovery logic --- backend/internal/auth/dashboard.go | 52 --- backend/internal/auth/forgotPassword.go | 2 +- backend/internal/auth/repository.go | 123 ------ docs/unicard.sql | 553 +++++++----------------- docs/unicardv1.sql | 136 ------ docs/unicardv3.sql | 135 ------ 6 files changed, 147 insertions(+), 854 deletions(-) delete mode 100644 backend/internal/auth/dashboard.go delete mode 100644 docs/unicardv1.sql delete mode 100644 docs/unicardv3.sql diff --git a/backend/internal/auth/dashboard.go b/backend/internal/auth/dashboard.go deleted file mode 100644 index 73f5e93..0000000 --- a/backend/internal/auth/dashboard.go +++ /dev/null @@ -1,52 +0,0 @@ -package authentication - -import ( - "fmt" - "net/http" - "unicard-go/backend/internal/pkg/structs" -) - -func (h *Handler) DashboardView(w http.ResponseWriter, r *http.Request) { - fmt.Println("Dashboard view is running...") - h.Tpl.ExecuteTemplate(w, "dashboard.html", nil) -} - -func (h *Handler) DashboardHandler(w http.ResponseWriter, r *http.Request) { - fmt.Println("Dashboard handler is running...") - - // Select user and card details from database - query := ` - SELECT - u.id, - u.user_id, - u.username, - u.name, - COALESCE(c.balance, 0), - COALESCE(c.loyalty_points, 0), - COALESCE(c.card_type, 'Regular') - FROM users u - LEFT JOIN cards c ON u.user_id = c.user_id - WHERE u.user_id = ? - ` - rows, err := h.DB.Query(query, r.URL.Query().Get("user_id")) - if err != nil { - fmt.Println(err) - return - } - defer rows.Close() - - var user structure.DashboardUser - if rows.Next() { - if err := rows.Scan(&user.ID, &user.UserID, &user.Username, &user.Name, &user.Balance, &user.LoyaltyPoints, &user.AccountType); err != nil { - fmt.Println(err) - return - } - } - - if user.ID == 0 { - fmt.Println("User not found") - return - } - - h.Tpl.ExecuteTemplate(w, "dashboard.html", user) -} \ No newline at end of file diff --git a/backend/internal/auth/forgotPassword.go b/backend/internal/auth/forgotPassword.go index e1d2dc0..239bfd2 100644 --- a/backend/internal/auth/forgotPassword.go +++ b/backend/internal/auth/forgotPassword.go @@ -120,7 +120,7 @@ func (h *Handler) ForgotPasswordSendOTP(w http.ResponseWriter, r *http.Request) // Fetch the user's name var fullName string - err = h.DB.QueryRowContext(ctx, "SELECT full_name FROM users WHERE email = ?", req.Email).Scan(&fullName) + err = h.DB.QueryRowContext(ctx, "SELECT name FROM users WHERE email = ?", req.Email).Scan(&fullName) if err != nil { fullName = "there" // Fallback if name is not found } diff --git a/backend/internal/auth/repository.go b/backend/internal/auth/repository.go index 2245d38..57f4982 100644 --- a/backend/internal/auth/repository.go +++ b/backend/internal/auth/repository.go @@ -1,12 +1,8 @@ package authentication import ( - "crypto/rand" "database/sql" - "fmt" "log" - "math/big" - "time" ) // isUserIDExist checks if a given user ID already exists in the database. @@ -24,104 +20,6 @@ func (h *Handler) isUserIDExist(userID int64) (bool, error) { return true, nil // It exists } -// GenerateUniqueUsername creates a random unique username -// Format: user + 12 random lowercase characters/numbers -// Example: user9d8a7c2b3e4f -func (h *Handler) GenerateUniqueUsername() (string, error) { - const charset = "abcdefghijklmnopqrstuvwxyz0123456789" - usernamePrefix := "user" - length := 7 - - loc, err := time.LoadLocation("Asia/Manila") - if err != nil { - //log.Printf("GenerateUniqueUsername error loading timezone: %v", err) - //return "", err - // Fallback to UTC if timezone loading fails - loc = time.UTC - } - time.Local = loc - - for { - // Get the current date in DDYY format (Go uses "010206" as reference) - userDate := time.Now().In(loc).Format("06") // e.g "06" for 2026, "27" for 2027, etc. - // time.Now().Format("1504") // e.g., "1530" for 3:30 PM - timePart := time.Now().In(loc).Format("0405") // e.g., "1530" for 3:30 PM - - // Combine date and time to form part of the username - //usernamePrefix = fmt.Sprintf("user%s%s", userDate, timePart) - - // Generate the random suffix - randomPart := "" - for i := 0; i < length; i++ { - num, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset)))) - if err != nil { - return "", err - } - randomPart += string(charset[num.Int64()]) - } - - // Combine prefix + date + time + random part - username := fmt.Sprintf("%s%s%s%s", usernamePrefix, userDate, randomPart, timePart) - - // Check DB for uniqueness - var existing string - query := "SELECT username FROM users WHERE username = ?" - err := h.DB.QueryRow(query, username).Scan(&existing) - - if err == sql.ErrNoRows { - //fmt.Println("Generated unique username:", username) - return username, nil // Found a unique one! - } else if err != nil { - return "", err // Real DB Error - } - - // Collision detected, loop runs again... - log.Println("Username collision! Retrying...") - } -} - -// It generates the unique cardID for every card users -// Checks the database for uniqueness. -// Returns the unique card ID as string or an error if any occurs. -// Example format: CARD-XXXXXXX -func (h *Handler) GenerateCardID() (string, error) { - // Define charset: letters and numbers - const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - cardIDPrefix := "CARD-" - randomLength := 7 - - for { - // Get the current date in MMDDYY format (Go uses "010206" as reference) - datePart := time.Now().Format("010206") - - // Generate the 7 random characters - randomPart := "" - for i := 0; i < randomLength; i++ { - num, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset)))) - if err != nil { - return "", err - } - randomPart += string(charset[num.Int64()]) - } - - // Combine them: CARD- + Date + Random - // Example output: CARD-012626Ab7z9X1 - cardID := fmt.Sprintf("%s%s%s", cardIDPrefix, datePart, randomPart) - - // Check database for uniqueness - var tmpCardID string - query := "SELECT card_id FROM users WHERE card_id = ?" - err := h.DB.QueryRow(query, cardID).Scan(&tmpCardID) - - if err == sql.ErrNoRows { - return cardID, nil // Unique ID found - } else if err != nil { - return "", err // DB error - } - log.Printf("Collision detected! Retrying... conflicting ID: %s", cardID) - } -} - // It check the initial balance based on card number prefix // Returns the initial balance as float64 or an error if any occurs. // Gets the initial balance from the "card" table in the database. @@ -158,24 +56,3 @@ func (h *Handler) isPhoneExist(phone string) (bool, error) { } return true, nil } - -// This function checks if a given full name already exists in the database. -// It executes a SQL query to search for the full name in the users table. -// If the full name is found, it returns true. If not found, it returns false. -// If an error occurs during the query, it returns the error. -func (h *Handler) isFullNameExist(fullName string) (bool, error) { - // Hold the existing full name - var existingName string - - // Check query - query := "SELECT name FROM users WHERE name = ?" - err := h.DB.QueryRow(query, fullName).Scan(&existingName) - if err == sql.ErrNoRows { - return false, nil - } - if err != nil { - log.Printf("Full name check error: %v", err) - return false, err - } - return true, nil -} diff --git a/docs/unicard.sql b/docs/unicard.sql index 98de3b5..221349a 100644 --- a/docs/unicard.sql +++ b/docs/unicard.sql @@ -1,407 +1,146 @@ -CREATE DATABASE IF NOT EXISTS `unicard` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci */ /*!80016 DEFAULT ENCRYPTION='N' */; -USE `unicard`; --- MySQL dump 10.13 Distrib 8.0.44, for Win64 (x86_64) --- --- Host: 127.0.0.1 Database: unicard --- ------------------------------------------------------ --- Server version 8.0.44 - -/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; -/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; -/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; -/*!50503 SET NAMES utf8 */; -/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; -/*!40103 SET TIME_ZONE='+00:00' */; -/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; -/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; -/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; -/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; - --- --- Table structure for table `admin_users` --- - -DROP TABLE IF EXISTS `admin_users`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `admin_users` ( - `id` int NOT NULL AUTO_INCREMENT, - `username` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, - `email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, - `full_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, - `password_hash` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'Bcrypt password hash', - `role` enum('Super admin','Admin','Support','Viewer') COLLATE utf8mb4_unicode_ci DEFAULT 'Admin', - `status` enum('Active','Inactive','Suspended') COLLATE utf8mb4_unicode_ci DEFAULT 'Active', - `last_login` timestamp NULL DEFAULT NULL, - `login_attempts` int DEFAULT '0', - `locked_until` timestamp NULL DEFAULT NULL, - `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - PRIMARY KEY (`id`), - UNIQUE KEY `username` (`username`), - UNIQUE KEY `email` (`email`), - KEY `idx_username` (`username`), - KEY `idx_email` (`email`), - KEY `idx_role` (`role`), - KEY `idx_status` (`status`) -) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='System administrator accounts'; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `admin_users` --- - -LOCK TABLES `admin_users` WRITE; -/*!40000 ALTER TABLE `admin_users` DISABLE KEYS */; -INSERT INTO `admin_users` VALUES (1,'devzeeh','roxas.johnerrol@gmail.com','john errol','$2a$12$tjycPwp4svJajA6cAIywK.61wL/236Eht6E/1PgyyG1zQJM3KnWue','Admin','Active',NULL,0,NULL,'2026-05-20 02:01:11','2026-05-20 02:01:11'); -/*!40000 ALTER TABLE `admin_users` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Table structure for table `card_reports` --- - -DROP TABLE IF EXISTS `card_reports`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `card_reports` ( - `id` int NOT NULL AUTO_INCREMENT, - `user_id` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, - `card_number` varchar(16) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'Printed 16-digit card number', - `reason` enum('Lost','Stolen','Fraud','User request','Admin action') COLLATE utf8mb4_unicode_ci NOT NULL, - `description` text COLLATE utf8mb4_unicode_ci, - `reported_by` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'User ID of the person who filed the report (user or admin)', - `status` enum('Reported','Resolved','Replaced') COLLATE utf8mb4_unicode_ci NOT NULL, - `replacement_card_number` varchar(16) COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `blocked_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - `resolved_at` timestamp NULL DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `replacement_card_number` (`replacement_card_number`), - KEY `idx_user_id` (`user_id`), - KEY `idx_card_number` (`card_number`), - KEY `idx_status` (`status`), - CONSTRAINT `card_reports_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`) ON DELETE CASCADE, - CONSTRAINT `card_reports_ibfk_2` FOREIGN KEY (`card_number`) REFERENCES `cards` (`card_number`), - CONSTRAINT `card_reports_ibfk_3` FOREIGN KEY (`replacement_card_number`) REFERENCES `cards` (`card_number`) ON DELETE SET NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Blocked and lost card records'; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `card_reports` --- - -LOCK TABLES `card_reports` WRITE; -/*!40000 ALTER TABLE `card_reports` DISABLE KEYS */; -/*!40000 ALTER TABLE `card_reports` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Table structure for table `cards` --- - -DROP TABLE IF EXISTS `cards`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `cards` ( - `id` int NOT NULL AUTO_INCREMENT, - `user_id` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `card_uid` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'RFID/NFC Tag ID (Internal)', - `card_holder` varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `card_number` varchar(16) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'Printed 16-digit card number', - `card_type` enum('Regular','PWD','Student','Senior') COLLATE utf8mb4_unicode_ci DEFAULT 'Regular', - `expiry_date` date DEFAULT NULL, - `cvv` varchar(3) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'Security code (optional storage)', - `initial_amount` decimal(10,2) DEFAULT '0.00' COMMENT 'Amount to credit user upon registration (Set first by ADMIN)', - `status` enum('Active','Inactive','Blocked','Lost','Expired') COLLATE utf8mb4_unicode_ci DEFAULT 'Inactive', - `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'When card was added to inventory', - `is_primary` tinyint(1) DEFAULT '0' COMMENT 'Is this the primary card for the user?', - `linked_at` timestamp NULL DEFAULT NULL COMMENT 'Card was linked to user', - `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - PRIMARY KEY (`id`), - UNIQUE KEY `card_uid` (`card_uid`), - UNIQUE KEY `card_number` (`card_number`), - KEY `user_id` (`user_id`), - KEY `idx_card_uid` (`card_uid`), - KEY `idx_card_number` (`card_number`), - KEY `idx_status` (`status`), - KEY `idx_card_holder` (`card_holder`), - CONSTRAINT `cards_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`) ON DELETE SET NULL -) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Master inventory of physical cards'; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `cards` --- - -LOCK TABLES `cards` WRITE; -/*!40000 ALTER TABLE `cards` DISABLE KEYS */; -INSERT INTO `cards` VALUES (1,'840570851385','A346F101','errol roxas','2621020251171655','Regular','2036-04-09',NULL,100.00,'Active','2026-02-14 08:50:13',0,NULL,'2026-04-09 09:33:05'),(3,'173886440021','93C13FED','john errol devss','2621028394671655','Regular','2036-02-21',NULL,100.00,'Active','2026-02-21 08:18:02',0,NULL,'2026-04-09 09:40:23'),(4,'615921401740','7310BCEC','john dev','2621020252341655','Regular','2036-02-21',NULL,100.00,'Active','2026-02-21 08:19:52',0,'2026-04-09 10:24:58','2026-04-09 10:24:58'),(5,'573343578549','53A327EC','maja salvador','2621028700527532','Regular','2036-02-21',NULL,100.00,'Active','2026-02-21 08:33:19',0,'2026-04-09 10:35:51','2026-04-09 10:35:51'),(6,'427079423054','F3BF44EC','julia baretto','2621062102879616','Regular','2036-04-09',NULL,100.00,'Active','2026-02-21 08:37:42',0,'2026-04-09 10:40:59','2026-04-09 10:40:59'),(7,'799817330001','038981EC','frances cruz','2621062102292676','Regular','2036-04-28',NULL,100.00,'Active','2026-02-21 08:41:57',0,'2026-04-28 06:05:53','2026-04-28 06:05:53'),(8,'849645895086','2067BE14','ash cue','2621028885926930','Regular','2036-04-29',NULL,100.00,'Active','2026-02-21 08:45:59',0,'2026-04-29 04:17:44','2026-04-29 04:17:44'),(9,'242070109521','50B32514','mhissy acosta','2621027620974570','Regular','2036-04-29',NULL,100.00,'Active','2026-02-21 08:47:26',0,'2026-04-29 04:31:54','2026-04-29 04:31:54'),(10,'100957470319','43006A19','lauren yen','2621020251771655','Regular','2036-05-01',NULL,100.00,'Active','2026-02-21 08:52:17',0,'2026-05-01 03:50:25','2026-05-01 03:50:25'),(11,NULL,'0005584041',NULL,'2601050762341385','Regular','2036-05-01',NULL,100.00,'Inactive','2026-05-01 04:08:10',0,NULL,'2026-05-01 04:08:10'); -/*!40000 ALTER TABLE `cards` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Table structure for table `fare_settings` --- - -DROP TABLE IF EXISTS `fare_settings`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `fare_settings` ( - `id` int NOT NULL AUTO_INCREMENT, - `merchant_code` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, - `business_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, - `base_fare` decimal(10,2) DEFAULT '13.00', - `per_km_rate` decimal(10,2) DEFAULT '1.50', - `minimum_fare` decimal(10,2) DEFAULT '13.00', - `pwd_discount_rate` decimal(5,2) DEFAULT '20.00', - `student_discount_rate` decimal(5,2) DEFAULT '20.00', - `senior_discount_rate` decimal(5,2) DEFAULT '20.00', - `loyalty_rate` decimal(5,4) DEFAULT '0.0020' COMMENT '0.0020 = 0.2% cashback', - `route_number` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'For transport: route or plate number', - `effective_from` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - `effective_to` timestamp NULL DEFAULT NULL, - `is_active` tinyint(1) DEFAULT '1', - `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (`id`), - UNIQUE KEY `merchant_code` (`merchant_code`), - KEY `idx_route_number` (`route_number`), - KEY `idx_merchant_code` (`merchant_code`), - KEY `idx_is_active` (`is_active`), - CONSTRAINT `fare_settings_ibfk_1` FOREIGN KEY (`merchant_code`) REFERENCES `merchants` (`merchant_code`) ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Configurable fare and discount settings'; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `fare_settings` --- - -LOCK TABLES `fare_settings` WRITE; -/*!40000 ALTER TABLE `fare_settings` DISABLE KEYS */; -/*!40000 ALTER TABLE `fare_settings` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Table structure for table `loyalty_redemptions` --- - -DROP TABLE IF EXISTS `loyalty_redemptions`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `loyalty_redemptions` ( - `id` int NOT NULL AUTO_INCREMENT, - `user_id` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, - `transaction_id` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `points_used` decimal(10,2) NOT NULL, - `value_php` decimal(10,2) NOT NULL COMMENT 'PHP value of redeemed points', - `status` enum('Pending','Completed','Cancelled') COLLATE utf8mb4_unicode_ci DEFAULT 'Completed', - `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (`id`), - UNIQUE KEY `transaction_id` (`transaction_id`), - KEY `idx_user_id` (`user_id`), - KEY `idx_created_at` (`created_at`), - KEY `idx_transaction_id` (`transaction_id`), - CONSTRAINT `loyalty_redemptions_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`) ON DELETE CASCADE, - CONSTRAINT `loyalty_redemptions_ibfk_2` FOREIGN KEY (`transaction_id`) REFERENCES `transactions` (`user_id`) ON DELETE SET NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Loyalty points redemption history'; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `loyalty_redemptions` --- - -LOCK TABLES `loyalty_redemptions` WRITE; -/*!40000 ALTER TABLE `loyalty_redemptions` DISABLE KEYS */; -/*!40000 ALTER TABLE `loyalty_redemptions` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Table structure for table `merchants` --- - -DROP TABLE IF EXISTS `merchants`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `merchants` ( - `id` int NOT NULL AUTO_INCREMENT, - `merchant_code` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'Public merchant identifier', - `business_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, - `business_type` enum('Transport','Retail','Both') COLLATE utf8mb4_unicode_ci NOT NULL, - `owner_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, - `email` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `phone` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL, - `address` text COLLATE utf8mb4_unicode_ci, - `route_number` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'For transport: route or plate number', - `store_location` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'For retail: store address', - `commission_rate` decimal(5,2) DEFAULT '0.00' COMMENT 'Commission percentage', - `settlement_account` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'Bank account for settlements', - `status` enum('Active','Suspended','Inactive','Removed') COLLATE utf8mb4_unicode_ci DEFAULT 'Active', - `verified` tinyint(1) DEFAULT '0' COMMENT 'Verified status indicates if the merchant has completed KYC/verification', - `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - PRIMARY KEY (`id`), - UNIQUE KEY `merchant_code` (`merchant_code`), - KEY `idx_merchant_code` (`merchant_code`), - KEY `idx_business_type` (`business_type`), - KEY `idx_status` (`status`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Merchant accounts (drivers and store owners)'; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `merchants` --- - -LOCK TABLES `merchants` WRITE; -/*!40000 ALTER TABLE `merchants` DISABLE KEYS */; -/*!40000 ALTER TABLE `merchants` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Table structure for table `receipts` --- - -DROP TABLE IF EXISTS `receipts`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `receipts` ( - `id` int NOT NULL AUTO_INCREMENT, - `transaction_id` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL, - `user_id` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, - `receipt_number` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, - `receipt_type` enum('Payment','Topup','Refund') COLLATE utf8mb4_unicode_ci NOT NULL, - `email_sent` tinyint(1) DEFAULT '0', - `email_sent_at` timestamp NULL DEFAULT NULL, - `email_opened` tinyint(1) DEFAULT '0', - `pdf_generated` tinyint(1) DEFAULT '0', - `pdf_data` longblob, - `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (`id`), - UNIQUE KEY `receipt_number` (`receipt_number`), - KEY `idx_transaction_id` (`transaction_id`), - KEY `idx_user_id` (`user_id`), - KEY `idx_receipt_number` (`receipt_number`), - CONSTRAINT `receipts_ibfk_1` FOREIGN KEY (`transaction_id`) REFERENCES `transactions` (`user_id`) ON DELETE CASCADE, - CONSTRAINT `receipts_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`) ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='E-receipt generation and tracking'; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `receipts` --- - -LOCK TABLES `receipts` WRITE; -/*!40000 ALTER TABLE `receipts` DISABLE KEYS */; -/*!40000 ALTER TABLE `receipts` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Table structure for table `transactions` --- - -DROP TABLE IF EXISTS `transactions`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `transactions` ( - `id` int NOT NULL AUTO_INCREMENT, - `user_id` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, - `card_number` varchar(16) COLLATE utf8mb4_unicode_ci NOT NULL, - `transaction_type` enum('Topup','Payment','Refund','Adjustment') COLLATE utf8mb4_unicode_ci NOT NULL, - `category` enum('Transport','Retail','Other') COLLATE utf8mb4_unicode_ci NOT NULL, - `amount` decimal(10,2) NOT NULL COMMENT 'Final transaction amount', - `balance_before` decimal(10,2) NOT NULL, - `balance_after` decimal(10,2) NOT NULL, - `discount_amount` decimal(10,2) DEFAULT '0.00' COMMENT 'If amount is 8.00 and discount is 2.00, the original price was 10.00.', - `discount_type` enum('Regular','PWD','Student','Senior','Promo') COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'Reason for discount (PWD, Student, Senior, Promo)', - `points_earned` decimal(10,2) DEFAULT '0.00', - `description` text COLLATE utf8mb4_unicode_ci COMMENT 'Optional notes or item details (e.g., "Monthly Pass")', - `location` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'Route number or store name(Human-readable location (e.g., "Main St. Coffee" or "Bus 42")', - `merchant_code` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'Link to the merchant who received payment (Nullable for system adjustments)', - `terminal_id` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'ID of the physical device/reader used (for debugging/tracking)', - `payment_method` enum('Stripe','E-Wallet','Bank','Cash','Points') COLLATE utf8mb4_unicode_ci NOT NULL, - `reference_id` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'External/Internal REF ID (e.g. Stripe Charge ID, GCash Ref No.)', - `transaction_id` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `status` enum('Pending','Completed','Failed','Refunded') COLLATE utf8mb4_unicode_ci DEFAULT 'Completed', - `failure_reason` text COLLATE utf8mb4_unicode_ci COMMENT 'Error message if status is "Failed" (e.g., "Insufficient funds")', - `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - `completed_at` timestamp NULL DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `idx_user_id` (`user_id`), - KEY `idx_card_number` (`card_number`), - KEY `idx_transaction_type` (`transaction_type`), - KEY `idx_status` (`status`), - KEY `idx_created_at` (`created_at`), - KEY `idx_merchant_code` (`merchant_code`), - KEY `idx_reference_id` (`reference_id`), - KEY `idx_transaction_id` (`transaction_id`), - CONSTRAINT `transactions_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`) ON DELETE CASCADE, - CONSTRAINT `transactions_ibfk_2` FOREIGN KEY (`merchant_code`) REFERENCES `merchants` (`merchant_code`) ON DELETE SET NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='All transaction records (payments, top-ups, refunds)'; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `transactions` --- - -LOCK TABLES `transactions` WRITE; -/*!40000 ALTER TABLE `transactions` DISABLE KEYS */; -/*!40000 ALTER TABLE `transactions` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Table structure for table `users` --- - -DROP TABLE IF EXISTS `users`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `users` ( - `id` int NOT NULL AUTO_INCREMENT, - `user_id` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'Public user ID (e.g., account number)', - `username` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'Login username', - `full_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, - `email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, - `phone` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `password_hash` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'Bcrypt password hash', - `card_id` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'RFID card unique identifier (internal)', - `card_number` varchar(16) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '16-digit printed card number', - `user_type` enum('Regular','PWD','Student','Senior') COLLATE utf8mb4_unicode_ci DEFAULT 'Regular', - `balance` decimal(10,2) DEFAULT '0.00' COMMENT 'Current card balance in PHP', - `loyalty_points` decimal(10,2) DEFAULT '0.00' COMMENT 'Accumulated loyalty points', - `status` enum('Active','Blocked','Inactive') COLLATE utf8mb4_unicode_ci DEFAULT 'Active', - `id_verified` tinyint(1) DEFAULT '0' COMMENT 'Has user uploaded valid ID', - `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - `last_login` timestamp NULL DEFAULT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `user_id` (`user_id`), - UNIQUE KEY `username` (`username`), - UNIQUE KEY `email` (`email`), - UNIQUE KEY `card_id` (`card_id`), - UNIQUE KEY `card_number` (`card_number`), - KEY `idx_user_id` (`user_id`), - KEY `idx_username` (`username`), - KEY `idx_email` (`email`), - KEY `idx_card_number` (`card_number`), - KEY `idx_status` (`status`) -) ENGINE=InnoDB AUTO_INCREMENT=52 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='User accounts and card information'; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `users` --- - -LOCK TABLES `users` WRITE; -/*!40000 ALTER TABLE `users` DISABLE KEYS */; -INSERT INTO `users` VALUES (1,'840570851385','user26fxvolv05729','errol roxas','one.devteam.25@gmail.com','09952329743','$2a$10$qpIlzDQLDucvSdNWZy/R3OMdhonkG2bo75WyHZjz8QnOB4NasmbMa','CARD-021426fgxRf9Q','459543887671','Regular',100.00,0.00,'Active',0,'2026-02-13 20:57:29','2026-02-21 04:33:59',NULL),(43,'173886440021','user26ehrzr3e2121','john errol devss','devzeeh@gmail.com','09123456789','$2a$10$1cDLHp3cfBoPtXYzGjkMquqv/whHWE/qlHLFNKgp6kQfZ/IsG8yJi','CARD-040926nzAdIIc','2621028394671655','Regular',100.00,0.00,'Active',0,'2026-04-08 20:21:21','2026-04-17 07:14:57',NULL),(44,'615921401740','user26ilbbt392458','john dev','jjohnroxas06@gmail.com','09123456788','$2a$10$ETcFIEEED8jmkwaK3OicpueNulxYPCEJToeKZjZWTQsA8lSNWHkR.','CARD-040926mcnG6F7','2621020252341655','Regular',100.00,0.00,'Active',0,'2026-04-08 22:24:58','2026-05-20 01:49:20',NULL),(45,'573343578549','user262evjp0v3551','maja salvador','majasalvador@gmail.com','09123123412','$2a$12$u3ySLle7MOBG/2FeOjS96eyCxITcjD10LvYPDOP9pkuvxLe6JHVx2','CARD-040926EvJOB49','2621028700527532','Regular',100.00,0.00,'Active',0,'2026-04-08 22:35:51','2026-04-14 05:08:25',NULL),(47,'427079423054','user26t5tp7ys4059','julia baretto','juliabaretto@gmail.com','0987654321','$2a$10$SnUcFjvugos89TQFqJqDm.6bKCtcrNOdAwvxdLMnvaMU8nrBiMDf.','CARD-040926VQUXMFZ','2621062102879616','Regular',100.00,0.00,'Active',0,'2026-04-08 22:40:59','2026-04-09 10:40:59',NULL),(48,'799817330001','user266iynhsu0553','frances cruz','francescruz@gmail.com','09987654321','$2a$12$xC4wv64kjhMF8Rt6dswkFOleSaU5olPTameBMh5uDX1rGLV6nHEDi','CARD-042826Z2nMhwE','2621062102292676','Regular',100.00,0.00,'Active',0,'2026-04-27 18:05:53','2026-04-28 06:29:20',NULL),(49,'849645895086','user26xx95elc1744','ash cue','jjohnroxas06+dev@gmail.com','09987252723','$2a$10$Hbu8bQ1iCBuIE8U5LCanOeDlqY8WHhE7bNzN6HIHvtSncBMi5fLoC','CARD-042926N1gZYZs','2621028885926930','Regular',100.00,0.00,'Active',0,'2026-04-29 04:17:44','2026-04-29 04:17:44',NULL),(50,'242070109521','user26i89wuxv3154','mhissy acosta','missacosta@gmail.com','09987526323','$2a$10$BimoLj3vyJqFcpquZ5LcauViuR0ZJwwFa0zEG/176yVNbBSTGy7/G','CARD-042926zX5v3kZ','2621027620974570','Regular',100.00,0.00,'Active',0,'2026-04-29 04:31:54','2026-04-29 04:31:54',NULL),(51,'100957470319','user26fw59znn5025','lauren yen','laurenyen@gmail.com','09765434526','$2a$10$/AOys2.oPFXDPOjNQDcBM.vG1WNAF0k3Ej1YvYerqzbIUy5upa8Pa','CARD-050126FFAanLE','2621020251771655','Regular',100.00,0.00,'Active',0,'2026-05-01 03:50:25','2026-05-01 03:50:25',NULL); -/*!40000 ALTER TABLE `users` ENABLE KEYS */; -UNLOCK TABLES; -/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; - -/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; -/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; -/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; -/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; -/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; -/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; -/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; - --- Dump completed on 2026-05-20 10:05:59 +-- updated sql file for unicard + +CREATE DATABASE IF NOT EXISTS unicard; +USE unicard; + +-- ========================================================================= +-- CORE IDENTITY & AUTHENTICATION TABLES +-- ========================================================================= + +CREATE TABLE users ( + id INT AUTO_INCREMENT PRIMARY KEY COMMENT 'Internal row index optimized for database indexing and fast joins', + user_id VARCHAR(50) NOT NULL UNIQUE COMMENT 'Custom public ID (e.g., UNI-YYMM-minsecxxxx) used in APIs and frontend', + username VARCHAR(50) NOT NULL UNIQUE COMMENT 'Unique handle for admin/staff to log in quickly without an email', + name VARCHAR(100) NOT NULL COMMENT 'Full name of the individual user or client contact person', + email VARCHAR(100) UNIQUE NOT NULL COMMENT 'Primary email address used for consumer logins and notifications', + phone_number VARCHAR(20) NULL UNIQUE COMMENT 'Mobile number (e.g., +639...) for OTPs and SMS transaction alerts', + password_hash VARCHAR(255) NOT NULL COMMENT 'Cryptographically secured password string handled via bcrypt in Go', + role ENUM('super_admin', 'merchant_admin', 'merchant_staff', 'customer') NOT NULL COMMENT 'Defines application-wide role-based access control', + status ENUM('active', 'suspended', 'inactive') DEFAULT 'active' COMMENT 'Account access state for platform security and compliance checks', + pending_email VARCHAR(100) NULL UNIQUE COMMENT 'Temporary email storage for account recovery and verification changes', + email_verification_token VARCHAR(255) NULL UNIQUE COMMENT 'Token for verifying email address updates', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'Auto-generated timestamp of account creation' +) COMMENT='Core identity table tracking authentication and tenancy access control levels'; + + +CREATE TABLE system_settings ( + setting_key VARCHAR(50) PRIMARY KEY COMMENT 'Unique string configuration key acting as the primary look-up token', + setting_value VARCHAR(255) NOT NULL COMMENT 'The active parameter threshold or value parsed directly by the Go backend', + description TEXT NULL COMMENT 'Descriptive documentation notes detailing exactly what system rules or parameters this alters', + updated_by VARCHAR(50) NOT NULL COMMENT 'The public users.user_id of the Super Admin who executed the latest configuration adjustment override', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'Auto-generated clock timestamp tracking when this specific configuration parameter was initialized', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Automatically locks the exact clock timestamp whenever this system parameter value is updated', + FOREIGN KEY (updated_by) REFERENCES users(user_id) +) COMMENT='Global platform configuration matrix driving dynamic fees, operational bounds, and system constants'; + + +-- ========================================================================= +-- MERCHANT TENANCY & HARDWARE REGISTRY TABLES +-- ========================================================================= + +CREATE TABLE merchants ( + id INT AUTO_INCREMENT PRIMARY KEY COMMENT 'Internal merchant row index used for fast database indexing', + merchant_id VARCHAR(50) NOT NULL UNIQUE COMMENT 'Custom public identifier for the business entity (e.g., MCH-2026-001)', + business_name VARCHAR(150) NOT NULL COMMENT 'Registered trade or company name of the client merchant', + business_type ENUM('retail', 'transportation', 'food_and_beverage', 'services', 'other') NOT NULL COMMENT 'Industry category for transaction filtering and analytics', + business_registration_number VARCHAR(100) NULL UNIQUE COMMENT 'Official government tracking number (e.g., DTI, SEC, or BIR TIN)', + business_address TEXT NOT NULL COMMENT 'Physical location of the main store or corporate headquarters', + user_id VARCHAR(50) NOT NULL COMMENT 'Links to the user_id in the users table who owns this business account', + owner_name VARCHAR(100) NOT NULL COMMENT 'Full name of the principal owner or authorized business representative', + business_email VARCHAR(100) NOT NULL UNIQUE COMMENT 'Official company contact email address for corporate updates and billing statements', + business_phone VARCHAR(20) NOT NULL UNIQUE COMMENT 'Official telephone or mobile number for merchant support and emergency updates', + commission_rate DECIMAL(5, 2) DEFAULT 2.00 COMMENT 'Percentage cut taken by UniCard per processed card transaction (e.g., 2.50 = 2.5%)', + settlement_account_name VARCHAR(100) NULL COMMENT 'The name on the merchant bank account or mobile wallet for payouts', + settlement_account_number VARCHAR(50) NULL COMMENT 'The actual bank account number or mobile number (GCash/Maya) for payouts', + settlement_bank_name VARCHAR(100) NULL COMMENT 'The target bank or e-wallet company name (e.g., BDO, BPI, GCash, Maya)', + status ENUM('pending approval', 'approved', 'rejected', 'active', 'suspended') DEFAULT 'pending approval' COMMENT 'Operational state of the merchant ecosystem tenancy', + dti_document VARCHAR(255) NULL COMMENT 'File path for the uploaded DTI registration document', + bir_document VARCHAR(255) NULL COMMENT 'File path for the uploaded BIR registration document', + other_document VARCHAR(255) NULL COMMENT 'File path for any other uploaded business documents', + business_document VARCHAR(255) NULL COMMENT 'Primary business registration document path (DTI or SEC depending on business_structure)', + business_structure ENUM('sole_proprietorship', 'partnership', 'corporation', 'cooperative') NULL COMMENT 'Legal structure of the business entity used to determine required registration documents', + city VARCHAR(100) NULL COMMENT 'City or municipality where the primary business is physically located', + postal_code VARCHAR(20) NULL COMMENT 'Postal or ZIP code of the primary business address location', + document_status ENUM('pending', 'approved', 'rejected') DEFAULT 'pending' COMMENT 'Verification state of the submitted business registration and compliance documents', + message TEXT NULL COMMENT 'Admin-written note or feedback message regarding document review or account status changes', + approved_by VARCHAR(50) NULL COMMENT 'The user_id of the Super Admin who verified and activated this company profile', + approved_at TIMESTAMP NULL COMMENT 'The specific date and timestamp when the business was activated', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'Auto-generated date and time record of the initial registration request', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Automatically updates whenever any merchant profile field is modified', + FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE RESTRICT, + FOREIGN KEY (approved_by) REFERENCES users(user_id) ON DELETE SET NULL +) COMMENT='Enterprise business registry tracking partner tenants, hardware mapping nodes, and financial settlement details'; + + +CREATE TABLE terminals ( + id INT AUTO_INCREMENT PRIMARY KEY COMMENT 'Internal hardware registry auto-increment row index', + terminal_id VARCHAR(50) NOT NULL UNIQUE COMMENT 'Custom public hardware identifier (e.g., TRM-2026-0001) used in API payloads', + terminal_sn VARCHAR(50) UNIQUE NOT NULL COMMENT 'Physical factory-assigned unique serial number or MAC address of the ESP32 board', + merchant_id VARCHAR(50) NULL COMMENT 'Links to the merchant_id of the managing merchant entity', + device_name VARCHAR(100) NOT NULL COMMENT 'Human-readable descriptor identifying placement (e.g., Counter 1, Jeepney Plate # ABC-123)', + location_details VARCHAR(255) NULL COMMENT 'Optional physical sector data, such as a branch route path or stall number designation', + status ENUM('active', 'suspended', 'inactive') DEFAULT 'inactive' COMMENT 'Operational network connectivity state of the edge node hardware', + last_heartbeat TIMESTAMP NULL COMMENT 'Tracks the precise timestamp of the last successful ping packet received from the ESP32 network stack', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'Auto-generated clock timestamp tracking initial edge device registration', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Automatically monitors configuration adjustments or state transitions over time', + -- Fixed: was merchants(user_id), now correctly points to merchants(merchant_id) + FOREIGN KEY (merchant_id) REFERENCES merchants(merchant_id) ON DELETE CASCADE +) COMMENT='Hardware node registry tracking deployed physical authentication nodes and network heartbeat states'; + + +-- ========================================================================= +-- UTILITY & USER TRANSACTION LOGS TABLES (HIGH GROWING DATASETS) +-- ========================================================================= + +CREATE TABLE cards ( + card_number VARCHAR(20) PRIMARY KEY COMMENT 'The visible consumer-facing identifier printed on the physical plastic token', + card_uid VARCHAR(50) NOT NULL UNIQUE COMMENT 'The physical hardware chip unique UID read directly from the MIFARE/RFID sectors', + user_id VARCHAR(50) NULL COMMENT 'Links cardholder account identity via the public users.user_id string identifier', + card_type ENUM('regular', 'student', 'pwd', 'senior') DEFAULT 'regular' COMMENT 'Drives dynamic discount calculation algorithms across transportation fares', + discount_verified BOOLEAN DEFAULT FALSE COMMENT 'Flags whether regulatory documents were verified for fare discount tier eligibility', + balance DECIMAL(10, 2) DEFAULT 0.00 COMMENT 'Current secure stored monetary value assigned to the physical card unit', + loyalty_points DECIMAL(10, 2) DEFAULT 0.00 COMMENT 'Accrued transaction reward points redeemable at verified retail merchant stations', + status ENUM('active', 'inactive', 'blocked', 'lost') DEFAULT 'inactive' COMMENT 'Lifecycle block status constraint to instantly freeze stolen or missing tokens', + expiry_date DATE NOT NULL COMMENT 'Expiration threshold date determining card block lifecycle validations', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'Timestamp tracking when the physical token record was registered in inventory', + linked_at TIMESTAMP NULL DEFAULT NULL COMMENT 'Timestamp tracking the exact moment the card was registered to a specific user', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Automatically updates when balances adjust or card status switches occur', + FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE SET NULL +) COMMENT='Ecosystem transit wallet asset tracker maintaining balances, hardware mapping tokens, and fare tier flags'; + + +CREATE TABLE transactions ( + id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 'Internal financial primary index scaled to 64-bit headroom to comfortably support billions of platform entries', + transaction_id VARCHAR(50) NOT NULL UNIQUE COMMENT 'Custom unique public reference string (e.g., TXN-2026-104294) printed on digital and paper receipts', + card_number VARCHAR(20) NULL COMMENT 'Links target token balance deduction via cards.card_number', + merchant_id VARCHAR(50) NULL COMMENT 'Identifies vendor company collecting the payment token via merchants.merchant_id', + terminal_id VARCHAR(50) NULL COMMENT 'Identifies physical ESP32 or terminal node hardware unit triggering the capture via terminals.terminal_id', + transaction_type ENUM('payment', 'refund', 'reversal', 'topup', 'withdrawal') DEFAULT 'payment' COMMENT 'Categorizes ledger records to process standard deductions or transaction void mappings cleanly', + amount DECIMAL(10, 2) NOT NULL COMMENT 'Total Gross fiat amount captured from the card wallet balance tracking column', + points_earned DECIMAL(10, 2) DEFAULT 0.00 COMMENT 'Total points earned from the transaction', + service_fee DECIMAL(10, 2) DEFAULT 0.00 COMMENT 'Platform revenue slice collected by UniCard ecosystem engine per tap processing action', + net_merchant_payout DECIMAL(10, 2) GENERATED ALWAYS AS (amount - service_fee) STORED COMMENT 'Automatically calculated column tracking exactly how much money goes to the merchant after our platform cut', + processed_by VARCHAR(50) NULL COMMENT 'Public string identifier users.user_id capturing the identity of the physical staff member operating the payment client terminal', + status ENUM('pending', 'completed', 'failed') DEFAULT 'completed' COMMENT 'Lifecycle state of the transaction for tracking settlement', + description VARCHAR(255) NULL COMMENT 'Optional human-readable note or system-generated label describing the transaction context', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'Cryptographic server node timestamp securing exactly when transaction settlement clearing finalized', + FOREIGN KEY (card_number) REFERENCES cards(card_number) +) COMMENT='High-growth financial master ledger capturing all terminal token taps, transaction classifications, and system fees'; + +CREATE TABLE top_ups ( + id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 'Internal tracking row index scaled to 64-bit headroom to safely accommodate massive historical logging growth', + topup_id VARCHAR(50) NOT NULL UNIQUE COMMENT 'Custom unique public transaction code (e.g., LD-2026-987153) used for user receipts and payment gateway queries', + card_number VARCHAR(20) NOT NULL COMMENT 'Links target token balance injection via cards.card_number', + amount DECIMAL(10, 2) NOT NULL COMMENT 'Gross load amount requested by the customer before convenience charges are applied', + convenience_fee DECIMAL(10, 2) DEFAULT 0.00 COMMENT 'Ecosystem engine collection fee applied to over-the-air channels like GCash or Maya webhooks', + gateway_cost DECIMAL(10, 2) DEFAULT 0.00 COMMENT 'Actual fee incurred from external payment providers (GCash, Maya, Bank) per transaction', + net_gateway_fee DECIMAL(10, 2) GENERATED ALWAYS AS (convenience_fee - gateway_cost) STORED COMMENT 'Automatically calculated net revenue kept by the platform after 3rd party costs', + total_charged DECIMAL(10, 2) GENERATED ALWAYS AS (amount + convenience_fee) STORED COMMENT 'Automatically calculated column representing the absolute total cash value collected from the external channel source', + payment_method ENUM('cash', 'gcash', 'maya', 'over_the_counter', 'xendit') NOT NULL COMMENT 'Drives system tracking to audit cash-drawer liquid positions against programmatic API callbacks', + handled_by VARCHAR(50) NULL COMMENT 'Public user_id string identifier referencing the administrative staff member who manually accepted physical bills if OTC cash-loaded', + external_id VARCHAR(255) NULL UNIQUE COMMENT 'External reference ID from payment gateways like Xendit to prevent double processing', + status ENUM('pending', 'completed', 'failed') DEFAULT 'completed' COMMENT 'State pipeline tracker handling payment gateway processing exceptions or drops', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'Auto-generated clock timestamp mapping exactly when wallet balance credits were finalized', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Tracks chronological life cycle changes, such as a top-up shifting from pending to completed', + FOREIGN KEY (card_number) REFERENCES cards(card_number) +) COMMENT='High-growth balance loader ledger maintaining immutable compliance auditing for all incoming ecosystem liquidity channels'; \ No newline at end of file diff --git a/docs/unicardv1.sql b/docs/unicardv1.sql deleted file mode 100644 index 2c8c5db..0000000 --- a/docs/unicardv1.sql +++ /dev/null @@ -1,136 +0,0 @@ --- unicardv2 is deprecated. Use unicardv3. - --- CREATE DATABASE IF NOT EXISTS unicardv1; --- USE unicardv2; - --- ========================================================================= --- 1. CORE IDENTITY & AUTHENTICATION TABLES --- ========================================================================= - -CREATE TABLE users ( - id INT AUTO_INCREMENT PRIMARY KEY COMMENT 'Internal row index optimized for database indexing and fast joins', - user_id VARCHAR(50) NOT NULL UNIQUE COMMENT 'Custom public ID (e.g., UNI-YYMM-minsecxxxx) used in APIs and frontend', - username VARCHAR(50) NOT NULL UNIQUE COMMENT 'Unique handle for admin/staff to log in quickly without an email', - name VARCHAR(100) NOT NULL COMMENT 'Full name of the individual user or client contact person', - email VARCHAR(100) UNIQUE NOT NULL COMMENT 'Primary email address used for consumer logins and notifications', - phone_number VARCHAR(20) NULL UNIQUE COMMENT 'Mobile number (e.g., +639...) for OTPs and SMS transaction alerts', - password_hash VARCHAR(255) NOT NULL COMMENT 'Cryptographically secured password string handled via bcrypt in Go', - role ENUM('super_admin', 'merchant_admin', 'merchant_staff', 'customer') NOT NULL COMMENT 'Defines application-wide role-based access control', - status ENUM('active', 'suspended', 'inactive') DEFAULT 'active' COMMENT 'Account access state for platform security and compliance checks', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'Auto-generated timestamp of account creation' -) COMMENT='Core identity table tracking authentication and tenancy access control levels'; - -CREATE TABLE system_settings ( - setting_key VARCHAR(50) PRIMARY KEY COMMENT 'Unique string configuration key acting as the primary look-up token', - setting_value VARCHAR(255) NOT NULL COMMENT 'The active parameter threshold or value parsed directly by the Go backend', - description TEXT NULL COMMENT 'Descriptive documentation notes detailing exactly what system rules or parameters this alters', - updated_by VARCHAR(50) NOT NULL COMMENT 'The public users.user_id of the Super Admin who executed the latest configuration adjustment override', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'Auto-generated clock timestamp tracking when this specific configuration parameter was initialized', - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Automatically locks the exact clock timestamp whenever this system parameter value is updated', - FOREIGN KEY (updated_by) REFERENCES users(user_id) -) COMMENT='Global platform configuration matrix driving dynamic fees, operational bounds, and system constants'; - --- ========================================================================= --- 2. MERCHANT TENANCY & HARDWARE REGISTRY TABLES --- ========================================================================= - -CREATE TABLE merchants ( - id INT AUTO_INCREMENT PRIMARY KEY COMMENT 'Internal merchant row index used for fast database indexing', - merchant_id VARCHAR(50) NOT NULL UNIQUE COMMENT 'Custom public identifier for the business entity (e.g., MCH-2026-001)', - business_name VARCHAR(150) NOT NULL COMMENT 'Registered trade or company name of the client merchant', - business_type ENUM('retail', 'transportation', 'food_and_beverage', 'services', 'other') NOT NULL COMMENT 'Industry category for transaction filtering and analytics', - business_registration_number VARCHAR(100) NULL UNIQUE COMMENT 'Official government tracking number (e.g., DTI, SEC, or BIR TIN)', - business_address TEXT NOT NULL COMMENT 'Physical location of the main store or corporate headquarters', - - user_id VARCHAR(50) NOT NULL COMMENT 'Links to the user_id in the users table who owns this business account', - owner_name VARCHAR(100) NOT NULL COMMENT 'Full name of the principal owner or authorized business representative', - business_email VARCHAR(100) NOT NULL UNIQUE COMMENT 'Official company contact email address for corporate updates and billing statements', - business_phone VARCHAR(20) NOT NULL UNIQUE COMMENT 'Official telephone or mobile number for merchant support and emergency updates', - - commission_rate DECIMAL(5, 2) DEFAULT 2.00 COMMENT 'Percentage cut taken by UniCard per processed card transaction (e.g., 2.50 = 2.5%)', - settlement_account_name VARCHAR(100) NULL COMMENT 'The name on the merchant bank account or mobile wallet for payouts', - settlement_account_number VARCHAR(50) NULL COMMENT 'The actual bank account number or mobile number (GCash/Maya) for payouts', - settlement_bank_name VARCHAR(100) NULL COMMENT 'The target bank or e-wallet company name (e.g., BDO, BPI, GCash, Maya)', - - status ENUM('pending approval', 'approved', 'rejected', 'active', 'suspended') DEFAULT 'pending approval' COMMENT 'Operational state of the merchant ecosystem tenancy', - dti_document VARCHAR(255) NULL COMMENT 'File path for the uploaded DTI registration document', - bir_document VARCHAR(255) NULL COMMENT 'File path for the uploaded BIR registration document', - other_document VARCHAR(255) NULL COMMENT 'File path for any other uploaded business documents', - approved_by VARCHAR(50) NULL COMMENT 'The user_id of the Super Admin who verified and activated this company profile', - approved_at TIMESTAMP NULL COMMENT 'The specific date and timestamp when the business was activated', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'Auto-generated date and time record of the initial registration request', - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Automatically updates whenever any merchant profile field is modified', - - FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE RESTRICT, - FOREIGN KEY (approved_by) REFERENCES users(user_id) ON DELETE SET NULL -) COMMENT='Enterprise business registry tracking partner tenants, hardware mapping nodes, and financial settlement details'; - -CREATE TABLE terminals ( - id INT AUTO_INCREMENT PRIMARY KEY COMMENT 'Internal hardware registry auto-increment row index', - terminal_id VARCHAR(50) NOT NULL UNIQUE COMMENT 'Custom public hardware identifier (e.g., TRM-2026-0001) used in API payloads', - terminal_sn VARCHAR(50) UNIQUE NOT NULL COMMENT 'Physical factory-assigned unique serial number or MAC address of the ESP32 board', - merchant_id varchar(50) NULL COMMENT 'Links to the internal auto-increment id of the managing merchant entity', - device_name VARCHAR(100) NOT NULL COMMENT 'Human-readable descriptor identifying placement (e.g., Counter 1, Jeepney Plate # ABC-123)', - location_details VARCHAR(255) NULL COMMENT 'Optional physical sector data, such as a branch route path or stall number designation', - status ENUM('active', 'suspended', 'inactive') DEFAULT 'inactive' COMMENT 'Operational network connectivity state of the edge node hardware', - last_heartbeat TIMESTAMP NULL COMMENT 'Tracks the precise timestamp of the last successful ping packet received from the ESP32 network stack', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'Auto-generated clock timestamp tracking initial edge device registration', - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Automatically monitors configuration adjustments or state transitions over time', - FOREIGN KEY (merchant_id) REFERENCES merchants(user_id) ON DELETE CASCADE -) COMMENT='Hardware node registry tracking deployed physical authentication nodes and network heartbeat states'; - --- ========================================================================= --- 3. UTILITY & USER TRANSACTION LOGS TABLES (HIGH GROWING DATASETS) --- ========================================================================= - -CREATE TABLE cards ( - card_number VARCHAR(20) PRIMARY KEY COMMENT 'The visible consumer-facing identifier printed on the physical plastic token', - card_uid VARCHAR(50) NOT NULL UNIQUE COMMENT 'The physical hardware chip unique UID read directly from the MIFARE/RFID sectors', - user_id VARCHAR(50) NULL COMMENT 'Links cardholder account identity via the public users.user_id string identifier', - card_type ENUM('regular', 'student', 'pwd', 'senior') DEFAULT 'regular' COMMENT 'Drives dynamic discount calculation algorithms across transportation fares', - discount_verified BOOLEAN DEFAULT FALSE COMMENT 'Flags whether regulatory documents were verified for fare discount tier eligibility', - balance DECIMAL(10, 2) DEFAULT 0.00 COMMENT 'Current secure stored monetary value assigned to the physical card unit', - loyalty_points DECIMAL(10, 2) DEFAULT 0.00 COMMENT 'Accrued transaction reward points redeemable at verified retail merchant stations', - status ENUM('active', 'inactive', 'blocked', 'lost') DEFAULT 'inactive' COMMENT 'Lifecycle block status constraint to instantly freeze stolen or missing tokens', - expiry_date DATE NOT NULL COMMENT 'Expiration threshold date determining card block lifecycle validations', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'Timestamp tracking when the physical token record was registered in inventory', - linked_at TIMESTAMP NULL DEFAULT NULL COMMENT 'Timestamp tracking the exact moment the card was registered to a specific user', - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Automatically updates when balances adjust or card status switches occur', - FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE SET NULL -) COMMENT='Ecosystem transit wallet asset tracker maintaining balances, hardware mapping tokens, and fare tier flags'; - -CREATE TABLE transactions ( - id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 'Internal financial primary index scaled to 64-bit headroom to comfortably support billions of platform entries', - transaction_id VARCHAR(50) NOT NULL UNIQUE COMMENT 'Custom unique public reference string (e.g., TXN-2026-104294) printed on digital and paper receipts', - card_number VARCHAR(20) NOT NULL COMMENT 'Links target token balance deduction via cards.card_number', - merchant_id varchar(50) NOT NULL COMMENT 'Identifies vendor company collecting the payment token via merchants.id', - terminal_id varchar(50) NOT NULL COMMENT 'Identifies physical ESP32 or terminal node hardware unit triggering the capture via terminals.id', - transaction_type ENUM('payment', 'refund', 'reversal', 'topup') DEFAULT 'payment' COMMENT 'Categorizes ledger records to process standard deductions or transaction void mappings cleanly', - amount DECIMAL(10, 2) NOT NULL COMMENT 'Total Gross fiat amount captured from the card wallet balance tracking column', - service_fee DECIMAL(10, 2) DEFAULT 0.00 COMMENT 'Platform revenue slice collected by UniCard ecosystem engine per tap processing action', - net_merchant_payout DECIMAL(10, 2) GENERATED ALWAYS AS (amount - service_fee) STORED COMMENT 'Automatically calculated column tracking exactly how much money goes to the merchant after our platform cut', - processed_by VARCHAR(50) NOT NULL COMMENT 'Public string identifier users.user_id capturing the identity of the physical staff member operating the payment client terminal', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'Cryptographic server node timestamp securing exactly when transaction settlement clearing finalized', - FOREIGN KEY (card_number) REFERENCES cards(card_number), - FOREIGN KEY (merchant_id) REFERENCES merchants(user_id), - FOREIGN KEY (terminal_id) REFERENCES terminals(terminal_id), - FOREIGN KEY (processed_by) REFERENCES users(user_id) -) COMMENT='High-growth financial master ledger capturing all terminal token taps, transaction classifications, and system fees'; - -CREATE TABLE top_ups ( - id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 'Internal tracking row index scaled to 64-bit headroom to safely accommodate massive historical logging growth', - topup_id VARCHAR(50) NOT NULL UNIQUE COMMENT 'Custom unique public transaction code (e.g., LD-2026-987153) used for user receipts and payment gateway queries', - card_number VARCHAR(20) NOT NULL COMMENT 'Links target token balance injection via cards.card_number', - amount DECIMAL(10, 2) NOT NULL COMMENT 'Gross load amount requested by the customer before convenience charges are applied', - convenience_fee DECIMAL(10, 2) DEFAULT 0.00 COMMENT 'Ecosystem engine collection fee applied to over-the-air channels like GCash or Maya webhooks', - gateway_cost DECIMAL(10, 2) DEFAULT 0.00 COMMENT 'Actual fee incurred from external payment providers (GCash, Maya, Bank) per transaction', - net_gateway_fee DECIMAL(10, 2) GENERATED ALWAYS AS (convenience_fee - gateway_cost) STORED COMMENT 'Automatically calculated net revenue kept by the platform after 3rd party costs', - total_charged DECIMAL(10, 2) GENERATED ALWAYS AS (amount + convenience_fee) STORED COMMENT 'Automatically calculated column representing the absolute total cash value collected from the external channel source', - payment_method ENUM('cash', 'gcash', 'maya', 'over_the_counter', 'stripe') NOT NULL COMMENT 'Drives system tracking to audit cash-drawer liquid positions against programmatic API callbacks', - handled_by VARCHAR(50) NULL COMMENT 'Public user_id string identifier referencing the administrative staff member who manually accepted physical bills if OTC cash-loaded', - status ENUM('pending', 'completed', 'failed') DEFAULT 'completed' COMMENT 'State pipeline tracker handling payment gateway processing exceptions or drops', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'Auto-generated clock timestamp mapping exactly when wallet balance credits were finalized', - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Tracks chronological life cycle changes, such as a top-up shifting from pending to completed', - FOREIGN KEY (card_number) REFERENCES cards(card_number), - FOREIGN KEY (handled_by) REFERENCES users(user_id) -) COMMENT='High-growth balance loader ledger maintaining immutable compliance auditing for all incoming ecosystem liquidity channels'; \ No newline at end of file diff --git a/docs/unicardv3.sql b/docs/unicardv3.sql deleted file mode 100644 index 376ca7c..0000000 --- a/docs/unicardv3.sql +++ /dev/null @@ -1,135 +0,0 @@ -CREATE DATABASE IF NOT EXISTS unicardv3; -USE unicardv3; - --- ========================================================================= --- CORE IDENTITY & AUTHENTICATION TABLES --- ========================================================================= - -CREATE TABLE users ( - id INT AUTO_INCREMENT PRIMARY KEY COMMENT 'Internal row index optimized for database indexing and fast joins', - user_id VARCHAR(50) NOT NULL UNIQUE COMMENT 'Custom public ID (e.g., UNI-YYMM-minsecxxxx) used in APIs and frontend', - username VARCHAR(50) NOT NULL UNIQUE COMMENT 'Unique handle for admin/staff to log in quickly without an email', - name VARCHAR(100) NOT NULL COMMENT 'Full name of the individual user or client contact person', - email VARCHAR(100) UNIQUE NOT NULL COMMENT 'Primary email address used for consumer logins and notifications', - phone_number VARCHAR(20) NULL UNIQUE COMMENT 'Mobile number (e.g., +639...) for OTPs and SMS transaction alerts', - password_hash VARCHAR(255) NOT NULL COMMENT 'Cryptographically secured password string handled via bcrypt in Go', - role ENUM('super_admin', 'merchant_admin', 'merchant_staff', 'customer') NOT NULL COMMENT 'Defines application-wide role-based access control', - status ENUM('active', 'suspended', 'inactive') DEFAULT 'active' COMMENT 'Account access state for platform security and compliance checks', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'Auto-generated timestamp of account creation' -) COMMENT='Core identity table tracking authentication and tenancy access control levels'; - - -CREATE TABLE system_settings ( - setting_key VARCHAR(50) PRIMARY KEY COMMENT 'Unique string configuration key acting as the primary look-up token', - setting_value VARCHAR(255) NOT NULL COMMENT 'The active parameter threshold or value parsed directly by the Go backend', - description TEXT NULL COMMENT 'Descriptive documentation notes detailing exactly what system rules or parameters this alters', - updated_by VARCHAR(50) NOT NULL COMMENT 'The public users.user_id of the Super Admin who executed the latest configuration adjustment override', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'Auto-generated clock timestamp tracking when this specific configuration parameter was initialized', - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Automatically locks the exact clock timestamp whenever this system parameter value is updated', - FOREIGN KEY (updated_by) REFERENCES users(user_id) -) COMMENT='Global platform configuration matrix driving dynamic fees, operational bounds, and system constants'; - - --- ========================================================================= --- MERCHANT TENANCY & HARDWARE REGISTRY TABLES --- ========================================================================= - -CREATE TABLE merchants ( - id INT AUTO_INCREMENT PRIMARY KEY COMMENT 'Internal merchant row index used for fast database indexing', - merchant_id VARCHAR(50) NOT NULL UNIQUE COMMENT 'Custom public identifier for the business entity (e.g., MCH-2026-001)', - business_name VARCHAR(150) NOT NULL COMMENT 'Registered trade or company name of the client merchant', - business_type ENUM('retail', 'transportation', 'food_and_beverage', 'services', 'other') NOT NULL COMMENT 'Industry category for transaction filtering and analytics', - business_registration_number VARCHAR(100) NULL UNIQUE COMMENT 'Official government tracking number (e.g., DTI, SEC, or BIR TIN)', - business_address TEXT NOT NULL COMMENT 'Physical location of the main store or corporate headquarters', - user_id VARCHAR(50) NOT NULL COMMENT 'Links to the user_id in the users table who owns this business account', - owner_name VARCHAR(100) NOT NULL COMMENT 'Full name of the principal owner or authorized business representative', - business_email VARCHAR(100) NOT NULL UNIQUE COMMENT 'Official company contact email address for corporate updates and billing statements', - business_phone VARCHAR(20) NOT NULL UNIQUE COMMENT 'Official telephone or mobile number for merchant support and emergency updates', - commission_rate DECIMAL(5, 2) DEFAULT 2.00 COMMENT 'Percentage cut taken by UniCard per processed card transaction (e.g., 2.50 = 2.5%)', - settlement_account_name VARCHAR(100) NULL COMMENT 'The name on the merchant bank account or mobile wallet for payouts', - settlement_account_number VARCHAR(50) NULL COMMENT 'The actual bank account number or mobile number (GCash/Maya) for payouts', - settlement_bank_name VARCHAR(100) NULL COMMENT 'The target bank or e-wallet company name (e.g., BDO, BPI, GCash, Maya)', - status ENUM('pending approval', 'approved', 'rejected', 'active', 'suspended') DEFAULT 'pending approval' COMMENT 'Operational state of the merchant ecosystem tenancy', - dti_document VARCHAR(255) NULL COMMENT 'File path for the uploaded DTI registration document', - bir_document VARCHAR(255) NULL COMMENT 'File path for the uploaded BIR registration document', - other_document VARCHAR(255) NULL COMMENT 'File path for any other uploaded business documents', - approved_by VARCHAR(50) NULL COMMENT 'The user_id of the Super Admin who verified and activated this company profile', - approved_at TIMESTAMP NULL COMMENT 'The specific date and timestamp when the business was activated', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'Auto-generated date and time record of the initial registration request', - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Automatically updates whenever any merchant profile field is modified', - FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE RESTRICT, - FOREIGN KEY (approved_by) REFERENCES users(user_id) ON DELETE SET NULL -) COMMENT='Enterprise business registry tracking partner tenants, hardware mapping nodes, and financial settlement details'; - - -CREATE TABLE terminals ( - id INT AUTO_INCREMENT PRIMARY KEY COMMENT 'Internal hardware registry auto-increment row index', - terminal_id VARCHAR(50) NOT NULL UNIQUE COMMENT 'Custom public hardware identifier (e.g., TRM-2026-0001) used in API payloads', - terminal_sn VARCHAR(50) UNIQUE NOT NULL COMMENT 'Physical factory-assigned unique serial number or MAC address of the ESP32 board', - merchant_id VARCHAR(50) NULL COMMENT 'Links to the merchant_id of the managing merchant entity', - device_name VARCHAR(100) NOT NULL COMMENT 'Human-readable descriptor identifying placement (e.g., Counter 1, Jeepney Plate # ABC-123)', - location_details VARCHAR(255) NULL COMMENT 'Optional physical sector data, such as a branch route path or stall number designation', - status ENUM('active', 'suspended', 'inactive') DEFAULT 'inactive' COMMENT 'Operational network connectivity state of the edge node hardware', - last_heartbeat TIMESTAMP NULL COMMENT 'Tracks the precise timestamp of the last successful ping packet received from the ESP32 network stack', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'Auto-generated clock timestamp tracking initial edge device registration', - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Automatically monitors configuration adjustments or state transitions over time', - -- Fixed: was merchants(user_id), now correctly points to merchants(merchant_id) - FOREIGN KEY (merchant_id) REFERENCES merchants(merchant_id) ON DELETE CASCADE -) COMMENT='Hardware node registry tracking deployed physical authentication nodes and network heartbeat states'; - - --- ========================================================================= --- UTILITY & USER TRANSACTION LOGS TABLES (HIGH GROWING DATASETS) --- ========================================================================= - -CREATE TABLE cards ( - card_number VARCHAR(20) PRIMARY KEY COMMENT 'The visible consumer-facing identifier printed on the physical plastic token', - card_uid VARCHAR(50) NOT NULL UNIQUE COMMENT 'The physical hardware chip unique UID read directly from the MIFARE/RFID sectors', - user_id VARCHAR(50) NULL COMMENT 'Links cardholder account identity via the public users.user_id string identifier', - card_type ENUM('regular', 'student', 'pwd', 'senior') DEFAULT 'regular' COMMENT 'Drives dynamic discount calculation algorithms across transportation fares', - discount_verified BOOLEAN DEFAULT FALSE COMMENT 'Flags whether regulatory documents were verified for fare discount tier eligibility', - balance DECIMAL(10, 2) DEFAULT 0.00 COMMENT 'Current secure stored monetary value assigned to the physical card unit', - loyalty_points DECIMAL(10, 2) DEFAULT 0.00 COMMENT 'Accrued transaction reward points redeemable at verified retail merchant stations', - status ENUM('active', 'inactive', 'blocked', 'lost') DEFAULT 'inactive' COMMENT 'Lifecycle block status constraint to instantly freeze stolen or missing tokens', - expiry_date DATE NOT NULL COMMENT 'Expiration threshold date determining card block lifecycle validations', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'Timestamp tracking when the physical token record was registered in inventory', - linked_at TIMESTAMP NULL DEFAULT NULL COMMENT 'Timestamp tracking the exact moment the card was registered to a specific user', - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Automatically updates when balances adjust or card status switches occur', - FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE SET NULL -) COMMENT='Ecosystem transit wallet asset tracker maintaining balances, hardware mapping tokens, and fare tier flags'; - - -CREATE TABLE transactions ( - id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 'Internal financial primary index scaled to 64-bit headroom to comfortably support billions of platform entries', - transaction_id VARCHAR(50) NOT NULL UNIQUE COMMENT 'Custom unique public reference string (e.g., TXN-2026-104294) printed on digital and paper receipts', - card_number VARCHAR(20) NULL COMMENT 'Links target token balance deduction via cards.card_number', - merchant_id VARCHAR(50) NULL COMMENT 'Identifies vendor company collecting the payment token via merchants.merchant_id', - terminal_id VARCHAR(50) NULL COMMENT 'Identifies physical ESP32 or terminal node hardware unit triggering the capture via terminals.terminal_id', - transaction_type ENUM('payment', 'refund', 'reversal', 'topup', 'withdrawal') DEFAULT 'payment' COMMENT 'Categorizes ledger records to process standard deductions or transaction void mappings cleanly', - amount DECIMAL(10, 2) NOT NULL COMMENT 'Total Gross fiat amount captured from the card wallet balance tracking column', - points_earned DECIMAL(10, 2) DEFAULT 0.00 COMMENT 'Total points earned from the transaction', - service_fee DECIMAL(10, 2) DEFAULT 0.00 COMMENT 'Platform revenue slice collected by UniCard ecosystem engine per tap processing action', - net_merchant_payout DECIMAL(10, 2) GENERATED ALWAYS AS (amount - service_fee) STORED COMMENT 'Automatically calculated column tracking exactly how much money goes to the merchant after our platform cut', - processed_by VARCHAR(50) NULL COMMENT 'Public string identifier users.user_id capturing the identity of the physical staff member operating the payment client terminal', - status ENUM('pending', 'completed', 'failed') DEFAULT 'completed' COMMENT 'Lifecycle state of the transaction for tracking settlement', - description VARCHAR(255) NULL COMMENT 'Optional human-readable note or system-generated label describing the transaction context', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'Cryptographic server node timestamp securing exactly when transaction settlement clearing finalized', - FOREIGN KEY (card_number) REFERENCES cards(card_number) -) COMMENT='High-growth financial master ledger capturing all terminal token taps, transaction classifications, and system fees'; - -CREATE TABLE top_ups ( - id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 'Internal tracking row index scaled to 64-bit headroom to safely accommodate massive historical logging growth', - topup_id VARCHAR(50) NOT NULL UNIQUE COMMENT 'Custom unique public transaction code (e.g., LD-2026-987153) used for user receipts and payment gateway queries', - card_number VARCHAR(20) NOT NULL COMMENT 'Links target token balance injection via cards.card_number', - amount DECIMAL(10, 2) NOT NULL COMMENT 'Gross load amount requested by the customer before convenience charges are applied', - convenience_fee DECIMAL(10, 2) DEFAULT 0.00 COMMENT 'Ecosystem engine collection fee applied to over-the-air channels like GCash or Maya webhooks', - gateway_cost DECIMAL(10, 2) DEFAULT 0.00 COMMENT 'Actual fee incurred from external payment providers (GCash, Maya, Bank) per transaction', - net_gateway_fee DECIMAL(10, 2) GENERATED ALWAYS AS (convenience_fee - gateway_cost) STORED COMMENT 'Automatically calculated net revenue kept by the platform after 3rd party costs', - total_charged DECIMAL(10, 2) GENERATED ALWAYS AS (amount + convenience_fee) STORED COMMENT 'Automatically calculated column representing the absolute total cash value collected from the external channel source', - payment_method ENUM('cash', 'gcash', 'maya', 'over_the_counter', 'xendit') NOT NULL COMMENT 'Drives system tracking to audit cash-drawer liquid positions against programmatic API callbacks', - handled_by VARCHAR(50) NULL COMMENT 'Public user_id string identifier referencing the administrative staff member who manually accepted physical bills if OTC cash-loaded', - status ENUM('pending', 'completed', 'failed') DEFAULT 'completed' COMMENT 'State pipeline tracker handling payment gateway processing exceptions or drops', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'Auto-generated clock timestamp mapping exactly when wallet balance credits were finalized', - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Tracks chronological life cycle changes, such as a top-up shifting from pending to completed', - FOREIGN KEY (card_number) REFERENCES cards(card_number) -) COMMENT='High-growth balance loader ledger maintaining immutable compliance auditing for all incoming ecosystem liquidity channels'; \ No newline at end of file From ea02c3564979f8d45caa8717de3fdd092810b5ae Mon Sep 17 00:00:00 2001 From: devzeeh <148837352+devzeeh@users.noreply.github.com> Date: Mon, 22 Jun 2026 18:44:00 +0800 Subject: [PATCH 2/4] feat: implement merchant account and dashboard API endpoints with frontend JS integration --- backend/internal/merchant/account.go | 16 +- backend/internal/merchant/dashboard.go | 10 +- backend/internal/merchant/incomes.go | 8 +- backend/internal/merchant/transactions.go | 10 +- frontend/assets/js/merchant_account.js | 118 +++++++ frontend/assets/js/merchant_transactions.js | 352 ++++++++++++++++++++ 6 files changed, 492 insertions(+), 22 deletions(-) create mode 100644 frontend/assets/js/merchant_account.js create mode 100644 frontend/assets/js/merchant_transactions.js diff --git a/backend/internal/merchant/account.go b/backend/internal/merchant/account.go index e47c936..58ac0b4 100644 --- a/backend/internal/merchant/account.go +++ b/backend/internal/merchant/account.go @@ -79,16 +79,16 @@ func (h *Handler) MerchantAccountDataHandler(w http.ResponseWriter, r *http.Requ err := h.DB.QueryRowContext(ctx, ` SELECT m.merchant_id, - m.status, - DATE_FORMAT(m.created_at, '%M %d, %Y') as created_at, + COALESCE(m.status, ''), + COALESCE(DATE_FORMAT(m.created_at, '%M %d, %Y'), '') as created_at, -- Business Info - m.business_name, - m.business_type, + COALESCE(m.business_name, ''), + COALESCE(m.business_type, ''), COALESCE(m.business_structure, ''), - m.business_email, - m.business_phone, - m.business_address, + COALESCE(m.business_email, ''), + COALESCE(m.business_phone, ''), + COALESCE(m.business_address, ''), -- Location Info COALESCE(m.city, ''), @@ -101,7 +101,7 @@ func (h *Handler) MerchantAccountDataHandler(w http.ResponseWriter, r *http.Requ -- Document Info COALESCE(m.business_structure, ''), - COALESCE(m.business_document), + COALESCE(m.business_document, ''), COALESCE(m.bir_document, ''), COALESCE(m.document_status, 'pending'), COALESCE(m.message, '') diff --git a/backend/internal/merchant/dashboard.go b/backend/internal/merchant/dashboard.go index dad0a67..a54a4c3 100644 --- a/backend/internal/merchant/dashboard.go +++ b/backend/internal/merchant/dashboard.go @@ -86,10 +86,10 @@ func (h *Handler) GetMerchantRecentTransactions(ctx context.Context, merchantID SELECT transaction_id, COALESCE(card_number, ''), merchant_id, terminal_id, - transaction_type, amount, - points_earned, service_fee, - net_merchant_payout, processed_by, - status, description, created_at + COALESCE(transaction_type, ''), amount, + COALESCE(points_earned, 0), COALESCE(service_fee, 0), + COALESCE(net_merchant_payout, 0), processed_by, + COALESCE(status, ''), description, COALESCE(created_at, '') FROM transactions WHERE merchant_id = ? ORDER BY created_at DESC @@ -101,7 +101,7 @@ func (h *Handler) GetMerchantRecentTransactions(ctx context.Context, merchantID } defer rows.Close() - var transactions []MerchantTransaction + transactions := []MerchantTransaction{} for rows.Next() { var t MerchantTransaction if err := rows.Scan( diff --git a/backend/internal/merchant/incomes.go b/backend/internal/merchant/incomes.go index 017819a..85f1dab 100644 --- a/backend/internal/merchant/incomes.go +++ b/backend/internal/merchant/incomes.go @@ -116,11 +116,11 @@ func (h *Handler) GetMerchantIncomeHistory(ctx context.Context, merchantID strin rows, err := h.DB.QueryContext(ctx, ` SELECT - created_at, description, + COALESCE(created_at, ''), description, transaction_id, COALESCE(card_number, ''), - transaction_type, amount, - net_merchant_payout, service_fee, - processed_by,terminal_id + COALESCE(transaction_type, ''), amount, + COALESCE(net_merchant_payout, 0), COALESCE(service_fee, 0), + processed_by, terminal_id FROM transactions WHERE merchant_id = ? AND status = 'completed' ORDER BY created_at DESC LIMIT 15 diff --git a/backend/internal/merchant/transactions.go b/backend/internal/merchant/transactions.go index 1b4216e..de47b46 100644 --- a/backend/internal/merchant/transactions.go +++ b/backend/internal/merchant/transactions.go @@ -61,10 +61,10 @@ func (h *Handler) TransactionHandler(w http.ResponseWriter, r *http.Request) { query := `SELECT transaction_id, COALESCE(card_number, ''), merchant_id, terminal_id, - transaction_type, amount, - points_earned, service_fee, - net_merchant_payout, processed_by, - status, description, created_at + COALESCE(transaction_type, ''), amount, + COALESCE(points_earned, 0), COALESCE(service_fee, 0), + COALESCE(net_merchant_payout, 0), processed_by, + COALESCE(status, ''), description, COALESCE(created_at, '') FROM transactions WHERE merchant_id = ?` @@ -100,7 +100,7 @@ func (h *Handler) TransactionHandler(w http.ResponseWriter, r *http.Request) { } defer rows.Close() - var transactions []MerchantTransaction + transactions := []MerchantTransaction{} for rows.Next() { var t MerchantTransaction if err := rows.Scan( diff --git a/frontend/assets/js/merchant_account.js b/frontend/assets/js/merchant_account.js new file mode 100644 index 0000000..c450c80 --- /dev/null +++ b/frontend/assets/js/merchant_account.js @@ -0,0 +1,118 @@ +document.addEventListener("DOMContentLoaded", () => { + if (!window.CURRENT_USERNAME) { + console.error("CURRENT_USERNAME is not defined. Cannot fetch account data."); + return; + } + + const fetchAccountData = async () => { + try { + const response = await fetch(`/v1/merchant/${window.CURRENT_USERNAME}/account`); + const json = await response.json(); + + if (json.success && json.data) { + const data = json.data; + const details = data.business_details; + const bank = data.business_bank_details; + const docs = data.business_document || []; + + // Basic Info Header + const merchantIdEl = document.getElementById('displayMerchantId'); + if (merchantIdEl) merchantIdEl.textContent = data.merchant_id || 'N/A'; + + const memberSinceEl = document.getElementById('displayMemberSince'); + if (memberSinceEl) memberSinceEl.textContent = data.member_since || 'N/A'; + + const statusEl = document.getElementById('displayAccountStatus'); + if (statusEl) { + const isOk = data.account_status.toLowerCase() === 'active' || data.account_status.toLowerCase() === 'approved'; + statusEl.innerHTML = `${data.account_status}`; + } + + // Business Details Form + if (document.getElementById('bizName')) document.getElementById('bizName').value = details.business_name || ''; + if (document.getElementById('bizType')) document.getElementById('bizType').value = details.business_type || ''; + if (document.getElementById('bizEmail')) document.getElementById('bizEmail').value = details.business_email || ''; + if (document.getElementById('bizPhone')) document.getElementById('bizPhone').value = details.business_phone || ''; + if (document.getElementById('bizAddress')) document.getElementById('bizAddress').value = details.business_address || ''; + if (document.getElementById('bizCity')) document.getElementById('bizCity').value = details.city || ''; + if (document.getElementById('bizPostal')) document.getElementById('bizPostal').value = details.postal_code || ''; + + // Bank Details + if (document.getElementById('bankName')) document.getElementById('bankName').value = bank.bank_name || ''; + if (document.getElementById('bankHolder')) document.getElementById('bankHolder').value = bank.account_holder_name || ''; + if (document.getElementById('bankAccount')) document.getElementById('bankAccount').value = bank.account_number || ''; + + // Documents + const docsContainer = document.getElementById('documentsContainer'); + if (docsContainer) { + if (docs.length === 0) { + docsContainer.innerHTML = '

No documents uploaded.

'; + } else { + docsContainer.innerHTML = ''; + docs.forEach(doc => { + const isApproved = doc.status.toLowerCase() === 'approved'; + const isPending = doc.status.toLowerCase() === 'pending'; + let statusColor = 'text-red-600 bg-red-50'; + let icon = ''; + if (isApproved) { + statusColor = 'text-emerald-600 bg-emerald-50'; + icon = ''; + } else if (isPending) { + statusColor = 'text-amber-600 bg-amber-50'; + icon = ''; + } + + const docDiv = document.createElement('div'); + docDiv.className = 'flex items-center justify-between p-4 border border-gray-100 rounded-xl bg-gray-50/50 hover:bg-gray-50 transition-colors'; + docDiv.innerHTML = ` +
+
+ +
+
+

${doc.document_type}

+ ${doc.message ? `

${doc.message}

` : ''} +
+
+
+ + ${icon} + ${doc.status} + + ${doc.document_url ? `` : ''} +
+ `; + docsContainer.appendChild(docDiv); + }); + } + } + + // Remove all animate-pulse classes to reveal inputs + document.querySelectorAll('.animate-pulse').forEach(el => { + el.classList.remove('animate-pulse'); + if(el.tagName === 'DIV' && el.classList.contains('h-10')) { + el.outerHTML = ''; // We will replace the pulse divs with actual inputs in the HTML + } + }); + + // Reveal the form content + const formContent = document.getElementById('accountFormContent'); + if (formContent) { + formContent.classList.remove('opacity-0'); + formContent.classList.add('opacity-100'); + } + const loadingSkeleton = document.getElementById('loadingSkeleton'); + if (loadingSkeleton) { + loadingSkeleton.style.display = 'none'; + } + + } else { + console.error("Failed to fetch account data:", json.message); + } + } catch (error) { + console.error("Error fetching account data:", error); + } + }; + + fetchAccountData(); +}); diff --git a/frontend/assets/js/merchant_transactions.js b/frontend/assets/js/merchant_transactions.js new file mode 100644 index 0000000..4294bb8 --- /dev/null +++ b/frontend/assets/js/merchant_transactions.js @@ -0,0 +1,352 @@ +document.addEventListener("DOMContentLoaded", () => { + if (!window.CURRENT_USERNAME) { + console.error("CURRENT_USERNAME is not defined. Cannot fetch transactions."); + return; + } + + const formatCurrency = (amount) => { + return new Intl.NumberFormat('en-PH', { + style: 'currency', + currency: 'PHP' + }).format(amount); + }; + + const formatCurrencyStats = (amount) => { + let absAmt = Math.abs(amount); + const getFormattedNum = (n) => { + if (n >= 100) return Math.floor(n * 10) / 10; + return Math.floor(n * 100) / 100; + }; + + if (absAmt >= 1000000000) { + let num = absAmt / 1000000000; + return (amount < 0 ? '-₱' : '₱') + getFormattedNum(num) + 'B'; + } else if (absAmt >= 1000000) { + let num = absAmt / 1000000; + return (amount < 0 ? '-₱' : '₱') + getFormattedNum(num) + 'M'; + } else if (absAmt >= 10000) { + let num = absAmt / 1000; + return (amount < 0 ? '-₱' : '₱') + getFormattedNum(num) + 'k'; + } + return new Intl.NumberFormat('en-PH', { style: 'currency', currency: 'PHP' }).format(amount); + }; + + const fetchStats = async () => { + try { + const response = await fetch(`/v1/merchant/${window.CURRENT_USERNAME}/incomes`); + const json = await response.json(); + if (json.success && json.data && json.data.stats) { + const stats = json.data.stats; + const availableBalanceEl = document.getElementById('availableBalance'); + if (availableBalanceEl) availableBalanceEl.textContent = formatCurrencyStats(stats.available_balance); + window.MERCHANT_AVAILABLE_BALANCE = parseFloat(stats.available_balance) || 0; + + const monthIncomesEl = document.getElementById('monthIncomes'); + if (monthIncomesEl) monthIncomesEl.textContent = formatCurrencyStats(stats.monthly_net_income); + + const grossRevenueEl = document.getElementById('grossRevenue'); + if (grossRevenueEl) grossRevenueEl.textContent = formatCurrencyStats(stats.gross_revenue); + + const platformFeeEl = document.getElementById('platformFee'); + if (platformFeeEl) platformFeeEl.textContent = formatCurrencyStats(stats.platform_fee); + } + } catch (error) { + console.error("Error fetching stats:", error); + } + }; + fetchStats(); + + const fetchTransactions = async () => { + try { + const searchVal = document.getElementById('txSearch')?.value || ''; + const typeVal = document.getElementById('txType')?.value || 'all'; + const sortVal = document.getElementById('txSort')?.value || 'desc'; + + const params = new URLSearchParams(); + if (searchVal) params.append('search', searchVal); + if (typeVal && typeVal !== 'all') params.append('type', typeVal); + if (sortVal) params.append('sort', sortVal); + + const queryString = params.toString() ? `?${params.toString()}` : ''; + const response = await fetch(`/v1/merchant/${window.CURRENT_USERNAME}/transactions${queryString}`); + const json = await response.json(); + + if (json.success && json.data) { + const tbody = document.getElementById('transactionsTableBody'); + if (!tbody) return; + + if (json.data.length === 0) { + tbody.innerHTML = ` + + +
+
+ +
+ No transactions found +

Your recent transaction history will appear here.

+
+ + + `; + return; + } + + tbody.innerHTML = ''; + json.data.forEach(tx => { + const dateObj = new Date(tx.created_at); + const formattedDate = dateObj.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); + const timeStr = dateObj.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true }); + + const isPayment = tx.transaction_type.toLowerCase() === 'payment'; + const amountColor = isPayment ? 'text-green-600' : 'text-red-600'; + const sign = isPayment ? '+' : '-'; + const amountStr = `${sign}${formatCurrency(Math.abs(tx.amount))}`; + + let statusBadgeClass = 'bg-green-100 text-green-800'; + if (tx.status.toLowerCase() === 'failed' || tx.status.toLowerCase() === 'declined') { + statusBadgeClass = 'bg-red-100 text-red-800'; + } else if (tx.status.toLowerCase() === 'pending') { + statusBadgeClass = 'bg-yellow-100 text-yellow-800'; + } else if (tx.status.toLowerCase() !== 'completed') { + statusBadgeClass = 'bg-gray-100 text-gray-800'; + } + + const tr = document.createElement('tr'); + tr.className = 'hover:bg-blue-50 transition-colors duration-150 cursor-pointer'; + tr.innerHTML = ` + +
${formattedDate}
+
${timeStr}
+ + + ${tx.description ? `
${tx.description}
` : ''} +
ID: ${tx.transaction_id}
+ + +
${tx.transaction_type}
+ + +
${amountStr}
+ ${tx.service_fee > 0 ? `
Fee: ${formatCurrency(Math.abs(tx.service_fee))}
` : ''} + + + + ${tx.status} + + + `; + tr.onclick = function () { + openTxnModal(tx); + }; + tbody.appendChild(tr); + }); + } else { + console.error("Failed to fetch transactions:", json.message); + } + } catch (error) { + console.error("Error fetching transactions:", error); + } + }; + + fetchTransactions(); + + // --- Modal Logic --- + const txnModal = document.getElementById("txnModal"); + const txnModalContent = document.getElementById("txnModalContent"); + const closeTxnModalBtn = document.getElementById("closeTxnModalBtn"); + const closeTxnModalBottomBtn = document.getElementById("closeTxnModalBottomBtn"); + + if (txnModal && closeTxnModalBtn) { + closeTxnModalBtn.onclick = closeTxnModal; + closeTxnModalBottomBtn.onclick = closeTxnModal; + txnModal.onclick = function (e) { + if (e.target === txnModal) closeTxnModal(); + }; + } + + function openTxnModal(tx) { + if (!txnModal) return; + + const dateObj = new Date(tx.created_at || tx.date); + const formattedDate = dateObj.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); + const timeStr = dateObj.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true }); + + document.getElementById("modalTxnId").textContent = tx.transaction_id || 'N/A'; + document.getElementById("modalTxnDate").textContent = `${formattedDate} at ${timeStr}`; + document.getElementById("modalTxnType").textContent = tx.transaction_type ? tx.transaction_type.charAt(0).toUpperCase() + tx.transaction_type.slice(1) : "N/A"; + + document.getElementById("modalTxnCardNumber").textContent = tx.card_number || 'N/A'; + const terminalIdEl = document.getElementById("modalTxnTerminalId"); + const terminalLabelEl = document.getElementById("modalTxnTerminalLabel"); + if (tx.terminal_id && tx.terminal_id !== 'N/A' && tx.terminal_id.trim() !== '') { + terminalIdEl.textContent = tx.terminal_id; + if (terminalLabelEl) terminalLabelEl.classList.remove("hidden"); + } else { + terminalIdEl.textContent = ''; + if (terminalLabelEl) terminalLabelEl.classList.add("hidden"); + } + + document.getElementById("modalTxnStatus").textContent = tx.status || 'completed'; + document.getElementById("modalTxnFee").textContent = formatCurrency(Number(tx.service_fee || 0)); + + document.getElementById("modalTxnDesc").textContent = tx.description || 'N/A'; + + const isPayment = (tx.transaction_type || '').toLowerCase() === "payment"; + const sign = isPayment ? "+" : "-"; + const colorClass = isPayment ? "text-green-600" : "text-red-600"; + + const grossAmt = Number(tx.amount || 0); + document.getElementById("modalTxnGross").textContent = formatCurrency(grossAmt); + + const netAmtEl = document.getElementById("modalTxnNet"); + const netValue = Number(tx.net_merchant_payout || grossAmt); + netAmtEl.textContent = `${sign}${formatCurrency(Math.abs(netValue))}`; + netAmtEl.className = `font-bold text-lg ${colorClass}`; + + txnModal.classList.remove('hidden'); + setTimeout(() => { + txnModal.classList.add('opacity-100'); + txnModalContent.classList.add('scale-100', 'opacity-100'); + txnModalContent.classList.remove('scale-95', 'opacity-0'); + }, 10); + } + + function closeTxnModal() { + txnModalContent.classList.add('scale-95', 'opacity-0'); + txnModalContent.classList.remove('scale-100', 'opacity-100'); + txnModal.classList.remove('opacity-100'); + setTimeout(() => { + txnModal.classList.add('hidden'); + }, 300); + } + + // --- Withdraw Modal Logic --- + const withdrawModal = document.getElementById('withdrawModal'); + const withdrawModalContent = document.getElementById('withdrawModalContent'); + const closeWithdrawModalBtn = document.getElementById('closeWithdrawModalBtn'); + const cancelWithdrawBtn = document.getElementById('cancelWithdrawBtn'); + const submitWithdrawBtn = document.getElementById('submitWithdrawBtn'); + const withdrawMaxBtn = document.getElementById('withdrawMaxBtn'); + const withdrawAmountInput = document.getElementById('withdrawAmount'); + const withdrawErrorMsg = document.getElementById('withdrawErrorMsg'); + const withdrawModalBalance = document.getElementById('withdrawModalBalance'); + + window.openWithdrawModal = function() { + const bal = window.MERCHANT_AVAILABLE_BALANCE || 0; + withdrawModalBalance.textContent = formatCurrency(bal); + withdrawAmountInput.value = ''; + withdrawErrorMsg.classList.add('hidden'); + + withdrawModal.classList.remove('hidden'); + setTimeout(() => { + withdrawModal.classList.add('opacity-100'); + withdrawModalContent.classList.add('scale-100', 'opacity-100'); + withdrawModalContent.classList.remove('scale-95', 'opacity-0'); + }, 10); + }; + + window.closeWithdrawModal = function() { + withdrawModalContent.classList.add('scale-95', 'opacity-0'); + withdrawModalContent.classList.remove('scale-100', 'opacity-100'); + withdrawModal.classList.remove('opacity-100'); + setTimeout(() => { + withdrawModal.classList.add('hidden'); + }, 300); + }; + + if (closeWithdrawModalBtn) closeWithdrawModalBtn.addEventListener('click', window.closeWithdrawModal); + if (cancelWithdrawBtn) cancelWithdrawBtn.addEventListener('click', window.closeWithdrawModal); + + const openBtn = document.getElementById('openWithdrawModalBtn'); + if (openBtn) openBtn.addEventListener('click', window.openWithdrawModal); + + const formatInputNumber = (val) => { + let str = String(val).replace(/[^0-9.]/g, ''); + const parts = str.split('.'); + if (parts.length > 2) { + str = parts[0] + '.' + parts.slice(1).join(''); + } + const beforeDot = str.split('.')[0]; + const afterDot = str.split('.')[1]; + let formatted = beforeDot.replace(/\B(?=(\d{3})+(?!\d))/g, ","); + if (afterDot !== undefined) { + formatted += '.' + afterDot; + } + return formatted; + }; + + if (withdrawAmountInput) { + withdrawAmountInput.addEventListener('input', (e) => { + e.target.value = formatInputNumber(e.target.value); + }); + } + + if (withdrawMaxBtn) { + withdrawMaxBtn.addEventListener('click', () => { + withdrawAmountInput.value = formatInputNumber(window.MERCHANT_AVAILABLE_BALANCE || 0); + }); + } + + if (submitWithdrawBtn) { + submitWithdrawBtn.addEventListener('click', async () => { + const amount = parseFloat(withdrawAmountInput.value.replace(/,/g, '')); + if (isNaN(amount) || amount <= 0) { + withdrawErrorMsg.textContent = "Please enter a valid amount to withdraw."; + withdrawErrorMsg.classList.remove('hidden'); + return; + } + if (amount > (window.MERCHANT_AVAILABLE_BALANCE || 0)) { + withdrawErrorMsg.textContent = "Amount exceeds available balance."; + withdrawErrorMsg.classList.remove('hidden'); + return; + } + + submitWithdrawBtn.disabled = true; + submitWithdrawBtn.innerHTML = ' Processing...'; + + try { + const res = await fetch(`/v1/merchant/${window.CURRENT_USERNAME}/withdraw`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ amount: amount }) + }); + const json = await res.json(); + + if (json.success) { + window.closeWithdrawModal(); + alert("Withdrawal successful! Note: it may take a few hours to reflect in your bank account."); + fetchStats(); + fetchTransactions(); + } else { + withdrawErrorMsg.textContent = json.message || "Failed to process withdrawal."; + withdrawErrorMsg.classList.remove('hidden'); + } + } catch (err) { + console.error(err); + withdrawErrorMsg.textContent = "Network error. Please try again."; + withdrawErrorMsg.classList.remove('hidden'); + } finally { + submitWithdrawBtn.disabled = false; + submitWithdrawBtn.innerHTML = 'Confirm Withdrawal'; + } + }); + } + + // --- Filter Event Listeners --- + const debounce = (func, delay) => { + let timeoutId; + return (...args) => { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => func.apply(null, args), delay); + }; + }; + + const txSearch = document.getElementById('txSearch'); + const txType = document.getElementById('txType'); + const txSort = document.getElementById('txSort'); + + if (txSearch) txSearch.addEventListener('input', debounce(fetchTransactions, 300)); + if (txType) txType.addEventListener('change', fetchTransactions); + if (txSort) txSort.addEventListener('change', fetchTransactions); +}); From 72e80463c899f8564cd062e06209e6cd3d5e8bd7 Mon Sep 17 00:00:00 2001 From: devzeeh <148837352+devzeeh@users.noreply.github.com> Date: Mon, 22 Jun 2026 18:45:50 +0800 Subject: [PATCH 3/4] feat: implement merchant account data retrieval and view handlers --- backend/internal/merchant/account.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/internal/merchant/account.go b/backend/internal/merchant/account.go index 58ac0b4..7e47b67 100644 --- a/backend/internal/merchant/account.go +++ b/backend/internal/merchant/account.go @@ -103,7 +103,7 @@ func (h *Handler) MerchantAccountDataHandler(w http.ResponseWriter, r *http.Requ COALESCE(m.business_structure, ''), COALESCE(m.business_document, ''), COALESCE(m.bir_document, ''), - COALESCE(m.document_status, 'pending'), + COALESCE(m.document_status, ''), COALESCE(m.message, '') FROM merchants m From d97caeb06b0de0e96c0ff391cd2609066fac359c Mon Sep 17 00:00:00 2001 From: devzeeh <148837352+devzeeh@users.noreply.github.com> Date: Thu, 25 Jun 2026 09:17:12 -0700 Subject: [PATCH 4/4] feat: integrate MQTT WebSocket for hardware auto-fill in add card form --- frontend/assets/admin/add_card.js | 96 ++++++++++++++++++-------- frontend/templates/admin/addCards.html | 1 + 2 files changed, 70 insertions(+), 27 deletions(-) diff --git a/frontend/assets/admin/add_card.js b/frontend/assets/admin/add_card.js index aeaf7eb..f341dc3 100644 --- a/frontend/assets/admin/add_card.js +++ b/frontend/assets/admin/add_card.js @@ -13,16 +13,49 @@ document.addEventListener("DOMContentLoaded", function () { const errorText = document.getElementById("error-text"); const successText = document.getElementById("success-text"); - if (form) { - // Dynamic Card Preview Listeners - const uidInput = document.getElementById("cardUID"); - const amountInput = document.getElementById("initialAmount"); - const previewUid = document.getElementById("preview-uid"); - const previewBalance = document.getElementById("preview-balance"); + const uidInput = document.getElementById("cardUID"); + const amountInput = document.getElementById("initialAmount"); + const previewUid = document.getElementById("preview-uid"); + const previewBalance = document.getElementById("preview-balance"); + + // ========================================== + // 1. MQTT WEBSOCKET INTEGRATION (Hardware Auto-fill) + // ========================================== + const brokerUrl = 'ws://localhost:9001'; // 192.168.254.104 + const mqttClient = typeof mqtt !== 'undefined' ? mqtt.connect(brokerUrl) : null; + + if (mqttClient) { + mqttClient.on('connect', () => { + console.log('✅ Connected to Mosquitto Broker via WebSockets.'); + mqttClient.subscribe('unicard/admin/register'); + }); + + mqttClient.on('message', (topic, message) => { + if (topic === 'unicard/admin/register') { + const scannedUID = message.toString().trim().toUpperCase(); + console.log('🔥 Hardware Scan Detected! UID:', scannedUID); + if (uidInput && previewUid) { + uidInput.value = scannedUID; + previewUid.textContent = scannedUID; + + // Visual feedback: Flash the input green + uidInput.classList.add('ring-2', 'ring-green-500'); + setTimeout(() => uidInput.classList.remove('ring-2', 'ring-green-500'), 1500); + } + } + }); + } else { + console.error("MQTT library not found. Add to your HTML."); + } + + // ========================================== + // 2. UI DYNAMICS (Manual Typing) + // ========================================== + if (form) { if(uidInput && previewUid) { uidInput.addEventListener('input', (e) => { - const val = e.target.value.trim(); + const val = e.target.value.trim().toUpperCase(); previewUid.textContent = val || 'A346F101'; }); } @@ -38,45 +71,54 @@ document.addEventListener("DOMContentLoaded", function () { }); } + // ========================================== + // 3. REAL BACKEND SUBMISSION (Go Database) + // ========================================== form.addEventListener("submit", function (e) { e.preventDefault(); if (errorAlert) errorAlert.classList.add("hidden"); if (successAlert) successAlert.classList.add("hidden"); - const cardUID = document.getElementById("cardUID").value; - const initialAmount = document.getElementById("initialAmount").value; + const cardUID = uidInput.value.trim().toUpperCase(); + const initialAmount = amountInput.value; + // Grab the admin username from the URL path const adminUsername = window.location.pathname.split('/')[2]; + fetch(`/v1/admin/${adminUsername}/addcardauth`, { method: "POST", headers: { "Content-Type": "application/json" }, + // Make sure these keys match exactly what your Go struct expects! body: JSON.stringify({ card_uid: cardUID, initial_amount: parseFloat(initialAmount) }) }) - .then(res => res.json()) - .then(data => { - if (data.success) { - if (successAlert && successText) { - successText.innerText = data.message; - successAlert.classList.remove("hidden"); - } - form.reset(); - } else { - if (errorAlert && errorText) { - errorText.innerText = data.message; - errorAlert.classList.remove("hidden"); - } + .then(res => res.json()) + .then(data => { + if (data.success) { + if (successAlert && successText) { + successText.innerText = data.message; + successAlert.classList.remove("hidden"); } - }) - .catch(err => { - console.error(err); + // Reset the form and the dynamic card preview + form.reset(); + if(previewUid) previewUid.textContent = 'A346F101'; + if(previewBalance) previewBalance.textContent = '0.00'; + } else { if (errorAlert && errorText) { - errorText.innerText = "Network error. Please try again."; + errorText.innerText = data.message; errorAlert.classList.remove("hidden"); } - }); + } + }) + .catch(err => { + console.error(err); + if (errorAlert && errorText) { + errorText.innerText = "Network error. Please try again."; + errorAlert.classList.remove("hidden"); + } + }); }); } }); \ No newline at end of file diff --git a/frontend/templates/admin/addCards.html b/frontend/templates/admin/addCards.html index 6995073..16ff446 100644 --- a/frontend/templates/admin/addCards.html +++ b/frontend/templates/admin/addCards.html @@ -175,5 +175,6 @@

UniCard

+ \ No newline at end of file