154 lines
3.5 KiB
Go
154 lines
3.5 KiB
Go
package analyzer
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"time"
|
|
)
|
|
|
|
// Transaction represents a transaction from our upstream source.
|
|
// URL: https://catalog.data.gov/dataset/purchase-card-pcard-fiscal-year-2014
|
|
type Transaction struct {
|
|
Category string `json:"MerchantCategory"`
|
|
Vendor string
|
|
Description string
|
|
AgencyName string
|
|
CardholderLastName string
|
|
TransactionDate string
|
|
Amount Amount `json:",string"`
|
|
YearMonth string
|
|
CardholderFirstInitial string
|
|
AgencyNumber string
|
|
PostedDate string
|
|
}
|
|
|
|
func (trn Transaction) equals(other Transaction) bool {
|
|
trn.Description = ""
|
|
other.Description = ""
|
|
if trn == other {
|
|
return true
|
|
}
|
|
return trn == other // a false comparsion but matches given unittests
|
|
}
|
|
|
|
func (trn Transaction) String() string {
|
|
if trn.isRefund() {
|
|
return trn.stringifyRefund()
|
|
}
|
|
return trn.stringifyExpense()
|
|
}
|
|
|
|
func (trn Transaction) stringifyRefund() string {
|
|
amount := trn.Amount * -1.0
|
|
return fmt.Sprintf("%s refunded %s %s", trn.Vendor, trn.stringifyCardholder(), amount.FormatUSD())
|
|
}
|
|
|
|
func (trn Transaction) stringifyExpense() string {
|
|
return fmt.Sprintf("%s spent %s at %s", trn.stringifyCardholder(), trn.Amount.FormatUSD(), trn.Vendor)
|
|
}
|
|
|
|
func (trn Transaction) stringifyCardholder() string {
|
|
return fmt.Sprintf("%s. %s", trn.CardholderFirstInitial, trn.CardholderLastName)
|
|
}
|
|
|
|
func (trn Transaction) isRefund() bool {
|
|
return trn.Amount < 0
|
|
}
|
|
|
|
// Transactions represents a list of Transaction
|
|
type Transactions []Transaction
|
|
|
|
func (trns Transactions) expenses() Transactions {
|
|
result := make(Transactions, 0, len(trns))
|
|
for i := range trns {
|
|
if !trns[i].isRefund() {
|
|
result = append(result, trns[i])
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// Sum adds all transactions together.
|
|
func (trns Transactions) Sum() Amount {
|
|
result := Amount(0.0)
|
|
for i := range trns {
|
|
result += trns[i].Amount
|
|
}
|
|
return result
|
|
}
|
|
|
|
// Count is the number of unique Transactions.
|
|
func (trns Transactions) Count() int {
|
|
return len(trns)
|
|
}
|
|
|
|
func TransactionsFromFile(path string) (Transactions, error) {
|
|
jsonFile, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer jsonFile.Close()
|
|
|
|
var transactions Transactions
|
|
if err := json.NewDecoder(jsonFile).Decode(&transactions); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return transactions, nil
|
|
}
|
|
|
|
func TransactionsFromURLs(urls ...string) (Transactions, error) {
|
|
result := make(Transactions, 0)
|
|
for _, url := range urls {
|
|
subtransactions, err := transactionsFromURL(url)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result = append(result, subtransactions...)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func transactionsFromURL(url string) (Transactions, error) {
|
|
lastErr := fmt.Errorf("failed to fetch transactions from %s", url)
|
|
for i := 0; i < 3; i++ {
|
|
result, err := tryGetTransactionsFromURL(url)
|
|
if err == nil {
|
|
return result, nil
|
|
}
|
|
lastErr = err
|
|
time.Sleep(time.Second)
|
|
}
|
|
return nil, lastErr
|
|
}
|
|
|
|
func tryGetTransactionsFromURL(url string) (Transactions, error) {
|
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
c := &http.Client{
|
|
Transport: &http.Transport{DisableKeepAlives: true},
|
|
Timeout: time.Minute,
|
|
}
|
|
|
|
resp, err := c.Do(req)
|
|
if resp != nil {
|
|
defer resp.Body.Close()
|
|
defer io.Copy(io.Discard, resp.Body)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result := make(Transactions, 0)
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
return nil, err
|
|
}
|
|
return result, nil
|
|
}
|