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 4be4cd5..297a44d 100644 --- a/database.go +++ b/database.go @@ -8,7 +8,7 @@ import ( "os" ) -type Card struct { +type card struct { card_id int card_guid string k0_auth_key string @@ -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 { @@ -36,6 +37,14 @@ type payment struct { paid_flag string } +type transaction struct { + card_id int + tx_id int + tx_type string + tx_amount_msats int + tx_time string +} + func db_open() (*sql.DB, error) { // get connection string from environment variables @@ -55,9 +64,9 @@ func db_open() (*sql.DB, error) { return db, nil } -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() if err != nil { @@ -191,7 +200,7 @@ func db_get_card_id_for_r_hash(r_hash string) (int, error) { return card_id, nil } -func db_get_cards_blank_uid() ([]Card, error) { +func db_get_cards_blank_uid() ([]card, error) { // open the database @@ -217,17 +226,17 @@ func db_get_cards_blank_uid() ([]Card, error) { // prepare the results - var cards []Card + var cards []card // Loop through rows, using Scan to assign column data to struct fields. for rows.Next() { - var card Card - err := rows.Scan(&card.card_id, &card.k2_cmac_key) + var c card + err := rows.Scan(&c.card_id, &c.k2_cmac_key) if err != nil { return cards, err } - cards = append(cards, card) + cards = append(cards, c) } err = rows.Err() @@ -262,9 +271,9 @@ func db_update_card_uid_ctr(card_id int, uid string, ctr uint32) error { return nil } -func db_get_card_from_uid(card_uid string) (*Card, error) { +func db_get_card_from_uid(card_uid string) (*card, error) { - c := Card{} + c := card{} db, err := db_open() if err != nil { @@ -293,9 +302,9 @@ func db_get_card_from_uid(card_uid string) (*Card, error) { return &c, nil } -func db_get_card_from_card_id(card_id int) (*Card, error) { +func db_get_card_from_card_id(card_id int) (*card, error) { - c := Card{} + c := card{} db, err := db_open() if err != nil { @@ -303,10 +312,11 @@ func db_get_card_from_card_id(card_id int) (*Card, error) { } defer db.Close() - 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` + - ` FROM cards WHERE card_id=$1;` + 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, ` + + `allow_negative_balance FROM cards WHERE card_id=$1;` row := db.QueryRow(sqlStatement, card_id) err = row.Scan( &c.card_id, @@ -318,7 +328,9 @@ func db_get_card_from_card_id(card_id int) (*Card, error) { &c.tx_limit_sats, &c.day_limit_sats, &c.email_enable, - &c.email_address) + &c.email_address, + &c.card_name, + &c.allow_negative_balance) if err != nil { return &c, err } @@ -584,3 +596,90 @@ func db_get_card_totals(card_id int) (int, error) { return day_total_sats, nil } + +func db_get_card_txs(card_id int, max_txs int) ([]transaction, error) { + // open the database + + db, err := db_open() + + if err != nil { + return nil, err + } + + defer db.Close() + + // query the database + + 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 LIMIT $2` + + rows, err := db.Query(sqlStatement, card_id, max_txs) + + if err != nil { + return nil, err + } + + defer rows.Close() + + // prepare the results + + 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) + + if err != nil { + return transactions, err + } + transactions = append(transactions, t) + } + + err = rows.Err() + + if err != nil { + return transactions, err + } + + return transactions, nil +} + +func db_get_card_total_sats(card_id int) (int, error) { + + db, err := db_open() + if err != nil { + return 0, err + } + + card_total_msats := 0 + + sqlStatement := `SELECT SUM(tx_amount_msats) FROM (SELECT card_id, ` + + `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 ` + + `AND receipt_status = 'SETTLED' ORDER BY tx_time) AS transactions;` + + row := db.QueryRow(sqlStatement, card_id) + err = row.Scan(&card_total_msats) + if err != nil { + return 0, err + } + + card_total_sats := card_total_msats / 1000 + + return card_total_sats, nil +} diff --git a/email.go b/email.go index ba2d6cc..100643b 100644 --- a/email.go +++ b/email.go @@ -8,8 +8,83 @@ import ( "github.com/aws/aws-sdk-go/service/ses" log "github.com/sirupsen/logrus" "os" + "strconv" + "strings" ) +func send_balance_email(recipient_email string, card_id int) { + + c, err := db_get_card_from_card_id(card_id) + if err != nil { + log.Warn(err.Error()) + return + } + + card_total_sats, err := db_get_card_total_sats(card_id) + if err != nil { + log.Warn(err.Error()) + return + } + + 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 + } + + subject := c.card_name + " balance = " + strconv.Itoa(card_total_sats) + " sats" + + // add transactions to the email body + + var html_body_sb strings.Builder + var text_body_sb strings.Builder + + html_body_sb.WriteString("
") + + html_body_sb.WriteString("| date | action | amount | ") + text_body_sb.WriteString("transactions\n\n") + + for i, tx := range txs { + + if i < email_max_txs { + html_body_sb.WriteString( + "
|---|---|---|
| " + tx.tx_time + " | " + + "" + tx.tx_type + " | " + + "" + strconv.Itoa(tx.tx_amount_msats/1000) + " | " + + "
| ... | " + + "... | " + + "... | " + + "