Catalyst / admin/Bid-Sentinel 14.8 GB / 57.8 GB 40.0 GB free
Help Sign in

admin / Bid-Sentinel

public

Bid Scrape and Tracking Application with AI Capability

Code Issues Pull requests Pipelines Packages Security Insights Wiki Settings
Bid-Sentinel / bid-sentinel-v2 / backend / app / schemas.py 8664 B · main
  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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
"""Pydantic request/response schemas."""
from datetime import datetime

from pydantic import BaseModel, ConfigDict, EmailStr

from app.models import BidOutcome, BidStatus, UserRole


# --- Auth -------------------------------------------------------------------
class Token(BaseModel):
    access_token: str
    token_type: str = "bearer"


class LoginRequest(BaseModel):
    email: EmailStr
    password: str


class SetupStatus(BaseModel):
    needs_setup: bool


class AdminSetup(BaseModel):
    email: EmailStr
    full_name: str | None = None
    password: str


# --- Users ------------------------------------------------------------------
class UserBase(BaseModel):
    email: EmailStr
    full_name: str | None = None
    role: UserRole = UserRole.standard


class UserCreate(UserBase):
    password: str


class UserUpdate(BaseModel):
    full_name: str | None = None
    role: UserRole | None = None
    is_active: bool | None = None
    password: str | None = None


class UserOut(UserBase):
    model_config = ConfigDict(from_attributes=True)

    id: int
    is_active: bool
    created_at: datetime


# --- Tenders ----------------------------------------------------------------
class TenderOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: int
    reference: str | None
    url: str
    title: str
    description: str | None
    published_date: datetime | None
    closing_date: datetime | None
    value_amount: float | None
    value_text: str | None
    currency: str
    duration: str | None
    buyer: str | None
    source: str | None
    matched_keyword: str | None
    bid_status: BidStatus
    outcome: BidOutcome
    required_certs: str
    fit_score: int | None
    fit_matched: str
    tech_requirements: str
    ai_summary: str
    created_at: datetime
    updated_at: datetime


class TenderCreate(BaseModel):
    title: str
    buyer: str | None = None
    url: str | None = None  # optional; a placeholder is generated if blank
    description: str | None = None
    value: str | None = None  # free text, parsed into amount/text
    duration: str | None = None
    published_date: datetime | None = None
    closing_date: datetime | None = None


class TenderUpdate(BaseModel):
    """Edit an opportunity. All fields optional; only those sent are applied."""

    title: str | None = None
    buyer: str | None = None
    url: str | None = None
    description: str | None = None
    value: str | None = None
    duration: str | None = None
    published_date: datetime | None = None
    closing_date: datetime | None = None


class BulkDeleteRequest(BaseModel):
    ids: list[int]


class BulkDeleteResult(BaseModel):
    deleted: int
    suppressed: int  # how many are now blocked from future scrapes
    batch_id: str  # pass to POST /tenders/undo/{batch_id} to restore


class DeleteResult(BaseModel):
    """Returned by single delete so the deletion can be undone."""

    batch_id: str


class UndoResult(BaseModel):
    restored: int


class BidStatusUpdate(BaseModel):
    bid_status: BidStatus


class OutcomeUpdate(BaseModel):
    outcome: BidOutcome


# --- Comments ---------------------------------------------------------------
class CommentOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: int
    tender_id: int
    author_email: str
    body: str
    created_at: datetime


class CommentCreate(BaseModel):
    body: str


# --- Audit trail ------------------------------------------------------------
class AuditOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: int
    created_at: datetime
    actor: str
    action: str
    detail: str
    status_code: int | None
    ip: str | None


class PortalStat(BaseModel):
    name: str
    candidates: int
    note: str = ""


class ScrapeResult(BaseModel):
    inserted: int
    skipped: int
    sources: list[str]
    keywords: list[str] = []
    message: str
    candidates: int = 0
    used_sample: bool = False
    portals: list[PortalStat] = []
    skipped_breakdown: dict[str, int] = {}


