-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathprotests.go
More file actions
138 lines (127 loc) · 4.82 KB
/
Copy pathprotests.go
File metadata and controls
138 lines (127 loc) · 4.82 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
package tango
import (
"context"
"net/url"
)
// ProtestRecord is the typed return model for GetProtest. Mirrors the
// canonical GAO / Court of Federal Claims protest case schema as defined
// in tango-node/src/types.ts:137-151.
//
// All fields except Docket are optional (protests come from multiple
// source systems and not every field is always populated). Extra captures
// any wire fields not first-classed on the struct so callers don't lose
// them — the same pattern used by the request-options structs in this
// package.
//
// Note: this is implementer-B's local typed model; tango-go may
// consolidate typed records into a shared models.go in a later wave.
type ProtestRecord struct {
CaseID string `json:"case_id,omitempty"`
CaseNumber string `json:"case_number,omitempty"`
SourceSystem string `json:"source_system,omitempty"`
Outcome string `json:"outcome,omitempty"`
CaseType string `json:"case_type,omitempty"`
FiledDate string `json:"filed_date,omitempty"`
DecisionDate string `json:"decision_date,omitempty"`
Agency map[string]any `json:"agency,omitempty"`
Protester map[string]any `json:"protester,omitempty"`
ResolvedAgency map[string]any `json:"resolved_agency,omitempty"`
ResolvedProtester map[string]any `json:"resolved_protester,omitempty"`
Docket []map[string]any `json:"docket,omitempty"`
// Extra captures any additional fields returned by the API that
// aren't first-classed above. Populated by callers who need to
// preserve unknown fields when re-marshaling; not auto-populated
// from JSON.
Extra map[string]any `json:"-"`
}
// ListProtestsOptions filters /api/protests/. Mirrors the Node
// `ListProtestsOptions` interface and the Python `list_protests` kwargs.
//
// The protests viewset does not accept ordering (the server rejects it),
// so this struct intentionally omits an Ordering field.
type ListProtestsOptions struct {
ListOptions
SourceSystem string
Outcome string
CaseType string
Agency string
CaseNumber string
SolicitationNumber string
Protester string
Search string
// Date bounds (wire format: YYYY-MM-DD). Note the
// `_after` / `_before` suffix convention — unique to protests; the
// Python client uses these exact names.
FiledDateAfter string
FiledDateBefore string
DecisionDateAfter string
DecisionDateBefore string
Extra map[string]any
}
func (o *ListProtestsOptions) toQuery() url.Values {
q := url.Values{}
if o == nil {
return q
}
o.ListOptions.applyTo(q)
setIfNotEmpty(q, "source_system", o.SourceSystem)
setIfNotEmpty(q, "outcome", o.Outcome)
setIfNotEmpty(q, "case_type", o.CaseType)
setIfNotEmpty(q, "agency", o.Agency)
setIfNotEmpty(q, "case_number", o.CaseNumber)
setIfNotEmpty(q, "solicitation_number", o.SolicitationNumber)
setIfNotEmpty(q, "protester", o.Protester)
setIfNotEmpty(q, "search", o.Search)
setIfNotEmpty(q, "filed_date_after", o.FiledDateAfter)
setIfNotEmpty(q, "filed_date_before", o.FiledDateBefore)
setIfNotEmpty(q, "decision_date_after", o.DecisionDateAfter)
setIfNotEmpty(q, "decision_date_before", o.DecisionDateBefore)
for k, v := range o.Extra {
q.Set(k, valueToString(v))
}
return q
}
// ListProtests queries /api/protests/ — bid protests from GAO + COFC.
func (c *Client) ListProtests(ctx context.Context, opts *ListProtestsOptions) (*PaginatedResponse[Record], error) {
q := url.Values{}
if opts != nil {
q = opts.toQuery()
}
return listGeneric[Record](ctx, c, "/api/protests/", q)
}
// IterateProtests walks every protest matching opts.
func (c *Client) IterateProtests(ctx context.Context, opts *ListProtestsOptions) *Iterator[Record] {
if opts == nil {
opts = &ListProtestsOptions{}
}
return &Iterator[Record]{
ctx: ctx,
fetch: func(ctx context.Context, page int, cursor string) (*PaginatedResponse[Record], error) {
next := *opts
next.Page = page
next.Cursor = cursor
return c.ListProtests(ctx, &next)
},
}
}
// GetProtest fetches a single protest by case number / case ID.
//
// Returns a typed *ProtestRecord (one of the few typed-struct return
// values in the SDK, mirroring the Node `ProtestRecord` interface). Use
// shape=...,dockets(...) to include nested docket entries.
func (c *Client) GetProtest(ctx context.Context, caseNumber string, opts *GetEntityOptions) (*ProtestRecord, error) {
if caseNumber == "" {
return nil, &ValidationError{&APIError{Message: "protest case number is required"}}
}
q := url.Values{}
if opts != nil {
setIfNotEmpty(q, "shape", opts.Shape)
if opts.Flat {
q.Set("flat", "true")
}
if opts.FlatLists {
q.Set("flat_lists", "true")
}
}
return getGeneric[*ProtestRecord](ctx, c, "/api/protests/"+pathEscape(caseNumber)+"/", q)
}