213 lines
6.3 KiB
SQL
213 lines
6.3 KiB
SQL
-- Migration 103: Verification evidence ledger and competitor status semantics
|
|
--
|
|
-- Goal:
|
|
-- fully_verified should mean "source-backed and resolved", not merely
|
|
-- "a competitor row was found". A product may be fully resolved when a
|
|
-- valid 1:1 competitor exists OR when research verified that no valid
|
|
-- public 1:1 competitor is available.
|
|
|
|
ALTER TABLE transceivers
|
|
ADD COLUMN IF NOT EXISTS competitor_status VARCHAR(32) NOT NULL DEFAULT 'unknown',
|
|
ADD COLUMN IF NOT EXISTS competitor_status_updated_at TIMESTAMPTZ,
|
|
ADD COLUMN IF NOT EXISTS no_match_verified_at TIMESTAMPTZ,
|
|
ADD COLUMN IF NOT EXISTS no_match_reason TEXT;
|
|
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1
|
|
FROM pg_constraint
|
|
WHERE conname = 'transceivers_competitor_status_check'
|
|
) THEN
|
|
ALTER TABLE transceivers
|
|
ADD CONSTRAINT transceivers_competitor_status_check
|
|
CHECK (competitor_status IN (
|
|
'unknown',
|
|
'matched',
|
|
'no_valid_match',
|
|
'needs_research',
|
|
'ambiguous'
|
|
));
|
|
END IF;
|
|
END $$;
|
|
|
|
UPDATE transceivers
|
|
SET competitor_status = CASE
|
|
WHEN competitor_verified = true THEN 'matched'
|
|
WHEN competitor_status = 'unknown' THEN 'needs_research'
|
|
ELSE competitor_status
|
|
END,
|
|
competitor_status_updated_at = COALESCE(competitor_status_updated_at, NOW())
|
|
WHERE competitor_status IS NULL
|
|
OR competitor_status = 'unknown'
|
|
OR competitor_verified = true;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_transceivers_competitor_status
|
|
ON transceivers (competitor_status);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_transceivers_no_valid_match
|
|
ON transceivers (no_match_verified_at)
|
|
WHERE competitor_status = 'no_valid_match';
|
|
|
|
CREATE TABLE IF NOT EXISTS transceiver_verification_evidence (
|
|
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
|
transceiver_id UUID NOT NULL REFERENCES transceivers(id) ON DELETE CASCADE,
|
|
verification_type VARCHAR(40) NOT NULL CHECK (verification_type IN (
|
|
'price',
|
|
'image',
|
|
'details',
|
|
'competitor_match',
|
|
'competitor_no_match',
|
|
'artifact_quarantine'
|
|
)),
|
|
source_url TEXT,
|
|
source_vendor_id UUID REFERENCES vendors(id) ON DELETE SET NULL,
|
|
evidence_value JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
evidence_hash TEXT,
|
|
robot_name TEXT NOT NULL DEFAULT 'unknown',
|
|
confidence NUMERIC(4,3) CHECK (confidence IS NULL OR confidence BETWEEN 0 AND 1),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_verification_evidence_tx
|
|
ON transceiver_verification_evidence (transceiver_id, verification_type, created_at DESC);
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_verification_evidence_dedupe
|
|
ON transceiver_verification_evidence (
|
|
transceiver_id,
|
|
verification_type,
|
|
COALESCE(evidence_hash, ''),
|
|
robot_name
|
|
);
|
|
|
|
COMMENT ON COLUMN transceivers.competitor_status IS
|
|
'Resolution state for competitor evidence: matched, no_valid_match, needs_research, ambiguous, unknown.';
|
|
|
|
COMMENT ON TABLE transceiver_verification_evidence IS
|
|
'Append-only evidence ledger for TIP verification decisions. Stores source-backed proof for price, image, details, competitor matches and verified no-match states.';
|
|
|
|
-- Seed the ledger from already verified rows so TIP starts with an auditable
|
|
-- baseline instead of an empty proof table.
|
|
INSERT INTO transceiver_verification_evidence (
|
|
transceiver_id,
|
|
verification_type,
|
|
source_url,
|
|
source_vendor_id,
|
|
evidence_value,
|
|
evidence_hash,
|
|
robot_name,
|
|
confidence
|
|
)
|
|
SELECT DISTINCT ON (t.id)
|
|
t.id,
|
|
'price',
|
|
po.url,
|
|
po.source_vendor_id,
|
|
jsonb_build_object(
|
|
'price', po.price,
|
|
'currency', po.currency,
|
|
'observed_at', po.time
|
|
),
|
|
md5(jsonb_build_object(
|
|
'type', 'price',
|
|
'price', po.price,
|
|
'currency', po.currency,
|
|
'url', COALESCE(po.url, '')
|
|
)::text),
|
|
'migration:103:price-backfill',
|
|
1.0
|
|
FROM transceivers t
|
|
JOIN price_observations po ON po.transceiver_id = t.id
|
|
WHERE t.price_verified = true
|
|
AND COALESCE(po.is_verified, true) = true
|
|
ORDER BY t.id, po.time DESC
|
|
ON CONFLICT DO NOTHING;
|
|
|
|
INSERT INTO transceiver_verification_evidence (
|
|
transceiver_id,
|
|
verification_type,
|
|
source_url,
|
|
evidence_value,
|
|
evidence_hash,
|
|
robot_name,
|
|
confidence
|
|
)
|
|
SELECT
|
|
id,
|
|
'image',
|
|
COALESCE(NULLIF(image_verified_url, ''), NULLIF(image_url, '')),
|
|
jsonb_build_object('image_url', COALESCE(NULLIF(image_verified_url, ''), NULLIF(image_url, ''))),
|
|
md5(jsonb_build_object('type', 'image', 'url', COALESCE(NULLIF(image_verified_url, ''), NULLIF(image_url, '')))::text),
|
|
'migration:103:image-backfill',
|
|
1.0
|
|
FROM transceivers
|
|
WHERE image_verified = true
|
|
AND COALESCE(NULLIF(image_verified_url, ''), NULLIF(image_url, '')) IS NOT NULL
|
|
ON CONFLICT DO NOTHING;
|
|
|
|
INSERT INTO transceiver_verification_evidence (
|
|
transceiver_id,
|
|
verification_type,
|
|
source_url,
|
|
evidence_value,
|
|
evidence_hash,
|
|
robot_name,
|
|
confidence
|
|
)
|
|
SELECT
|
|
id,
|
|
'details',
|
|
COALESCE(NULLIF(details_source_url, ''), NULLIF(product_page_url, '')),
|
|
jsonb_build_object(
|
|
'form_factor', form_factor,
|
|
'speed_gbps', speed_gbps,
|
|
'reach_label', reach_label,
|
|
'fiber_type', fiber_type
|
|
),
|
|
md5(jsonb_build_object(
|
|
'type', 'details',
|
|
'source_url', COALESCE(NULLIF(details_source_url, ''), NULLIF(product_page_url, '')),
|
|
'form_factor', form_factor,
|
|
'speed_gbps', speed_gbps,
|
|
'reach_label', reach_label,
|
|
'fiber_type', fiber_type
|
|
)::text),
|
|
'migration:103:details-backfill',
|
|
1.0
|
|
FROM transceivers
|
|
WHERE details_verified = true
|
|
AND COALESCE(NULLIF(details_source_url, ''), NULLIF(product_page_url, '')) IS NOT NULL
|
|
ON CONFLICT DO NOTHING;
|
|
|
|
INSERT INTO transceiver_verification_evidence (
|
|
transceiver_id,
|
|
verification_type,
|
|
evidence_value,
|
|
evidence_hash,
|
|
robot_name,
|
|
confidence
|
|
)
|
|
SELECT DISTINCT ON (eq.flexoptix_id)
|
|
eq.flexoptix_id,
|
|
'competitor_match',
|
|
jsonb_build_object(
|
|
'equivalence_id', eq.id,
|
|
'competitor_id', eq.competitor_id,
|
|
'status', eq.status,
|
|
'match_basis', eq.match_basis,
|
|
'match_notes', eq.match_notes
|
|
),
|
|
md5(jsonb_build_object(
|
|
'type', 'competitor_match',
|
|
'equivalence_id', eq.id,
|
|
'competitor_id', eq.competitor_id,
|
|
'status', eq.status
|
|
)::text),
|
|
'migration:103:competitor-match-backfill',
|
|
eq.confidence
|
|
FROM transceiver_equivalences eq
|
|
WHERE eq.status IN ('approved', 'auto_approved')
|
|
ORDER BY eq.flexoptix_id, eq.confidence DESC, eq.updated_at DESC
|
|
ON CONFLICT DO NOTHING;
|
|
|