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 }