~mendelmaleh/pfin

b8d4ee5c21941590445e34911909d121b1926c75 — Mendel Elmaleh 10 months ago 9e9eb5b
Add cmd/capitalone
3 files changed, 156 insertions(+), 0 deletions(-)

A cmd/capitalone/main.go
A cmd/capitalone/type.go
M parser/util/date.go
A cmd/capitalone/main.go => cmd/capitalone/main.go +80 -0
@@ 0,0 1,80 @@
package main

import (
	"encoding/csv"
	"encoding/json"
	"fmt"
	"log"
	"os"
	"strconv"
	"strings"

	"git.sr.ht/~mendelmaleh/pfin/parser/capitalone"
	"git.sr.ht/~mendelmaleh/pfin/parser/util"
	"github.com/jszwec/csvutil"
)

func check(err error) {
	if err != nil {
		log.Fatal(err)
	}
}

func main() {
	data, err := os.ReadFile("sample.json")
	check(err)

	var raw T
	err = json.Unmarshal(data, &raw)
	check(err)

	last, err := util.NewDateISO("2023-12-15")
	check(err)

	var txns []capitalone.Raw
	for _, v := range raw.Entries {
		if t := v.TransactionBeginningStatementTimeStamp; t.IsZero() || t.After(last.Time) {
			tx := capitalone.Raw{
				TransactionDate: util.DateISO{v.TransactionDate},
				PostedDate:      util.DateISO{v.TransactionPostedDate},
				CardNumber:      v.TransactingCardLastFour,
				Description:     v.TransactionDescription,
				Category:        v.DisplayCategory,
				Debit:           0,
				Credit:          0,
			}

			if tx.TransactionDate.IsZero() {
				tx.TransactionDate = util.DateISO{v.TransactionDisplayDate}
			}

			if s := v.StatementDescription; s != "" {
				tx.Description = strings.TrimSpace(strings.Split(s, v.TransactionMerchant.Address.City)[0])
			}

			switch v.TransactionDebitCredit {
			case "Debit":
				tx.Debit = v.TransactionAmount
			case "Credit":
				tx.Credit = v.TransactionAmount
			default:
				check(fmt.Errorf("unexpected tx type %q", v.TransactionDebitCredit))
			}

			txns = append(txns, tx)
		}
	}

	w := csv.NewWriter(os.Stdout)
	enc := csvutil.NewEncoder(w)
	enc.Register(func(f float64) ([]byte, error) {
		if f == 0 {
			return []byte{}, nil
		}
		return []byte(strconv.FormatFloat(f, 'f', 2, 64)), nil
	})

	err = enc.Encode(txns)
	check(err)
	w.Flush()
}

A cmd/capitalone/type.go => cmd/capitalone/type.go +71 -0
@@ 0,0 1,71 @@
package main

import (
	"time"
)

type T struct {
	Entries []struct {
		AcquirerTransactionReferenceNumber     string    `json:"acquirerTransactionReferenceNumber,omitempty"`
		AuthorizationIssuerCode                string    `json:"authorizationIssuerCode,omitempty"`
		AuthorizationResponse                  string    `json:"authorizationResponse,omitempty"`
		AuthorizationStatus                    string    `json:"authorizationStatus,omitempty"`
		AuthorizationTimeStamp                 time.Time `json:"authorizationTimeStamp,omitempty"`
		AuthorizationType                      string    `json:"authorizationType,omitempty"`
		CategoryIconURL                        string    `json:"categoryIconURL,omitempty"`
		CategoryImageURL                       string    `json:"categoryImageURL,omitempty"`
		DisplayCategory                        string    `json:"displayCategory"`
		DisputedCount                          int       `json:"disputedCount"`
		HasDisputeIndicator                    bool      `json:"hasDisputeIndicator"`
		IsMemoPosted                           bool      `json:"isMemoPosted"`
		IsPrintable                            bool      `json:"isPrintable,omitempty"`
		IsReportedAsFraud                      bool      `json:"isReportedAsFraud"`
		LastIssuedCardLastFourDigits           string    `json:"lastIssuedCardLastFourDigits"`
		MaskedVirtualCardNumber                string    `json:"maskedVirtualCardNumber,omitempty"`
		StatementDescription                   string    `json:"statementDescription,omitempty"`
		TransactingCardLastFour                string    `json:"transactingCardLastFour"`
		TransactionAmount                      float64   `json:"transactionAmount"`
		TransactionBeginningStatementTimeStamp time.Time `json:"transactionBeginningStatementTimeStamp,omitempty"`
		TransactionCategoryCode                string    `json:"transactionCategoryCode,omitempty"`
		TransactionDate                        time.Time `json:"transactionDate,omitempty"`
		TransactionDebitCredit                 string    `json:"transactionDebitCredit"`
		TransactionDescription                 string    `json:"transactionDescription"`
		TransactionDisplayDate                 time.Time `json:"transactionDisplayDate"`
		TransactionLifecycleID                 string    `json:"transactionLifecycleId,omitempty"`
		TransactionMerchant                    struct {
			Address struct {
				AddressLine1 string `json:"addressLine1,omitempty"`
				City         string `json:"city"`
				CountryCode  string `json:"countryCode"`
				PostalCode   string `json:"postalCode"`
				StateCode    string `json:"stateCode"`
			} `json:"address"`
			Category         string `json:"category,omitempty"`
			CategoryCode     string `json:"categoryCode,omitempty"`
			ChainPhoneNumber string `json:"chainPhoneNumber,omitempty"`
			GeoLocation      *struct {
				Latitude  string `json:"latitude,omitempty"`
				Longitude string `json:"longitude,omitempty"`
			} `json:"geoLocation,omitempty"`
			LogoURL          string `json:"logoURL,omitempty"`
			MerchantID       string `json:"merchantId,omitempty"`
			MerchantType     string `json:"merchantType,omitempty"`
			MerchantTypeCode string `json:"merchantTypeCode,omitempty"`
			Name             string `json:"name"`
			ParentCategory   string `json:"parentCategory,omitempty"`
			PhoneNumber      string `json:"phoneNumber"`
			WebsiteURL       string `json:"websiteURL,omitempty"`
		} `json:"transactionMerchant"`
		TransactionPostedDate           time.Time `json:"transactionPostedDate,omitempty"`
		TransactionPostedSequenceNumber int       `json:"transactionPostedSequenceNumber,omitempty"`
		TransactionReferenceID          string    `json:"transactionReferenceId"`
		TransactionState                string    `json:"transactionState"`
		TransactionSubcategoryCode      string    `json:"transactionSubcategoryCode,omitempty"`
		TransactionType                 string    `json:"transactionType,omitempty"`
		TransactionTypeCode             string    `json:"transactionTypeCode,omitempty"`
		ViewOrderURL                    string    `json:"viewOrderUrl,omitempty"`
	} `json:"entries"`
	IsHaMode          bool `json:"isHAMode"`
	IsPartialResponse bool `json:"isPartialResponse"`
	IsVcnRedacted     bool `json:"isVCNRedacted"`
}

M parser/util/date.go => parser/util/date.go +5 -0
@@ 32,6 32,11 @@ func (d DateISO) MarshalText() ([]byte, error) {
	return []byte(d.String()), nil
}

func NewDateISO(s string) (d DateISO, err error) {
	err = d.UnmarshalText([]byte(s))
	return
}

type DateUS struct {
	time.Time
}