package main import ( "fmt" "log" "time" ) type BillService struct { db *DB bills map[int64]Bill } const paymentColumns = "id, billid, monthFor, paymentDate" const billColumns = "id, name" func (s *BillService) GetPaymentsForDate(date time.Time) ([]Payment, error) { log.Printf("GetPaymentsForDate for %v", date) res := []Payment{} if s == nil { return res, fmt.Errorf("calling GetPaymentsFor on nil BillService") } if s.db == nil || !s.db.Ready { return res, fmt.Errorf("cannot get payments, db is nil or not ready - %v", s.db) } rows, err := s.db.readConn.Query(fmt.Sprintf(` SELECT %s FROM Payment WHERE monthFor = date(strftime('%%Y-%%m-01', ?)); `, paymentColumns), date) if err != nil { return res, fmt.Errorf("query for payments of %s failed with error: %v", date, err) } for rows.Next() { payment := Payment{} err = rows.Scan(&payment.Id, &payment.BillId, &payment.MonthFor, &payment.PaymentDate) if err != nil { Error.Printf("failed to scan row: %v", err) continue } res = append(res, payment) } return res, nil } func (s *BillService) GetPaymentForBillAndDate(billid int64, date time.Time) (Payment, error) { log.Printf("GetPaymentForBillAndDate for %d and %s", billid, date) res := Payment{} if s == nil { return res, fmt.Errorf("calling GetPaymentsFor on nil BillService") } if s.db == nil || !s.db.Ready { return res, fmt.Errorf("cannot get payments, db is nil or not ready - %v", s.db) } row := s.db.readConn.QueryRow(fmt.Sprintf(` SELECT %s FROM Payment WHERE billid = ? AND monthFor = date(strftime('%%Y-%%m-01', ?)); `, paymentColumns), billid, date) err := row.Scan(&res.Id, &res.BillId, &res.MonthFor, &res.PaymentDate) if err != nil { return res, fmt.Errorf("failed scanning row: %v", err) } return res, nil } func (s *BillService) GetAllBills() ([]Bill, error) { log.Printf("GetAllBills") res := []Bill{} if s == nil { return res, fmt.Errorf("calling GetAllBills on nil BillService") } if s.db == nil || !s.db.Ready { return res, fmt.Errorf("cannot get bills, db is nil or not ready - %v", s.db) } rows, err := s.db.readConn.Query(fmt.Sprintf(`SELECT %s FROM Bill ORDER BY name`, billColumns)) if err != nil { return res, fmt.Errorf("failed to query for bills: %w", err) } for rows.Next() { bill := Bill{} err := rows.Scan(&bill.Id, &bill.Name) if err != nil { Error.Printf("failed to scan row: %v", err) continue } res = append(res, bill) } s.bills = make(map[int64]Bill) for _, bill := range res { s.bills[bill.Id] = bill } return res, nil } func (s *BillService) MarkPaid(billid int64, monthFor time.Time, when time.Time) (Payment, error) { log.Printf("MarkPaid for %d, %v and %v", billid, monthFor, when) res := Payment{} if s == nil { return res, fmt.Errorf("calling MarkPaid on nil BillService") } if s.db == nil || !s.db.Ready { return res, fmt.Errorf("cannot mark bill paid, db is nil or not ready - %v", s.db) } qres, err := s.db.writeConn.Exec(` INSERT INTO Payment (billid, monthFor, paymentDate) VALUES (?, date(strftime('%Y-%m-01', ?)), ?) ON CONFLICT(billid, monthFor) DO UPDATE SET paymentDate = excluded.paymentDate WHERE Payment.paymentDate IS NULL `, billid, monthFor, when) if err != nil { return res, fmt.Errorf("failed upserting into payment with error: %w", err) } rows, err := qres.RowsAffected() if err != nil { return res, fmt.Errorf("failed to get rows affected: %w", err) } if rows == 0 { return res, fmt.Errorf("no rows affected") } return s.GetPaymentForBillAndDate(billid, monthFor) } func (s *BillService) MovePayment(billid int64, fromMonth time.Time, toMonth time.Time) (Payment, error) { log.Printf("MovePayment for %d from %v to %v", billid, fromMonth, toMonth) res := Payment{} if s == nil { return res, fmt.Errorf("calling MovePayment on nil BillService") } if s.db == nil || !s.db.Ready { return res, fmt.Errorf("cannot move payment, db is nil or not ready - %v", s.db) } // First get the existing payment existingPayment, err := s.GetPaymentForBillAndDate(billid, fromMonth) if err != nil { return res, fmt.Errorf("failed to get existing payment: %w", err) } // Delete the old payment _, err = s.db.writeConn.Exec(` DELETE FROM Payment WHERE billid = ? AND monthFor = date(strftime('%Y-%m-01', ?)) `, billid, fromMonth) if err != nil { return res, fmt.Errorf("failed to delete old payment: %w", err) } // Create new payment in the target month payment, err := s.MarkPaid(billid, toMonth, existingPayment.PaymentDate) if err != nil { return res, fmt.Errorf("failed to create new payment: %w", err) } return payment, nil } func (s *BillService) AddBill(name string) (Bill, error) { log.Printf("AddBill with name %s", name) res := Bill{} if s == nil { return res, fmt.Errorf("calling AddBill on nil BillService") } if s.db == nil || !s.db.Ready { return res, fmt.Errorf("cannot add bill, db is nil or not ready - %v", s.db) } qres, err := s.db.writeConn.Exec(` INSERT INTO Bill (name) VALUES (?) `, name) if err != nil { return res, fmt.Errorf("failed to insert bill: %w", err) } id, err := qres.LastInsertId() if err != nil { return res, fmt.Errorf("failed to get last insert id: %w", err) } res.Id = id res.Name = name // Refresh the bills cache _, _ = s.GetAllBills() return res, nil } func (s *BillService) RemoveBill(billid int64) error { log.Printf("RemoveBill with id %d", billid) if s == nil { return fmt.Errorf("calling RemoveBill on nil BillService") } if s.db == nil || !s.db.Ready { return fmt.Errorf("cannot remove bill, db is nil or not ready - %v", s.db) } // Delete all payments for this bill first _, err := s.db.writeConn.Exec(` DELETE FROM Payment WHERE billid = ? `, billid) if err != nil { return fmt.Errorf("failed to delete payments for bill: %w", err) } // Delete the bill _, err = s.db.writeConn.Exec(` DELETE FROM Bill WHERE id = ? `, billid) if err != nil { return fmt.Errorf("failed to delete bill: %w", err) } // Refresh the bills cache _, _ = s.GetAllBills() return nil } func (s *BillService) UnmarkPaid(billid int64, month time.Time) error { log.Printf("UnmarkPaid for %d and %v", billid, month) if s == nil { return fmt.Errorf("calling UnmarkPaid on nil BillService") } if s.db == nil || !s.db.Ready { return fmt.Errorf("cannot unmark paid, db is nil or not ready - %v", s.db) } _, err := s.db.writeConn.Exec(` DELETE FROM Payment WHERE billid = ? AND monthFor = date(strftime('%Y-%m-01', ?)) `, billid, month) if err != nil { return fmt.Errorf("failed to delete payment: %w", err) } return nil }