From 463476bdc85484b6d61496e3cc304db52c28c0ac Mon Sep 17 00:00:00 2001 From: tavo-wasd Date: Sat, 31 Aug 2024 15:56:52 -0600 Subject: [PATCH] server, still need to correct public --- .env.example | 17 +- .gitignore | 4 - Makefile | 13 +- README.org | 195 ++++++++++++++++++++ main.go | 481 ------------------------------------------------- server/main.go | 388 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 603 insertions(+), 495 deletions(-) create mode 100644 README.org delete mode 100644 main.go create mode 100644 server/main.go diff --git a/.env.example b/.env.example index feb638f..e7539ba 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,15 @@ -BASE_URL="" +BASE_URL="https://api-m.sandbox.paypal.com" +PORT="8080" + CLIENT_ID="" CLIENT_SECRET="" -PLAN_ID="" -RETURN_URL="https://builder.conex.one/?success" -CANCEL_URL="https://builder.conex.one/?cancel" +RETURN_URL="http://localhost:8080/?success" +CANCEL_URL="http://localhost:8080/?cancel" + +DB_HOST="localhost" +DB_PORT="5432" +DB_USER="conex" +DB_PASS="1234" +DB_NAME="iterone" + +PRICE="20.00" diff --git a/.gitignore b/.gitignore index 2ac10f8..0358852 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,2 @@ builder -bin -go.mod -go.sum -public/submit/* .env diff --git a/Makefile b/Makefile index 87b70e6..def6325 100644 --- a/Makefile +++ b/Makefile @@ -1,16 +1,17 @@ BIN = builder -SRC = main.go -GOFILES = go.sum go.mod +SRCDIR = server +SRC = ${SRCDIR}/main.go +GOFILES = ${SRCDIR}/go.sum ${SRCDIR}/go.mod GOMODS = github.com/joho/godotenv github.com/lib/pq all: ${BIN} ${BIN}: ${SRC} ${GOFILES} - go build -o builder + (cd ${SRCDIR} && go build -o ../${BIN}) ${GOFILES}: - go mod init ${BIN} - go get ${GOMODS} + (cd ${SRCDIR} && go mod init ${BIN}) + (cd ${SRCDIR} && go get ${GOMODS}) start: ${BIN} @./$< & @@ -26,4 +27,4 @@ clean: rm -f ${BIN} clean-mods: - rm -f go.* + rm -f ${SRCDIR}/go.* diff --git a/README.org b/README.org new file mode 100644 index 0000000..43a739e --- /dev/null +++ b/README.org @@ -0,0 +1,195 @@ +#+TITLE: Conex Builder Documentation +#+PROPERTY: header-args:sql :engine postgres :dbhost "localhost" :dbport 5432 :dbuser "conex" :dbpassword "1234" :database "iterone" + +* Database + +** Create DB + +#+begin_src sh +sudo su - postgres +psql +#+end_src + +Then: + +#+BEGIN_SRC sql +CREATE DATABASE iterone OWNER conex; +#+END_SRC + +** Sites table + +#+BEGIN_SRC sql :results silent +DROP TABLE IF EXISTS changes; +DROP TABLE IF EXISTS payments; +DROP TABLE IF EXISTS sites; + +CREATE TABLE sites ( + id SERIAL PRIMARY KEY, + folder VARCHAR(35) UNIQUE NOT NULL, + status VARCHAR(4), + due TIMESTAMPTZ NOT NULL, + name VARCHAR(50), + sur VARCHAR(50), + email VARCHAR(100) NOT NULL, + phone VARCHAR(20), + code VARCHAR(2) +); +#+END_SRC + +#+BEGIN_SRC sql :results silent +INSERT INTO sites (folder, status, due, name, sur, email, phone, code) +VALUES ('athos', 'up', '2025-08-31T20:26:58Z', 'John', 'Doe', 'john@doe', '8888-8888', 'CR'); +#+END_SRC + +#+BEGIN_SRC sql +SELECT * FROM sites; +#+END_SRC + +#+RESULTS: +| id | folder | status | due | name | sur | email | phone | code | +|----+-----------+--------+------------------------+------+-----+---------------------------------------+------------+------| +| 1 | athos | up | 2026-08-31 15:27:00-06 | John | Doe | sb-8kx8c32267916@personal.example.com | 5068031951 | CR | +| 2 | gofitness | up | 2025-08-31 15:29:01-06 | John | Doe | sb-8kx8c32267916@personal.example.com | 5068031951 | CR | + +** Payments table + +#+BEGIN_SRC sql :results silent +DROP TABLE IF EXISTS changes; +DROP TABLE IF EXISTS payments; + +CREATE TABLE payments ( + id SERIAL PRIMARY KEY, + capture VARCHAR(100) NOT NULL, + site INTEGER REFERENCES sites(id) NOT NULL, + amount DECIMAL(10, 2) NOT NULL, + currency VARCHAR(3) NOT NULL, + status VARCHAR(18) NOT NULL, -- PayPal capture status length -- https://developer.paypal.com/docs/api/orders/v2/#orders_capture + date DATE NOT NULL +); +#+END_SRC + +#+BEGIN_SRC sql :results silent +INSERT INTO payments (capture, site, amount, currency, date, status) +VALUES ('5PS47268T4115691X', 1, 20.00, 'USD', '2024-08-30', 'COMPLETED'); +#+END_SRC + +#+BEGIN_SRC sql +SELECT * FROM payments; +#+END_SRC + +#+RESULTS: +| id | capture | site | amount | currency | status | date | +|----+-------------------+------+--------+----------+-----------+------------| +| 1 | 6H6838025H7236834 | 1 | 20.00 | USD | COMPLETED | 2024-08-31 | +| 2 | 48H30563GU472432N | 1 | 20.00 | USD | COMPLETED | 2024-08-31 | +| 3 | 3UD50608FD4050042 | 2 | 20.00 | USD | COMPLETED | 2024-08-31 | + +** Changes table + +#+BEGIN_SRC sql :results silent +DROP TABLE IF EXISTS changes; + +CREATE TABLE changes ( + id INTEGER PRIMARY KEY, + by VARCHAR(20) NOT NULL, + site INTEGER REFERENCES sites(id), + payment INTEGER REFERENCES payments(id), + col VARCHAR(6) NOT NULL, + prev VARCHAR(8) NOT NULL, + next VARCHAR(8) NOT NULL, + date DATE NOT NULL +); +#+END_SRC + +#+BEGIN_SRC sql +SELECT * FROM changes; +#+END_SRC + +#+RESULTS: +| id | by | site | payment | col | prev | next | date | +|----+----+------+---------+-----+------+------+------| + +** Types of changes + +*** Payments + +- status: complete/refunded/modified +- amount: prev/next + +*** Sites + +- folder: prev/next +- status: up/down +- due: date/date +- name: prev/next +- sur: prev/next +- email: prev/next +- phone: prev/next +- code: prev/next + +* Error codes + +** http.Error + +** Fatalf + +Fatal error will cause program shutdown by calling ~os.Exit(1)~. + +*** Error 000: Missing credentials + +*Package*: ~main~ +*Function*: ~init()~ +*Libraries*: ~os~, ~log~, ~github.com/joho/godotenv~ + +Authentication and other parameters are located in the ~.env~ file which mist be +located at the root of main binary execution. + +Possible causes for error are: + +- Binary execution directory doesn't have the ~.env~ file +- Missing parameters for initializing environment values +- Corruption of ~.env~ file +- Library error + +Steps to troubleshoot: + +1. Check ~.env~ exists +2. Check ~.env~ authentication values +3. Check ~.env~ file integrity +4. Update, rollback or troubleshoot library + +*** Error 001: Can't connect to database + +*Package*: ~main~ +*Function*: ~init()~ +*Libraries*: ~os~, ~log~ + +The ~db~ object manages database queries. This object is used to ping the +database, a correct ping depends on correctly set credentials, and properly +initialized ~db~ object. + +Possible causes for error are: + +- Wrong database credentials +- Missing database credentials + +Steps to troubleshoot: + +1. Check set, correct and valid credentials in ~.env~ file + +*** Error: 002: Can't start server + +*Package*: ~main~ +*Function*: ~main()~ +*Libraries*: ~os~, ~log~, ~net/http~, ~os/signal~ + +The server runs in a Goroutine, started on a port defined in ~.env~. + +Possible causes for error are: + +- Port is in use +- Port usage denied + +Steps to troubleshoot: + +1. Check set, correct and valid port in ~.env~ file diff --git a/main.go b/main.go deleted file mode 100644 index 834dbff..0000000 --- a/main.go +++ /dev/null @@ -1,481 +0,0 @@ -package main - -import ( - "bytes" - "database/sql" - "encoding/json" - "fmt" - "io/ioutil" - "log" - "net/http" - "os" - "os/signal" - "strings" - "syscall" - "time" - - "github.com/joho/godotenv" - _ "github.com/lib/pq" -) - -var ( - baseURL string - clientID string - clientSecret string - planID string - returnUrl string - cancelUrl string - - exists bool - err error - - dbHost string - dbPort string - dbUser string - dbPass string - dbName string - db *sql.DB - query string -) - -func init() { - // Load .env - if err := godotenv.Load(); err != nil { - log.Fatalf("Error loading .env file: %v", err) - } - - // Set variables - baseURL = os.Getenv("BASE_URL") - clientID = os.Getenv("CLIENT_ID") - clientSecret = os.Getenv("CLIENT_SECRET") - planID = os.Getenv("PLAN_ID") - returnUrl = os.Getenv("RETURN_URL") - cancelUrl = os.Getenv("CANCEL_URL") - // DB creds - dbHost = os.Getenv("DB_HOST") - dbPort = os.Getenv("DB_PORT") - dbUser = os.Getenv("DB_USER") - dbPass = os.Getenv("DB_PASS") - dbName = os.Getenv("DB_NAME") - - // Error if empty - if baseURL == "" || clientID == "" || clientSecret == "" || planID == "" || returnUrl == "" || cancelUrl == "" || - dbHost == "" || dbPort == "" || dbUser == "" || dbPass == "" || dbName == "" { - log.Fatalf("Error setting credentials") - } - - // Connect to DB - var err error - connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", - dbHost, dbPort, dbUser, dbPass, dbName) - db, err = sql.Open("postgres", connStr) - if err != nil { - log.Fatalf("Failed to connect to database: %v", err) - } - - // Ping DB - if err = db.Ping(); err != nil { - log.Fatalf("Failed to ping database: %v", err) - } -} - -type CreateOrderResponse struct { - ID string `json:"id"` -} - -type OrderResponse struct { - ID string `json:"id"` - Status string `json:"status"` - PurchaseUnits []struct { - Payments struct { - Captures []struct { - ID string `json:"id"` - Status string `json:"status"` - CreateTime time.Time `json:"create_time"` - } `json:"captures"` - } `json:"payments"` - } `json:"purchase_units"` - Payer struct { - Name struct { - GivenName string `json:"given_name"` - Surname string `json:"surname"` - } `json:"name"` - EmailAddress string `json:"email_address"` - Phone struct { - PhoneType string `json:"phone_type"` - PhoneNumber struct { - NationalNumber string `json:"national_number"` - } `json:"phone_number"` - } `json:"phone"` - Address struct { - CountryCode string `json:"country_code"` - } `json:"address"` - } `json:"payer"` -} - -type SubscriptionResponse struct { - Status string `json:"status"` - StatusUpdateTime time.Time `json:"status_update_time"` - StartTime time.Time `json:"start_time"` - Subscriber struct { - Name struct { - GivenName string `json:"given_name"` - Surname string `json:"surname"` - } `json:"name"` - EmailAddress string `json:"email_address"` - } `json:"subscriber"` - CreateTime time.Time `json:"create_time"` -} - -type Cart struct { - Directory string `json:"directory"` -} - -func main() { - http.HandleFunc("/api/order", CreateOrder) - http.HandleFunc("/api/order/", CaptureOrder) - http.HandleFunc("/api/paypal/subscribe", CreateSubscription) - http.HandleFunc("/api/paypal/subscribe/", CaptureSubscription) - http.Handle("/", http.FileServer(http.Dir("./public"))) - - // Channel to listen for signals - stop := make(chan os.Signal, 1) - signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM) - - // Run the server in a goroutine so that it doesn't block - go func() { - log.Println("Starting server on :8080...") - if err := http.ListenAndServe(":8080", nil); err != nil { - log.Fatalf("Could not listen on :8080: %v\n", err) - } - }() - - <-stop // Shutdown signal recieved - log.Println("Server shutdown gracefully.") -} - -func Token() (string, error) { - // Create - req, err := http.NewRequest("POST", baseURL+"/v1/oauth2/token", strings.NewReader(`grant_type=client_credentials`)) - if err != nil { - return "", fmt.Errorf("Error creating request: %v", err) - } - - // Send - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - req.SetBasicAuth(os.Getenv("CLIENT_ID"), os.Getenv("CLIENT_SECRET")) - resp, err := http.DefaultClient.Do(req) - if err != nil { - return "", fmt.Errorf("Error sending request: %v", err) - } - defer resp.Body.Close() - - // Decode - var result struct { - AccessToken string `json:"access_token"` - } - if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { - return "", fmt.Errorf("Error decoding response: %v", err) - } - - // Return - return result.AccessToken, nil -} - -func RegisterOrder(order OrderResponse, directory string) { - var ( - capture string - status string - name string - surname string - email string - phone string - country string - date time.Time - ) - - for _, Unit := range order.PurchaseUnits { - for _, Capture := range Unit.Payments.Captures { - capture = Capture.ID - status = Capture.Status - date = Capture.CreateTime - } - } - - name = order.Payer.Name.GivenName - surname = order.Payer.Name.Surname - email = order.Payer.EmailAddress - phone = order.Payer.Phone.PhoneNumber.NationalNumber - country = order.Payer.Address.CountryCode - - // Register Payment - _, err = db.Exec(`INSERT INTO payments (id, client, directory, status, step, date) VALUES ($1, $2, $3, $4, $5, $6);`, - capture, email, directory, status, "REGISTERED", date) - if err != nil { - fmt.Printf("$v", err) // TODO consider logging in server - } - - // Register Client - err = db.QueryRow(`SELECT EXISTS(SELECT 1 FROM clients WHERE email = $1);`, email).Scan(&exists) - if err != nil { - fmt.Printf("$v", err) // TODO consider logging in server - } - if !exists { - _, err = db.Exec(`INSERT INTO clients (email, name, surname, phone, country) VALUES ($1, $2, $3, $4, $5);`, - email, name, surname, phone, country) - if err != nil { - fmt.Printf("$v", err) // TODO consider logging in server - } - } - - // Register Site - _, err = db.Exec(`INSERT INTO sites (directory, client, status, ends) VALUES ($1, $2, $3, $4);`, - directory, email, "ACTIVE", date.AddDate(1, 0, 0)) // Ends a year later - if err != nil { - fmt.Printf("$v", err) // TODO consider logging in server - } -} - -func CreateOrder(w http.ResponseWriter, r *http.Request) { - token, err := Token() - if err != nil { - http.Error(w, "Failed to get access token", http.StatusInternalServerError) - return - } - - data := `{ - "intent": "CAPTURE", - "purchase_units": [{ - "amount": { - "currency_code": "USD", - "value": "20.00" - } - }], - "payment_source": { - "paypal": { - "address" : { - "country_code": "CR" - } - } - }, - "application_context": { - "shipping_preference": "NO_SHIPPING" - } - }` - - // Create - req, err := http.NewRequest("POST", baseURL+"/v2/checkout/orders", bytes.NewBuffer([]byte(data))) - if err != nil { - http.Error(w, "Failed to create request", http.StatusInternalServerError) - return - } - - // Send - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+token) - resp, err := http.DefaultClient.Do(req) - if err != nil { - http.Error(w, "Failed to send request", http.StatusInternalServerError) - return - } - defer resp.Body.Close() - - // Decode - var result CreateOrderResponse - if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { - http.Error(w, "Failed to decode response", http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]string{"id": result.ID}) - return -} - -func CaptureOrder(w http.ResponseWriter, r *http.Request) { - // Read body from order - body, err := ioutil.ReadAll(r.Body) - if err != nil { - http.Error(w, "Failed to read request body", http.StatusInternalServerError) - return - } - - // Parse to get directory - var cart Cart - err = json.Unmarshal(body, &cart) - if err != nil { - http.Error(w, "Failed to parse request body", http.StatusBadRequest) - return - } - directory := cart.Directory - - // Get orderID - path := strings.TrimPrefix(r.URL.Path, "/api/orders/") - parts := strings.Split(path, "/") - orderID := parts[0] - if orderID == "" { - http.Error(w, "Failed to get orderID from client URL", http.StatusInternalServerError) - return - } - - token, err := Token() - if err != nil { - http.Error(w, "Failed to get access token", http.StatusInternalServerError) - return - } - - // Create - req, err := http.NewRequest("POST", baseURL+"/v2/checkout/orders/"+orderID+"/capture", nil) - if err != nil { - http.Error(w, "Failed to create request", http.StatusInternalServerError) - return - } - - // Check if directory already exists - err = db.QueryRow("SELECT EXISTS (SELECT 1 FROM sites WHERE directory = $1 LIMIT 1);", directory).Scan(&exists) - if err != nil { - http.Error(w, "Failed to check directory ID against database", http.StatusBadRequest) - return - } - if exists { - http.Error(w, "This directory ID is already taken", http.StatusBadRequest) - return - } - - // Send, PAYMENT MADE HERE - req.Header.Set("Authorization", "Bearer "+token) - req.Header.Set("Content-Type", "application/json") - resp, err := http.DefaultClient.Do(req) - if err != nil { - http.Error(w, "Failed to send request", http.StatusInternalServerError) - return - } - defer resp.Body.Close() - - // Decode - var order OrderResponse - if err := json.NewDecoder(resp.Body).Decode(&order); err != nil { - http.Error(w, "Failed to decode response", http.StatusInternalServerError) - return - } - - RegisterOrder(order, directory) - - // Respond - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(order); err != nil { - http.Error(w, "Failed to encode response", http.StatusInternalServerError) - } -} - -func CreateSubscription(w http.ResponseWriter, r *http.Request) { - // Read body from order - body, err := ioutil.ReadAll(r.Body) - if err != nil { - http.Error(w, "Failed to read request body", http.StatusInternalServerError) - return - } - - // Parse to get directory - var cart Cart - err = json.Unmarshal(body, &cart) - if err != nil { - http.Error(w, "Failed to parse request body", http.StatusBadRequest) - return - } - directory := cart.Directory - - token, err := Token() - if err != nil { - http.Error(w, "Failed to get access token", http.StatusInternalServerError) - return - } - - payload := map[string]interface{}{ - "plan_id": planID, - "application_context": map[string]string{ - "shipping_preference": "NO_SHIPPING", - "return_url": returnUrl, - "cancel_url": cancelUrl, - }, - } - jsonData, err := json.Marshal(payload) - if err != nil { - http.Error(w, "Server error", http.StatusInternalServerError) - return - } - - // Create request - log.Printf("Creating request") - req, err := http.NewRequest("POST", baseURL+"/v1/billing/subscriptions", bytes.NewBuffer(jsonData)) - if err != nil { - http.Error(w, "Failed to create request", http.StatusInternalServerError) - return - } - - // Check if directory already exists - err = db.QueryRow("SELECT EXISTS (SELECT 1 FROM sites WHERE directory = $1 LIMIT 1);", directory).Scan(&exists) - if err != nil { - http.Error(w, "Failed to check directory ID against database", http.StatusBadRequest) - return - } - if exists { - http.Error(w, "This directory ID is already taken", http.StatusBadRequest) - return - } - - // Send - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+token) - req.Header.Set("Accept", "application/json") - req.Header.Set("Prefer", "return=representation") - resp, err := http.DefaultClient.Do(req) - if err != nil { - http.Error(w, "Failed to send request", http.StatusInternalServerError) - return - } - defer resp.Body.Close() - - // Decode - var result map[string]interface{} - if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { - http.Error(w, "Failed to decode response", http.StatusInternalServerError) - return - } - - // Respond - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(result); err != nil { - http.Error(w, "Failed to encode response", http.StatusInternalServerError) - } - log.Printf("sent response to client") -} - -// Capture just like CaptureOrder, but with response from paypal -// https://developer.paypal.com/docs/api/subscriptions/v1/#subscriptions_get -func CaptureSubscription(w http.ResponseWriter, r *http.Request) { - // Read body from order - body, err := ioutil.ReadAll(r.Body) - if err != nil { - http.Error(w, "Failed to read request body", http.StatusInternalServerError) - return - } - - // Parse to get directory - var cart Cart - err = json.Unmarshal(body, &cart) - if err != nil { - http.Error(w, "Failed to parse request body", http.StatusBadRequest) - return - } - // directory := cart.Directory - - // Get subID - path := strings.TrimPrefix(r.URL.Path, "/api/subscribe/") - parts := strings.Split(path, "/") - subID := parts[0] - if subID == "" { - http.Error(w, "Failed to get subID from client URL", http.StatusInternalServerError) - return - } -} diff --git a/server/main.go b/server/main.go new file mode 100644 index 0000000..7d23af2 --- /dev/null +++ b/server/main.go @@ -0,0 +1,388 @@ +package main + +import ( + "bytes" + "database/sql" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "os" + "os/signal" + "strings" + "syscall" + "time" + + "github.com/joho/godotenv" + _ "github.com/lib/pq" +) + +var db *sql.DB + +type Capture struct { + ID string `json:"id"` + Status string `json:"status"` + PurchaseUnits []struct { + Payments struct { + Captures []struct { + ID string `json:"id"` + Status string `json:"status"` + Amount struct { + CurrencyCode string `json:"currency_code"` + Value string `json:"value"` + } `json:"amount"` + CreateTime time.Time `json:"create_time"` + } `json:"captures"` + } `json:"payments"` + } `json:"purchase_units"` + Payer struct { + Name struct { + GivenName string `json:"given_name"` + Surname string `json:"surname"` + } `json:"name"` + EmailAddress string `json:"email_address"` + Phone struct { + PhoneType string `json:"phone_type"` + PhoneNumber struct { + NationalNumber string `json:"national_number"` + } `json:"phone_number"` + } `json:"phone"` + Address struct { + CountryCode string `json:"country_code"` + } `json:"address"` + } `json:"payer"` +} + +func init() { + godotenv.Load() + + if os.Getenv("BASE_URL") == "" || + os.Getenv("CLIENT_ID") == "" || + os.Getenv("CLIENT_SECRET") == "" || + os.Getenv("RETURN_URL") == "" || + os.Getenv("CANCEL_URL") == "" || + os.Getenv("PORT") == "" { + log.Fatalf("Error 000: Missing credentials") + } + + var err error + db, err = sql.Open("postgres", "host="+os.Getenv("DB_HOST")+ + " port="+os.Getenv("DB_PORT")+ + " user="+os.Getenv("DB_USER")+ + " password="+os.Getenv("DB_PASS")+ + " dbname="+os.Getenv("DB_NAME")) + if err != nil { + log.Fatalf("Error 001: Can't connect to database: %v", err) + } + + if err := db.Ping(); err != nil { + log.Fatalf("Error 001: Can't connect to database: %v", err) + } + + log.Println("Established database connection") +} + +func main() { + http.HandleFunc("/api/orders", CreateOrder) + http.HandleFunc("/api/orders/", CaptureOrder) + http.Handle("/", http.FileServer(http.Dir("./public"))) + + stop := make(chan os.Signal, 1) + signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM) + port := os.Getenv("PORT") + + go func() { + log.Println("Starting server on " + port + "...") + if err := http.ListenAndServe(":"+port, nil); err != nil { + log.Fatalf("Error: 002: Can't start server: %v\n", err) + } + }() + + <-stop + + defer func() { + if db != nil { + if err := db.Close(); err != nil { + log.Fatalf("Error: Can't close database connection: %v", err) + } + } + }() + log.Println("Server shutdown gracefully.") +} + +func Token() (string, error) { + req, err := http.NewRequest("POST", + os.Getenv("BASE_URL")+"/v1/oauth2/token", + strings.NewReader(`grant_type=client_credentials`)) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.SetBasicAuth(os.Getenv("CLIENT_ID"), os.Getenv("CLIENT_SECRET")) + + raw, err := http.DefaultClient.Do(req) + if err != nil { + return "", fmt.Errorf("Error sending request: %v", err) + } + defer raw.Body.Close() + + var response struct { + AccessToken string `json:"access_token"` + } + + if err := json.NewDecoder(raw.Body).Decode(&response); err != nil { + return "", fmt.Errorf("Error decoding response: %v", err) + } + + return response.AccessToken, nil +} + +func RegisterOrder(capture Capture, directory string) { + var ( + // Payment + id string + amount string + currency string + pstatus string + date time.Time + // Website + wstatus string + due time.Time + name string + surname string + email string + phone string + country string + ) + + id = capture.PurchaseUnits[0].Payments.Captures[0].ID + amount = capture.PurchaseUnits[0].Payments.Captures[0].Amount.Value + currency = capture.PurchaseUnits[0].Payments.Captures[0].Amount.CurrencyCode + pstatus = capture.PurchaseUnits[0].Payments.Captures[0].Status + date = capture.PurchaseUnits[0].Payments.Captures[0].CreateTime + wstatus = "up" + due = date.AddDate(1, 0, 0) + name = capture.Payer.Name.GivenName + surname = capture.Payer.Name.Surname + email = capture.Payer.EmailAddress + phone = capture.Payer.Phone.PhoneNumber.NationalNumber + country = capture.Payer.Address.CountryCode + + var pkey int + + newSite := db.QueryRow(`SELECT id FROM sites WHERE folder = $1`, directory).Scan(&pkey) + + if newSite == sql.ErrNoRows { + if err := db.QueryRow( + `INSERT INTO sites (folder, status, due, name, sur, email, phone, code) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + RETURNING id`, + directory, wstatus, due, + name, surname, email, phone, country).Scan(&pkey); err != nil { + log.Printf("Error: Could not register site to database: %v", err) + return + } + } else { + if err := db.QueryRow( + `UPDATE sites SET due = due + INTERVAL '1 year' + WHERE id = $1 + RETURNING id`, + pkey).Scan(&pkey); err != nil { + log.Fatalf("Error: Could not update due date: %v", err) + return + } + } + + if _, err := db.Exec( + `INSERT INTO payments (capture, site, amount, currency, date, status) + VALUES ($1, $2, $3, $4, $5, $6)`, + id, pkey, amount, currency, date, pstatus); err != nil { + log.Printf("Error: Could not register payment to database: %v", err) + return + } + + return +} + +func CreateOrder(w http.ResponseWriter, r *http.Request) { + token, err := Token() + if err != nil { + http.Error(w, + "Failed to get access token", + http.StatusInternalServerError) + return + } + + type Amount struct { + CurrencyCode string `json:"currency_code"` + Value string `json:"value"` + } + type PurchaseUnits struct { + Amount Amount `json:"amount"` + } + type Address struct { + CountryCode string `json:"country_code"` + } + type Paypal struct { + Address Address `json:"address"` + } + type PaymentSource struct { + Paypal Paypal `json:"paypal"` + } + type ApplicationContext struct { + ShippingPreference string `json:"shipping_preference"` + } + type Order struct { + Intent string `json:"intent"` + PurchaseUnits []PurchaseUnits `json:"purchase_units"` + PaymentSource PaymentSource `json:"payment_source"` + ApplicationContext ApplicationContext `json:"application_context"` + } + + // This payload will fill out defaults in PayPal + // checkout window (CR code, no shipping, etc) + order := Order{ + Intent: "CAPTURE", + PurchaseUnits: []PurchaseUnits{{Amount: Amount{ + CurrencyCode: "USD", + Value: os.Getenv("PRICE"), + }}}, + PaymentSource: PaymentSource{Paypal: Paypal{Address: Address{ + CountryCode: "CR", + }}}, + ApplicationContext: ApplicationContext{ + ShippingPreference: "NO_SHIPPING", + }, + } + + payload, err := json.Marshal(order) + req, err := http.NewRequest("POST", + os.Getenv("BASE_URL")+"/v2/checkout/orders", + bytes.NewBuffer(payload)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+token) + + raw, err := http.DefaultClient.Do(req) + if err != nil { + http.Error(w, + "Failed to send request", + http.StatusInternalServerError) + return + } + defer raw.Body.Close() + + var response struct { + ID string `json:"id"` + } + + if err := json.NewDecoder(raw.Body).Decode(&response); err != nil { + http.Error(w, + "Failed to decode response", + http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + return +} + +func CaptureOrder(w http.ResponseWriter, r *http.Request) { + info, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, + "Failed to read request body", + http.StatusInternalServerError) + return + } + + var cart struct { + Directory string `json:"directory"` + } + + err = json.Unmarshal(info, &cart) + if err != nil { + http.Error(w, + "Failed to parse request body", + http.StatusBadRequest) + return + } + directory := cart.Directory + + path := strings.TrimPrefix(r.URL.Path, "/api/orders/") + parts := strings.Split(path, "/") + orderID := parts[0] + if orderID == "" { + http.Error(w, + "Failed to get orderID from client URL", + http.StatusInternalServerError) + return + } + + token, err := Token() + if err != nil { + http.Error(w, + "Failed to get access token", + http.StatusInternalServerError) + return + } + + req, err := http.NewRequest("POST", + os.Getenv("BASE_URL")+"/v2/checkout/orders/"+orderID+"/capture", + nil) + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", "application/json") + + raw, err := http.DefaultClient.Do(req) + if err != nil { + http.Error(w, + "Failed to send request", + http.StatusInternalServerError) + return + } + defer raw.Body.Close() + + body, err := io.ReadAll(raw.Body) + if err != nil { + http.Error(w, + "Failed to read response body", + http.StatusInternalServerError) + return + } + + var capture Capture + if err := json.Unmarshal(body, &capture); err != nil { + http.Error(w, + "Failed to decode response into capture", + http.StatusInternalServerError) + return + } + + var receipt = struct { + PurchaseUnits []struct { + Payments struct { + Captures []struct { + ID string `json:"id"` + Status string `json:"status"` + } `json:"captures"` + } `json:"payments"` + } `json:"purchase_units"` + }{} + + if err := json.Unmarshal(body, &receipt); err != nil { + http.Error(w, + "Failed to decode response into receipt", + http.StatusInternalServerError) + return + } + + RegisterOrder(capture, directory) + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(receipt); err != nil { + http.Error(w, + "Failed to encode response", + http.StatusInternalServerError) + return + } + + return +}