diff --git a/supabase/migrations/20260321000300_gs1_country_hint.sql b/supabase/migrations/20260321000300_gs1_country_hint.sql new file mode 100644 index 00000000..1ff1c111 --- /dev/null +++ b/supabase/migrations/20260321000300_gs1_country_hint.sql @@ -0,0 +1,73 @@ +-- Migration: Add gs1_country_hint() utility function +-- Purpose: Extract GS1 country-of-registration hint from EAN-13 prefix +-- Rollback: DROP FUNCTION IF EXISTS public.gs1_country_hint; +-- Idempotency: CREATE OR REPLACE — safe to run multiple times + +-- ═══════════════════════════════════════════════════════════════════════ +-- GS1 prefix → country hint (EAN-13 first 2–3 digits) +-- ═══════════════════════════════════════════════════════════════════════ +-- GS1 prefix indicates where the barcode was REGISTERED, not where +-- the product was manufactured or sold. Many imported products carry +-- foreign prefixes. Use as an admin hint, never as blocking validation. +-- ═══════════════════════════════════════════════════════════════════════ + +CREATE OR REPLACE FUNCTION public.gs1_country_hint(p_ean text) +RETURNS jsonb +LANGUAGE sql +IMMUTABLE STRICT +SET search_path = public +AS $$ + SELECT CASE + -- NULL / too-short handled by STRICT (returns NULL automatically) + WHEN length(p_ean) < 3 THEN NULL + + -- Poland (590) + WHEN substring(p_ean, 1, 3) = '590' + THEN '{"code":"PL","name":"Poland","confidence":"high"}'::jsonb + + -- Germany (400–440) + WHEN substring(p_ean, 1, 2) BETWEEN '40' AND '44' + THEN '{"code":"DE","name":"Germany","confidence":"high"}'::jsonb + + -- France (300–379) + WHEN substring(p_ean, 1, 2) BETWEEN '30' AND '37' + THEN '{"code":"FR","name":"France","confidence":"high"}'::jsonb + + -- United Kingdom (50) + WHEN substring(p_ean, 1, 2) = '50' + THEN '{"code":"GB","name":"United Kingdom","confidence":"high"}'::jsonb + + -- Ireland (539) + WHEN substring(p_ean, 1, 3) = '539' + THEN '{"code":"IE","name":"Ireland","confidence":"high"}'::jsonb + + -- Italy (800–839) + WHEN substring(p_ean, 1, 3) BETWEEN '800' AND '839' + THEN '{"code":"IT","name":"Italy","confidence":"high"}'::jsonb + + -- Spain (840–849) + WHEN substring(p_ean, 1, 3) BETWEEN '840' AND '849' + THEN '{"code":"ES","name":"Spain","confidence":"high"}'::jsonb + + -- Store-internal (020–029, 200–299) + WHEN substring(p_ean, 1, 3) BETWEEN '020' AND '029' + THEN '{"code":"STORE","name":"Store-internal","confidence":"low"}'::jsonb + WHEN substring(p_ean, 1, 1) = '2' + THEN '{"code":"STORE","name":"Store-internal","confidence":"low"}'::jsonb + + -- Unknown — return prefix for debugging + ELSE jsonb_build_object( + 'code', 'UNKNOWN', + 'name', 'Unknown origin', + 'confidence', 'none', + 'prefix', substring(p_ean, 1, 3) + ) + END; +$$; + +COMMENT ON FUNCTION public.gs1_country_hint IS + 'Returns GS1 country-of-registration hint from EAN prefix. + NOT a definitive origin — imported products carry foreign prefixes. + Use as admin hint only, never as blocking validation. + Returns: {code, name, confidence} or NULL for invalid/NULL input. + Confidence: high (known GS1 prefix), low (store-internal), none (unknown).'; diff --git a/supabase/tests/scanner_functions.test.sql b/supabase/tests/scanner_functions.test.sql index 38a8d1a9..c48a982f 100644 --- a/supabase/tests/scanner_functions.test.sql +++ b/supabase/tests/scanner_functions.test.sql @@ -7,7 +7,7 @@ -- ───────────────────────────────────────────────────────────────────────────── BEGIN; -SELECT plan(80); +SELECT plan(92); -- ─── Fixtures ─────────────────────────────────────────────────────────────── @@ -654,5 +654,91 @@ SELECT is( ); +-- ═══════════════════════════════════════════════════════════════════════════ +-- gs1_country_hint — GS1 prefix to country hint utility (#928) +-- ═══════════════════════════════════════════════════════════════════════════ + +-- 1. PL prefix (590) → Poland +SELECT is( + (public.gs1_country_hint('5901234123457'))->>'code', + 'PL', + 'gs1_country_hint: 590 prefix returns PL (#928)' +); + +-- 2. DE prefix (400–440 range) → Germany +SELECT is( + (public.gs1_country_hint('4000000000000'))->>'code', + 'DE', + 'gs1_country_hint: 400 prefix returns DE (#928)' +); + +SELECT is( + (public.gs1_country_hint('4400000000000'))->>'code', + 'DE', + 'gs1_country_hint: 440 prefix returns DE (#928)' +); + +-- 3. FR prefix (300–379) → France +SELECT is( + (public.gs1_country_hint('3000000000000'))->>'code', + 'FR', + 'gs1_country_hint: 300 prefix returns FR (#928)' +); + +-- 4. GB prefix (50) → United Kingdom +SELECT is( + (public.gs1_country_hint('5000000000000'))->>'code', + 'GB', + 'gs1_country_hint: 50 prefix returns GB (#928)' +); + +-- 5. IE prefix (539) → Ireland +SELECT is( + (public.gs1_country_hint('5390000000000'))->>'code', + 'IE', + 'gs1_country_hint: 539 prefix returns IE (#928)' +); + +-- 6. IT prefix (800–839) → Italy +SELECT is( + (public.gs1_country_hint('8000000000000'))->>'code', + 'IT', + 'gs1_country_hint: 800 prefix returns IT (#928)' +); + +-- 7. ES prefix (840–849) → Spain +SELECT is( + (public.gs1_country_hint('8400000000000'))->>'code', + 'ES', + 'gs1_country_hint: 840 prefix returns ES (#928)' +); + +-- 8. Store-internal (020–029) +SELECT is( + (public.gs1_country_hint('0200000000000'))->>'code', + 'STORE', + 'gs1_country_hint: 020 prefix returns STORE (#928)' +); + +-- 9. Store-internal (200–299) +SELECT is( + (public.gs1_country_hint('2000000000000'))->>'code', + 'STORE', + 'gs1_country_hint: 200 prefix returns STORE (#928)' +); + +-- 10. Unknown prefix → UNKNOWN with prefix field +SELECT is( + (public.gs1_country_hint('9990000000000'))->>'code', + 'UNKNOWN', + 'gs1_country_hint: unknown prefix returns UNKNOWN (#928)' +); + +SELECT ok( + (public.gs1_country_hint('9990000000000')) ? 'prefix', + 'gs1_country_hint: unknown result includes prefix field (#928)' +); + + SELECT * FROM finish(); ROLLBACK; diff --git a/supabase/tests/schema_contracts.test.sql b/supabase/tests/schema_contracts.test.sql index e36d5858..e5a1ff57 100644 --- a/supabase/tests/schema_contracts.test.sql +++ b/supabase/tests/schema_contracts.test.sql @@ -7,7 +7,7 @@ -- ───────────────────────────────────────────────────────────────────────────── BEGIN; -SELECT plan(302); +SELECT plan(303); -- ═══════════════════════════════════════════════════════════════════════════ -- 1. Core data tables exist @@ -446,5 +446,9 @@ SELECT has_function('public', 'api_record_scan', ARRAY['text', 'text'], SELECT has_function('public', 'api_submit_product', ARRAY['text', 'text', 'text', 'text', 'text', 'text', 'text', 'text'], 'api_submit_product(text ×8) — 8-param country-aware signature (#923)'); +-- ─── GS1 Country Hint (#928, epic #920) ────────────────────────────────────── +SELECT has_function('public', 'gs1_country_hint', ARRAY['text'], + 'gs1_country_hint(text) — GS1 prefix to country JSONB (#928)'); + SELECT * FROM finish(); ROLLBACK;