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
}