From 7c50792f269ed1affb2bd9c973cd04986a112930 Mon Sep 17 00:00:00 2001 From: tavo-wasd Date: Sun, 1 Sep 2024 01:22:21 -0600 Subject: [PATCH] separate in files --- README.org | 6 +- server/db.go | 78 ++++++++++ server/init.go | 49 +++++++ server/main.go | 371 +---------------------------------------------- server/paypal.go | 265 +++++++++++++++++++++++++++++++++ 5 files changed, 398 insertions(+), 371 deletions(-) create mode 100644 server/db.go create mode 100644 server/init.go create mode 100644 server/paypal.go diff --git a/README.org b/README.org index d5f622d..30f84dc 100644 --- a/README.org +++ b/README.org @@ -47,9 +47,9 @@ SELECT * FROM sites; #+END_SRC #+RESULTS: -| id | folder | status | due | name | sur | email | phone | code | raw | -|----+-----------+--------+------------------------+------+-----+---------------------------------------+------------+------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| 2 | gofitness | up | 2025-09-01 00:18:06-06 | John | Doe | sb-8kx8c32267916@personal.example.com | 5068031951 | CR | {"time": 1725171485533, "blocks": [{"id": "hLu8z-l1u5", "data": {"text": "asdfasfasdf", "level": 2}, "type": "header"}, {"id": "frc77fNxnu", "data": {"text": "asdfasfasdf"}, "type": "paragraph"}], "version": "2.30.5"} | +| id | folder | status | due | name | sur | email | phone | code | raw | +|----+-----------+--------+------------------------+------+-----+---------------------------------------+------------+------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 1 | gofitness | down | 2025-09-01 01:18:08-06 | John | Doe | sb-8kx8c32267916@personal.example.com | 5068031951 | CR | {"time": 1725175087581, "blocks": [{"id": "dGd_EGtUWN", "data": {"text": "asdfasdfasdfasdf", "level": 2}, "type": "header"}, {"id": "z9UoyyRH_4", "data": {"text": "asdfasdfasdfasdfa Hi
"}, "type": "paragraph"}], "version": "2.30.5"} | ** Payments table diff --git a/server/db.go b/server/db.go new file mode 100644 index 0000000..4d465b7 --- /dev/null +++ b/server/db.go @@ -0,0 +1,78 @@ +package main + +import ( + "database/sql" + "encoding/json" + "log" + "time" + + _ "github.com/lib/pq" +) + +func RegisterSite(capture Capture, directory string, editorData json.RawMessage) { + 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 = "down" + 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, raw) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + RETURNING id`, + directory, wstatus, due, + name, surname, email, phone, country, + editorData).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 +} diff --git a/server/init.go b/server/init.go new file mode 100644 index 0000000..3e7c621 --- /dev/null +++ b/server/init.go @@ -0,0 +1,49 @@ +package main + +import ( + "database/sql" + "log" + "os" + + "github.com/joho/godotenv" + _ "github.com/lib/pq" +) + +var db *sql.DB + +func initialize() { + 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 shutdown() { + if db != nil { + if err := db.Close(); err != nil { + log.Fatalf("Error: Can't close database connection: %v", err) + } + } +} diff --git a/server/main.go b/server/main.go index b401b46..36aed65 100644 --- a/server/main.go +++ b/server/main.go @@ -1,89 +1,16 @@ 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() { + initialize() + http.HandleFunc("/api/orders", CreateOrder) http.HandleFunc("/api/orders/", CaptureOrder) http.Handle("/", http.FileServer(http.Dir("./public"))) @@ -101,298 +28,6 @@ func main() { <-stop - defer func() { - if db != nil { - if err := db.Close(); err != nil { - log.Fatalf("Error: Can't close database connection: %v", err) - } - } - }() + shutdown() 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, editorData json.RawMessage) { - 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 = "down" - 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, raw) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) - RETURNING id`, - directory, wstatus, due, - name, surname, email, phone, country, - editorData).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"` - EditorData json.RawMessage `json:"editor_data"` - } - - err = json.Unmarshal(info, &cart) - if err != nil { - http.Error(w, - "Failed to parse request body", - http.StatusBadRequest) - return - } - - directory := cart.Directory - editorData := cart.EditorData - if err != nil { - http.Error(w, - "Failed to parse request body", - http.StatusBadRequest) - return - } - - 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, editorData) - - 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 -} diff --git a/server/paypal.go b/server/paypal.go new file mode 100644 index 0000000..ce353e9 --- /dev/null +++ b/server/paypal.go @@ -0,0 +1,265 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "strings" + "time" +) + +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 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 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"` + EditorData json.RawMessage `json:"editor_data"` + } + + err = json.Unmarshal(info, &cart) + if err != nil { + http.Error(w, + "Failed to parse request body", + http.StatusBadRequest) + return + } + + directory := cart.Directory + editorData := cart.EditorData + if err != nil { + http.Error(w, + "Failed to parse request body", + http.StatusBadRequest) + return + } + + 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 + } + + RegisterSite(capture, directory, editorData) + + 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 +} +