@cliwant/mcp-sam-gov 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.ja.md +184 -0
- package/README.ko.md +184 -0
- package/README.md +397 -0
- package/dist/ecfr.d.ts +44 -0
- package/dist/ecfr.d.ts.map +1 -0
- package/dist/ecfr.js +86 -0
- package/dist/ecfr.js.map +1 -0
- package/dist/federal-register.d.ts +82 -0
- package/dist/federal-register.d.ts.map +1 -0
- package/dist/federal-register.js +117 -0
- package/dist/federal-register.js.map +1 -0
- package/dist/grants.d.ts +63 -0
- package/dist/grants.d.ts.map +1 -0
- package/dist/grants.js +93 -0
- package/dist/grants.js.map +1 -0
- package/dist/sam-gov/client.d.ts +69 -0
- package/dist/sam-gov/client.d.ts.map +1 -0
- package/dist/sam-gov/client.js +401 -0
- package/dist/sam-gov/client.js.map +1 -0
- package/dist/sam-gov/index.d.ts +19 -0
- package/dist/sam-gov/index.d.ts.map +1 -0
- package/dist/sam-gov/index.js +18 -0
- package/dist/sam-gov/index.js.map +1 -0
- package/dist/sam-gov/types.d.ts +109 -0
- package/dist/sam-gov/types.d.ts.map +1 -0
- package/dist/sam-gov/types.js +7 -0
- package/dist/sam-gov/types.js.map +1 -0
- package/dist/server.d.ts +20 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +685 -0
- package/dist/server.js.map +1 -0
- package/dist/usaspending.d.ts +369 -0
- package/dist/usaspending.d.ts.map +1 -0
- package/dist/usaspending.js +555 -0
- package/dist/usaspending.js.map +1 -0
- package/package.json +88 -0
- package/src/ecfr.ts +127 -0
- package/src/federal-register.ts +191 -0
- package/src/grants.ts +155 -0
- package/src/sam-gov/client.ts +492 -0
- package/src/sam-gov/index.ts +28 -0
- package/src/sam-gov/types.ts +130 -0
- package/src/server.ts +856 -0
- package/src/usaspending.ts +925 -0
|
@@ -0,0 +1,925 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* USAspending v2 wrappers (keyless).
|
|
3
|
+
*
|
|
4
|
+
* Coverage map (every endpoint here is verified KEYLESS):
|
|
5
|
+
* Awards / Recipients
|
|
6
|
+
* - search/spending_by_category/recipient → searchAwards
|
|
7
|
+
* - search/spending_by_award (subawards: false) → searchIndividualAwards
|
|
8
|
+
* - search/spending_by_award (subawards: true) → searchSubawards
|
|
9
|
+
* - search/spending_by_award (recipient filter) → searchAwardsByRecipient
|
|
10
|
+
* - awards/{generated_internal_id} → getAwardDetail
|
|
11
|
+
* - search/spending_by_award + awards/{id} pair → searchExpiringContracts
|
|
12
|
+
* Aggregate analysis
|
|
13
|
+
* - search/spending_over_time → spendingOverTime
|
|
14
|
+
* - search/spending_by_category/psc → searchPscSpending
|
|
15
|
+
* - search/spending_by_category/state_territory → searchStateSpending
|
|
16
|
+
* - search/spending_by_category/cfda → searchCfdaSpending
|
|
17
|
+
* - search/spending_by_category/federal_account → searchFederalAccountSpending
|
|
18
|
+
* - search/spending_by_category/awarding_agency → searchAgencySpending
|
|
19
|
+
* - search/spending_by_category/awarding_subagency → searchSubAgencySpending
|
|
20
|
+
* Agency profile
|
|
21
|
+
* - agency/{toptier_code} → getAgencyProfile
|
|
22
|
+
* - agency/{toptier_code}/awards → getAgencyAwardsSummary
|
|
23
|
+
* - agency/{toptier_code}/budget_function → getAgencyBudgetFunction
|
|
24
|
+
* Recipient profile
|
|
25
|
+
* - recipient/ POST → searchRecipients
|
|
26
|
+
* - recipient/{id} → getRecipientProfile
|
|
27
|
+
* Reference / autocomplete (anti-hallucination)
|
|
28
|
+
* - autocomplete/funding_agency → lookupAgency
|
|
29
|
+
* - autocomplete/naics → autocompleteNaics
|
|
30
|
+
* - autocomplete/recipient → autocompleteRecipient
|
|
31
|
+
* - references/naics → naicsHierarchy
|
|
32
|
+
* - references/glossary → glossary
|
|
33
|
+
* - references/toptier_agencies → listToptierAgencies
|
|
34
|
+
*
|
|
35
|
+
* Total: 22 endpoints across the USAspending surface, all keyless.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
const USAS = "https://api.usaspending.gov/api/v2";
|
|
39
|
+
|
|
40
|
+
export type UsasFilters = Record<string, unknown>;
|
|
41
|
+
|
|
42
|
+
function buildFilters(args: {
|
|
43
|
+
agency?: string;
|
|
44
|
+
naics?: string;
|
|
45
|
+
fiscalYear?: number;
|
|
46
|
+
setAside?: string;
|
|
47
|
+
pscCodes?: string[];
|
|
48
|
+
}): UsasFilters {
|
|
49
|
+
const filters: UsasFilters = { award_type_codes: ["A", "B", "C", "D"] };
|
|
50
|
+
if (args.agency) {
|
|
51
|
+
filters.agencies = [
|
|
52
|
+
{ type: "awarding", tier: "toptier", name: args.agency },
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
if (args.naics) filters.naics_codes = [args.naics];
|
|
56
|
+
if (args.fiscalYear) {
|
|
57
|
+
filters.time_period = [
|
|
58
|
+
{
|
|
59
|
+
start_date: `${args.fiscalYear - 1}-10-01`,
|
|
60
|
+
end_date: `${args.fiscalYear}-09-30`,
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
if (args.setAside) filters.set_aside_type_codes = [args.setAside];
|
|
65
|
+
if (args.pscCodes?.length) filters.psc_codes = args.pscCodes;
|
|
66
|
+
return filters;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function postUsas<T>(
|
|
70
|
+
endpoint: string,
|
|
71
|
+
body: Record<string, unknown>,
|
|
72
|
+
): Promise<T> {
|
|
73
|
+
const r = await fetch(`${USAS}/${endpoint}`, {
|
|
74
|
+
method: "POST",
|
|
75
|
+
headers: { "Content-Type": "application/json" },
|
|
76
|
+
body: JSON.stringify(body),
|
|
77
|
+
signal: AbortSignal.timeout(15_000),
|
|
78
|
+
});
|
|
79
|
+
if (!r.ok) {
|
|
80
|
+
throw new Error(`USAspending POST ${endpoint} returned ${r.status}`);
|
|
81
|
+
}
|
|
82
|
+
return (await r.json()) as T;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function getUsas<T>(endpoint: string): Promise<T> {
|
|
86
|
+
const r = await fetch(`${USAS}/${endpoint}`, {
|
|
87
|
+
signal: AbortSignal.timeout(15_000),
|
|
88
|
+
});
|
|
89
|
+
if (!r.ok) {
|
|
90
|
+
throw new Error(`USAspending GET ${endpoint} returned ${r.status}`);
|
|
91
|
+
}
|
|
92
|
+
return (await r.json()) as T;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ─── Aggregate share-of-wallet ───────────────────────────────────
|
|
96
|
+
|
|
97
|
+
export async function searchAwards(args: {
|
|
98
|
+
agency?: string;
|
|
99
|
+
naics?: string;
|
|
100
|
+
fiscalYear?: number;
|
|
101
|
+
setAside?: string;
|
|
102
|
+
}) {
|
|
103
|
+
const filters = buildFilters(args);
|
|
104
|
+
type Resp = {
|
|
105
|
+
results?: { name?: string; amount?: number; count?: number }[];
|
|
106
|
+
};
|
|
107
|
+
const json = await postUsas<Resp>(
|
|
108
|
+
"search/spending_by_category/recipient",
|
|
109
|
+
{ filters, limit: 10, page: 1 },
|
|
110
|
+
);
|
|
111
|
+
const results = json.results ?? [];
|
|
112
|
+
return {
|
|
113
|
+
totalAwards: results.reduce((s, r) => s + (r.count ?? 0), 0),
|
|
114
|
+
totalValue: results.reduce((s, r) => s + (r.amount ?? 0), 0),
|
|
115
|
+
topRecipients: results.map((r) => ({
|
|
116
|
+
name: r.name ?? "—",
|
|
117
|
+
value: r.amount ?? 0,
|
|
118
|
+
awards: r.count ?? 0,
|
|
119
|
+
})),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ─── Line-item awards ─────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
export async function searchIndividualAwards(args: {
|
|
126
|
+
agency?: string;
|
|
127
|
+
naics?: string;
|
|
128
|
+
fiscalYear?: number;
|
|
129
|
+
setAside?: string;
|
|
130
|
+
limit?: number;
|
|
131
|
+
}) {
|
|
132
|
+
const filters = buildFilters(args);
|
|
133
|
+
type Resp = {
|
|
134
|
+
results?: {
|
|
135
|
+
"Award ID"?: string;
|
|
136
|
+
"Recipient Name"?: string;
|
|
137
|
+
"Award Amount"?: number;
|
|
138
|
+
"Awarding Agency"?: string;
|
|
139
|
+
"Awarding Sub Agency"?: string;
|
|
140
|
+
"Place of Performance State Code"?: string;
|
|
141
|
+
Description?: string;
|
|
142
|
+
generated_internal_id?: string;
|
|
143
|
+
}[];
|
|
144
|
+
};
|
|
145
|
+
const json = await postUsas<Resp>("search/spending_by_award", {
|
|
146
|
+
filters,
|
|
147
|
+
fields: [
|
|
148
|
+
"Award ID",
|
|
149
|
+
"Recipient Name",
|
|
150
|
+
"Award Amount",
|
|
151
|
+
"Awarding Agency",
|
|
152
|
+
"Awarding Sub Agency",
|
|
153
|
+
"Place of Performance State Code",
|
|
154
|
+
"Description",
|
|
155
|
+
],
|
|
156
|
+
limit: args.limit ?? 10,
|
|
157
|
+
page: 1,
|
|
158
|
+
subawards: false,
|
|
159
|
+
});
|
|
160
|
+
return {
|
|
161
|
+
awards: (json.results ?? []).map((r) => ({
|
|
162
|
+
awardId: r["Award ID"] ?? "",
|
|
163
|
+
recipient: r["Recipient Name"] ?? "",
|
|
164
|
+
amount: r["Award Amount"] ?? 0,
|
|
165
|
+
awardingAgency: r["Awarding Agency"] ?? "",
|
|
166
|
+
awardingSubAgency: r["Awarding Sub Agency"],
|
|
167
|
+
placeOfPerformanceState: r["Place of Performance State Code"],
|
|
168
|
+
description: r.Description,
|
|
169
|
+
generatedInternalId: r.generated_internal_id ?? "",
|
|
170
|
+
})),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ─── Recipient win history ────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
export async function searchAwardsByRecipient(args: {
|
|
177
|
+
recipientName: string;
|
|
178
|
+
agency?: string;
|
|
179
|
+
naics?: string;
|
|
180
|
+
fiscalYear?: number;
|
|
181
|
+
limit?: number;
|
|
182
|
+
}) {
|
|
183
|
+
const filters = buildFilters(args);
|
|
184
|
+
filters.recipient_search_text = [args.recipientName];
|
|
185
|
+
type Resp = {
|
|
186
|
+
results?: {
|
|
187
|
+
"Award ID"?: string;
|
|
188
|
+
"Recipient Name"?: string;
|
|
189
|
+
"Award Amount"?: number;
|
|
190
|
+
"Awarding Agency"?: string;
|
|
191
|
+
"Awarding Sub Agency"?: string;
|
|
192
|
+
NAICS?: { code?: string; description?: string };
|
|
193
|
+
Description?: string;
|
|
194
|
+
generated_internal_id?: string;
|
|
195
|
+
}[];
|
|
196
|
+
};
|
|
197
|
+
const json = await postUsas<Resp>("search/spending_by_award", {
|
|
198
|
+
filters,
|
|
199
|
+
fields: [
|
|
200
|
+
"Award ID",
|
|
201
|
+
"Recipient Name",
|
|
202
|
+
"Award Amount",
|
|
203
|
+
"Awarding Agency",
|
|
204
|
+
"Awarding Sub Agency",
|
|
205
|
+
"NAICS",
|
|
206
|
+
"Description",
|
|
207
|
+
],
|
|
208
|
+
limit: args.limit ?? 15,
|
|
209
|
+
page: 1,
|
|
210
|
+
subawards: false,
|
|
211
|
+
});
|
|
212
|
+
const results = json.results ?? [];
|
|
213
|
+
return {
|
|
214
|
+
awards: results.map((r) => ({
|
|
215
|
+
awardId: r["Award ID"] ?? "",
|
|
216
|
+
recipient: r["Recipient Name"] ?? "",
|
|
217
|
+
amount: r["Award Amount"] ?? 0,
|
|
218
|
+
awardingAgency: r["Awarding Agency"] ?? "",
|
|
219
|
+
awardingSubAgency: r["Awarding Sub Agency"],
|
|
220
|
+
naicsCode: r.NAICS?.code,
|
|
221
|
+
naicsDescription: r.NAICS?.description,
|
|
222
|
+
description: r.Description,
|
|
223
|
+
generatedInternalId: r.generated_internal_id ?? "",
|
|
224
|
+
})),
|
|
225
|
+
totalRecords: results.length,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ─── Subaward enumeration ─────────────────────────────────────────
|
|
230
|
+
|
|
231
|
+
export async function searchSubawards(args: {
|
|
232
|
+
primeRecipientName?: string;
|
|
233
|
+
agency?: string;
|
|
234
|
+
naics?: string;
|
|
235
|
+
fiscalYear?: number;
|
|
236
|
+
limit?: number;
|
|
237
|
+
}) {
|
|
238
|
+
const filters = buildFilters(args);
|
|
239
|
+
if (args.primeRecipientName) {
|
|
240
|
+
filters.recipient_search_text = [args.primeRecipientName];
|
|
241
|
+
}
|
|
242
|
+
type Resp = {
|
|
243
|
+
results?: {
|
|
244
|
+
"Sub-Award ID"?: string;
|
|
245
|
+
"Sub-Award Recipient"?: string;
|
|
246
|
+
"Sub-Award Amount"?: number;
|
|
247
|
+
"Sub-Award Date"?: string;
|
|
248
|
+
prime_award_generated_internal_id?: string;
|
|
249
|
+
}[];
|
|
250
|
+
};
|
|
251
|
+
const json = await postUsas<Resp>("search/spending_by_award", {
|
|
252
|
+
filters,
|
|
253
|
+
fields: [
|
|
254
|
+
"Sub-Award ID",
|
|
255
|
+
"Sub-Award Recipient",
|
|
256
|
+
"Sub-Award Amount",
|
|
257
|
+
"Sub-Award Date",
|
|
258
|
+
"Sub-Award NAICS",
|
|
259
|
+
],
|
|
260
|
+
limit: args.limit ?? 15,
|
|
261
|
+
page: 1,
|
|
262
|
+
subawards: true,
|
|
263
|
+
});
|
|
264
|
+
return {
|
|
265
|
+
subawards: (json.results ?? []).map((r) => ({
|
|
266
|
+
subAwardId: r["Sub-Award ID"] ?? "",
|
|
267
|
+
subRecipient: r["Sub-Award Recipient"] ?? "(name redacted)",
|
|
268
|
+
amount: r["Sub-Award Amount"] ?? 0,
|
|
269
|
+
actionDate: r["Sub-Award Date"] ?? "",
|
|
270
|
+
primeAwardId: r.prime_award_generated_internal_id ?? "",
|
|
271
|
+
})),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ─── Per-award detail ─────────────────────────────────────────────
|
|
276
|
+
|
|
277
|
+
export async function getAwardDetail(generatedInternalId: string) {
|
|
278
|
+
try {
|
|
279
|
+
const r = await fetch(
|
|
280
|
+
`${USAS}/awards/${encodeURIComponent(generatedInternalId)}/`,
|
|
281
|
+
{ signal: AbortSignal.timeout(10_000) },
|
|
282
|
+
);
|
|
283
|
+
if (!r.ok) return null;
|
|
284
|
+
type Resp = {
|
|
285
|
+
piid?: string;
|
|
286
|
+
description?: string;
|
|
287
|
+
total_obligation?: number;
|
|
288
|
+
base_and_all_options?: number;
|
|
289
|
+
period_of_performance?: {
|
|
290
|
+
start_date?: string;
|
|
291
|
+
end_date?: string;
|
|
292
|
+
potential_end_date?: string;
|
|
293
|
+
};
|
|
294
|
+
latest_transaction_contract_data?: {
|
|
295
|
+
type_set_aside?: string;
|
|
296
|
+
type_set_aside_description?: string;
|
|
297
|
+
extent_competed?: string;
|
|
298
|
+
number_of_offers_received?: string;
|
|
299
|
+
naics?: string;
|
|
300
|
+
naics_description?: string;
|
|
301
|
+
};
|
|
302
|
+
awarding_agency?: {
|
|
303
|
+
toptier_agency?: { name?: string };
|
|
304
|
+
subtier_agency?: { name?: string };
|
|
305
|
+
};
|
|
306
|
+
recipient?: { recipient_name?: string };
|
|
307
|
+
};
|
|
308
|
+
const json = (await r.json()) as Resp;
|
|
309
|
+
const ltc = json.latest_transaction_contract_data ?? {};
|
|
310
|
+
return {
|
|
311
|
+
awardId: json.piid ?? "",
|
|
312
|
+
recipient: json.recipient?.recipient_name ?? "",
|
|
313
|
+
totalObligation: json.total_obligation ?? 0,
|
|
314
|
+
baseAndAllOptions: json.base_and_all_options ?? 0,
|
|
315
|
+
periodOfPerformance: {
|
|
316
|
+
startDate: json.period_of_performance?.start_date ?? null,
|
|
317
|
+
endDate: json.period_of_performance?.end_date ?? null,
|
|
318
|
+
potentialEndDate: json.period_of_performance?.potential_end_date ?? null,
|
|
319
|
+
},
|
|
320
|
+
description: json.description ?? "",
|
|
321
|
+
setAsideType: ltc.type_set_aside,
|
|
322
|
+
setAsideDescription: ltc.type_set_aside_description,
|
|
323
|
+
competitionExtent: ltc.extent_competed,
|
|
324
|
+
numberOfOffers: ltc.number_of_offers_received,
|
|
325
|
+
awardingAgency: json.awarding_agency?.toptier_agency?.name,
|
|
326
|
+
awardingSubAgency: json.awarding_agency?.subtier_agency?.name,
|
|
327
|
+
naicsCode: ltc.naics,
|
|
328
|
+
naicsDescription: ltc.naics_description,
|
|
329
|
+
};
|
|
330
|
+
} catch {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ─── Recompete radar ──────────────────────────────────────────────
|
|
336
|
+
|
|
337
|
+
export async function searchExpiringContracts(args: {
|
|
338
|
+
agency?: string;
|
|
339
|
+
naics?: string;
|
|
340
|
+
fiscalYear?: number;
|
|
341
|
+
monthsUntilExpiry?: number;
|
|
342
|
+
minAwardValue?: number;
|
|
343
|
+
limit?: number;
|
|
344
|
+
}) {
|
|
345
|
+
const filters = buildFilters(args);
|
|
346
|
+
type SearchResp = {
|
|
347
|
+
results?: {
|
|
348
|
+
"Award ID"?: string;
|
|
349
|
+
"Recipient Name"?: string;
|
|
350
|
+
"Award Amount"?: number;
|
|
351
|
+
generated_internal_id?: string;
|
|
352
|
+
}[];
|
|
353
|
+
};
|
|
354
|
+
const search = await postUsas<SearchResp>("search/spending_by_award", {
|
|
355
|
+
filters,
|
|
356
|
+
fields: ["Award ID", "Recipient Name", "Award Amount"],
|
|
357
|
+
limit: 50,
|
|
358
|
+
page: 1,
|
|
359
|
+
subawards: false,
|
|
360
|
+
sort: "Award Amount",
|
|
361
|
+
order: "desc",
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
const candidates = (search.results ?? []).filter(
|
|
365
|
+
(r) =>
|
|
366
|
+
(r["Award Amount"] ?? 0) >= (args.minAwardValue ?? 100_000) &&
|
|
367
|
+
r.generated_internal_id,
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
// Enrich up to 8 in parallel — be polite to USAspending.
|
|
371
|
+
const enrich = candidates.slice(0, 8);
|
|
372
|
+
const details = await Promise.all(
|
|
373
|
+
enrich.map((r) => getAwardDetail(r.generated_internal_id!)),
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
const now = Date.now();
|
|
377
|
+
const cutoffDays = (args.monthsUntilExpiry ?? 12) * 30;
|
|
378
|
+
const contracts = details
|
|
379
|
+
.map((d, idx) => {
|
|
380
|
+
if (!d || !d.periodOfPerformance.endDate) return null;
|
|
381
|
+
const end = new Date(d.periodOfPerformance.endDate).getTime();
|
|
382
|
+
if (Number.isNaN(end)) return null;
|
|
383
|
+
const days = Math.ceil((end - now) / (24 * 60 * 60 * 1000));
|
|
384
|
+
if (days < -30 || days > cutoffDays) return null;
|
|
385
|
+
const orig = enrich[idx] ?? {};
|
|
386
|
+
return {
|
|
387
|
+
awardId: d.awardId || orig["Award ID"] || "",
|
|
388
|
+
recipient: d.recipient || orig["Recipient Name"] || "",
|
|
389
|
+
amount: d.totalObligation || orig["Award Amount"] || 0,
|
|
390
|
+
endDate: d.periodOfPerformance.endDate,
|
|
391
|
+
potentialEndDate: d.periodOfPerformance.potentialEndDate,
|
|
392
|
+
awardingAgency: d.awardingAgency ?? "",
|
|
393
|
+
awardingSubAgency: d.awardingSubAgency,
|
|
394
|
+
naicsCode: d.naicsCode,
|
|
395
|
+
setAsideDescription: d.setAsideDescription,
|
|
396
|
+
description: d.description,
|
|
397
|
+
daysUntilExpiry: days,
|
|
398
|
+
};
|
|
399
|
+
})
|
|
400
|
+
.filter((x): x is NonNullable<typeof x> => x !== null)
|
|
401
|
+
.slice(0, args.limit ?? 10)
|
|
402
|
+
.sort((a, b) => a.daysUntilExpiry - b.daysUntilExpiry);
|
|
403
|
+
|
|
404
|
+
return { contracts, searchedCount: candidates.length };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// ─── Aggregate analysis: time series ──────────────────────────────
|
|
408
|
+
|
|
409
|
+
export async function spendingOverTime(args: {
|
|
410
|
+
group?: "fiscal_year" | "quarter" | "month";
|
|
411
|
+
agency?: string;
|
|
412
|
+
naics?: string;
|
|
413
|
+
setAside?: string;
|
|
414
|
+
}) {
|
|
415
|
+
const filters = buildFilters(args);
|
|
416
|
+
type Resp = {
|
|
417
|
+
group?: string;
|
|
418
|
+
results?: {
|
|
419
|
+
time_period?: { fiscal_year?: string; quarter?: string; month?: string };
|
|
420
|
+
aggregated_amount?: number;
|
|
421
|
+
Contract_Obligations?: number;
|
|
422
|
+
Grant_Obligations?: number;
|
|
423
|
+
Idv_Obligations?: number;
|
|
424
|
+
}[];
|
|
425
|
+
};
|
|
426
|
+
const json = await postUsas<Resp>("search/spending_over_time/", {
|
|
427
|
+
group: args.group ?? "fiscal_year",
|
|
428
|
+
filters,
|
|
429
|
+
});
|
|
430
|
+
return {
|
|
431
|
+
group: json.group,
|
|
432
|
+
timeline: (json.results ?? []).map((r) => ({
|
|
433
|
+
timePeriod: r.time_period ?? {},
|
|
434
|
+
total: r.aggregated_amount ?? 0,
|
|
435
|
+
contractObligations: r.Contract_Obligations ?? 0,
|
|
436
|
+
grantObligations: r.Grant_Obligations ?? 0,
|
|
437
|
+
idvObligations: r.Idv_Obligations ?? 0,
|
|
438
|
+
})),
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ─── Aggregate analysis: PSC spending ─────────────────────────────
|
|
443
|
+
|
|
444
|
+
export async function searchPscSpending(args: {
|
|
445
|
+
agency?: string;
|
|
446
|
+
naics?: string;
|
|
447
|
+
fiscalYear?: number;
|
|
448
|
+
limit?: number;
|
|
449
|
+
}) {
|
|
450
|
+
const filters = buildFilters(args);
|
|
451
|
+
type Resp = {
|
|
452
|
+
results?: { code?: string; name?: string; amount?: number }[];
|
|
453
|
+
};
|
|
454
|
+
const json = await postUsas<Resp>(
|
|
455
|
+
"search/spending_by_category/psc",
|
|
456
|
+
{ filters, limit: args.limit ?? 10, page: 1 },
|
|
457
|
+
);
|
|
458
|
+
return {
|
|
459
|
+
psc: (json.results ?? []).map((r) => ({
|
|
460
|
+
pscCode: r.code ?? "",
|
|
461
|
+
pscName: r.name ?? "",
|
|
462
|
+
amount: r.amount ?? 0,
|
|
463
|
+
})),
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// ─── Aggregate analysis: state / territory ─────────────────────────
|
|
468
|
+
|
|
469
|
+
export async function searchStateSpending(args: {
|
|
470
|
+
agency?: string;
|
|
471
|
+
naics?: string;
|
|
472
|
+
fiscalYear?: number;
|
|
473
|
+
limit?: number;
|
|
474
|
+
}) {
|
|
475
|
+
const filters = buildFilters(args);
|
|
476
|
+
type Resp = {
|
|
477
|
+
results?: { code?: string; name?: string; amount?: number }[];
|
|
478
|
+
};
|
|
479
|
+
const json = await postUsas<Resp>(
|
|
480
|
+
"search/spending_by_category/state_territory",
|
|
481
|
+
{ filters, limit: args.limit ?? 10, page: 1 },
|
|
482
|
+
);
|
|
483
|
+
return {
|
|
484
|
+
states: (json.results ?? []).map((r) => ({
|
|
485
|
+
stateCode: r.code ?? "",
|
|
486
|
+
stateName: r.name ?? "",
|
|
487
|
+
amount: r.amount ?? 0,
|
|
488
|
+
})),
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// ─── Aggregate analysis: CFDA (grants) ─────────────────────────────
|
|
493
|
+
|
|
494
|
+
export async function searchCfdaSpending(args: {
|
|
495
|
+
agency?: string;
|
|
496
|
+
fiscalYear?: number;
|
|
497
|
+
limit?: number;
|
|
498
|
+
}) {
|
|
499
|
+
// CFDA is grants — different award_type_codes
|
|
500
|
+
const filters: UsasFilters = {
|
|
501
|
+
award_type_codes: ["02", "03", "04", "05"], // grants
|
|
502
|
+
};
|
|
503
|
+
if (args.agency) {
|
|
504
|
+
filters.agencies = [
|
|
505
|
+
{ type: "awarding", tier: "toptier", name: args.agency },
|
|
506
|
+
];
|
|
507
|
+
}
|
|
508
|
+
if (args.fiscalYear) {
|
|
509
|
+
filters.time_period = [
|
|
510
|
+
{
|
|
511
|
+
start_date: `${args.fiscalYear - 1}-10-01`,
|
|
512
|
+
end_date: `${args.fiscalYear}-09-30`,
|
|
513
|
+
},
|
|
514
|
+
];
|
|
515
|
+
}
|
|
516
|
+
type Resp = {
|
|
517
|
+
results?: { code?: string; name?: string; amount?: number }[];
|
|
518
|
+
};
|
|
519
|
+
const json = await postUsas<Resp>(
|
|
520
|
+
"search/spending_by_category/cfda",
|
|
521
|
+
{ filters, limit: args.limit ?? 10, page: 1 },
|
|
522
|
+
);
|
|
523
|
+
return {
|
|
524
|
+
programs: (json.results ?? []).map((r) => ({
|
|
525
|
+
cfdaCode: r.code ?? "",
|
|
526
|
+
programName: r.name ?? "",
|
|
527
|
+
amount: r.amount ?? 0,
|
|
528
|
+
})),
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// ─── Aggregate analysis: federal account (TAS) ─────────────────────
|
|
533
|
+
|
|
534
|
+
export async function searchFederalAccountSpending(args: {
|
|
535
|
+
agency?: string;
|
|
536
|
+
naics?: string;
|
|
537
|
+
fiscalYear?: number;
|
|
538
|
+
limit?: number;
|
|
539
|
+
}) {
|
|
540
|
+
const filters = buildFilters(args);
|
|
541
|
+
type Resp = {
|
|
542
|
+
results?: { code?: string; name?: string; amount?: number }[];
|
|
543
|
+
};
|
|
544
|
+
const json = await postUsas<Resp>(
|
|
545
|
+
"search/spending_by_category/federal_account",
|
|
546
|
+
{ filters, limit: args.limit ?? 10, page: 1 },
|
|
547
|
+
);
|
|
548
|
+
return {
|
|
549
|
+
accounts: (json.results ?? []).map((r) => ({
|
|
550
|
+
tasCode: r.code ?? "",
|
|
551
|
+
accountName: r.name ?? "",
|
|
552
|
+
amount: r.amount ?? 0,
|
|
553
|
+
})),
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// ─── Aggregate analysis: awarding agency ──────────────────────────
|
|
558
|
+
|
|
559
|
+
export async function searchAgencySpending(args: {
|
|
560
|
+
naics?: string;
|
|
561
|
+
fiscalYear?: number;
|
|
562
|
+
setAside?: string;
|
|
563
|
+
limit?: number;
|
|
564
|
+
}) {
|
|
565
|
+
const filters = buildFilters(args);
|
|
566
|
+
type Resp = {
|
|
567
|
+
results?: {
|
|
568
|
+
name?: string;
|
|
569
|
+
code?: string;
|
|
570
|
+
amount?: number;
|
|
571
|
+
agency_slug?: string;
|
|
572
|
+
}[];
|
|
573
|
+
};
|
|
574
|
+
const json = await postUsas<Resp>(
|
|
575
|
+
"search/spending_by_category/awarding_agency",
|
|
576
|
+
{ filters, limit: args.limit ?? 10, page: 1 },
|
|
577
|
+
);
|
|
578
|
+
return {
|
|
579
|
+
agencies: (json.results ?? []).map((r) => ({
|
|
580
|
+
name: r.name ?? "",
|
|
581
|
+
code: r.code ?? "",
|
|
582
|
+
slug: r.agency_slug ?? "",
|
|
583
|
+
amount: r.amount ?? 0,
|
|
584
|
+
})),
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// ─── Sub-agency breakdown ─────────────────────────────────────────
|
|
589
|
+
|
|
590
|
+
export async function searchSubAgencySpending(args: {
|
|
591
|
+
agency: string;
|
|
592
|
+
fiscalYear?: number;
|
|
593
|
+
}) {
|
|
594
|
+
const filters = buildFilters(args);
|
|
595
|
+
type Resp = {
|
|
596
|
+
results?: { name?: string; amount?: number; count?: number }[];
|
|
597
|
+
};
|
|
598
|
+
const json = await postUsas<Resp>(
|
|
599
|
+
"search/spending_by_category/awarding_subagency",
|
|
600
|
+
{ filters, limit: 10, page: 1 },
|
|
601
|
+
);
|
|
602
|
+
return {
|
|
603
|
+
subAgencies: (json.results ?? []).map((r) => ({
|
|
604
|
+
name: r.name ?? "",
|
|
605
|
+
amount: r.amount ?? 0,
|
|
606
|
+
awards: r.count ?? 0,
|
|
607
|
+
})),
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// ─── Agency profile ───────────────────────────────────────────────
|
|
612
|
+
|
|
613
|
+
export async function getAgencyProfile(toptierCode: string) {
|
|
614
|
+
type Resp = {
|
|
615
|
+
fiscal_year?: number;
|
|
616
|
+
toptier_code?: string;
|
|
617
|
+
name?: string;
|
|
618
|
+
abbreviation?: string;
|
|
619
|
+
mission?: string;
|
|
620
|
+
website?: string;
|
|
621
|
+
subtier_agency_count?: number;
|
|
622
|
+
congressional_justification_url?: string;
|
|
623
|
+
};
|
|
624
|
+
const json = await getUsas<Resp>(`agency/${toptierCode}/`);
|
|
625
|
+
return {
|
|
626
|
+
fiscalYear: json.fiscal_year,
|
|
627
|
+
toptierCode: json.toptier_code,
|
|
628
|
+
name: json.name,
|
|
629
|
+
abbreviation: json.abbreviation,
|
|
630
|
+
mission: json.mission,
|
|
631
|
+
website: json.website,
|
|
632
|
+
subtierAgencyCount: json.subtier_agency_count,
|
|
633
|
+
congressionalJustificationUrl: json.congressional_justification_url,
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
export async function getAgencyAwardsSummary(args: {
|
|
638
|
+
toptierCode: string;
|
|
639
|
+
fiscalYear?: number;
|
|
640
|
+
}) {
|
|
641
|
+
const fy = args.fiscalYear ?? new Date().getUTCFullYear();
|
|
642
|
+
type Resp = {
|
|
643
|
+
fiscal_year?: number;
|
|
644
|
+
toptier_code?: string;
|
|
645
|
+
transaction_count?: number;
|
|
646
|
+
obligations?: number;
|
|
647
|
+
latest_action_date?: string;
|
|
648
|
+
};
|
|
649
|
+
const json = await getUsas<Resp>(
|
|
650
|
+
`agency/${args.toptierCode}/awards/?fiscal_year=${fy}`,
|
|
651
|
+
);
|
|
652
|
+
return {
|
|
653
|
+
fiscalYear: json.fiscal_year,
|
|
654
|
+
toptierCode: json.toptier_code,
|
|
655
|
+
transactionCount: json.transaction_count ?? 0,
|
|
656
|
+
obligations: json.obligations ?? 0,
|
|
657
|
+
latestActionDate: json.latest_action_date,
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
export async function getAgencyBudgetFunction(args: {
|
|
662
|
+
toptierCode: string;
|
|
663
|
+
fiscalYear?: number;
|
|
664
|
+
limit?: number;
|
|
665
|
+
}) {
|
|
666
|
+
const fy = args.fiscalYear ?? new Date().getUTCFullYear();
|
|
667
|
+
type Resp = {
|
|
668
|
+
toptier_code?: string;
|
|
669
|
+
fiscal_year?: number;
|
|
670
|
+
results?: {
|
|
671
|
+
name?: string;
|
|
672
|
+
children?: {
|
|
673
|
+
name?: string;
|
|
674
|
+
obligated_amount?: number;
|
|
675
|
+
gross_outlay_amount?: number;
|
|
676
|
+
}[];
|
|
677
|
+
}[];
|
|
678
|
+
};
|
|
679
|
+
const json = await getUsas<Resp>(
|
|
680
|
+
`agency/${args.toptierCode}/budget_function/?fiscal_year=${fy}&limit=${args.limit ?? 10}`,
|
|
681
|
+
);
|
|
682
|
+
return {
|
|
683
|
+
toptierCode: json.toptier_code,
|
|
684
|
+
fiscalYear: json.fiscal_year,
|
|
685
|
+
functions: (json.results ?? []).map((r) => ({
|
|
686
|
+
name: r.name ?? "",
|
|
687
|
+
programs: (r.children ?? []).map((c) => ({
|
|
688
|
+
name: c.name ?? "",
|
|
689
|
+
obligated: c.obligated_amount ?? 0,
|
|
690
|
+
outlays: c.gross_outlay_amount ?? 0,
|
|
691
|
+
})),
|
|
692
|
+
})),
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// ─── Recipient list + profile ─────────────────────────────────────
|
|
697
|
+
|
|
698
|
+
export async function searchRecipients(args: {
|
|
699
|
+
keyword: string;
|
|
700
|
+
recipientLevel?: "P" | "C" | "R";
|
|
701
|
+
limit?: number;
|
|
702
|
+
}) {
|
|
703
|
+
type Resp = {
|
|
704
|
+
page_metadata?: { total?: number };
|
|
705
|
+
results?: {
|
|
706
|
+
id?: string;
|
|
707
|
+
duns?: string;
|
|
708
|
+
uei?: string;
|
|
709
|
+
name?: string;
|
|
710
|
+
recipient_level?: string;
|
|
711
|
+
amount?: number;
|
|
712
|
+
}[];
|
|
713
|
+
};
|
|
714
|
+
const body: Record<string, unknown> = {
|
|
715
|
+
keyword: args.keyword,
|
|
716
|
+
limit: args.limit ?? 10,
|
|
717
|
+
page: 1,
|
|
718
|
+
};
|
|
719
|
+
if (args.recipientLevel) {
|
|
720
|
+
body.recipient_level = args.recipientLevel;
|
|
721
|
+
}
|
|
722
|
+
const json = await postUsas<Resp>("recipient/", body);
|
|
723
|
+
return {
|
|
724
|
+
totalRecords: json.page_metadata?.total ?? 0,
|
|
725
|
+
recipients: (json.results ?? []).map((r) => ({
|
|
726
|
+
id: r.id ?? "",
|
|
727
|
+
duns: r.duns,
|
|
728
|
+
uei: r.uei,
|
|
729
|
+
name: r.name ?? "",
|
|
730
|
+
level: r.recipient_level,
|
|
731
|
+
totalAmount: r.amount ?? 0,
|
|
732
|
+
})),
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
export async function getRecipientProfile(recipientId: string) {
|
|
737
|
+
type Resp = {
|
|
738
|
+
name?: string;
|
|
739
|
+
alternate_names?: string[];
|
|
740
|
+
duns?: string;
|
|
741
|
+
uei?: string;
|
|
742
|
+
recipient_id?: string;
|
|
743
|
+
recipient_level?: string;
|
|
744
|
+
parent_id?: string;
|
|
745
|
+
parent_name?: string;
|
|
746
|
+
business_types?: string[];
|
|
747
|
+
location?: {
|
|
748
|
+
address_line1?: string;
|
|
749
|
+
city_name?: string;
|
|
750
|
+
state_code?: string;
|
|
751
|
+
country_name?: string;
|
|
752
|
+
zip5?: string;
|
|
753
|
+
};
|
|
754
|
+
total_transaction_amount?: number;
|
|
755
|
+
total_transactions?: number;
|
|
756
|
+
};
|
|
757
|
+
const json = await getUsas<Resp>(
|
|
758
|
+
`recipient/${encodeURIComponent(recipientId)}/`,
|
|
759
|
+
);
|
|
760
|
+
return {
|
|
761
|
+
name: json.name ?? "",
|
|
762
|
+
alternateNames: json.alternate_names ?? [],
|
|
763
|
+
duns: json.duns,
|
|
764
|
+
uei: json.uei,
|
|
765
|
+
recipientId: json.recipient_id,
|
|
766
|
+
level: json.recipient_level,
|
|
767
|
+
parentId: json.parent_id,
|
|
768
|
+
parentName: json.parent_name,
|
|
769
|
+
businessTypes: json.business_types ?? [],
|
|
770
|
+
location: json.location ?? {},
|
|
771
|
+
totalAmount: json.total_transaction_amount ?? 0,
|
|
772
|
+
totalTransactions: json.total_transactions ?? 0,
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// ─── Reference / autocomplete ─────────────────────────────────────
|
|
777
|
+
|
|
778
|
+
export async function lookupAgency(searchText: string) {
|
|
779
|
+
try {
|
|
780
|
+
const r = await fetch(`${USAS}/autocomplete/funding_agency/`, {
|
|
781
|
+
method: "POST",
|
|
782
|
+
headers: { "Content-Type": "application/json" },
|
|
783
|
+
body: JSON.stringify({ search_text: searchText, limit: 5 }),
|
|
784
|
+
signal: AbortSignal.timeout(10_000),
|
|
785
|
+
});
|
|
786
|
+
if (!r.ok) return { matches: [] };
|
|
787
|
+
type Resp = {
|
|
788
|
+
results?: {
|
|
789
|
+
toptier_flag?: boolean;
|
|
790
|
+
toptier_agency?: {
|
|
791
|
+
name?: string;
|
|
792
|
+
abbreviation?: string;
|
|
793
|
+
toptier_code?: string;
|
|
794
|
+
};
|
|
795
|
+
}[];
|
|
796
|
+
};
|
|
797
|
+
const json = (await r.json()) as Resp;
|
|
798
|
+
return {
|
|
799
|
+
matches: (json.results ?? []).map((r) => ({
|
|
800
|
+
name: r.toptier_agency?.name ?? "",
|
|
801
|
+
abbreviation: r.toptier_agency?.abbreviation,
|
|
802
|
+
toptierCode: r.toptier_agency?.toptier_code,
|
|
803
|
+
isToptier: !!r.toptier_flag,
|
|
804
|
+
})),
|
|
805
|
+
};
|
|
806
|
+
} catch {
|
|
807
|
+
return { matches: [] };
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
export async function autocompleteNaics(args: {
|
|
812
|
+
searchText: string;
|
|
813
|
+
limit?: number;
|
|
814
|
+
}) {
|
|
815
|
+
type Resp = {
|
|
816
|
+
results?: {
|
|
817
|
+
naics?: string;
|
|
818
|
+
naics_description?: string;
|
|
819
|
+
year_retired?: string | null;
|
|
820
|
+
}[];
|
|
821
|
+
};
|
|
822
|
+
const json = await postUsas<Resp>("autocomplete/naics/", {
|
|
823
|
+
search_text: args.searchText,
|
|
824
|
+
limit: args.limit ?? 10,
|
|
825
|
+
});
|
|
826
|
+
return {
|
|
827
|
+
naics: (json.results ?? []).map((r) => ({
|
|
828
|
+
code: r.naics ?? "",
|
|
829
|
+
description: r.naics_description ?? "",
|
|
830
|
+
retired: !!r.year_retired,
|
|
831
|
+
})),
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
export async function autocompleteRecipient(args: {
|
|
836
|
+
searchText: string;
|
|
837
|
+
limit?: number;
|
|
838
|
+
}) {
|
|
839
|
+
type Resp = {
|
|
840
|
+
results?: {
|
|
841
|
+
recipient_name?: string;
|
|
842
|
+
uei?: string;
|
|
843
|
+
duns?: string;
|
|
844
|
+
}[];
|
|
845
|
+
};
|
|
846
|
+
const json = await postUsas<Resp>("autocomplete/recipient/", {
|
|
847
|
+
search_text: args.searchText,
|
|
848
|
+
limit: args.limit ?? 10,
|
|
849
|
+
});
|
|
850
|
+
return {
|
|
851
|
+
recipients: (json.results ?? []).map((r) => ({
|
|
852
|
+
name: r.recipient_name ?? "",
|
|
853
|
+
uei: r.uei,
|
|
854
|
+
duns: r.duns,
|
|
855
|
+
})),
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
export async function naicsHierarchy(args: { naicsFilter?: string }) {
|
|
860
|
+
type Resp = {
|
|
861
|
+
results?: {
|
|
862
|
+
naics?: string;
|
|
863
|
+
naics_description?: string;
|
|
864
|
+
count?: number;
|
|
865
|
+
children?: unknown[];
|
|
866
|
+
}[];
|
|
867
|
+
};
|
|
868
|
+
const path = args.naicsFilter
|
|
869
|
+
? `references/naics/?filter=${encodeURIComponent(args.naicsFilter)}`
|
|
870
|
+
: "references/naics/";
|
|
871
|
+
const json = await getUsas<Resp>(path);
|
|
872
|
+
return {
|
|
873
|
+
hierarchy: (json.results ?? []).map((r) => ({
|
|
874
|
+
code: r.naics ?? "",
|
|
875
|
+
description: r.naics_description ?? "",
|
|
876
|
+
count: r.count ?? 0,
|
|
877
|
+
hasChildren: !!(r.children && r.children.length > 0),
|
|
878
|
+
})),
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
export async function glossary(args: { limit?: number; search?: string }) {
|
|
883
|
+
type Resp = {
|
|
884
|
+
page_metadata?: { count?: number };
|
|
885
|
+
results?: { term?: string; slug?: string; plain?: string }[];
|
|
886
|
+
};
|
|
887
|
+
const params = new URLSearchParams();
|
|
888
|
+
params.set("limit", String(args.limit ?? 25));
|
|
889
|
+
if (args.search) params.set("search", args.search);
|
|
890
|
+
const json = await getUsas<Resp>(`references/glossary/?${params.toString()}`);
|
|
891
|
+
return {
|
|
892
|
+
totalRecords: json.page_metadata?.count ?? 0,
|
|
893
|
+
terms: (json.results ?? []).map((r) => ({
|
|
894
|
+
term: r.term ?? "",
|
|
895
|
+
slug: r.slug ?? "",
|
|
896
|
+
definition: r.plain ?? "",
|
|
897
|
+
})),
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
export async function listToptierAgencies(args: { limit?: number }) {
|
|
902
|
+
type Resp = {
|
|
903
|
+
results?: {
|
|
904
|
+
agency_name?: string;
|
|
905
|
+
abbreviation?: string;
|
|
906
|
+
toptier_code?: string;
|
|
907
|
+
agency_slug?: string;
|
|
908
|
+
active_fy?: string;
|
|
909
|
+
obligated_amount?: number;
|
|
910
|
+
}[];
|
|
911
|
+
};
|
|
912
|
+
const json = await getUsas<Resp>(
|
|
913
|
+
`references/toptier_agencies/?limit=${args.limit ?? 50}`,
|
|
914
|
+
);
|
|
915
|
+
return {
|
|
916
|
+
agencies: (json.results ?? []).map((r) => ({
|
|
917
|
+
name: r.agency_name ?? "",
|
|
918
|
+
abbreviation: r.abbreviation,
|
|
919
|
+
toptierCode: r.toptier_code,
|
|
920
|
+
slug: r.agency_slug,
|
|
921
|
+
activeFiscalYear: r.active_fy,
|
|
922
|
+
obligatedAmount: r.obligated_amount ?? 0,
|
|
923
|
+
})),
|
|
924
|
+
};
|
|
925
|
+
}
|