mirror of
https://github.com/tavo-wasd-gh/conex-builder.git
synced 2025-07-12 18:23:30 -06:00
server, still need to correct public
This commit is contained in:
parent
e9e0429e7c
commit
463476bdc8
6 changed files with 603 additions and 495 deletions
17
.env.example
17
.env.example
|
@ -1,6 +1,15 @@
|
||||||
BASE_URL=""
|
BASE_URL="https://api-m.sandbox.paypal.com"
|
||||||
|
PORT="8080"
|
||||||
|
|
||||||
CLIENT_ID=""
|
CLIENT_ID=""
|
||||||
CLIENT_SECRET=""
|
CLIENT_SECRET=""
|
||||||
PLAN_ID=""
|
RETURN_URL="http://localhost:8080/?success"
|
||||||
RETURN_URL="https://builder.conex.one/?success"
|
CANCEL_URL="http://localhost:8080/?cancel"
|
||||||
CANCEL_URL="https://builder.conex.one/?cancel"
|
|
||||||
|
DB_HOST="localhost"
|
||||||
|
DB_PORT="5432"
|
||||||
|
DB_USER="conex"
|
||||||
|
DB_PASS="1234"
|
||||||
|
DB_NAME="iterone"
|
||||||
|
|
||||||
|
PRICE="20.00"
|
||||||
|
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1,6 +1,2 @@
|
||||||
builder
|
builder
|
||||||
bin
|
|
||||||
go.mod
|
|
||||||
go.sum
|
|
||||||
public/submit/*
|
|
||||||
.env
|
.env
|
||||||
|
|
13
Makefile
13
Makefile
|
@ -1,16 +1,17 @@
|
||||||
BIN = builder
|
BIN = builder
|
||||||
SRC = main.go
|
SRCDIR = server
|
||||||
GOFILES = go.sum go.mod
|
SRC = ${SRCDIR}/main.go
|
||||||
|
GOFILES = ${SRCDIR}/go.sum ${SRCDIR}/go.mod
|
||||||
GOMODS = github.com/joho/godotenv github.com/lib/pq
|
GOMODS = github.com/joho/godotenv github.com/lib/pq
|
||||||
|
|
||||||
all: ${BIN}
|
all: ${BIN}
|
||||||
|
|
||||||
${BIN}: ${SRC} ${GOFILES}
|
${BIN}: ${SRC} ${GOFILES}
|
||||||
go build -o builder
|
(cd ${SRCDIR} && go build -o ../${BIN})
|
||||||
|
|
||||||
${GOFILES}:
|
${GOFILES}:
|
||||||
go mod init ${BIN}
|
(cd ${SRCDIR} && go mod init ${BIN})
|
||||||
go get ${GOMODS}
|
(cd ${SRCDIR} && go get ${GOMODS})
|
||||||
|
|
||||||
start: ${BIN}
|
start: ${BIN}
|
||||||
@./$< &
|
@./$< &
|
||||||
|
@ -26,4 +27,4 @@ clean:
|
||||||
rm -f ${BIN}
|
rm -f ${BIN}
|
||||||
|
|
||||||
clean-mods:
|
clean-mods:
|
||||||
rm -f go.*
|
rm -f ${SRCDIR}/go.*
|
||||||
|
|
195
README.org
Normal file
195
README.org
Normal file
|
@ -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
|
481
main.go
481
main.go
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
388
server/main.go
Normal file
388
server/main.go
Normal file
|
@ -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
|
||||||
|
}
|
Loading…
Reference in a new issue