570 lines
20 KiB
Go
570 lines
20 KiB
Go
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()
|
|
}
|