From 9c5149e373afb91af105c6ead9da7dfb1235c50e Mon Sep 17 00:00:00 2001 From: Peter Rounce Date: Sun, 25 Sep 2022 19:01:52 +0000 Subject: [PATCH] add max txs setting for email, add setting to stop negative card balance --- create_db.sql | 1 + database.go | 87 ++++++++++++++++++++++++---------------------- email.go | 41 +++++++++++++++------- lnurlw_callback.go | 32 +++++++++++++---- 4 files changed, 99 insertions(+), 62 deletions(-) diff --git a/create_db.sql b/create_db.sql index 83abc02..45bafe3 100644 --- a/create_db.sql +++ b/create_db.sql @@ -25,6 +25,7 @@ CREATE TABLE cards ( one_time_code CHAR(32) NOT NULL DEFAULT '', one_time_code_expiry TIMESTAMPTZ DEFAULT NOW() + INTERVAL '1 DAY', one_time_code_used CHAR(1) NOT NULL DEFAULT 'Y', + allow_negative_balance CHAR(1) NOT NULL DEFAULT 'N', PRIMARY KEY(card_id) ); diff --git a/database.go b/database.go index 42453f0..297a44d 100644 --- a/database.go +++ b/database.go @@ -22,11 +22,12 @@ type card struct { lnurlw_enable string tx_limit_sats int day_limit_sats int - lnurlp_enable string - email_address string - email_enable string + lnurlp_enable string + email_address string + email_enable string one_time_code string card_name string + allow_negative_balance string } type payment struct { @@ -37,11 +38,11 @@ type payment struct { } type transaction struct { - card_id int - tx_id int - tx_type string + card_id int + tx_id int + tx_type string tx_amount_msats int - tx_time string + tx_time string } 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) { - c :=card{} + c := card{} db, err := db_open() 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, ` + `last_counter_value, lnurlw_request_timeout_sec, ` + `lnurlw_enable, tx_limit_sats, day_limit_sats, ` + - `email_enable, email_address, card_name ` + - `FROM cards WHERE card_id=$1;` + `email_enable, email_address, card_name, ` + + `allow_negative_balance FROM cards WHERE card_id=$1;` row := db.QueryRow(sqlStatement, card_id) err = row.Scan( &c.card_id, @@ -328,7 +329,8 @@ func db_get_card_from_card_id(card_id int) (*card, error) { &c.day_limit_sats, &c.email_enable, &c.email_address, - &c.card_name) + &c.card_name, + &c.allow_negative_balance) if err != nil { return &c, err } @@ -595,64 +597,64 @@ func db_get_card_totals(card_id int) (int, error) { 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 - db, err := db_open() + db, err := db_open() - if err != nil { - return nil, err - } + if err != nil { + 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, ` + `amount_msats as tx_amount_msats, ` + `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' ` + + `AND payment_status != '' ` + `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, ` + `TO_CHAR(receipt_status_time, 'DD/MM/YYYY HH:MI:SS') AS tx_time ` + `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 { - return nil, err - } + if err != nil { + return nil, err + } - defer rows.Close() + defer rows.Close() // prepare the results - var transactions []transaction + var transactions []transaction - // Loop through rows, using Scan to assign column data to struct fields. - for rows.Next() { - var t transaction - err := rows.Scan(&t.card_id, &t.tx_id, &t.tx_type, &t.tx_amount_msats, &t.tx_time) + // Loop through rows, using Scan to assign column data to struct fields. + for rows.Next() { + var t transaction + err := rows.Scan(&t.card_id, &t.tx_id, &t.tx_type, &t.tx_amount_msats, &t.tx_time) - if err != nil { - return transactions, err - } - transactions = append(transactions, t) - } + if err != nil { + return transactions, err + } + transactions = append(transactions, t) + } - err = rows.Err() + err = rows.Err() - if err != nil { - return transactions, err - } + if err != nil { + 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() 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, ` + `-amount_msats as tx_amount_msats, payment_status_time AS tx_time ` + `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, ` + `'receipt' AS tx_type, amount_msats as tx_amount_msats, ` + `receipt_status_time AS tx_time FROM card_receipts WHERE card_id = $1 ` + diff --git a/email.go b/email.go index 203ecc2..100643b 100644 --- a/email.go +++ b/email.go @@ -20,13 +20,19 @@ func send_balance_email(recipient_email string, card_id int) { 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 { log.Warn(err.Error()) 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 { log.Warn(err.Error()) return @@ -46,17 +52,26 @@ func send_balance_email(recipient_email string, card_id int) { html_body_sb.WriteString("

transactions

") 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 { + html_body_sb.WriteString( + "" + + "" + + "" + + "" + + "") + } else { + html_body_sb.WriteString( + "" + + "" + + "" + + "" + + "") + } 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("
dateactionamount
" + tx.tx_time + "" + tx.tx_type + "" + strconv.Itoa(tx.tx_amount_msats / 1000) + "
" + tx.tx_time + "" + tx.tx_type + "" + strconv.Itoa(tx.tx_amount_msats/1000) + "
... ... ...
") @@ -64,10 +79,10 @@ func send_balance_email(recipient_email string, card_id int) { html_body := html_body_sb.String() text_body := text_body_sb.String() - send_email(recipient_email, + send_email(recipient_email, subject, - html_body, - text_body) + html_body, + text_body) } // https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/ses-example-send-email.html diff --git a/lnurlw_callback.go b/lnurlw_callback.go index 5137969..cfd864b 100644 --- a/lnurlw_callback.go +++ b/lnurlw_callback.go @@ -9,12 +9,12 @@ import ( func lnurlw_callback(w http.ResponseWriter, req *http.Request) { - env_host_domain := os.Getenv("HOST_DOMAIN") - if req.Host != env_host_domain { - log.Warn("wrong host domain") - write_error(w) - return - } + env_host_domain := os.Getenv("HOST_DOMAIN") + if req.Host != env_host_domain { + log.Warn("wrong host domain") + write_error(w) + return + } url := req.URL.RequestURI() log.WithFields(log.Fields{"url": url}).Debug("cb request") @@ -120,6 +120,24 @@ func lnurlw_callback(w http.ResponseWriter, req *http.Request) { 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") // 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) - log.Debug("sending 'status OK' response"); + log.Debug("sending 'status OK' response") w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK)