Compare commits

..

7 Commits

26 changed files with 267 additions and 26 deletions

View File

@ -2,7 +2,7 @@ STRIPE_SECRET=$(shell sed '2q;d' cred.txt)
STRIPE_KEY=$(shell sed '2q;d' cred.txt)
GOSTRIPE_PORT=4000
API_PORT=4001
DSN=root@tcp(localhost:6379)/widgets?parseTime=true&tls=false
DSN=vinchent:secret@tcp(localhost:3306)/widgets?parseTime=true&tls=false
## build: builds all binaries
build: clean build_front build_back

View File

@ -5,6 +5,7 @@ import (
"fmt"
"log"
"myapp/internal/driver"
"myapp/internal/models"
"net/http"
"os"
"time"
@ -31,6 +32,7 @@ type application struct {
infoLog *log.Logger
errorLog *log.Logger
version string
DB models.DBModel
}
func (app *application) serve() error {
@ -87,6 +89,7 @@ func main() {
config: cfg,
infoLog: infoLog,
errorLog: errorLog,
DB: models.DBModel{DB: conn},
}
app.infoLog.Println("Connected to MariaDB")

View File

@ -5,6 +5,8 @@ import (
"myapp/internal/cards"
"net/http"
"strconv"
"github.com/go-chi/chi/v5"
)
type stripePayload struct {
@ -65,3 +67,19 @@ func (app *application) GetPaymentIntent(w http.ResponseWriter, r *http.Request)
w.Header().Set("Content-Type", "application/json")
w.Write(out)
}
func (app *application) GetWidgetByID(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
widgetID, _ := strconv.Atoi(id)
widget, err := app.DB.GetWidget(widgetID)
if err != nil {
app.errorLog.Println(err)
}
out, err := json.MarshalIndent(widget, "", " ")
if err != nil {
app.errorLog.Println(err)
}
w.Header().Set("Content-Type", "application/json")
w.Write(out)
}

View File

@ -19,5 +19,6 @@ func (app *application) routes() http.Handler {
}))
mux.Post("/api/payment-intent", app.GetPaymentIntent)
mux.Get("/api/widget/{id}", app.GetWidgetByID)
return mux
}

View File

