Grouped by the stage that writes it. Raw reads, their locations, and every step to the comparable value are kept, so any number can be traced back.
-- Draft
rfq(id, title, category, contract_from, contract_to, properties, status,
reply_by, portal_closes_at, accept_email_replies)
basis(rfq_id, unit_rule, currency, gst_basis, delivery_basis, valid_until)
line_item(id, rfq_id, group, name, spec, base_unit, weight_per_unit,
annual_qty, last_price_inr, source, source_ref)
vendor(id, name, type, region, city, country, contacts, groups, on_time_pct)
rule(id, text, kb_version, effective_from)
shortlist_entry(rfq_id, vendor_id, rule_id, selected, reachable, note)
-- Send
invite(id, rfq_id, vendor_id, link_token, sent_at, reminded_at) -- one email; the link is the key
session(token, invite_id, created_at, expires_at) -- opened from the private link
outbox(id, rfq_id, to_addr, subject, body, created_at) -- invites, nudges, drafted asks
-- Submit
submission(id, rfq_id, vendor_id, version, supersedes_id, status, completion_pct,
timeline, portal_terms, submitted_at, is_late, is_current)
-- status: invited → logged_in → files_shared → submitted → closed · no_reply
-- portal_terms: the terms form as typed; saved as commercial_term rows anchored “typed in the portal”,
-- or terms_in_files: the vendor says the terms are in their files, read from there
submission_basis(submission_id, currency, gst_basis, delivery_basis, valid_until, anchor_id)
upload(id, submission_id, vendor_id, filename, detected_type, doc_role, bytes, sha256, r2_key, via,
sender, received_at, read_status, outcome, reject_reason, rung_reached, unread_parts,
doc_class, letterhead_name, letterhead_gstin, identity_status,
instructions_found)
-- via: portal · email · note · external (a document about the vendor: vendor_id set, no submission)
-- read_status: pending · reading · done · held
-- outcome: read · partly_read · rejected (couldn’t open)
-- doc_role: quote · supporting · vendor_profile (never a quote, never in the comparison)
-- doc_class: quote · invoice · brochure · unrelated · other_rfq
questionnaire_question(id, rfq_id, seq, text, type, unit, min, max, options, must, pass_rule, help)
-- nine questions, set in Draft · type: yesno · truefalse · choice · number · range · text
questionnaire_answer(submission_id, question_id, answer, pass) -- pass: meets the question’s rule
-- Reading and provenance (after Submit)
source_anchor(id, upload_id, kind, page, cell_ref, bbox, text_span,
raw_text, locate) -- kind: cell · pdf_text · image_box · paragraph · email_span · typed
-- locate: exact · verified · approximate
extracted_value(id, upload_id, anchor_id, value, price_min, price_max, tiers,
condition, currency, unit_as_written, item_as_written,
is_substitute, from_hidden_sheet, confidence, alternatives, second_read)
quote_line(id, submission_id, line_id, extracted_value_id,
value_inr, landed_unit_inr, certainty, settled_by,
blank_reason, bundled_into_line_id)
-- certainty: read · assumed · unresolved
-- settled_by: none · vendor · buyer · override
-- blank_reason: not_quoted · unreadable · bundled
value_step(id, quote_line_id, seq, op, input, output, factor,
factor_source, anchor_id, certainty)
-- op: parse · unit · fx · gst · freight · discount
commercial_term(id, submission_id, kind, value, pct, threshold_inr, condition, anchor_id, certainty)
fx_rate(id, currency, rate, source, rate_date) -- RBI reference rate
prior_contract(vendor_id, line_id, price_inr, contract_ref, valid_to, anchor_id)
-- the FY26 award (Vendor A, RC-2025-114); the last three awards also give each line its 3-year average
unit_definition(id, submission_id, upload_id, anchor_id, line_id, item_text,
unit_word, qty, qty_unit, text_as_written) -- “1 carton = 500 pieces”
-- Review
review_item(id, rfq_id, submission_id, line_id, upload_id, kind, question,
options, answer, status, answered_by, answered_at)
-- status: open · answered · void (a later read cleared it)
vendor_question(id, rfq_id, vendor_id, ref, text, asked_by, at) -- “Ask the vendor”, sent and logged
item_mapping(submission_id, item_as_written, line_id, mapped_by, at) -- buyer maps unmatched items
override(id, quote_line_id, step_id, by, old_value, new_value, reason, at)
-- Analyse
coverage_v = per submission: lines matched, items mapped to no line, lines quoted twice
comparison_v = quote_line ⋈ line_item ⋈ vendor ⋈ basis ⋈ questionnaire, overrides applied
-- Read-only views for Ask (with comparison_v, the only names the Ask tool may read)
vendor_terms_v(vendor_id, vendor, vendor_type, vendor_country, submission_status,
payment_terms_text, payment_days, payment_days_from, validity_text,
freight_kind, freight_pct, freight_certainty, discount_pct, discount_threshold_inr,
delivery_timeline, delivers_all_properties, questionnaire_cleared)
-- one row per vendor whose quote reached the buyer; terms only from files that count
-- payment_days: parsed in SQL where the term reads “N days”, else NULL
-- freight_kind: included · extra · unknown
questionnaire_v -- one row per vendor and question
award_v(award_id, award_status, criteria, line_id, line_name, line_group, vendor_id, vendor,
awarded_unit_inr, annual_qty, line_total_inr, vendor_lines, vendor_value_inr, vendor_discount_inr,
vendor_total_after_discount_inr, total_inr, vs_last_year_pct)
-- one row per awarded line in the latest award not superseded
ask_turn(id, rfq_id, asked_by, question, answer, rule_text, query_sql, exclusions,
rests_on_unresolved, declined_reason, view_spec, created_at, proposal)
-- proposal: a profile change the co-pilot proposed, with the ranking before and after; applied only by the buyer
-- Sourcing profile (set at client onboarding; Analyse ranks under it)
client_profile(id, client_id, sector, category, version, status, created_by, created_at, note,
missing_rule, excluded)
-- status: suggested · active · superseded · created_by: ai · buyer
-- missing_rule: renormalise · neutral · excluded: criteria considered and dropped, with reasons
profile_criterion(id, profile_id, seq, key, name, why_it_matters, source, weight_pct,
direction, data_source, formula_text)
-- source: past_rfqs · sector_norm · questionnaire · buyer · weights per profile sum to 100, in code
-- data_source: comparison · questionnaire · terms · vendor_master · external:<doc_type>
vendor_fact(id, vendor_id, key, value_text, value_num, as_of, certainty, anchor_id, upload_id, created_at)
-- one fact from an external document; anchor_id (page and text as printed) is required
-- key: credit_rating · revenue_band · years_operating · incorporation_year · gstin_status · litigation_flag · complaint_record · …
profile_v = the profile in use (active, else the latest suggestion), one row per criterion
vendor_fact_v(vendor_id, vendor, key, value_text, value_num, as_of, stale, certainty, file_status,
doc_class, issuer, file, page, quote, upload_id)
-- stale: more than a year old · file_status: counted · held (waiting for the buyer) · left_out
-- Award
award(id, rfq_id, criteria, total_inr, vs_last_year_pct, status,
assumptions_accepted, requested_by, approved_by, decided_at, note)
-- criteria: the scenario’s rules in words; the rules and the sourcing profile version are kept with the award
award_line(award_id, line_id, vendor_id, landed_value_inr, reason)
award_vendor(award_id, vendor_id, lines, value_inr, discount_inr, reason)
decision_event(id, rfq_id, at, actor, kind, text) -- append-only