package adapters import ( "context" "errors" "log" "time" oppintel "github.com/missionworkforce/missionmatch/internal/modules/opportunity_intelligence" "github.com/missionworkforce/missionmatch/internal/platform/samgov" ) // SAMSource adapts the existing SAM client and normalizer to the OpportunitySource protocol with no // change to their behavior. It iterates the profile NAICS codes (one ncode per SAM call) over a posted // window, paginates each, and yields one active solicitation record per notice. A SAM 429 stops the // pull gracefully (the records already yielded are kept). type SAMSource struct { client *samgov.Client profile samgov.Profile pageCap int now func() time.Time } // NewSAMSource builds the adapter over an existing samgov.Client and a profile (the NAICS scope). A // pageCap of zero defaults to 5 pages per NAICS, matching the standalone sam-sync. func NewSAMSource(client *samgov.Client, profile samgov.Profile, pageCap int) *SAMSource { if pageCap <= 0 { pageCap = 5 } return &SAMSource{client: client, profile: profile, pageCap: pageCap, now: time.Now} } // Name is the provenance label stamped on every SAM record. func (s *SAMSource) Name() string { return "SAM.gov" } // Fetch pulls the profile NAICS codes over the lookback window and yields each notice as an active // solicitation record. func (s *SAMSource) Fetch(ctx context.Context, yield func(oppintel.OpportunityRecord) error) error { now := s.now().UTC() from := now.AddDate(0, 0, -s.profile.LookbackDays) for _, naics := range s.profile.NAICS { recs, _, _, err := s.client.SearchAll(ctx, samgov.SearchParams{ PostedFrom: from, PostedTo: now, NAICS: naics, }, s.pageCap) if err != nil { if errors.Is(err, samgov.ErrThrottled) { log.Printf("SAM source throttled on naics %s, stopping the pull", naics) return oppintel.ErrThrottled } return err } for _, r := range recs { if yerr := yield(fromSAM(samgov.Normalize(r), now)); yerr != nil { return yerr } } } return nil }