diff --git a/Makefile b/Makefile index def6325..511f3c8 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,10 @@ BIN = builder SRCDIR = server -SRC = ${SRCDIR}/main.go +SRC = ${SRCDIR}/init.go \ + ${SRCDIR}/main.go \ + ${SRCDIR}/paypal.go \ + ${SRCDIR}/db.go \ + GOFILES = ${SRCDIR}/go.sum ${SRCDIR}/go.mod GOMODS = github.com/joho/godotenv github.com/lib/pq diff --git a/server/db.go b/server/db.go index 4d465b7..75c7fa5 100644 --- a/server/db.go +++ b/server/db.go @@ -3,13 +3,13 @@ package main import ( "database/sql" "encoding/json" - "log" + "fmt" "time" _ "github.com/lib/pq" ) -func RegisterSite(capture Capture, directory string, editorData json.RawMessage) { +func RegisterSitePayment(capture Capture, directory string, editorData json.RawMessage) error { var ( // Payment id string @@ -41,7 +41,6 @@ func RegisterSite(capture Capture, directory string, editorData json.RawMessage) 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 { @@ -52,8 +51,7 @@ func RegisterSite(capture Capture, directory string, editorData json.RawMessage) 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 + return fmt.Errorf("Error: Could not register site to database: %v", err) } } else { if err := db.QueryRow( @@ -61,8 +59,7 @@ func RegisterSite(capture Capture, directory string, editorData json.RawMessage) WHERE id = $1 RETURNING id`, pkey).Scan(&pkey); err != nil { - log.Fatalf("Error: Could not update due date: %v", err) - return + return fmt.Errorf("Error: Could not update due date: %v", err) } } @@ -70,9 +67,47 @@ func RegisterSite(capture Capture, directory string, editorData json.RawMessage) `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 fmt.Errorf("Error: Could not register payment to database: %v", err) } - return + return nil +} + +func UpdateSite(by string, pkey int, editorData json.RawMessage) error { + tx, err := db.Begin() + if err != nil { + return fmt.Errorf("Error: Could not start transaction: %v", err) + } + + defer func() { + if err != nil { + tx.Rollback() + } + }() + + var prev json.RawMessage + if err := tx.QueryRow( + `SELECT raw FROM sites WHERE id = $1`, + pkey).Scan(&prev); err != nil { + return fmt.Errorf("Error: Could not retrieve old value: %v", err) + } + + if _, err = tx.Exec( + `UPDATE sites SET raw = $1 WHERE id = $2`, + editorData, pkey); err != nil { + return fmt.Errorf("Error: Could not update raw column: %v", err) + } + + if _, err = tx.Exec( + `INSERT INTO changes (by, site, payment, col, prev, next, date) + VALUES ($1, $2, NULL, 'raw', $3, $4, CURRENT_DATE);`, + by, pkey, prev, editorData); err != nil { + return fmt.Errorf("Error: Could not register change to database: %v", err) + } + + if err := tx.Commit(); err != nil { + return fmt.Errorf("Error: Could not commit transaction: %v", err) + } + + return nil } diff --git a/server/main.go b/server/main.go index 36aed65..b06fcf9 100644 --- a/server/main.go +++ b/server/main.go @@ -1,18 +1,21 @@ package main import ( + "encoding/json" + "io" "log" "net/http" "os" "os/signal" + "strings" "syscall" ) func main() { initialize() - http.HandleFunc("/api/orders", CreateOrder) - http.HandleFunc("/api/orders/", CaptureOrder) + http.HandleFunc("/api/orders", CreateOrderHandler) + http.HandleFunc("/api/orders/", CaptureOrderHandler) http.Handle("/", http.FileServer(http.Dir("./public"))) stop := make(chan os.Signal, 1) @@ -31,3 +34,71 @@ func main() { shutdown() log.Println("Server shutdown gracefully.") } + +func fail(w http.ResponseWriter, err error, notice string) { + log.Printf("%s: %v", notice, err) + http.Error(w, notice, http.StatusInternalServerError) +} + +func CreateOrderHandler(w http.ResponseWriter, r *http.Request) { + orderID, err := CreateOrder() + if err != nil { + fail(w, err, "Failed to obtain orderID") + return + } + + var response struct { + ID string `json:"id"` + } + response.ID = orderID + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + return +} + +func CaptureOrderHandler(w http.ResponseWriter, r *http.Request) { + info, err := io.ReadAll(r.Body) + if err != nil { + fail(w, err, "Failed to read request body") + return + } + var cart struct { + Directory string `json:"directory"` + EditorData json.RawMessage `json:"editor_data"` + } + err = json.Unmarshal(info, &cart) + if err != nil { + fail(w, err, "Failed to parse request body") + return + } + directory := cart.Directory + editorData := cart.EditorData + + path := strings.TrimPrefix(r.URL.Path, "/api/orders/") + parts := strings.Split(path, "/") + orderID := parts[0] + if orderID == "" { + fail(w, err, "Failed to get orderID from client URL") + return + } + + capture, receipt, err := CaptureOrder(orderID) + if err != nil { + fail(w, err, "Failed to capture order") + return + } + + if err := RegisterSitePayment(capture, directory, editorData); err != nil { + fail(w, err, "Failed to register '"+directory+"'in database") + return + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(receipt); err != nil { + fail(w, err, "Failed to encode response") + return + } + + return +} diff --git a/server/paypal.go b/server/paypal.go index ce353e9..8357e9d 100644 --- a/server/paypal.go +++ b/server/paypal.go @@ -45,6 +45,17 @@ type Capture struct { } `json:"payer"` } +type Receipt struct { + PurchaseUnits []struct { + Payments struct { + Captures []struct { + ID string `json:"id"` + Status string `json:"status"` + } `json:"captures"` + } `json:"payments"` + } `json:"purchase_units"` +} + func Token() (string, error) { req, err := http.NewRequest("POST", os.Getenv("BASE_URL")+"/v1/oauth2/token", @@ -69,13 +80,10 @@ func Token() (string, error) { return response.AccessToken, nil } -func CreateOrder(w http.ResponseWriter, r *http.Request) { +func CreateOrder() (string, error) { token, err := Token() if err != nil { - http.Error(w, - "Failed to get access token", - http.StatusInternalServerError) - return + return "", fmt.Errorf("Failed to get acess token: %v", err) } type Amount struct { @@ -129,10 +137,7 @@ func CreateOrder(w http.ResponseWriter, r *http.Request) { raw, err := http.DefaultClient.Do(req) if err != nil { - http.Error(w, - "Failed to send request", - http.StatusInternalServerError) - return + return "", fmt.Errorf("Failed to send request: %v", err) } defer raw.Body.Close() @@ -141,64 +146,16 @@ func CreateOrder(w http.ResponseWriter, r *http.Request) { } if err := json.NewDecoder(raw.Body).Decode(&response); err != nil { - http.Error(w, - "Failed to decode response", - http.StatusInternalServerError) - return + return "", fmt.Errorf("Failed to decode response: %v", err) } - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(response) - return + return response.ID, nil } -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 - } - +func CaptureOrder(orderID string) (Capture, Receipt, error) { token, err := Token() if err != nil { - http.Error(w, - "Failed to get access token", - http.StatusInternalServerError) - return + return Capture{}, Receipt{}, fmt.Errorf("Failed to get acess token: %v", err) } req, err := http.NewRequest("POST", @@ -209,57 +166,25 @@ func CaptureOrder(w http.ResponseWriter, r *http.Request) { raw, err := http.DefaultClient.Do(req) if err != nil { - http.Error(w, - "Failed to send request", - http.StatusInternalServerError) - return + return Capture{}, Receipt{}, fmt.Errorf("Failed to send request: %v", err) } defer raw.Body.Close() + var capture Capture + var receipt Receipt + body, err := io.ReadAll(raw.Body) if err != nil { - http.Error(w, - "Failed to read response body", - http.StatusInternalServerError) - return + return Capture{}, Receipt{}, fmt.Errorf("Failed to read response body: %v", err) } - var capture Capture - if err := json.Unmarshal(body, &capture); err != nil { - http.Error(w, - "Failed to decode response into capture", - http.StatusInternalServerError) - return + if err := json.NewDecoder(bytes.NewReader(body)).Decode(&capture); err != nil { + return Capture{}, Receipt{}, fmt.Errorf("Failed to decode into capture: %v", err) } - 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 + if err := json.NewDecoder(bytes.NewReader(body)).Decode(&receipt); err != nil { + return Capture{}, Receipt{}, fmt.Errorf("Failed to decode into capture: %v", err) } - 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 + return capture, receipt, nil } -