# --- Portals ----------------------------------------------------------------
class PortalOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: int
    key: str
    name: str
    url: str
    scope: str | None
    live: bool
    enabled: bool
    custom: bool


class PortalUpdate(BaseModel):
    enabled: bool


class PortalCreate(BaseModel):
    name: str
    url: str
    scope: str | None = None


# --- Keywords ---------------------------------------------------------------
class KeywordOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: int
    keyword: str


class KeywordCreate(BaseModel):
    keyword: str


class KeywordList(BaseModel):
    builtin: list[str]
    custom: list[KeywordOut]


# --- Schedule ---------------------------------------------------------------
class ScheduleSlotOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: int
    day_of_week: int
    hour: int
    minute: int
    enabled: bool


class ScheduleSlotCreate(BaseModel):
    """Bulk create: cartesian product of the chosen days and times."""

    days: list[int]  # 0=Mon .. 6=Sun
    times: list[str]  # "HH:MM"


class ScheduleSlotToggle(BaseModel):
    enabled: bool


class ScheduleInfo(BaseModel):
    timezone: str
    slots: list[ScheduleSlotOut]
    next_run: datetime | None


# --- Capability profile & analysis (deterministic, non-AI) ------------------
class CapabilityOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: int
    term: str


class CapabilityCreate(BaseModel):
    term: str


class CapabilityList(BaseModel):
    terms: list[CapabilityOut]


class CapabilityUploadResult(BaseModel):
    filename: str
    extracted_chars: int
    suggested: list[str]  # capability terms generated from the document
    detected_certs: list[str]
    generated_by: str  # "AI" or "keyword extraction"


class RecomputeResult(BaseModel):
    updated: int
    message: str


# --- CPV codes (scrape criteria) --------------------------------------------
class CpvOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: int
    code: str
    description: str
    enabled: bool


class CpvCreate(BaseModel):
    code: str
    description: str | None = None


class CpvUpdate(BaseModel):
    enabled: bool


class CpvStrictConfig(BaseModel):
    """Whether the CPV filter is strict (require a match from every notice) or
    lenient (only narrow notices that actually declare a CPV classification)."""

    strict: bool


# --- Tune-out list (negative filter) ----------------------------------------
class TuneOutOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: int
    term: str


class TuneOutCreate(BaseModel):
    term: str


class TuneOutAddResult(BaseModel):
    """Result of adding a tune-out term: the stored term plus how many existing
    (non-cyber) opportunities were removed immediately."""

    term: TuneOutOut
    removed: int


# --- Accreditations held ----------------------------------------------------
class AccreditationOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: int
    name: str


class AccreditationCreate(BaseModel):
    name: str


# --- Intelligence (analyst) -------------------------------------------------
class IntelSummary(BaseModel):
    opportunities: int
    pipeline_value: float
    win_rate: int | None
    capability_gaps: int
    cert_gaps: int
    target_buyers: int


class CapabilityGapItem(BaseModel):
    term: str
    opportunities: int
    total_value: float


class CertGapItem(BaseModel):
    name: str
    required_count: int
    demand_pct: int
    held: bool


class BuyerItem(BaseModel):
    buyer: str
    opportunities: int
    total_value: float
    avg_fit: int | None
    won: int
    lost: int
    bidding: int
    is_target: bool
    top_services: list[str]
    top_certs: list[str]


class WinLossBand(BaseModel):
    label: str
    won: int
    lost: int
    win_rate: int | None


class WinLoss(BaseModel):
    overall_win_rate: int | None
    won: int
    lost: int
    by_service: list[WinLossBand]
    by_fit: list[WinLossBand]


class TrendPoint(BaseModel):
    label: str
    count: int
    value: float


class SalesAngleItem(BaseModel):
    subject: str
    angle: str
    source: str  # "rules" or "AI"


# --- AI bolt-on -------------------------------------------------------------
class AiConfig(BaseModel):
    enabled: bool
    available: bool  # API key configured?
    model: str


class AiUpdate(BaseModel):
    enabled: bool