add max txs setting for email, add setting to stop negative card balance

This commit is contained in:
Peter Rounce 2022-09-25 19:01:52 +00:00
parent 94267c47cf
commit 9c5149e373
4 changed files with 99 additions and 62 deletions

View file

@ -25,6 +25,7 @@ CREATE TABLE cards (
one_time_code CHAR(32) NOT NULL DEFAULT '', one_time_code CHAR(32) NOT NULL DEFAULT '',
one_time_code_expiry TIMESTAMPTZ DEFAULT NOW() + INTERVAL '1 DAY', one_time_code_expiry TIMESTAMPTZ DEFAULT NOW() + INTERVAL '1 DAY',
one_time_code_used CHAR(1) NOT NULL DEFAULT 'Y', one_time_code_used CHAR(1) NOT NULL DEFAULT 'Y',
allow_negative_balance CHAR(1) NOT NULL DEFAULT 'N',
PRIMARY KEY(card_id) PRIMARY KEY(card_id)
); );

View file

@ -22,11 +22,12 @@ type card struct {
lnurlw_enable string lnurlw_enable string
tx_limit_sats int tx_limit_sats int
day_limit_sats int day_limit_sats int
lnurlp_enable string lnurlp_enable string
email_address string email_address string
email_enable string email_enable string
one_time_code string one_time_code string
card_name string card_name string
allow_negative_balance string
} }
type payment struct { type payment struct {
@ -37,11 +38,11 @@ type payment struct {
} }
type transaction struct { type transaction struct {
card_id int card_id int
tx_id int tx_id int
tx_type string tx_type string
tx_amount_msats int tx_amount_msats int
tx_time string tx_time string
} }
func db_open() (*sql.DB, error) { func db_open() (*sql.DB, error) {
@ -65,7 +66,7 @@ func db_open() (*sql.DB, error) {
func db_get_new_card(one_time_code string) (*card, error) { func db_get_new_card(one_time_code string) (*card, error) {
c :=card{} c := card{}
db, err := db_open() db, err := db_open()
if err != nil { if err != nil {
@ -314,8 +315,8 @@ func db_get_card_from_card_id(card_id int) (*card, error) {
sqlStatement := `SELECT card_id, k2_cmac_key, uid, ` + sqlStatement := `SELECT card_id, k2_cmac_key, uid, ` +
`last_counter_value, lnurlw_request_timeout_sec, ` + `last_counter_value, lnurlw_request_timeout_sec, ` +
`lnurlw_enable, tx_limit_sats, day_limit_sats, ` + `lnurlw_enable, tx_limit_sats, day_limit_sats, ` +
`email_enable, email_address, card_name ` + `email_enable, email_address, card_name, ` +
`FROM cards WHERE card_id=$1;` `allow_negative_balance FROM cards WHERE card_id=$1;`
row := db.QueryRow(sqlStatement, card_id) row := db.QueryRow(sqlStatement, card_id)
err = row.Scan( err = row.Scan(
&c.card_id, &c.card_id,
@ -328,7 +329,8 @@ func db_get_card_from_card_id(card_id int) (*card, error) {
&c.day_limit_sats, &c.day_limit_sats,
&c.email_enable, &c.email_enable,
&c.email_address, &c.email_address,
&c.card_name) &c.card_name,
&c.allow_negative_balance)
if err != nil { if err != nil {
return &c, err return &c, err
} }
@ -595,64 +597,64 @@ func db_get_card_totals(card_id int) (int, error) {
return day_total_sats, nil return day_total_sats, nil
} }
func db_get_card_txs(card_id int) ([]transaction, error) { func db_get_card_txs(card_id int, max_txs int) ([]transaction, error) {
// open the database // open the database
db, err := db_open() db, err := db_open()
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer db.Close() defer db.Close()
// query the database // query the database
//TODO: LIMIT 10 + COUNT sqlStatement := `SELECT card_id, ` +
sqlStatement := `SELECT card_id, ` +
`card_payments.card_payment_id AS tx_id, 'payment' AS tx_type, ` + `card_payments.card_payment_id AS tx_id, 'payment' AS tx_type, ` +
`amount_msats as tx_amount_msats, ` + `amount_msats as tx_amount_msats, ` +
`TO_CHAR(payment_status_time, 'DD/MM/YYYY HH:MI:SS') AS tx_time ` + `TO_CHAR(payment_status_time, 'DD/MM/YYYY HH:MI:SS') AS tx_time ` +
`FROM card_payments WHERE card_id = $1 AND payment_status != 'FAILED' ` + `FROM card_payments WHERE card_id = $1 AND payment_status != 'FAILED' ` +
`AND payment_status != '' ` +
`AND amount_msats != 0 UNION SELECT card_id, card_receipts.card_receipt_id AS tx_id, ` + `AND amount_msats != 0 UNION SELECT card_id, card_receipts.card_receipt_id AS tx_id, ` +
`'receipt' AS tx_type, amount_msats as tx_amount_msats, ` + `'receipt' AS tx_type, amount_msats as tx_amount_msats, ` +
`TO_CHAR(receipt_status_time, 'DD/MM/YYYY HH:MI:SS') AS tx_time ` + `TO_CHAR(receipt_status_time, 'DD/MM/YYYY HH:MI:SS') AS tx_time ` +
`FROM card_receipts WHERE card_id = $1 ` + `FROM card_receipts WHERE card_id = $1 ` +
`AND receipt_status = 'SETTLED' ORDER BY tx_time DESC` `AND receipt_status = 'SETTLED' ORDER BY tx_time DESC LIMIT $2`
rows, err := db.Query(sqlStatement, card_id) rows, err := db.Query(sqlStatement, card_id, max_txs)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer rows.Close()
// prepare the results // prepare the results
var transactions []transaction var transactions []transaction
// Loop through rows, using Scan to assign column data to struct fields. // Loop through rows, using Scan to assign column data to struct fields.
for rows.Next() { for rows.Next() {
var t transaction var t transaction
err := rows.Scan(&t.card_id, &t.tx_id, &t.tx_type, &t.tx_amount_msats, &t.tx_time) err := rows.Scan(&t.card_id, &t.tx_id, &t.tx_type, &t.tx_amount_msats, &t.tx_time)
if err != nil { if err != nil {
return transactions, err return transactions, err
} }
transactions = append(transactions, t) transactions = append(transactions, t)
} }
err = rows.Err() err = rows.Err()
if err != nil { if err != nil {
return transactions, err return transactions, err
} }
return transactions, nil return transactions, nil
} }
func db_get_card_total(card_id int) (int, error) { func db_get_card_total_sats(card_id int) (int, error) {
db, err := db_open() db, err := db_open()
if err != nil { if err != nil {
@ -665,6 +667,7 @@ func db_get_card_total(card_id int) (int, error) {
`card_payments.card_payment_id AS tx_id, 'payment' AS tx_type, ` + `card_payments.card_payment_id AS tx_id, 'payment' AS tx_type, ` +
`-amount_msats as tx_amount_msats, payment_status_time AS tx_time ` + `-amount_msats as tx_amount_msats, payment_status_time AS tx_time ` +
`FROM card_payments WHERE card_id = $1 AND payment_status != 'FAILED' ` + `FROM card_payments WHERE card_id = $1 AND payment_status != 'FAILED' ` +
`AND payment_status != '' ` +
`AND amount_msats != 0 UNION SELECT card_id, card_receipts.card_receipt_id AS tx_id, ` + `AND amount_msats != 0 UNION SELECT card_id, card_receipts.card_receipt_id AS tx_id, ` +
`'receipt' AS tx_type, amount_msats as tx_amount_msats, ` + `'receipt' AS tx_type, amount_msats as tx_amount_msats, ` +
`receipt_status_time AS tx_time FROM card_receipts WHERE card_id = $1 ` + `receipt_status_time AS tx_time FROM card_receipts WHERE card_id = $1 ` +

View file

@ -20,13 +20,19 @@ func send_balance_email(recipient_email string, card_id int) {
return return
} }
card_total_sats, err := db_get_card_total(card_id) card_total_sats, err := db_get_card_total_sats(card_id)
if err != nil { if err != nil {
log.Warn(err.Error()) log.Warn(err.Error())
return return
} }
txs, err := db_get_card_txs(card_id) email_max_txs, err := strconv.Atoi(os.Getenv("EMAIL_MAX_TXS"))
if err != nil {
log.Warn(err.Error())
return
}
txs, err := db_get_card_txs(card_id, email_max_txs+1)
if err != nil { if err != nil {
log.Warn(err.Error()) log.Warn(err.Error())
return return
@ -46,17 +52,26 @@ func send_balance_email(recipient_email string, card_id int) {
html_body_sb.WriteString("<h3>transactions</h3><table><tr><th>date</th><th>action</th><th>amount</th>") html_body_sb.WriteString("<h3>transactions</h3><table><tr><th>date</th><th>action</th><th>amount</th>")
text_body_sb.WriteString("transactions\n\n") text_body_sb.WriteString("transactions\n\n")
for _, tx := range txs { for i, tx := range txs {
html_body_sb.WriteString( if i < email_max_txs {
"<tr>" + html_body_sb.WriteString(
"<td>" + tx.tx_time + "</td>" + "<tr>" +
"<td>" + tx.tx_type + "</td>" + "<td>" + tx.tx_time + "</td>" +
"<td style='text-align:right'>" + strconv.Itoa(tx.tx_amount_msats / 1000) + "</td>" + "<td>" + tx.tx_type + "</td>" +
"</tr>") "<td style='text-align:right'>" + strconv.Itoa(tx.tx_amount_msats/1000) + "</td>" +
"</tr>")
} else {
html_body_sb.WriteString(
"<tr>" +
"<td style='text-align:center'> ... </td>" +
"<td style='text-align:center'> ... </td>" +
"<td style='text-align:center'> ... </td>" +
"</tr>")
}
text_body_sb.WriteString(tx.tx_type + text_body_sb.WriteString(tx.tx_type +
" " + strconv.Itoa(tx.tx_amount_msats / 1000)) " " + strconv.Itoa(tx.tx_amount_msats/1000))
} }
html_body_sb.WriteString("</table></body></html>") html_body_sb.WriteString("</table></body></html>")
@ -64,10 +79,10 @@ func send_balance_email(recipient_email string, card_id int) {
html_body := html_body_sb.String() html_body := html_body_sb.String()
text_body := text_body_sb.String() text_body := text_body_sb.String()
send_email(recipient_email, send_email(recipient_email,
subject, subject,
html_body, html_body,
text_body) text_body)
} }
// https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/ses-example-send-email.html // https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/ses-example-send-email.html

View file

@ -9,12 +9,12 @@ import (
func lnurlw_callback(w http.ResponseWriter, req *http.Request) { func lnurlw_callback(w http.ResponseWriter, req *http.Request) {
env_host_domain := os.Getenv("HOST_DOMAIN") env_host_domain := os.Getenv("HOST_DOMAIN")
if req.Host != env_host_domain { if req.Host != env_host_domain {
log.Warn("wrong host domain") log.Warn("wrong host domain")
write_error(w) write_error(w)
return return
} }
url := req.URL.RequestURI() url := req.URL.RequestURI()
log.WithFields(log.Fields{"url": url}).Debug("cb request") log.WithFields(log.Fields{"url": url}).Debug("cb request")
@ -120,6 +120,24 @@ func lnurlw_callback(w http.ResponseWriter, req *http.Request) {
return return
} }
// check the card balance if marked as 'must stay above zero' (default)
// i.e. cards.allow_negative_balance == 'N'
if c.allow_negative_balance != "Y" {
card_total, err := db_get_card_total_sats(p.card_id)
if err != nil {
log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Warn(err)
write_error(w)
return
}
if card_total-invoice_sats < 0 {
log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Warn("not enough balance")
write_error(w)
return
}
}
log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Info("paying invoice") log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Info("paying invoice")
// update paid_flag so we only attempt payment once // update paid_flag so we only attempt payment once
@ -138,7 +156,7 @@ func lnurlw_callback(w http.ResponseWriter, req *http.Request) {
go pay_invoice(p.card_payment_id, param_pr) go pay_invoice(p.card_payment_id, param_pr)
log.Debug("sending 'status OK' response"); log.Debug("sending 'status OK' response")
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)