package forecast import ( "strings" "testing" "github.com/shopspring/decimal" ) // fcoCSV is an FCO shaped export: mixed header spellings, a money field with commas (quoted), a dollar // range, a fiscal quarter and an explicit date, a TBD row, and one empty row that has no identity. const fcoCSV = `Listing ID,Title,Agency,Organization,NAICS Code,Place of Performance State,Acquisition Strategy,Contract Type,Award Status,Estimated Value,Estimated Award Date FC-1,Cyber support,DEPT OF DEFENSE,Air Force,541512,HI,8(a) Set Aside,FFP,Forecast,"$5,000,000",Q3 FY2026 FC-2,Cloud migration,GSA,FAS,518210,NM,Full and Open,T&M,Planned,"$1M to $5M",06/15/2026 ,,,,,,,,,, FC-3,No date job,GSA,FAS,541512,VA,Set Aside,FFP,Planned,TBD,TBD ` func TestParseGSAFCO(t *testing.T) { recs, skipped, err := Parse(strings.NewReader(fcoCSV), GSAFCOMapping()) if err != nil { t.Fatalf("Parse: %v", err) } if skipped != 1 { t.Fatalf("skipped = %d, want 1 (the empty row with no identity)", skipped) } if len(recs) != 3 { t.Fatalf("records = %d, want 3", len(recs)) } byID := map[string]Record{} for _, r := range recs { byID[r.SourceID] = r } fc1 := byID["FC-1"] if fc1.Title != "Cyber support" || fc1.Agency != "DEPT OF DEFENSE" || fc1.Organization != "Air Force" { t.Fatalf("fc1 fields: %+v", fc1) } if fc1.NAICS != "541512" || fc1.PlaceState != "HI" || fc1.SetAside != "8(a) Set Aside" || fc1.ContractType != "FFP" { t.Fatalf("fc1 mapped fields: %+v", fc1) } if fc1.EstimatedValue == nil || !fc1.EstimatedValue.Equal(decimal.RequireFromString("5000000")) { t.Fatalf("fc1 value = %v", fc1.EstimatedValue) } if fc1.EstimatedAward == nil || fc1.EstimatedAward.Format("2006-01-02") != "2026-04-01" { t.Fatalf("fc1 award date = %v, want 2026-04-01 (Q3 FY2026)", fc1.EstimatedAward) } fc2 := byID["FC-2"] if fc2.EstimatedValue != nil { t.Fatalf("a dollar range must not become a single value, got %v", fc2.EstimatedValue) } if fc2.EstimatedAward == nil || fc2.EstimatedAward.Format("2006-01-02") != "2026-06-15" { t.Fatalf("fc2 award date = %v", fc2.EstimatedAward) } fc3 := byID["FC-3"] if fc3.EstimatedValue != nil || fc3.EstimatedAward != nil { t.Fatal("a TBD value and date must stay nil, never fabricated") } if fc3.Raw["Award Status"] != "Planned" { t.Fatalf("raw row not preserved: %v", fc3.Raw) } } func TestParseValue(t *testing.T) { cases := map[string]string{ "$5,000,000": "5000000", "5000000": "5000000", "$1.5M": "1500000", "2 billion": "2000000000", "$1M to $5M": "", "Approx. 500K": "", "Item #500": "", "TBD": "", "": "", "Various": "", } for in, want := range cases { got := parseValue(in) if want == "" { if got != nil { t.Fatalf("parseValue(%q) = %v, want nil", in, got) } continue } if got == nil || !got.Equal(decimal.RequireFromString(want)) { t.Fatalf("parseValue(%q) = %v, want %s", in, got, want) } } } func TestParseDate(t *testing.T) { cases := map[string]string{ "2026-06-15": "2026-06-15", "06/15/2026": "2026-06-15", "Q3 FY2026": "2026-04-01", "Q1 FY2026": "2025-10-01", "4th Quarter FY 2026": "2026-07-01", "06/2026": "2026-06-01", "TBD": "", "": "", "sometime": "", } for in, want := range cases { got := parseDate(in) if want == "" { if got != nil { t.Fatalf("parseDate(%q) = %v, want nil", in, got) } continue } if got == nil || got.Format("2006-01-02") != want { t.Fatalf("parseDate(%q) = %v, want %s", in, got, want) } } }