package adapters import ( "context" "errors" "time" oppintel "github.com/missionworkforce/missionmatch/internal/modules/opportunity_intelligence" "github.com/missionworkforce/missionmatch/internal/platform/samawards" ) // SAMContractAwardsSource adapts the SAM Contract Awards client to the OpportunitySource protocol. It is // the primary federal award feed, replacing the retiring FPDS Atom feed, and pulls award and incumbent // data with the same SAM key the opportunities feed uses. The Contract Awards API accepts all NAICS // codes in a single query, so unlike the opportunities pull there is no per NAICS loop, just pagination. // A 429 stops the pull gracefully and keeps the records already collected. type SAMContractAwardsSource struct { client *samawards.Client params samawards.SearchParams pageCap int now func() time.Time } // NewSAMContractAwardsSource builds the adapter over an existing client and a query scope. A pageCap of // zero defaults to 5 pages. func NewSAMContractAwardsSource(client *samawards.Client, params samawards.SearchParams, pageCap int) *SAMContractAwardsSource { if pageCap <= 0 { pageCap = 5 } return &SAMContractAwardsSource{client: client, params: params, pageCap: pageCap, now: time.Now} } // Name is the provenance label stamped on every record from this feed. func (s *SAMContractAwardsSource) Name() string { return "SAM Contract Awards" } // Fetch paginates the award query and yields each award as a contract award record with incumbent // context. A record without a PIID is skipped (no upsert key can be formed). func (s *SAMContractAwardsSource) Fetch(ctx context.Context, yield func(oppintel.OpportunityRecord) error) error { now := s.now().UTC() recs, _, _, err := s.client.SearchAll(ctx, s.params, s.pageCap) for _, raw := range recs { a, ok := samawards.Normalize(raw) if !ok { continue } if yerr := yield(fromSAMAward(a, now)); yerr != nil { return yerr } } if err != nil { if errors.Is(err, samawards.ErrThrottled) { return oppintel.ErrThrottled } return err } return nil }