Compare commits
	
		
			2 Commits
		
	
	
		
			059638f3a7
			...
			63585e31f6
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 63585e31f6 | |||
| c697da63b9 | 
| @ -164,6 +164,8 @@ func (app *application) CreateCustomerAndSubscribeToPlan(w http.ResponseWriter, | ||||
| 			ExpiryMonth:         data.ExpiryMonth, | ||||
| 			ExpiryYear:          data.ExpiryYear, | ||||
| 			TransactionStatusID: 2, | ||||
| 			PaymentIntent:       subscription.ID, | ||||
| 			PaymentMethod:       data.PaymentMethod, | ||||
| 		} | ||||
| 		txnID, err := app.SaveTransaction(txn) | ||||
| 		if err != nil { | ||||
| @ -514,11 +516,35 @@ func (app *application) ResetPassword(w http.ResponseWriter, r *http.Request) { | ||||
| } | ||||
|  | ||||
| func (app *application) AllSales(w http.ResponseWriter, r *http.Request) { | ||||
| 	allSales, err := app.DB.GetAllOrders(false) | ||||
| 	var payload struct { | ||||
| 		PageSize    int `json:"page_size"` | ||||
| 		CurrentPage int `json:"page"` | ||||
| 	} | ||||
| 	err := app.readJSON(w, r, &payload) | ||||
| 	if err != nil { | ||||
| 		app.badRequest(w, r, err) | ||||
| 	} | ||||
| 	app.writeJSON(w, http.StatusOK, allSales) | ||||
|  | ||||
| 	allSales, lastPage, totalRecords, err := app.DB.GetAllOrdersPaginated(false, 2, 1) | ||||
| 	if err != nil { | ||||
| 		app.badRequest(w, r, err) | ||||
| 	} | ||||
|  | ||||
| 	var resp struct { | ||||
| 		CurrentPage  int             `json:"current_page"` | ||||
| 		PageSize     int             `json:"page_size"` | ||||
| 		LastPage     int             `json:"last_page"` | ||||
| 		TotalRecords int             `json:"total_records"` | ||||
| 		Orders       []*models.Order `json:"orders"` | ||||
| 	} | ||||
|  | ||||
| 	resp.CurrentPage = 1 | ||||
| 	resp.PageSize = payload.PageSize | ||||
| 	resp.LastPage = lastPage | ||||
| 	resp.TotalRecords = totalRecords | ||||
| 	resp.Orders = allSales | ||||
|  | ||||
| 	app.writeJSON(w, http.StatusOK, resp) | ||||
| } | ||||
|  | ||||
| func (app *application) AllSubscriptions(w http.ResponseWriter, r *http.Request) { | ||||
| @ -587,3 +613,50 @@ func (app *application) RefundCharge(w http.ResponseWriter, r *http.Request) { | ||||
|  | ||||
| 	app.writeJSON(w, http.StatusOK, resp) | ||||
| } | ||||
|  | ||||
| func (app *application) CancelSubscription(w http.ResponseWriter, r *http.Request) { | ||||
| 	var subToCancel struct { | ||||
| 		ID            int    `json:"id"` | ||||
| 		PaymentIntent string `json:"pi"` | ||||
| 		Currency      string `json:"currency"` | ||||
| 	} | ||||
|  | ||||
| 	err := app.readJSON(w, r, &subToCancel) | ||||
| 	if err != nil { | ||||
| 		app.errorLog.Println(err) | ||||
| 		app.badRequest(w, r, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// validate | ||||
|  | ||||
| 	card := cards.Card{ | ||||
| 		Secret:   app.config.stripe.secret, | ||||
| 		Key:      app.config.stripe.key, | ||||
| 		Currency: subToCancel.Currency, | ||||
| 	} | ||||
|  | ||||
| 	err = card.CancelSubscription(subToCancel.PaymentIntent) | ||||
| 	if err != nil { | ||||
| 		app.errorLog.Println(err) | ||||
| 		app.badRequest(w, r, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// update status in DB | ||||
| 	err = app.DB.UpdateOrderStatus(subToCancel.ID, 3) | ||||
| 	if err != nil { | ||||
| 		app.badRequest( | ||||
| 			w, | ||||
| 			r, | ||||
| 			errors.New("the subscription was refunded, but the database could not be updated"), | ||||
| 		) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var resp jsonResponse | ||||
| 	resp.OK = true | ||||
| 	resp.Message = "Subscription canceled" | ||||
|  | ||||
| 	app.writeJSON(w, http.StatusOK, resp) | ||||
| } | ||||
|  | ||||
| @ -53,12 +53,9 @@ func (app *application) writeJSON( | ||||
| } | ||||
|  | ||||
| func (app *application) badRequest(w http.ResponseWriter, r *http.Request, err error) error { | ||||
| 	var payload struct { | ||||
| 		Error   bool   `json:"error"` | ||||
| 		Message string `json:"message"` | ||||
| 	} | ||||
| 	var payload jsonResponse | ||||
|  | ||||
| 	payload.Error = true | ||||
| 	payload.OK = false | ||||
| 	payload.Message = err.Error() | ||||
|  | ||||
| 	out, err := json.MarshalIndent(payload, "", "\t") | ||||
| @ -73,11 +70,8 @@ func (app *application) badRequest(w http.ResponseWriter, r *http.Request, err e | ||||
| } | ||||
|  | ||||
| func (app *application) invalidCredentials(w http.ResponseWriter) error { | ||||
| 	var payload struct { | ||||
| 		Error   bool   `json:"error"` | ||||
| 		Message string `json:"message"` | ||||
| 	} | ||||
| 	payload.Error = true | ||||
| 	var payload jsonResponse | ||||
| 	payload.OK = false | ||||
| 	payload.Message = "invalid authentication credentials" | ||||
|  | ||||
| 	err := app.writeJSON(w, http.StatusUnauthorized, payload) | ||||
|  | ||||
| @ -36,6 +36,7 @@ func (app *application) routes() http.Handler { | ||||
| 		mux.Post("/all-subscriptions", app.AllSubscriptions) | ||||
| 		mux.Post("/get-sale/{id}", app.GetSale) | ||||
| 		mux.Post("/refund", app.RefundCharge) | ||||
| 		mux.Post("/cancel-subscription", app.CancelSubscription) | ||||
| 	}) | ||||
| 	mux.Post("/api/forgot-password", app.SendPasswordResetEmail) | ||||
| 	mux.Post("/api/reset-password", app.ResetPassword) | ||||
|  | ||||
| @ -384,9 +384,16 @@ func (app *application) ShowSale(w http.ResponseWriter, r *http.Request) { | ||||
| 	stringMap := make(map[string]string) | ||||
| 	stringMap["title"] = "Sale" | ||||
| 	stringMap["cancel"] = "/admin/all-sales" | ||||
| 	stringMap["refund-url"] = "/api/admin/refund" | ||||
| 	stringMap["refund-btn"] = "Refund Order" | ||||
| 	stringMap["refund-badge"] = "Refunded" | ||||
|  | ||||
| 	intMap := make(map[string]int) | ||||
| 	intMap["isRefund"] = 1 | ||||
|  | ||||
| 	if err := app.renderTemplate(w, r, "sale", &templateData{ | ||||
| 		StringMap: stringMap, | ||||
| 		IntMap:    intMap, | ||||
| 	}); err != nil { | ||||
| 		app.errorLog.Println(err) | ||||
| 	} | ||||
| @ -396,9 +403,16 @@ func (app *application) ShowSubscriptions(w http.ResponseWriter, r *http.Request | ||||
| 	stringMap := make(map[string]string) | ||||
| 	stringMap["title"] = "Subscriptions" | ||||
| 	stringMap["cancel"] = "/admin/all-subscriptions" | ||||
| 	stringMap["refund-url"] = "/api/admin/cancel-subscription" | ||||
| 	stringMap["refund-btn"] = "Cancel Subscription" | ||||
| 	stringMap["refund-badge"] = "Cancelled" | ||||
|  | ||||
| 	intMap := make(map[string]int) | ||||
| 	intMap["is-refund"] = 0 | ||||
|  | ||||
| 	if err := app.renderTemplate(w, r, "sale", &templateData{ | ||||
| 		StringMap: stringMap, | ||||
| 		IntMap:    intMap, | ||||
| 	}); err != nil { | ||||
| 		app.errorLog.Println(err) | ||||
| 	} | ||||
|  | ||||
| @ -14,6 +14,7 @@ All Subscriptions | ||||
|             <th>Customer</th> | ||||
|             <th>Product</th> | ||||
|             <th>Amount</th> | ||||
|             <th>Status</th> | ||||
|         </tr> | ||||
|     </thead> | ||||
|     <tbody> | ||||
|  | ||||
| @ -4,7 +4,7 @@ | ||||
| {{ end }} | ||||
| {{ define "content" }} | ||||
| <h2 class="mt-5">Sale</h2> | ||||
| <span id="refunded" class="badge bg-danger d-none">Refunded</span> | ||||
| <span id="refunded" class="badge bg-danger d-none">{{index .StringMap "refund-badge"}}</span> | ||||
| <span id="charged" class="badge bg-success d-none">Charged</span> | ||||
| <hr> | ||||
| <div class="alert alert-danger text-center d-none" id="messages"></div> | ||||
| @ -22,7 +22,7 @@ | ||||
| </div> | ||||
| <hr> | ||||
| <a href='{{ index .StringMap "cancel" }}' class="btn btn-info">Cancel</a> | ||||
| <a id="refund-btn" href="#!" class="btn btn-warning d-none">Refund Order</a> | ||||
| <a id="refund-btn" href="#!" class="btn btn-warning d-none">{{index .StringMap "refund-btn"}}</a> | ||||
| <input type="hidden" id="pi" value=""> | ||||
| <input type="hidden" id="charge-amount" value=""> | ||||
| <input type="hidden" id="currency" value=""> | ||||
| @ -32,8 +32,10 @@ | ||||
| <script type="module"> | ||||
|     import {showInfo, refund} from "/static/js/sale.js" | ||||
|     showInfo({{.API}}); | ||||
|  | ||||
|     const api = {{.API}} + {{index .StringMap "refund-url"}} | ||||
|     document.getElementById("refund-btn").addEventListener("click", function(event) { | ||||
|         refund({{.API}}); | ||||
|         refund(api, {{index .IntMap "is-refund"}}); | ||||
|     }); | ||||
| </script> | ||||
| {{ end }} | ||||
|  | ||||
| @ -135,6 +135,20 @@ func (c *Card) Refund(pi string, amount int) error { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (c *Card) CancelSubscription(subID string) error { | ||||
| 	stripe.Key = c.Secret | ||||
|  | ||||
| 	params := &stripe.SubscriptionParams{ | ||||
| 		CancelAtPeriodEnd: stripe.Bool(true), | ||||
| 	} | ||||
|  | ||||
| 	_, err := subscription.Update(subID, params) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func cardErrorMessage(code stripe.ErrorCode) string { | ||||
| 	msg := "" | ||||
|  | ||||
|  | ||||
| @ -361,6 +361,93 @@ func (m *DBModel) GetAllOrders(isRecurring bool) ([]*Order, error) { | ||||
| 	return orders, nil | ||||
| } | ||||
|  | ||||
| func (m *DBModel) GetAllOrdersPaginated( | ||||
| 	isRecurring bool, | ||||
| 	pageSize, page int, | ||||
| ) ([]*Order, int, int, error) { | ||||
| 	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) | ||||
| 	defer cancel() | ||||
|  | ||||
| 	offset := (page - 1) * pageSize | ||||
|  | ||||
| 	orders := []*Order{} | ||||
|  | ||||
| 	query := ` | ||||
|         SELECT | ||||
|             o.id, o.widget_id, o.transaction_id, o.customer_id, | ||||
|             o.status_id, o.quantity, o.amount, o.created_at, o.updated_at, | ||||
|             w.id, w.name, t.id, t.amount, t.currency, t.last_four, | ||||
|             t.expiry_month, t.expiry_year, t.payment_intent, t.bank_return_code, | ||||
|             c.id, c.first_name, c.last_name, c.email | ||||
|         FROM orders o | ||||
|             LEFT JOIN widgets w on (o.widget_id = w.id) | ||||
|             LEFT JOIN transactions t on (o.transaction_id = t.id) | ||||
|             LEFT JOIN customers c on (o.customer_id = c.id) | ||||
|         WHERE | ||||
|             w.is_recurring = ? | ||||
|         ORDER BY | ||||
|             o.created_at DESC | ||||
|         LIMIT ? OFFSET ? | ||||
|     ` | ||||
|  | ||||
| 	rows, err := m.DB.QueryContext(ctx, query, isRecurring, pageSize, offset) | ||||
| 	if err != nil { | ||||
| 		return nil, 0, 0, err | ||||
| 	} | ||||
| 	defer rows.Close() | ||||
|  | ||||
| 	for rows.Next() { | ||||
| 		var o Order | ||||
| 		err = rows.Scan( | ||||
| 			&o.ID, | ||||
| 			&o.WidgetID, | ||||
| 			&o.TransactionID, | ||||
| 			&o.CustomerID, | ||||
| 			&o.StatusID, | ||||
| 			&o.Quantity, | ||||
| 			&o.Amount, | ||||
| 			&o.CreatedAt, | ||||
| 			&o.UpdatedAt, | ||||
| 			&o.Widget.ID, | ||||
| 			&o.Widget.Name, | ||||
| 			&o.Transaction.ID, | ||||
| 			&o.Transaction.Amount, | ||||
| 			&o.Transaction.Currency, | ||||
| 			&o.Transaction.LastFour, | ||||
| 			&o.Transaction.ExpiryMonth, | ||||
| 			&o.Transaction.ExpiryYear, | ||||
| 			&o.Transaction.PaymentIntent, | ||||
| 			&o.Transaction.BankReturnCode, | ||||
| 			&o.Customer.ID, | ||||
| 			&o.Customer.FirstName, | ||||
| 			&o.Customer.LastName, | ||||
| 			&o.Customer.Email, | ||||
| 		) | ||||
| 		if err != nil { | ||||
| 			return nil, 0, 0, err | ||||
| 		} | ||||
| 		orders = append(orders, &o) | ||||
| 	} | ||||
|  | ||||
| 	query = ` | ||||
|         SELECT COUNT(o.id) | ||||
|         FROM orders o | ||||
|         LEFT JOIN widgets w on (o.widget_id = w.id) | ||||
|         WHERE w.is_recurring = ? | ||||
|     ` | ||||
|  | ||||
| 	var totalRecords int | ||||
| 	countRow := m.DB.QueryRowContext(ctx, query, isRecurring) | ||||
| 	err = countRow.Scan(&totalRecords) | ||||
| 	if err != nil { | ||||
| 		return nil, 0, 0, err | ||||
| 	} | ||||
|  | ||||
| 	lastPage := totalRecords / pageSize | ||||
|  | ||||
| 	return orders, lastPage, totalRecords, nil | ||||
| } | ||||
|  | ||||
| func (m *DBModel) GetOrderByID(ID int) (Order, error) { | ||||
| 	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) | ||||
| 	defer cancel() | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import {formatCurrency} from "./common.js" | ||||
| import { formatCurrency } from "./common.js" | ||||
|  | ||||
| export function showTable(api) { | ||||
|     let token = localStorage.getItem("token"); | ||||
| @ -17,26 +17,32 @@ export function showTable(api) { | ||||
|         .then(response => response.json()) | ||||
|         .then(function (data) { | ||||
|             if (data) { | ||||
|             data.forEach(function (i) { | ||||
|                 let newRow = tbody.insertRow(); | ||||
|                 let newCell = newRow.insertCell(); | ||||
|                 data.forEach(function (i) { | ||||
|                     let newRow = tbody.insertRow(); | ||||
|                     let newCell = newRow.insertCell(); | ||||
|  | ||||
|                 newCell.innerHTML = `<a href="/admin/subscriptions/${i.id}">Order ${i.id}</a>`; | ||||
|                     newCell.innerHTML = `<a href="/admin/subscriptions/${i.id}">Order ${i.id}</a>`; | ||||
|  | ||||
|                 newCell = newRow.insertCell(); | ||||
|                 let item = document.createTextNode(i.customer.last_name + ", " + i.customer.first_name); | ||||
|                 newCell.appendChild(item) | ||||
|                     newCell = newRow.insertCell(); | ||||
|                     let item = document.createTextNode(i.customer.last_name + ", " + i.customer.first_name); | ||||
|                     newCell.appendChild(item) | ||||
|  | ||||
|                 newCell = newRow.insertCell(); | ||||
|                 item = document.createTextNode(i.widget.name); | ||||
|                 newCell.appendChild(item) | ||||
|                     newCell = newRow.insertCell(); | ||||
|                     item = document.createTextNode(i.widget.name); | ||||
|                     newCell.appendChild(item) | ||||
|  | ||||
|                 let cur = formatCurrency(i.transaction.amount) | ||||
|                 newCell = newRow.insertCell(); | ||||
|                 item = document.createTextNode(cur + "/month"); | ||||
|                 newCell.appendChild(item) | ||||
|                     let cur = formatCurrency(i.transaction.amount) | ||||
|                     newCell = newRow.insertCell(); | ||||
|                     item = document.createTextNode(cur + "/month"); | ||||
|                     newCell.appendChild(item) | ||||
|  | ||||
|             }); | ||||
|                     newCell = newRow.insertCell(); | ||||
|                     if (i.status_id != 1) { | ||||
|                         newCell.innerHTML = `<span class="badge bg-danger">Cancelled</span>` | ||||
|                     } else { | ||||
|                         newCell.innerHTML = `<span class="badge bg-success">Charged</span>` | ||||
|                     } | ||||
|                 }); | ||||
|             } else { | ||||
|                 let newRow = tbody.insertRow(); | ||||
|                 let newCell = newRow.insertCell(); | ||||
|  | ||||
| @ -39,7 +39,7 @@ export function showInfo(api) { | ||||
|         }); | ||||
| } | ||||
|  | ||||
| export function refund(api) { | ||||
| export function refund(api, isRefund) { | ||||
|     Swal.fire({ | ||||
|         title: "Are you sure?", | ||||
|         text: "You won't be able to undo this!", | ||||
| @ -47,7 +47,7 @@ export function refund(api) { | ||||
|         showCancelButton: true, | ||||
|         confirmButtonColor: "#3085d6", | ||||
|         cancelButtonColor: "#d33", | ||||
|         confirmButtonText: "Refund" | ||||
|         confirmButtonText: isRefund === 1 ? "Refund" : "Cancel subscription" | ||||
|     }).then((result) => { | ||||
|         if (result.isConfirmed) { | ||||
|             let payload = { | ||||
| @ -66,15 +66,15 @@ export function refund(api) { | ||||
|                 body: JSON.stringify(payload), | ||||
|             }; | ||||
|  | ||||
|             fetch(api + "/api/admin/refund", requestOptions) | ||||
|             fetch(api, requestOptions) | ||||
|                 .then(response => response.json()) | ||||
|                 .then(function (data) { | ||||
|                     console.log(data); | ||||
|                     if (!data.ok) { | ||||
|                         showError(data.message) | ||||
|                         showError("messages", data.message) | ||||
|  | ||||
|                     } else { | ||||
|                         showSuccess("messages", "Refunded!") | ||||
|                         showSuccess("messages",  isRefund === 1 ? "Refunded" : "Subscription cancelled") | ||||
|                         document.getElementById("refund-btn").classList.add("d-none"); | ||||
|                         document.getElementById("refunded").classList.remove("d-none"); | ||||
|                         document.getElementById("charged").classList.add("d-none"); | ||||
|  | ||||
		Reference in New Issue
	
	Block a user