Skip to content

Commit

Permalink
Merge pull request #61 from msalbrain/feature/webhook-support-51
Browse files Browse the repository at this point in the history
🔥added webhook support
  • Loading branch information
shurco authored Nov 13, 2023
2 parents 07f96a1 + 57b9307 commit c6f19b2
Show file tree
Hide file tree
Showing 8 changed files with 249 additions and 4 deletions.
115 changes: 114 additions & 1 deletion internal/handlers/public/cart.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package handlers

import (
"encoding/json"
"fmt"
"log"
"time"

"github.com/gofiber/fiber/v2"
"github.com/stripe/stripe-go/v74"
Expand All @@ -10,6 +13,7 @@ import (
"github.com/shurco/litecart/internal/models"
"github.com/shurco/litecart/internal/queries"
"github.com/shurco/litecart/pkg/security"
"github.com/shurco/litecart/pkg/webhook"
"github.com/shurco/litecart/pkg/webutil"
)

Expand Down Expand Up @@ -41,6 +45,7 @@ func Checkout(c *fiber.Ctx) error {

lineItems := []*stripe.CheckoutSessionLineItemParams{}
for _, item := range products.Products {

images := []string{}
for _, image := range item.Images {
path := fmt.Sprintf("https://%s/uploads/%s_md.%s", domain, image.Name, image.Ext)
Expand Down Expand Up @@ -93,6 +98,38 @@ func Checkout(c *fiber.Ctx) error {
PaymentStatus: string(stripeSession.PaymentStatus),
})

if err = settingStripe.Payment.Validate(); err != nil {
log.Printf("update payment webhook url", err)
} else {
resData := map[string]any{
"event": "payment_initiation",
"timestamp": stripeSession.Created,
"data": map[string]any{
"payment_id": stripeSession.ID,
"total_amount": stripeSession.AmountTotal,
"currency": stripeSession.Currency,
"cart_items": items,
},
}

jsonData, err := json.Marshal(resData)
if err != nil {
log.Println("Error:", err)
}

go func() {
res, err := webhook.SendHook(settingStripe.Payment.WebhookUrl, jsonData)

if err != nil {
log.Println(err)
}
if res.StatusCode != 200 {
log.Print("An issue has been identified with the payment webhook URL. Please verify that it responds with a status code of 200 OK.")
}
}()

}

return webutil.Response(c, fiber.StatusOK, "Checkout url", stripeSession.URL)
}

Expand All @@ -101,6 +138,8 @@ func Checkout(c *fiber.Ctx) error {
func CheckoutSuccess(c *fiber.Ctx) error {
db := queries.DB()

settingStripe, err := db.SettingStripe()

cartID := c.Params("cart_id")
sessionID := c.Params("session_id")

Expand All @@ -122,6 +161,40 @@ func CheckoutSuccess(c *fiber.Ctx) error {
return webutil.StatusBadRequest(c, err)
}

if err = settingStripe.Payment.Validate(); err != nil {
log.Printf("update payment webhook url", err)
} else {
resData := map[string]any{
"event": "payment_success",
"timestamp": sessionStripe.Created,
"data": map[string]any{
"payment_system": "stripe",
"cart_id": cartID,
"payment_id": sessionStripe.PaymentIntent.ID,
"total_amount": sessionStripe.PaymentIntent.Amount,
"currency": sessionStripe.PaymentIntent.Currency,
"user_email": sessionStripe.Customer.Email,
},
}

jsonData, err := json.Marshal(resData)
if err != nil {
log.Println("Error:", err)
}

go func() {
res, err := webhook.SendHook(settingStripe.Payment.WebhookUrl, jsonData)

if err != nil {
log.Println(err)
}

if res.StatusCode != 200 {
log.Print("An issue has been identified with the payment webhook URL. Please verify that it responds with a status code of 200 OK.")
}
}()
}

return c.Render("success", nil, "layouts/main")
}

Expand All @@ -130,15 +203,55 @@ func CheckoutSuccess(c *fiber.Ctx) error {
func CheckoutCancel(c *fiber.Ctx) error {
cartID := c.Params("cart_id")
db := queries.DB()
err := db.UpdateCart(&models.Cart{
settingStripe, err := db.SettingStripe()
cartStripe, err := db.Cart(cartID)

err = db.UpdateCart(&models.Cart{
Core: models.Core{
ID: cartID,
},
PaymentStatus: "cancel",
})

if err != nil {
return webutil.StatusBadRequest(c, err)
}

currentTime := time.Now().UTC()

if err = settingStripe.Payment.Validate(); err != nil {
log.Print("update payment webhook url", err)
} else {
resData := map[string]any{
"event": "payment_error",
"timestamp": currentTime.Unix(),
"data": map[string]any{
"payment_system": "stripe",
"cart_id": cartID,
"payment_id": cartStripe.PaymentID,
"total_amount": cartStripe.AmountTotal,
"currency": cartStripe.Currency,
"user_email": cartStripe.Email,
},
}

jsonData, err := json.Marshal(resData)
if err != nil {
log.Println("Error:", err)
}

go func() {
res, err := webhook.SendHook(settingStripe.Payment.WebhookUrl, jsonData)

if err != nil {
log.Println(err)
}

if res.StatusCode != 200 {
log.Print("An issue has been identified with the payment webhook URL. Please verify that it responds with a status code of 200 OK.")
}
}()
}

return c.Render("cancel", nil, "layouts/main")
}
12 changes: 12 additions & 0 deletions internal/models/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ type Setting struct {
Password Password `json:"password,omitempty"`
Stripe Stripe `json:"stripe,omitempty"`
Social Social `json:"social,omitempty"`
Payment Payment `json:"payment,omitempty"`
SMTP SMTP `json:"smtp,omitempty"`
}

Expand Down Expand Up @@ -81,6 +82,17 @@ func (v Stripe) Validate() error {
)
}


type Payment struct {
WebhookUrl string `json:"webhook_url"`
}

// Validate is ...
func (v Payment) Validate() error {
return validation.ValidateStruct(&v,
validation.Field(&v.WebhookUrl, is.URL))
}

type Social struct {
Facebook string `json:"facebook,omitempty"`
Instagram string `json:"instagram,omitempty"`
Expand Down
50 changes: 50 additions & 0 deletions internal/queries/cart.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,56 @@ func (q *CartQueries) Carts() ([]*models.Cart, error) {
return carts, nil
}

// Carts is ...
func (q *CartQueries) Cart(cartId string) (*models.Cart, error) {

query := `
SELECT
id,
email,
name,
amount_total,
currency,
payment_id,
payment_status,
strftime('%s', created),
strftime('%s', updated)
FROM cart
WHERE id = ?
`

rows, err := q.DB.QueryContext(context.TODO(), query, cartId)
if err != nil {
return nil, err
}
defer rows.Close()

var email, name, paymentID sql.NullString
var updated sql.NullInt64
cart := &models.Cart{}

err = rows.Scan(
&cart.ID,
&email,
&name,
&cart.AmountTotal,
&cart.Currency,
&paymentID,
&cart.PaymentStatus,
&cart.Created,
&updated,
)
if err != nil {
return nil, err
}

if err := rows.Err(); err != nil {
return nil, err
}

return cart, nil
}

// AddCart is ...
func (q *CartQueries) AddCart(cart *models.Cart) error {
byteCart, err := json.Marshal(cart.Cart)
Expand Down
12 changes: 10 additions & 2 deletions internal/queries/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ func (q *SettingQueries) Settings(private bool) (*models.Setting, error) {
keys = append(keys,
"jwt_secret", "jwt_secret_expire_hours", // 2
"stripe_secret_key", "stripe_webhook_secret_key", // 2
"payment_webhook_url", // 1
"smtp_host", "smtp_port", "smtp_username", "smtp_password", "smtp_encryption", // 5
)
}
Expand All @@ -53,6 +54,7 @@ func (q *SettingQueries) Settings(private bool) (*models.Setting, error) {
"jwt_secret_expire_hours": &settings.Main.JWT.ExpireHours,
"stripe_secret_key": &settings.Stripe.SecretKey,
"stripe_webhook_secret_key": &settings.Stripe.WebhookSecretKey,
"payment_webhook_url": &settings.Payment.WebhookUrl,
"social_facebook": &settings.Social.Facebook,
"social_instagram": &settings.Social.Instagram,
"social_twitter": &settings.Social.Twitter,
Expand Down Expand Up @@ -157,6 +159,10 @@ func (q *SettingQueries) UpdateSettings(settings *models.Setting, section string
"social_dribbble": settings.Social.Dribbble,
"social_github": settings.Social.Github,
}
case "payment":
sectionSettings = map[string]any{
"payment_webhook_url": settings.Payment.WebhookUrl,
}
case "smtp":
sectionSettings = map[string]any{
"smtp_host": settings.SMTP.Host,
Expand Down Expand Up @@ -306,8 +312,8 @@ func (q *SettingQueries) SettingJWT() (*jwtutil.Setting, error) {
func (q *SettingQueries) SettingStripe() (*models.Setting, error) {
settings := &models.Setting{}

query := `SELECT key, value FROM setting WHERE key IN (?, ?, ?)`
rows, err := q.DB.QueryContext(context.TODO(), query, "stripe_secret_key", "stripe_webhook_secret_key", "domain")
query := `SELECT key, value FROM setting WHERE key IN (?, ?, ?, ?)`
rows, err := q.DB.QueryContext(context.TODO(), query, "stripe_secret_key", "stripe_webhook_secret_key", "domain", "payment_webhook_url")
if err != nil {
return nil, err
}
Expand All @@ -327,6 +333,8 @@ func (q *SettingQueries) SettingStripe() (*models.Setting, error) {
settings.Stripe.WebhookSecretKey = value
case "domain":
settings.Main.Domain = fmt.Sprintf("https://%s", value)
case "payment_webhook_url":
settings.Payment.WebhookUrl = value
}
}

Expand Down
10 changes: 10 additions & 0 deletions migrations/20231110193417_added_payment_field.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-- +goose Up
-- +goose StatementBegin
INSERT INTO setting (id, key, value) VALUES
('7HkP2nYgR4sL8Qo', 'payment_webhook_url', '');
-- +goose StatementEnd

-- +goose Down
-- +goose StatementBegin
DELETE FROM setting WHERE id = '7HkP2nYgR4sL8Qo';
-- +goose StatementEnd
26 changes: 26 additions & 0 deletions pkg/webhook/webhook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package webhook

import (
"bytes"
"net/http"
"fmt"

)

func SendHook(url string, payload []byte) (*http.Response, error) {

req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload))
if err != nil {
return nil, fmt.Errorf("error creating request: %v", err)
}

req.Header.Set("Content-Type", "application/json")

client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("error making request: %v", err)
}

return resp, nil
}
26 changes: 26 additions & 0 deletions web/admin/src/pages/Settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,26 @@
<hr class="mt-5" />
</div>

<div class="mt-5">
<h2 class="mb-5">Payment Event</h2>

<div class="mb-5 flex items-center justify-between bg-red-600 px-2 py-3 text-white" v-if="!payment.webhook_url">
<p class="text-sm font-medium">This section is required to recieve payment events externally!</p>
</div>

<Form @submit="updateSetting('payment')" v-slot="{ errors }">
<div class="flex">
<div>
<FormInput v-model.trim="payment.webhook_url" :error="errors.webhook_url" rules="required" class="w-96" id="webhook_url" type="text" title="webhook url" ico="key" />
</div>
</div>
<div class="pt-8">
<FormButton type="submit" name="Save" color="green" />
</div>
</Form>
<hr class="mt-5" />
</div>

<div class="mt-5">
<h2 class="mb-5">Socials</h2>
<Form @submit="updateSetting('social')" v-slot="{ errors }">
Expand Down Expand Up @@ -181,6 +201,7 @@ const main = ref({
});
const password = ref({});
const stripe = ref({});
const payment = ref({});
const social = ref({});
const smtp = ref({});
Expand Down Expand Up @@ -216,8 +237,10 @@ const settingsList = async () => {
stripe.value = res.result.stripe;
social.value = res.result.social;
smtp.value = res.result.smtp;
payment.value = res.result.payment;
}
});
};
const updateSetting = async (section) => {
Expand All @@ -235,6 +258,9 @@ const updateSetting = async (section) => {
case "stripe":
update.stripe = stripe.value;
break;
case "payment":
update.payment = payment.value;
break;
case "social":
update.social = social.value;
break;
Expand Down
2 changes: 1 addition & 1 deletion web/site/public/assets/css/style.css

Large diffs are not rendered by default.

0 comments on commit c6f19b2

Please sign in to comment.