package database import ( "context" "database/sql" "encoding/json" "fmt" "log" "os" "path/filepath" "strconv" "time" _ "github.com/joho/godotenv/autoload" _ "github.com/mattn/go-sqlite3" ) // Product represents a product in the database type Product struct { SKU string `json:"sku"` Name string `json:"name"` HSNCode string `json:"hsn_code"` BasePrice float64 `json:"base_price"` WholesalePrice float64 `json:"wholesale_price"` GSTRate float64 `json:"gst_rate"` SmallOrderQty int `json:"small_order_qty"` SmallOrderFee float64 `json:"small_order_fee"` // Convenience fee for orders below SmallOrderQty Unit string `json:"unit"` // Unit of measurement (e.g., "pcs", "kg", "box") UserID string `json:"user_id"` CreatedAt string `json:"created_at"` } // Invoice represents a stored invoice type Invoice struct { ID string `json:"id"` // UUID HumanReadableID string `json:"human_readable_id"` // Formatted ID like INV/12-2025/001 Data string `json:"data"` // JSON blob of invoice details UserID string `json:"user_id"` CreatedAt string `json:"created_at"` } // User represents an authenticated user type User struct { ID string `json:"id"` Email string `json:"email"` Password string `json:"-"` CompanyDetails string `json:"company_details"` // Multiline company details for invoice header BankDetails string `json:"bank_details"` // Multiline bank details for invoice footer InvoicePrefix string `json:"invoice_prefix"` // Prefix for invoice IDs (e.g., INV, BILL) InvoiceCounter int `json:"invoice_counter"` // Auto-incrementing counter for invoice serial numbers CreatedAt string `json:"created_at"` } // BuyerDetails represents a buyer/customer for invoices type BuyerDetails struct { ID string `json:"id"` UserID string `json:"user_id"` Name string `json:"name"` // Display name for selection Details string `json:"details"` // Multiline buyer details CreatedAt string `json:"created_at"` } // Service represents a service that interacts with a database. // Service represents a service that interacts with a database. type Service interface { // Health returns a map of health status information. Health() map[string]string // Close terminates the database connection. Close() error // Product operations (user-scoped) CreateProduct(p Product, userID string) error UpdateProduct(p Product, userID string) error GetAllProducts(userID string) ([]Product, error) GetProductBySKU(sku string, userID string) (*Product, error) DeleteProduct(sku string, userID string) error // Invoice operations (user-scoped) CreateInvoice(id string, humanReadableID string, data interface{}, userID string) error GetInvoice(id string, userID string) (*Invoice, error) GetAllInvoices(userID string) ([]Invoice, error) GetRecentProducts(userID string, limit int) ([]Product, error) GetRecentInvoices(userID string, limit int) ([]Invoice, error) GetNextInvoiceNumber(userID string) (string, error) // Returns formatted invoice ID and increments counter // User operations CreateUser(email, passwordHash string) (*User, error) GetUserByEmail(email string) (*User, error) GetUserByID(id string) (*User, error) UpdateUserPassword(id string, passwordHash string) error UpdateUserDetails(id string, companyDetails string, bankDetails string, invoicePrefix string) error // Buyer details operations CreateBuyerDetails(userID string, name string, details string) (*BuyerDetails, error) UpdateBuyerDetails(id string, userID string, name string, details string) error GetBuyerDetails(id string, userID string) (*BuyerDetails, error) GetAllBuyerDetails(userID string) ([]BuyerDetails, error) DeleteBuyerDetails(id string, userID string) error } type service struct { db *sql.DB } var ( dburl = os.Getenv("DB_PATH") dbInstance *service ) func New() Service { // Reuse Connection if dbInstance != nil { return dbInstance } // Ensure the directory for the database file exists if dburl != "" && dburl != ":memory:" { dir := filepath.Dir(dburl) if dir != "" && dir != "." { if err := os.MkdirAll(dir, 0755); err != nil { log.Fatalf("Failed to create database directory: %v", err) } } } db, err := sql.Open("sqlite3", dburl) if err != nil { log.Fatal(err) } dbInstance = &service{ db: db, } // Initialize tables dbInstance.initTables() return dbInstance } func (s *service) initTables() { // Products table with user ownership _, err := s.db.Exec(` CREATE TABLE IF NOT EXISTS products ( sku TEXT NOT NULL, name TEXT NOT NULL, hsn_code TEXT, base_price REAL NOT NULL, wholesale_price REAL, gst_rate REAL NOT NULL DEFAULT 0.18, small_order_qty INTEGER DEFAULT 1, small_order_fee REAL DEFAULT 0, unit TEXT DEFAULT 'pcs', user_id TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (sku, user_id) ) `) if err != nil { log.Printf("Error creating products table: %v", err) } // Add user_id column if not exists (migration for existing DBs) s.db.Exec(`ALTER TABLE products ADD COLUMN user_id TEXT DEFAULT ''`) // Add unit column if not exists (migration for existing DBs) s.db.Exec(`ALTER TABLE products ADD COLUMN unit TEXT DEFAULT 'pcs'`) // Invoices table with user ownership _, err = s.db.Exec(` CREATE TABLE IF NOT EXISTS invoices ( id TEXT PRIMARY KEY, human_readable_id TEXT DEFAULT '', data TEXT NOT NULL, user_id TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `) if err != nil { log.Printf("Error creating invoices table: %v", err) } // Add columns if not exists (migration for existing DBs) s.db.Exec(`ALTER TABLE invoices ADD COLUMN user_id TEXT DEFAULT ''`) s.db.Exec(`ALTER TABLE invoices ADD COLUMN human_readable_id TEXT DEFAULT ''`) // Create index on user_id for fast lookups s.db.Exec(`CREATE INDEX IF NOT EXISTS idx_products_user ON products(user_id)`) s.db.Exec(`CREATE INDEX IF NOT EXISTS idx_invoices_user ON invoices(user_id)`) // Users table _, err = s.db.Exec(` CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, email TEXT UNIQUE NOT NULL, password TEXT NOT NULL, company_details TEXT DEFAULT '', bank_details TEXT DEFAULT '', invoice_prefix TEXT DEFAULT 'INV', invoice_counter INTEGER DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `) if err != nil { log.Printf("Error creating users table: %v", err) } // Add columns if not exists (migration for existing DBs) s.db.Exec(`ALTER TABLE users ADD COLUMN company_details TEXT DEFAULT ''`) s.db.Exec(`ALTER TABLE users ADD COLUMN bank_details TEXT DEFAULT ''`) s.db.Exec(`ALTER TABLE users ADD COLUMN invoice_prefix TEXT DEFAULT 'INV'`) s.db.Exec(`ALTER TABLE users ADD COLUMN invoice_counter INTEGER DEFAULT 0`) s.db.Exec(`ALTER TABLE users ADD COLUMN invoice_prefix TEXT DEFAULT 'INV'`) s.db.Exec(`ALTER TABLE users ADD COLUMN invoice_counter INTEGER DEFAULT 0`) // Create index on email for fast lookups _, err = s.db.Exec(`CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)`) if err != nil { log.Printf("Error creating users email index: %v", err) } // Buyer details table _, err = s.db.Exec(` CREATE TABLE IF NOT EXISTS buyer_details ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, name TEXT NOT NULL, details TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `) if err != nil { log.Printf("Error creating buyer_details table: %v", err) } // Create index on user_id for fast lookups s.db.Exec(`CREATE INDEX IF NOT EXISTS idx_buyer_details_user ON buyer_details(user_id)`) } // CreateProduct inserts a new product for a user func (s *service) CreateProduct(p Product, userID string) error { _, err := s.db.Exec(` INSERT INTO products (sku, name, hsn_code, base_price, wholesale_price, gst_rate, small_order_qty, small_order_fee, unit, user_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, p.SKU, p.Name, p.HSNCode, p.BasePrice, p.WholesalePrice, p.GSTRate, p.SmallOrderQty, p.SmallOrderFee, p.Unit, userID) return err } // UpdateProduct updates an existing product for a user func (s *service) UpdateProduct(p Product, userID string) error { _, err := s.db.Exec(` UPDATE products SET name=?, hsn_code=?, base_price=?, wholesale_price=?, gst_rate=?, small_order_qty=?, small_order_fee=?, unit=? WHERE sku=? AND user_id=? `, p.Name, p.HSNCode, p.BasePrice, p.WholesalePrice, p.GSTRate, p.SmallOrderQty, p.SmallOrderFee, p.Unit, p.SKU, userID) return err } // GetAllProducts returns all products for a user func (s *service) GetAllProducts(userID string) ([]Product, error) { rows, err := s.db.Query(`SELECT sku, name, hsn_code, base_price, wholesale_price, gst_rate, small_order_qty, small_order_fee, unit, user_id, created_at FROM products WHERE user_id=? ORDER BY name`, userID) if err != nil { return nil, err } defer rows.Close() var products []Product for rows.Next() { var p Product if err := rows.Scan(&p.SKU, &p.Name, &p.HSNCode, &p.BasePrice, &p.WholesalePrice, &p.GSTRate, &p.SmallOrderQty, &p.SmallOrderFee, &p.Unit, &p.UserID, &p.CreatedAt); err != nil { return nil, err } products = append(products, p) } return products, nil } // GetProductBySKU returns a single product by SKU for a user func (s *service) GetProductBySKU(sku string, userID string) (*Product, error) { var p Product err := s.db.QueryRow(`SELECT sku, name, hsn_code, base_price, wholesale_price, gst_rate, small_order_qty, small_order_fee, unit, user_id, created_at FROM products WHERE sku=? AND user_id=?`, sku, userID). Scan(&p.SKU, &p.Name, &p.HSNCode, &p.BasePrice, &p.WholesalePrice, &p.GSTRate, &p.SmallOrderQty, &p.SmallOrderFee, &p.Unit, &p.UserID, &p.CreatedAt) if err == sql.ErrNoRows { return nil, nil } if err != nil { return nil, err } return &p, nil } // DeleteProduct removes a product by SKU for a user func (s *service) DeleteProduct(sku string, userID string) error { _, err := s.db.Exec(`DELETE FROM products WHERE sku=? AND user_id=?`, sku, userID) return err } // GetNextInvoiceNumber generates the next invoice ID in format PREFIX/MMM-YYYY/XXX and increments the counter func (s *service) GetNextInvoiceNumber(userID string) (string, error) { var prefix string var counter int // Get current prefix and counter err := s.db.QueryRow(`SELECT COALESCE(invoice_prefix, 'INV'), COALESCE(invoice_counter, 0) FROM users WHERE id = ?`, userID). Scan(&prefix, &counter) if err != nil { return "", err } // Increment counter counter++ // Update counter in database _, err = s.db.Exec(`UPDATE users SET invoice_counter = ? WHERE id = ?`, counter, userID) if err != nil { return "", err } // Generate formatted invoice ID: PREFIX/MMM-YYYY/XXX now := time.Now() humanReadableID := fmt.Sprintf("%s/%s-%d/%03d", prefix, now.Month().String()[:3], now.Year(), counter) return humanReadableID, nil } // CreateInvoice stores an invoice with UUID and human-readable ID for a user func (s *service) CreateInvoice(id string, humanReadableID string, data interface{}, userID string) error { jsonData, err := json.Marshal(data) if err != nil { return err } _, err = s.db.Exec(`INSERT INTO invoices (id, human_readable_id, data, user_id) VALUES (?, ?, ?, ?)`, id, humanReadableID, string(jsonData), userID) return err } // GetInvoice retrieves an invoice by ID for a user func (s *service) GetInvoice(id string, userID string) (*Invoice, error) { var inv Invoice err := s.db.QueryRow(`SELECT id, COALESCE(human_readable_id, ''), data, user_id, created_at FROM invoices WHERE id=? AND user_id=?`, id, userID). Scan(&inv.ID, &inv.HumanReadableID, &inv.Data, &inv.UserID, &inv.CreatedAt) if err == sql.ErrNoRows { return nil, nil } if err != nil { return nil, err } return &inv, nil } // GetAllInvoices retrieves all invoices for a user func (s *service) GetAllInvoices(userID string) ([]Invoice, error) { rows, err := s.db.Query(`SELECT id, COALESCE(human_readable_id, ''), data, user_id, created_at FROM invoices WHERE user_id=? ORDER BY created_at DESC`, userID) if err != nil { return nil, err } defer rows.Close() var invoices []Invoice for rows.Next() { var inv Invoice if err := rows.Scan(&inv.ID, &inv.HumanReadableID, &inv.Data, &inv.UserID, &inv.CreatedAt); err != nil { return nil, err } invoices = append(invoices, inv) } return invoices, nil } // GetRecentProducts returns the most recently added products for a user func (s *service) GetRecentProducts(userID string, limit int) ([]Product, error) { rows, err := s.db.Query(`SELECT sku, name, hsn_code, base_price, wholesale_price, gst_rate, small_order_qty, small_order_fee, unit, user_id, created_at FROM products WHERE user_id=? ORDER BY created_at DESC LIMIT ?`, userID, limit) if err != nil { return nil, err } defer rows.Close() var products []Product for rows.Next() { var p Product if err := rows.Scan(&p.SKU, &p.Name, &p.HSNCode, &p.BasePrice, &p.WholesalePrice, &p.GSTRate, &p.SmallOrderQty, &p.SmallOrderFee, &p.Unit, &p.UserID, &p.CreatedAt); err != nil { return nil, err } products = append(products, p) } return products, nil } // GetRecentInvoices returns the most recently generated invoices for a user func (s *service) GetRecentInvoices(userID string, limit int) ([]Invoice, error) { rows, err := s.db.Query(`SELECT id, COALESCE(human_readable_id, ''), data, user_id, created_at FROM invoices WHERE user_id=? ORDER BY created_at DESC LIMIT ?`, userID, limit) if err != nil { return nil, err } defer rows.Close() var invoices []Invoice for rows.Next() { var inv Invoice if err := rows.Scan(&inv.ID, &inv.HumanReadableID, &inv.Data, &inv.UserID, &inv.CreatedAt); err != nil { return nil, err } invoices = append(invoices, inv) } return invoices, nil } // CreateUser creates a new user with hashed password func (s *service) CreateUser(email, passwordHash string) (*User, error) { id := fmt.Sprintf("%d", time.Now().UnixNano()) _, err := s.db.Exec(`INSERT INTO users (id, email, password) VALUES (?, ?, ?)`, id, email, passwordHash) if err != nil { return nil, err } return s.GetUserByID(id) } // GetUserByEmail retrieves a user by email func (s *service) GetUserByEmail(email string) (*User, error) { var u User err := s.db.QueryRow(`SELECT id, email, password, COALESCE(company_details, ''), COALESCE(bank_details, ''), COALESCE(invoice_prefix, 'INV'), COALESCE(invoice_counter, 0), created_at FROM users WHERE email = ?`, email). Scan(&u.ID, &u.Email, &u.Password, &u.CompanyDetails, &u.BankDetails, &u.InvoicePrefix, &u.InvoiceCounter, &u.CreatedAt) if err == sql.ErrNoRows { return nil, nil } if err != nil { return nil, err } return &u, nil } // GetUserByID retrieves a user by ID func (s *service) GetUserByID(id string) (*User, error) { var u User err := s.db.QueryRow(`SELECT id, email, password, COALESCE(company_details, ''), COALESCE(bank_details, ''), COALESCE(invoice_prefix, 'INV'), COALESCE(invoice_counter, 0), created_at FROM users WHERE id = ?`, id). Scan(&u.ID, &u.Email, &u.Password, &u.CompanyDetails, &u.BankDetails, &u.InvoicePrefix, &u.InvoiceCounter, &u.CreatedAt) if err == sql.ErrNoRows { return nil, nil } if err != nil { return nil, err } return &u, nil } // UpdateUserPassword updates a user's password hash func (s *service) UpdateUserPassword(id string, passwordHash string) error { _, err := s.db.Exec(`UPDATE users SET password = ? WHERE id = ?`, passwordHash, id) return err } // UpdateUserDetails updates a user's company and bank details func (s *service) UpdateUserDetails(id string, companyDetails string, bankDetails string, invoicePrefix string) error { _, err := s.db.Exec(`UPDATE users SET company_details = ?, bank_details = ?, invoice_prefix = ? WHERE id = ?`, companyDetails, bankDetails, invoicePrefix, id) return err } // CreateBuyerDetails creates a new buyer details entry func (s *service) CreateBuyerDetails(userID string, name string, details string) (*BuyerDetails, error) { id := fmt.Sprintf("%d", time.Now().UnixNano()) _, err := s.db.Exec(`INSERT INTO buyer_details (id, user_id, name, details) VALUES (?, ?, ?, ?)`, id, userID, name, details) if err != nil { return nil, err } return s.GetBuyerDetails(id, userID) } // UpdateBuyerDetails updates an existing buyer details entry func (s *service) UpdateBuyerDetails(id string, userID string, name string, details string) error { _, err := s.db.Exec(`UPDATE buyer_details SET name = ?, details = ? WHERE id = ? AND user_id = ?`, name, details, id, userID) return err } // GetBuyerDetails retrieves a buyer details entry by ID func (s *service) GetBuyerDetails(id string, userID string) (*BuyerDetails, error) { var b BuyerDetails err := s.db.QueryRow(`SELECT id, user_id, name, details, created_at FROM buyer_details WHERE id = ? AND user_id = ?`, id, userID). Scan(&b.ID, &b.UserID, &b.Name, &b.Details, &b.CreatedAt) if err == sql.ErrNoRows { return nil, nil } if err != nil { return nil, err } return &b, nil } // GetAllBuyerDetails retrieves all buyer details for a user func (s *service) GetAllBuyerDetails(userID string) ([]BuyerDetails, error) { rows, err := s.db.Query(`SELECT id, user_id, name, details, created_at FROM buyer_details WHERE user_id = ? ORDER BY name`, userID) if err != nil { return nil, err } defer rows.Close() var buyers []BuyerDetails for rows.Next() { var b BuyerDetails if err := rows.Scan(&b.ID, &b.UserID, &b.Name, &b.Details, &b.CreatedAt); err != nil { return nil, err } buyers = append(buyers, b) } return buyers, nil } // DeleteBuyerDetails removes a buyer details entry func (s *service) DeleteBuyerDetails(id string, userID string) error { _, err := s.db.Exec(`DELETE FROM buyer_details WHERE id = ? AND user_id = ?`, id, userID) return err } // Health checks the health of the database connection by pinging the database. // It returns a map with keys indicating various health statistics. func (s *service) Health() map[string]string { ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() stats := make(map[string]string) // Ping the database err := s.db.PingContext(ctx) if err != nil { stats["status"] = "down" stats["error"] = fmt.Sprintf("db down: %v", err) log.Fatalf("db down: %v", err) // Log the error and terminate the program return stats } // Database is up, add more statistics stats["status"] = "up" stats["message"] = "It's healthy" // Get database stats (like open connections, in use, idle, etc.) dbStats := s.db.Stats() stats["open_connections"] = strconv.Itoa(dbStats.OpenConnections) stats["in_use"] = strconv.Itoa(dbStats.InUse) stats["idle"] = strconv.Itoa(dbStats.Idle) stats["wait_count"] = strconv.FormatInt(dbStats.WaitCount, 10) stats["wait_duration"] = dbStats.WaitDuration.String() stats["max_idle_closed"] = strconv.FormatInt(dbStats.MaxIdleClosed, 10) stats["max_lifetime_closed"] = strconv.FormatInt(dbStats.MaxLifetimeClosed, 10) // Evaluate stats to provide a health message if dbStats.OpenConnections > 40 { // Assuming 50 is the max for this example stats["message"] = "The database is experiencing heavy load." } if dbStats.WaitCount > 1000 { stats["message"] = "The database has a high number of wait events, indicating potential bottlenecks." } if dbStats.MaxIdleClosed > int64(dbStats.OpenConnections)/2 { stats["message"] = "Many idle connections are being closed, consider revising the connection pool settings." } if dbStats.MaxLifetimeClosed > int64(dbStats.OpenConnections)/2 { stats["message"] = "Many connections are being closed due to max lifetime, consider increasing max lifetime or revising the connection usage pattern." } return stats } // Close closes the database connection. // It logs a message indicating the disconnection from the specific database. // If the connection is successfully closed, it returns nil. // If an error occurs while closing the connection, it returns the error. func (s *service) Close() error { log.Printf("Disconnected from database: %s", dburl) return s.db.Close() }