diff --git a/Portifolio b/Portifolio new file mode 100755 index 0000000..21c048b Binary files /dev/null and b/Portifolio differ diff --git a/app.db b/app.db new file mode 100644 index 0000000..42cdb24 Binary files /dev/null and b/app.db differ diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..d09b6dc --- /dev/null +++ b/build.sh @@ -0,0 +1 @@ +CGO_CFLAGS="-w" go build Portifolio \ No newline at end of file diff --git a/go.mod b/go.mod index 9decd78..c570ca5 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module Portifolio go 1.25.7 + +require github.com/mattn/go-sqlite3 v1.14.37 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..9c79a75 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/mattn/go-sqlite3 v1.14.37 h1:3DOZp4cXis1cUIpCfXLtmlGolNLp2VEqhiB/PARNBIg= +github.com/mattn/go-sqlite3 v1.14.37/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= diff --git a/internal/database/main.go b/internal/database/main.go new file mode 100644 index 0000000..4a5faad --- /dev/null +++ b/internal/database/main.go @@ -0,0 +1,32 @@ +package database + +import ( + "database/sql" + "fmt" + "log" + + _ "github.com/mattn/go-sqlite3" +) + +func InitDB(db *sql.DB) { + schema := ` + CREATE TABLE IF NOT EXISTS currencies ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + code TEXT NOT NULL UNIQUE, + name TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS companies ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + shares_outstanding INTEGER NOT NULL, + price REAL NOT NULL, + currency_id INTEGER NOT NULL, + FOREIGN KEY (currency_id) REFERENCES currencies(id) + );` + + if _, err := db.Exec(schema); err != nil { + log.Fatal("Failed to create tables:", err) + } + fmt.Println("Tables ready") +} diff --git a/internal/handlers/main.go b/internal/handlers/main.go new file mode 100644 index 0000000..c8ae945 --- /dev/null +++ b/internal/handlers/main.go @@ -0,0 +1,52 @@ +package handlers + +import ( + "Portifolio/internal/model" + "Portifolio/internal/service" + "database/sql" + "encoding/json" + "net/http" + + _ "github.com/mattn/go-sqlite3" +) + +func HealthHandler(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + dbStatus := "ok" + if err := db.Ping(); err != nil { + dbStatus = "error: " + err.Error() + w.WriteHeader(http.StatusServiceUnavailable) + } + + json.NewEncoder(w).Encode(map[string]string{ + "status": "ok", + "database": dbStatus, + }) + } +} + +func AddCompanyHandler(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + + var input model.CompanyInput + if err := json.NewDecoder(r.Body).Decode(&input); err != nil { + http.Error(w, "invalid json", http.StatusBadRequest) + return + } + + if err := service.AddCompany(input, db); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(map[string]string{"status": "created"}) + } +} diff --git a/internal/model/company.go b/internal/model/company.go new file mode 100644 index 0000000..92c3260 --- /dev/null +++ b/internal/model/company.go @@ -0,0 +1,28 @@ +package model + +type Currency struct { + ID int + Code string + Name string +} + +type Company struct { + ID int + Name string + SharesOutstanding int + Price float64 + CurrencyID int + Currency *Currency // populated on joins +} + +type CompanyInput struct { + Name string `json:"name"` + SharesOutstanding int `json:"shares_outstanding"` + Price float64 `json:"price"` + CurrencyID int `json:"currency_id"` +} + +type CurrencyInput struct { + Code string `json:"code"` + Name string `json:"name"` +} diff --git a/internal/service/company.go b/internal/service/company.go new file mode 100644 index 0000000..9d5c20b --- /dev/null +++ b/internal/service/company.go @@ -0,0 +1,48 @@ +package service + +import ( + "Portifolio/internal/model" + "database/sql" +) + +func InsertCompany(db *sql.DB, input model.CompanyInput) (int, error) { + res, err := db.Exec( + `INSERT INTO companies (name, shares_outstanding, price, currency_id) VALUES (?, ?, ?, ?)`, + input.Name, input.SharesOutstanding, input.Price, input.CurrencyID, + ) + if err != nil { + return 0, err + } + id, err := res.LastInsertId() + return int(id), err +} + +func GetAllCompanies(db *sql.DB) ([]model.Company, error) { + rows, err := db.Query(` + SELECT c.id, c.name, c.shares_outstanding, c.price, + cu.id, cu.code, cu.name + FROM companies c + JOIN currencies cu ON c.currency_id = cu.id + ORDER BY c.name + `) + if err != nil { + return nil, err + } + defer rows.Close() + + var companies []model.Company + for rows.Next() { + var c model.Company + var cu model.Currency + if err := rows.Scan( + &c.ID, &c.Name, &c.SharesOutstanding, &c.Price, + &cu.ID, &cu.Code, &cu.Name, + ); err != nil { + return nil, err + } + c.CurrencyID = cu.ID + c.Currency = &cu + companies = append(companies, c) + } + return companies, rows.Err() +} diff --git a/internal/service/currency.go b/internal/service/currency.go new file mode 100644 index 0000000..34b0456 --- /dev/null +++ b/internal/service/currency.go @@ -0,0 +1,47 @@ +package service + +import ( + "Portifolio/internal/model" + "database/sql" +) + +func InsertCurrency(db *sql.DB, input model.CurrencyInput) (int, error) { + res, err := db.Exec( + `INSERT INTO currencies (code, name) VALUES (?, ?)`, + input.Code, input.Name, + ) + if err != nil { + return 0, err + } + id, err := res.LastInsertId() + return int(id), err +} + +func GetCurrencyByCode(db *sql.DB, code string) (*model.Currency, error) { + c := &model.Currency{} + err := db.QueryRow( + `SELECT id, code, name FROM currencies WHERE code = ?`, code, + ).Scan(&c.ID, &c.Code, &c.Name) + if err == sql.ErrNoRows { + return nil, nil + } + return c, err +} + +func GetAllCurrencies(db *sql.DB) ([]model.Currency, error) { + rows, err := db.Query(`SELECT id, code, name FROM currencies ORDER BY code`) + if err != nil { + return nil, err + } + defer rows.Close() + + var currencies []model.Currency + for rows.Next() { + var c model.Currency + if err := rows.Scan(&c.ID, &c.Code, &c.Name); err != nil { + return nil, err + } + currencies = append(currencies, c) + } + return currencies, rows.Err() +} diff --git a/internal/service/main.go b/internal/service/main.go new file mode 100644 index 0000000..5699681 --- /dev/null +++ b/internal/service/main.go @@ -0,0 +1,16 @@ +package service + +import ( + "Portifolio/internal/model" + "database/sql" + + _ "github.com/mattn/go-sqlite3" +) + +func AddCompany(input model.CompanyInput, db *sql.DB) error { + _, err := db.Exec( + `INSERT INTO companies (name, shares_outstanding, price, currency_id) VALUES (?, ?, ?, ?)`, + input.Name, input.SharesOutstanding, input.Price, input.CurrencyID, + ) + return err +} diff --git a/internal/shell/company.go b/internal/shell/company.go new file mode 100644 index 0000000..6ebb4a1 --- /dev/null +++ b/internal/shell/company.go @@ -0,0 +1,54 @@ +package shell + +import ( + "Portifolio/internal/model" + "Portifolio/internal/service" + "bufio" + "database/sql" + "fmt" + "strconv" + "strings" + + _ "github.com/mattn/go-sqlite3" +) + +func AddCompany(scanner *bufio.Scanner, db *sql.DB) { + input := model.CompanyInput{} + + fmt.Print(" Name: ") + scanner.Scan() + input.Name = strings.TrimSpace(scanner.Text()) + + fmt.Print(" Shares outstanding: ") + scanner.Scan() + shares, err := strconv.Atoi(strings.TrimSpace(scanner.Text())) + if err != nil { + fmt.Println(" Invalid number for shares.") + return + } + input.SharesOutstanding = shares + + fmt.Print(" Price: ") + scanner.Scan() + price, err := strconv.ParseFloat(strings.TrimSpace(scanner.Text()), 64) + if err != nil { + fmt.Println(" Invalid number for price.") + return + } + input.Price = price + + fmt.Print(" Currency ID: ") + scanner.Scan() + cid, err := strconv.Atoi(strings.TrimSpace(scanner.Text())) + if err != nil { + fmt.Println(" Invalid currency ID.") + return + } + input.CurrencyID = cid + + if err := service.AddCompany(input, db); err != nil { + fmt.Println(" Error:", err) + return + } + fmt.Printf(" ✓ Company '%s' added.\n", input.Name) +} diff --git a/internal/shell/currency.go b/internal/shell/currency.go new file mode 100644 index 0000000..551014c --- /dev/null +++ b/internal/shell/currency.go @@ -0,0 +1,44 @@ +package shell + +import ( + "Portifolio/internal/model" + "Portifolio/internal/service" + "bufio" + "database/sql" + "fmt" + "strings" + + _ "github.com/mattn/go-sqlite3" +) + +func AddCurrency(scanner *bufio.Scanner, db *sql.DB) { + input := model.CurrencyInput{} + + fmt.Print(" Code (e.g. DKK): ") + scanner.Scan() + input.Code = strings.ToUpper(strings.TrimSpace(scanner.Text())) + + fmt.Print(" Name (e.g. Danish Krone): ") + scanner.Scan() + input.Name = strings.TrimSpace(scanner.Text()) + + id, err := service.InsertCurrency(db, input) + if err != nil { + fmt.Println(" ✗ Error:", err) + return + } + fmt.Printf(" ✓ Currency '%s' (%s) added with ID %d\n", input.Name, input.Code, id) +} + +func ListCurrencies(db *sql.DB) { + currencies, err := service.GetAllCurrencies(db) + if err != nil { + fmt.Println(" ✗ Error:", err) + return + } + fmt.Printf(" %-5s %-6s %s\n", "ID", "CODE", "NAME") + fmt.Println(" " + strings.Repeat("-", 30)) + for _, c := range currencies { + fmt.Printf(" %-5d %-6s %s\n", c.ID, c.Code, c.Name) + } +} diff --git a/main.go b/main.go index 65d9935..5d0a968 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,85 @@ -package portifolio +package main -import "fmt" +import ( + "Portifolio/internal/database" + "Portifolio/internal/handlers" + "Portifolio/internal/shell" + "bufio" + "database/sql" + "fmt" + "log" + "net/http" + "os" + "strings" + + _ "github.com/mattn/go-sqlite3" +) + +var db *sql.DB func main() { - fmt.Println("hello world") + var err error + db, err = sql.Open("sqlite3", "./app.db?_foreign_keys=on") + if err != nil { + log.Fatal("Failed to open database:", err) + } + defer db.Close() + + if err = db.Ping(); err != nil { + log.Fatal("Failed to connect to database:", err) + } + + database.InitDB(db) + fmt.Println("Connected to SQLite database") + + http.HandleFunc("/health", handlers.HealthHandler(db)) + http.HandleFunc("/add/company", handlers.AddCompanyHandler(db)) + + fmt.Println("Server running on :8080") + go func() { + log.Fatal(http.ListenAndServe(":8080", nil)) + }() + + runShell(db) +} + +func runShell(db *sql.DB) { + scanner := bufio.NewScanner(os.Stdin) + fmt.Println("\nShell ready. Commands: add-company, help, exit") + + for { + fmt.Print("> ") + if !scanner.Scan() { + break + } + + parts := strings.Fields(scanner.Text()) + if len(parts) == 0 { + continue + } + + switch parts[0] { + case "add-company": + shell.AddCompany(scanner, db) + case "add-currency": + shell.AddCurrency(scanner, db) + case "list-currency": + shell.ListCurrencies(db) + + case "help": + fmt.Println("Commands:") + fmt.Println(" add-company - add a new company interactively") + fmt.Println(" add-currency - add a new currency interactively") + fmt.Println(" list-currency - lists all currencies") + + fmt.Println(" exit - quit") + + case "exit": + fmt.Println("Bye!") + os.Exit(0) + + default: + fmt.Printf("Unknown command: %s. Type 'help' for commands.\n", parts[0]) + } + } }