conex-builder/main.go
2024-08-21 00:55:17 -06:00

481 lines
12 KiB
Go

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
}
}