@ -1,15 +1,12 @@
package main
import (
"myapp/internal/models"
"net/http"
)
func (app *application) VirtualTerminal(w http.ResponseWriter, r *http.Request) {
stringMap := make(map[string]string)
stringMap["publishable_key"] = app.config.stripe.key
if err := app.renderTemplate(w, r, "terminal", &templateData{
StringMap: stringMap,
}, "stripe-js"); err != nil {
if err := app.renderTemplate(w, r, "terminal", &templateData{}, "stripe-js"); err != nil {
app.errorLog.Println(err)
}
}
@ -46,7 +43,20 @@ func (app *application) PaymentSucceeded(w http.ResponseWriter, r *http.Request)
// ChargeOnce displays the page to buy one widget
func (app *application) ChargeOnce(w http.ResponseWriter, r *http.Request) {
if err := app.renderTemplate(w, r, "buy-once", nil, "stripe-js"); err != nil {
widget := models.Widget{
ID: 1,
Name: "Custom Widget",
Description: "Paris 2024",
InventoryLevel: 10,
Price: 1000,
}
data := make(map[string]interface{})
data["widget"] = widget
if err := app.renderTemplate(w, r, "buy-once", &templateData{
Data: data,
}, "stripe-js"); err != nil {
app.errorLog.Println(err)
}
}

View File

@ -6,6 +6,7 @@ import (
"html/template"
"log"
"myapp/internal/driver"
"myapp/internal/models"
"net/http"
"os"
"time"
@ -35,6 +36,7 @@ type application struct {
errorLog *log.Logger
templateCache map[string]*template.Template
version string
DB models.DBModel
}
func (app *application) serve() error {
@ -95,6 +97,7 @@ func main() {
errorLog: errorLog,
templateCache: tc,
version: version,
DB: models.DBModel{DB: conn},
}
app.infoLog.Println("Connected to MariaDB")

View File

@ -20,15 +20,26 @@ type templateData struct {
API string
CSSVersion string
IsAuthenticated int
StripeSecretKey string
StripePubKey string
}
var functions = template.FuncMap{}
var functions = template.FuncMap{
"formatCurrency": formatCurrency,
}
func formatCurrency(n int) string {
f := float32(n / 100)
return fmt.Sprintf("€%.2f", f)
}
//go:embed templates
var templateFS embed.FS
func (app *application) addDefaultData(td *templateData, r *http.Request) *templateData {
td.API = app.config.api
td.StripePubKey = app.config.stripe.key
td.StripeSecretKey = app.config.stripe.secret
return td
}

View File

@ -3,6 +3,7 @@
Buy one widget
{{ end }}
{{ define "content" }}
{{$widget := index .Data "widget"}}
<h2 class="mt-3 text-center">Buy One Widget</h2>
<hr>
<img src="/static/img/widget.jpeg"
@ -17,15 +18,12 @@ Buy one widget
class="d-blick needs-validation charge-form"
autocomplete="off"
novalidate="">
<div class="mb-3">
<label for="amount" class="form-label">Amount</label>
<input type="text"
id="amount"
name="amount"
autocomplete="amount-new"
required=""
class="form-control">
</div>
<input type="hidden" name="product_id" value="{{$widget.ID}}">
<input type="hidden" id="amount" name="amount" value="{{$widget.Price}}">
<h3 class="mt-2 mb-3 text-center">{{$widget.Name}}: {{formatCurrency $widget.Price}}</h3>
<p class="mt-2 mb-3 text-center">{{$widget.Description}}</p>
<hr>
<div class="mb-3">
<label for="cardholder-name" class="form-label">Cardholder Name</label>
<input type="text"

View File

@ -8,7 +8,7 @@ const cardMessages = document.getElementById("card-messages");
const payButton = document.getElementById("pay-button");
const processing = document.getElementById("processing-payment");
stripe = Stripe('{{index .StringMap "publishable_key"}}');
stripe = Stripe('{{.StripePubKey}}');
function hidePayButton() {
payButton.classList.add("d-none");
@ -46,7 +46,7 @@ function val() {
form.classList.add("was-validated");
hidePayButton();
let amountToCharge = String(parseFloat(document.getElementById("amount").value) * 100);
let amountToCharge = String(parseFloat(document.getElementById("amount").value));
let payload = {
amount: amountToCharge,
currency: 'eur',
@ -61,7 +61,7 @@ function val() {
body: JSON.stringify(payload),
};
fetch("{{index .API}}/api/payment-intent", requestOptions)
fetch("{{.API}}/api/payment-intent", requestOptions)
.then(response => response.text())
.then(response => {
let data;

103
internal/models/models.go Normal file
View File

@ -0,0 +1,103 @@
package models
import (
"context"
"database/sql"
"time"
)
// DBModel is the type for database connection values
type DBModel struct {
DB *sql.DB
}
// Models is the wrapper for all models
type Models struct {
DB DBModel
}
// NewModels returns a model type with database connection pool
func NewModels(db *sql.DB) Models {
return Models{
DB: DBModel{DB: db},
}
}
// Widget is the type for all widgets
type Widget struct {
ID int `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
InventoryLevel int `json:"inventory_level"`
Price int `json:"price"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
Image string `json:"image"`
}
// Order is the type for all orders
type Order struct {
ID int `json:"id"`
WidgetID int `json:"widget_id"`
TransactionID int `json:"transaction_id"`
StatusID int `json:"status_id"`
Quantity int `json:"quantity"`
Amount int `json:"amount"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
}
// Status is the type for orders statuses
type Status struct {
ID int `json:"id"`
Name string `json:"name"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
}
// TransactionStatus is the type for transaction statuses
type TransactionStatus struct {
ID int `json:"id"`
Name string `json:"name"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
}
// Transaction is the type for transactions
type Transaction struct {
ID int `json:"id"`
Amount int `json:"amount"`
Currency string `json:"currency"`
LastFour string `json:"last_four"`
BankReturnCode string `json:bank_return_code`
TransactionStatusID int `json:transaction_status_id`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
}
// User is the type for users
type User struct {
ID int `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"`
Password string `json:"password"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
}
func (m *DBModel) GetWidget(id int) (Widget, error) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
var widget Widget
query := `SELECT id, name FROM widgets WHERE id = ?`
row := m.DB.QueryRowContext(ctx, query, id)
err := row.Scan(&widget.ID, &widget.Name)
if err != nil {
return widget, err
}
return widget, nil
}

BIN
migrations/migrations/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,12 @@
create_table("widgets") {
t.Column("id", "integer", {primary: true})
t.Column("name", "string", {"default": ""})
t.Column("description", "text", {"default": ""})
t.Column("inventory_level", "integer", {})
t.Column("price", "integer", {})
}
sql("alter table widgets alter column created_at set default now();")
sql("alter table widgets alter column updated_at set default now();")
sql("insert into widgets (name, description, inventory_level, price, created_at, updated_at) values ('Widget', 'A very nice widget.', 10, 1000, now(), now());")

View File

@ -0,0 +1,13 @@
create_table("transaction_statuses") {
t.Column("id", "integer", {primary: true})
t.Column("name", "string", {})
}
sql("alter table transaction_statuses alter column created_at set default now();")
sql("alter table transaction_statuses alter column updated_at set default now();")
sql("insert into transaction_statuses (name) values ('Pending');")
sql("insert into transaction_statuses (name) values ('Cleared');")
sql("insert into transaction_statuses (name) values ('Declined');")
sql("insert into transaction_statuses (name) values ('Refunded');")
sql("insert into transaction_statuses (name) values ('Partially refunded');")

View File

@ -0,0 +1 @@
drop_table("transactions")

View File

@ -0,0 +1,16 @@
create_table("transactions") {
t.Column("id", "integer", {primary: true})
t.Column("amount", "integer", {})
t.Column("currency", "string", {})
t.Column("last_four", "string", {})
t.Column("bank_return_code", "string", {})
t.Column("transaction_status_id", "integer", {"unsigned": true})
}
sql("alter table transactions alter column created_at set default now();")
sql("alter table transactions alter column updated_at set default now();")
add_foreign_key("transactions", "transaction_status_id", {"transaction_statuses": ["id"]}, {
"on_delete": "cascade",
"on_update": "cascade",
})

View File

@ -0,0 +1 @@
drop_table("orders")

View File

@ -0,0 +1,21 @@
create_table("orders") {
t.Column("id", "integer", {primary: true})
t.Column("widget_id", "integer", {"unsigned":true})
t.Column("transaction_id", "integer", {"unsigned":true})
t.Column("status_id", "integer", {"unsigned":true})
t.Column("quantity", "integer", {})
t.Column("amount", "integer", {})
}
sql("alter table orders alter column created_at set default now();")
sql("alter table orders alter column updated_at set default now();")
add_foreign_key("orders", "widget_id", {"widgets": ["id"]}, {
"on_delete": "cascade",
"on_update": "cascade",
})
add_foreign_key("orders", "transaction_id", {"transactions": ["id"]}, {
"on_delete": "cascade",
"on_update": "cascade",
})

View File

@ -0,0 +1 @@
drop_table("statuses")

View File

@ -0,0 +1,16 @@
create_table("statuses") {
t.Column("id", "integer", {primary: true})
t.Column("name", "string", {})
}
sql("alter table statuses alter column created_at set default now();")
sql("alter table statuses alter column updated_at set default now();")
sql("insert into statuses (name) values ('Cleared');")
sql("insert into statuses (name) values ('Refunded');")
sql("insert into statuses (name) values ('Cancelled');")
add_foreign_key("orders", "status_id", {"statuses": ["id"]}, {
"on_delete": "cascade",
"on_update": "cascade",
})

View File

@ -0,0 +1 @@
drop_table("users")

View File

@ -0,0 +1,12 @@
create_table("users") {
t.Column("id", "integer", {primary: true})
t.Column("first_name", "string", {"size": 255})
t.Column("last_name", "string", {"size": 255})
t.Column("email", "string", {})
t.Column("password", "string", {"size": 60})
}
sql("alter table users alter column created_at set default now();")
sql("alter table users alter column updated_at set default now();")
sql("insert into users (first_name, last_name, email, password) values ('Admin','User','admin@example.com', '$2a$12$VR1wDmweaF3ZTVgEHiJrNOSi8VcS4j0eamr96A/7iOe8vlum3O3/q');")

View File

@ -0,0 +1 @@
drop_column("widgets", "image")

View File

@ -0,0 +1 @@
add_column("widgets", "image", "string", {"default":""})

View File

@ -4,8 +4,7 @@ const cardMessages = document.getElementById("card-messages");
const payButton = document.getElementById("pay-button");
const processing = document.getElementById("processing-payment");
// FIXME: not working in this way
stripe = Stripe('{{index .StringMap "publishable_key"}}');
stripe = Stripe('{{.StripePubKey}}');
function hidePayButton() {
payButton.classList.add("d-none");
@ -31,7 +30,7 @@ function showCardSuccess() {
cardMessages.innerText = "Trasaction successful";
}
function val() {
function val(stripe) {
let form = document.getElementById("charge_form");
if (form.checkValidity() === false) {
@ -43,7 +42,7 @@ function val() {
form.classList.add("was-validated");
hidePayButton();
let amountToCharge = String(parseFloat(document.getElementById("amount").value) * 100);
let amountToCharge = String(parseFloat(document.getElementById("amount").value));
let payload = {
amount: amountToCharge,
currency: 'eur',
@ -58,7 +57,7 @@ function val() {
body: JSON.stringify(payload),
};
fetch("{{index .API}}/api/payment-intent", requestOptions)
fetch("{{.API}}/api/payment-intent", requestOptions)
.then(response => response.text())
.then(response => {
let data;
@ -128,4 +127,3 @@ function val() {
}
});
})();