package samawards import ( "encoding/json" "strings" "time" "github.com/shopspring/decimal" ) // Award is the flat, app facing shape of a SAM contract award. It uses only Go native types. Optional // money and dates are pointers (nil = absent), never a fabricated zero. The recipient is the incumbent. type Award struct { UniqueKey string // piid plus modification plus transaction, the upsert key for a contract action PIID string AwardeeName string // the incumbent AwardeeUEI string TotalValue *decimal.Decimal // base and all options value (the contract ceiling) ObligatedValue *decimal.Decimal // this action's obligation Department string Subtier string NAICSCode string NAICSDescription string Description string SetAside string DateSigned *time.Time POPStart *time.Time POPEnd *time.Time Raw json.RawMessage } // awardRecord mirrors the nested shape of one awardSummary element. Only the fields the app uses are // decoded; everything else is ignored. The dollars are json.Number so both an integer and a float form // parse exactly into a decimal. type awardRecord struct { ContractID struct { PIID string `json:"piid"` ModificationNumber string `json:"modificationNumber"` TransactionNumber string `json:"transactionNumber"` Subtier struct { Name string `json:"name"` } `json:"subtier"` } `json:"contractId"` AwardDetails struct { AwardeeData struct { AwardeeHeader struct { AwardeeName string `json:"awardeeName"` } `json:"awardeeHeader"` AwardeeUEIInformation struct { UniqueEntityID string `json:"uniqueEntityId"` } `json:"awardeeUEIInformation"` } `json:"awardeeData"` Dollars struct { ActionObligation NumericText `json:"actionObligation"` BaseAndAllOptionsValue NumericText `json:"baseAndAllOptionsValue"` } `json:"dollars"` Dates struct { DateSigned string `json:"dateSigned"` PeriodOfPerformanceStartDate string `json:"periodOfPerformanceStartDate"` CurrentCompletionDate string `json:"currentCompletionDate"` } `json:"dates"` } `json:"awardDetails"` CoreData struct { FederalOrganization struct { ContractingInformation struct { ContractingDepartment struct { Name string `json:"name"` } `json:"contractingDepartment"` } `json:"contractingInformation"` } `json:"federalOrganization"` ProductOrServiceInformation struct { PrincipalNaics []struct { Code string `json:"code"` Description string `json:"description"` } `json:"principalNaics"` } `json:"productOrServiceInformation"` PreferencePrograms struct { TypeOfSetAside struct { Name string `json:"name"` } `json:"typeOfSetAside"` } `json:"preferencePrograms"` ContractData struct { DescriptionOfContractRequirement string `json:"descriptionOfContractRequirement"` } `json:"contractData"` } `json:"coreData"` } // Normalize maps one raw award record to an Award. It returns false when the record has no PIID (an // upsert key cannot be formed), so the caller skips it rather than writing a keyless row. func Normalize(raw json.RawMessage) (Award, bool) { var r awardRecord if err := json.Unmarshal(raw, &r); err != nil { return Award{}, false } piid := strings.TrimSpace(r.ContractID.PIID) if piid == "" { return Award{}, false } a := Award{ UniqueKey: actionKey(piid, r.ContractID.ModificationNumber, r.ContractID.TransactionNumber), PIID: piid, AwardeeName: strings.TrimSpace(r.AwardDetails.AwardeeData.AwardeeHeader.AwardeeName), AwardeeUEI: strings.TrimSpace(r.AwardDetails.AwardeeData.AwardeeUEIInformation.UniqueEntityID), TotalValue: decimalFromNumber(r.AwardDetails.Dollars.BaseAndAllOptionsValue), ObligatedValue: decimalFromNumber(r.AwardDetails.Dollars.ActionObligation), Department: strings.TrimSpace(r.CoreData.FederalOrganization.ContractingInformation.ContractingDepartment.Name), Subtier: strings.TrimSpace(r.ContractID.Subtier.Name), Description: strings.TrimSpace(r.CoreData.ContractData.DescriptionOfContractRequirement), SetAside: strings.TrimSpace(r.CoreData.PreferencePrograms.TypeOfSetAside.Name), DateSigned: parseFlexDate(r.AwardDetails.Dates.DateSigned), POPStart: parseFlexDate(r.AwardDetails.Dates.PeriodOfPerformanceStartDate), POPEnd: parseFlexDate(r.AwardDetails.Dates.CurrentCompletionDate), Raw: raw, } if len(r.CoreData.ProductOrServiceInformation.PrincipalNaics) > 0 { a.NAICSCode = strings.TrimSpace(r.CoreData.ProductOrServiceInformation.PrincipalNaics[0].Code) a.NAICSDescription = strings.TrimSpace(r.CoreData.ProductOrServiceInformation.PrincipalNaics[0].Description) } return a, true } // actionKey is the unique key for a contract action: a PIID plus its modification and transaction // numbers identify one action uniquely, so a base award and its modifications are distinct rows. func actionKey(piid, mod, txn string) string { parts := []string{piid} if m := strings.TrimSpace(mod); m != "" { parts = append(parts, m) } if t := strings.TrimSpace(txn); t != "" { parts = append(parts, t) } return strings.Join(parts, "|") } // decimalFromNumber parses a numeric text value to a decimal, returning nil for an empty or unparseable // value so an absent dollar figure stays absent rather than becoming a fabricated zero. func decimalFromNumber(n NumericText) *decimal.Decimal { s := strings.TrimSpace(string(n)) if s == "" { return nil } d, err := decimal.NewFromString(s) if err != nil { return nil } return &d } // parseFlexDate parses the several date shapes the API returns: dateSigned is RFC3339 with a Z, while the // period of performance dates are space separated with milliseconds and no zone. func parseFlexDate(s string) *time.Time { s = strings.TrimSpace(s) if s == "" { return nil } layouts := []string{ time.RFC3339, "2006-01-02 15:04:05.000", "2006-01-02 15:04:05.000000", "2006-01-02 15:04:05", "2006-01-02T15:04:05", "2006-01-02", } for _, layout := range layouts { if t, err := time.Parse(layout, s); err == nil { return &t } } return nil }