From ac0de76e0f006a9305a626509cddf5e9523bb1f0 Mon Sep 17 00:00:00 2001 From: ericsocrat Date: Tue, 17 Mar 2026 11:28:37 +0100 Subject: [PATCH] feat(scanner): pass user region through api_record_scan and api_submit_product (#923) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - api_record_scan: new p_scan_country text DEFAULT NULL param Country resolution: p_scan_country > user_preferences.country Response adds: scan_country, product_country keys - api_submit_product: new p_scan_country, p_suggested_country params Country resolution: scan_country from param or prefs suggested_country from param or defaults to resolved scan_country Response adds: scan_country, suggested_country keys - DROP old 1-param api_record_scan and 6-param api_submit_product - All new params DEFAULT NULL for backward compatibility - 6 new pgTAP tests in scanner_functions.test.sql (plan 64->70) - 2 new schema contract assertions (plan 303->305) - API_CONTRACTS.md: document both RPC signatures under ยง8 --- docs/API_CONTRACTS.md | 88 ++++++ ...60320000300_country_aware_scanner_rpcs.sql | 295 ++++++++++++++++++ supabase/tests/scanner_functions.test.sql | 39 ++- supabase/tests/schema_contracts.test.sql | 8 +- 4 files changed, 428 insertions(+), 2 deletions(-) create mode 100644 supabase/migrations/20260320000300_country_aware_scanner_rpcs.sql diff --git a/docs/API_CONTRACTS.md b/docs/API_CONTRACTS.md index 41ee469b..ea335636 100644 --- a/docs/API_CONTRACTS.md +++ b/docs/API_CONTRACTS.md @@ -687,6 +687,94 @@ Pre-computed confidence for all 1,025 products. Faster than calling `compute_dat **Access:** anon, authenticated, service_role +### `api_record_scan(p_ean text, p_scan_country text DEFAULT NULL)` + +**Purpose:** Records a barcode scan in `scan_history` and returns product info if the EAN matches. + +**PostgREST:** `POST /rpc/api_record_scan` with `{ "p_ean": "5900259135360" }` or `{ "p_ean": "5900259135360", "p_scan_country": "PL" }` + +**Parameters:** + +| Name | Type | Default | Description | +| ---------------- | ---- | ------- | --------------------------------------------------------------------------------------------------------------- | +| `p_ean` | text | โ€” | Barcode (EAN-8 or EAN-13). Validated via `is_valid_ean()`. | +| `p_scan_country` | text | NULL | Country scope of the scan. Auto-resolves from `user_preferences.country` when NULL and caller is authenticated. | + +**Country resolution order:** explicit `p_scan_country` โ†’ `user_preferences.country` โ†’ NULL. + +**Found Response:** +```jsonc +{ + "api_version": "1.0", + "found": true, + "product_id": 42, + "product_name": "Lay's Solone", + "product_name_en": "Lay's Salted", + "product_name_display": "Lay's Solone", // localized via resolve_language + "brand": "Lay's", + "category": "Chips", + "category_display": "Chipsy", // from category_ref + "category_icon": "๐Ÿฅ”", + "unhealthiness_score": 41, + "nutri_score": "D", + "scan_country": "PL", // resolved country (#923) + "product_country": "PL" // the product's stored country (#923) +} +``` + +**Not-Found Response:** +```jsonc +{ + "api_version": "1.0", + "found": false, + "ean": "0000000000000", + "has_pending_submission": false, + "scan_country": "PL" // resolved country (#923) +} +``` + +**Error cases:** invalid EAN checksum, rate limit (100/24h per user). + +**Access:** authenticated, service_role + +### `api_submit_product(p_ean, p_product_name, p_brand, p_category, p_photo_url, p_notes, p_scan_country, p_suggested_country)` + +**Purpose:** Submit a missing product for review. Stores in `product_submissions` with country metadata. + +**PostgREST:** `POST /rpc/api_submit_product` with `{ "p_ean": "...", "p_product_name": "..." }` + +**Parameters:** + +| Name | Type | Default | Description | +| --------------------- | ---- | ------- | ----------------------------------------------------------------------------- | +| `p_ean` | text | โ€” | Barcode (validated via `is_valid_ean()`). | +| `p_product_name` | text | โ€” | Product name (required, non-empty). | +| `p_brand` | text | NULL | Brand name. | +| `p_category` | text | NULL | Category key. | +| `p_photo_url` | text | NULL | Product photo URL. | +| `p_notes` | text | NULL | Submission notes. | +| `p_scan_country` | text | NULL | Country of the scan. Auto-resolves from `user_preferences.country` when NULL. | +| `p_suggested_country` | text | NULL | Suggested product country. Defaults to resolved `scan_country` when NULL. | + +**Country resolution order:** `p_scan_country` โ†’ `user_preferences.country` โ†’ NULL. `p_suggested_country` โ†’ resolved scan_country. + +**Success Response:** +```jsonc +{ + "api_version": "1.0", + "submission_id": "a1b2c3d4-...", + "ean": "5900259135360", + "product_name": "New Product", + "status": "pending", + "scan_country": "PL", // resolved country (#923) + "suggested_country": "PL" // resolved or explicit (#923) +} +``` + +**Error cases:** authentication required, invalid EAN checksum, product_name required, EAN already exists in products, pending submission exists, rate limit (10/24h per user). + +**Access:** authenticated only + --- ## 9. Preference-Aware Filtering diff --git a/supabase/migrations/20260320000300_country_aware_scanner_rpcs.sql b/supabase/migrations/20260320000300_country_aware_scanner_rpcs.sql new file mode 100644 index 00000000..810e746d --- /dev/null +++ b/supabase/migrations/20260320000300_country_aware_scanner_rpcs.sql @@ -0,0 +1,295 @@ +-- Migration: feat(scanner): pass user region through api_record_scan and api_submit_product (#923) +-- Part of epic #920 โ€” scanner country awareness +-- Depends on: 20260320000100 (scan_history.scan_country), 20260320000200 (product_submissions.scan_country + suggested_country) +-- +-- Changes: +-- api_record_scan: adds p_scan_country DEFAULT NULL โ†’ resolve from user_preferences โ†’ store in scan_history.scan_country โ†’ return scan_country + product_country +-- api_submit_product: adds p_scan_country + p_suggested_country DEFAULT NULL โ†’ resolve from user_preferences โ†’ store in product_submissions โ†’ return both +-- +-- Backward compatible: all new params have DEFAULT NULL, existing callers unchanged +-- Rollback: re-run previous version from 20260315000200_rate_limiting.sql + +-- โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +-- 1. api_record_scan โ€” add p_scan_country, resolve from user_preferences, +-- store in scan_history.scan_country, return scan_country + product_country +-- โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +CREATE OR REPLACE FUNCTION public.api_record_scan( + p_ean text, + p_scan_country text DEFAULT NULL +) +RETURNS jsonb +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = public +AS $$ +DECLARE + v_user_id uuid := auth.uid(); + v_product record; + v_found boolean := false; + v_product_id bigint; + v_language text; + v_country_lang text; + v_cat_display text; + v_cat_icon text; + v_rate_check jsonb; + v_scan_country text; +BEGIN + -- Validate EAN format + IF p_ean IS NULL OR LENGTH(TRIM(p_ean)) NOT IN (8, 13) THEN + RETURN jsonb_build_object( + 'api_version', '1.0', + 'error', 'EAN must be 8 or 13 digits' + ); + END IF; + + -- Rate limit check (only for authenticated users who will write) + IF v_user_id IS NOT NULL THEN + v_rate_check := check_scan_rate_limit(v_user_id); + IF NOT (v_rate_check->>'allowed')::boolean THEN + RETURN jsonb_build_object( + 'api_version', '1.0', + 'error', 'rate_limit_exceeded', + 'message', 'Too many scans. Please try again later.', + 'retry_after_seconds', (v_rate_check->>'retry_after_seconds')::integer, + 'current_count', (v_rate_check->>'current_count')::integer, + 'max_allowed', (v_rate_check->>'max_allowed')::integer + ); + END IF; + END IF; + + -- Resolve scan_country: explicit param โ†’ user_preferences โ†’ NULL + v_scan_country := p_scan_country; + IF v_scan_country IS NULL AND v_user_id IS NOT NULL THEN + SELECT up.country INTO v_scan_country + FROM public.user_preferences up + WHERE up.user_id = v_user_id; + END IF; + + -- Resolve user language + v_language := resolve_language(NULL); + + -- Lookup product by EAN (now includes name_translations) + SELECT p.product_id, p.product_name, p.product_name_en, p.name_translations, + p.brand, p.category, p.country, p.unhealthiness_score, p.nutri_score_label + INTO v_product + FROM public.products p + WHERE p.ean = TRIM(p_ean) + LIMIT 1; + + IF FOUND THEN + v_found := true; + v_product_id := v_product.product_id; + + -- Resolve country default language + SELECT cref.default_language INTO v_country_lang + FROM public.country_ref cref + WHERE cref.country_code = v_product.country; + v_country_lang := COALESCE(v_country_lang, LOWER(v_product.country)); + + -- Resolve category display + icon + SELECT COALESCE(ct.display_name, cr.display_name), + COALESCE(cr.icon_emoji, '๐Ÿ“ฆ') + INTO v_cat_display, v_cat_icon + FROM public.category_ref cr + LEFT JOIN public.category_translations ct + ON ct.category = cr.category AND ct.language_code = v_language + WHERE cr.category = v_product.category; + END IF; + + -- Record scan (only for authenticated users) + IF v_user_id IS NOT NULL THEN + INSERT INTO public.scan_history (user_id, ean, product_id, found, scan_country) + VALUES (v_user_id, TRIM(p_ean), v_product_id, v_found, v_scan_country); + END IF; + + -- Return result + IF v_found THEN + RETURN jsonb_build_object( + 'api_version', '1.0', + 'found', true, + 'product_id', v_product.product_id, + 'product_name', v_product.product_name, + 'product_name_en', v_product.product_name_en, + 'product_name_display', CASE + WHEN v_language = v_country_lang THEN v_product.product_name + WHEN v_language = 'en' THEN COALESCE(v_product.product_name_en, v_product.product_name) + ELSE COALESCE( + v_product.name_translations->>v_language, + v_product.product_name_en, + v_product.product_name + ) + END, + 'brand', v_product.brand, + 'category', v_product.category, + 'category_display', v_cat_display, + 'category_icon', v_cat_icon, + 'unhealthiness_score', v_product.unhealthiness_score, + 'nutri_score', v_product.nutri_score_label, + 'scan_country', v_scan_country, + 'product_country', v_product.country + ); + ELSE + RETURN jsonb_build_object( + 'api_version', '1.0', + 'found', false, + 'ean', TRIM(p_ean), + 'has_pending_submission', EXISTS ( + SELECT 1 FROM public.product_submissions + WHERE ean = TRIM(p_ean) AND status = 'pending' + ), + 'scan_country', v_scan_country + ); + END IF; +END; +$$; + +COMMENT ON FUNCTION public.api_record_scan(text, text) IS + 'Record a barcode scan and lookup product. Resolves scan_country from param or user_preferences. Enforces 100/24h rate limit per user.'; + +-- Grant must reference the new 2-param signature +REVOKE ALL ON FUNCTION public.api_record_scan(text, text) FROM PUBLIC; +REVOKE ALL ON FUNCTION public.api_record_scan(text, text) FROM anon; +GRANT EXECUTE ON FUNCTION public.api_record_scan(text, text) TO authenticated; +GRANT EXECUTE ON FUNCTION public.api_record_scan(text, text) TO service_role; + +-- Drop the old 1-param signature so only the new one exists +DROP FUNCTION IF EXISTS public.api_record_scan(text); + + +-- โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +-- 2. api_submit_product โ€” add p_scan_country + p_suggested_country, +-- resolve from user_preferences, store in product_submissions, return both +-- โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +CREATE OR REPLACE FUNCTION public.api_submit_product( + p_ean text, + p_product_name text, + p_brand text DEFAULT NULL, + p_category text DEFAULT NULL, + p_photo_url text DEFAULT NULL, + p_notes text DEFAULT NULL, + p_scan_country text DEFAULT NULL, + p_suggested_country text DEFAULT NULL +) +RETURNS jsonb +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = public +AS $$ +DECLARE + v_uid uuid; + v_ean text; + v_existing uuid; + v_result jsonb; + v_rate_check jsonb; + v_scan_country text; + v_suggested_country text; +BEGIN + -- Auth check + v_uid := auth.uid(); + IF v_uid IS NULL THEN + RETURN jsonb_build_object( + 'api_version', '1.0', + 'error', 'Authentication required' + ); + END IF; + + -- Rate limit check (before any processing) + v_rate_check := check_submission_rate_limit(v_uid); + IF NOT (v_rate_check->>'allowed')::boolean THEN + RETURN jsonb_build_object( + 'api_version', '1.0', + 'error', 'rate_limit_exceeded', + 'message', 'Too many submissions. Please try again later.', + 'retry_after_seconds', (v_rate_check->>'retry_after_seconds')::integer, + 'current_count', (v_rate_check->>'current_count')::integer, + 'max_allowed', (v_rate_check->>'max_allowed')::integer + ); + END IF; + + -- Trim EAN + v_ean := TRIM(COALESCE(p_ean, '')); + + -- Validate EAN (checksum + format) + IF NOT is_valid_ean(v_ean) THEN + RETURN jsonb_build_object( + 'api_version', '1.0', + 'error', 'Invalid EAN โ€” must be a valid EAN-8 or EAN-13 barcode with correct checksum' + ); + END IF; + + -- Check product_name required + IF p_product_name IS NULL OR TRIM(p_product_name) = '' THEN + RETURN jsonb_build_object( + 'api_version', '1.0', + 'error', 'Product name is required' + ); + END IF; + + -- Check if EAN already exists in products + IF EXISTS (SELECT 1 FROM products WHERE ean = v_ean AND is_deprecated IS NOT TRUE) THEN + RETURN jsonb_build_object( + 'api_version', '1.0', + 'error', 'Product with this EAN already exists in database' + ); + END IF; + + -- Check if EAN already has a pending submission + SELECT id INTO v_existing + FROM product_submissions + WHERE ean = v_ean AND status = 'pending' + LIMIT 1; + + IF v_existing IS NOT NULL THEN + RETURN jsonb_build_object( + 'api_version', '1.0', + 'error', 'A submission for this EAN is already pending review' + ); + END IF; + + -- Resolve scan_country: explicit param โ†’ user_preferences โ†’ NULL + v_scan_country := p_scan_country; + IF v_scan_country IS NULL THEN + SELECT up.country INTO v_scan_country + FROM public.user_preferences up + WHERE up.user_id = v_uid; + END IF; + + -- Resolve suggested_country: explicit param โ†’ scan_country โ†’ NULL + v_suggested_country := COALESCE(p_suggested_country, v_scan_country); + + -- Insert submission + INSERT INTO product_submissions ( + user_id, ean, product_name, brand, category, photo_url, notes, + scan_country, suggested_country + ) + VALUES ( + v_uid, v_ean, TRIM(p_product_name), NULLIF(TRIM(p_brand), ''), + NULLIF(TRIM(p_category), ''), NULLIF(TRIM(p_photo_url), ''), + NULLIF(TRIM(p_notes), ''), + v_scan_country, v_suggested_country + ) + RETURNING jsonb_build_object( + 'api_version', '1.0', + 'submission_id', id::text, + 'ean', ean, + 'product_name', product_name, + 'status', status, + 'scan_country', scan_country, + 'suggested_country', suggested_country + ) INTO v_result; + + RETURN v_result; +END; +$$; + +COMMENT ON FUNCTION public.api_submit_product(text, text, text, text, text, text, text, text) IS + 'Submit a new product for review. Resolves scan_country and suggested_country from params or user_preferences. Validates EAN checksum and enforces 10/24h rate limit.'; + +-- Grant must reference the new 8-param signature +REVOKE ALL ON FUNCTION public.api_submit_product(text, text, text, text, text, text, text, text) FROM anon; +GRANT EXECUTE ON FUNCTION public.api_submit_product(text, text, text, text, text, text, text, text) TO authenticated; + +-- Drop the old 6-param signature so only the new one exists +DROP FUNCTION IF EXISTS public.api_submit_product(text, text, text, text, text, text); diff --git a/supabase/tests/scanner_functions.test.sql b/supabase/tests/scanner_functions.test.sql index c3129f73..148e4e99 100644 --- a/supabase/tests/scanner_functions.test.sql +++ b/supabase/tests/scanner_functions.test.sql @@ -7,7 +7,7 @@ -- โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ BEGIN; -SELECT plan(64); +SELECT plan(70); -- โ”€โ”€โ”€ Fixtures โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ @@ -91,6 +91,16 @@ SELECT ok( 'found response contains nutri_score key (mapped from nutri_score_label)' ); +SELECT ok( + (public.api_record_scan('5901234123457')) ? 'scan_country', + 'found response contains scan_country key (#923)' +); + +SELECT ok( + (public.api_record_scan('5901234123457')) ? 'product_country', + 'found response contains product_country key (#923)' +); + -- โ”€โ”€โ”€ 3. Returned values match fixture data โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ SELECT is( @@ -155,6 +165,11 @@ SELECT ok( 'not-found response contains has_pending_submission key' ); +SELECT ok( + (public.api_record_scan('0000000000000')) ? 'scan_country', + 'not-found response contains scan_country key (#923)' +); + -- โ”€โ”€โ”€ 6. Invalid EAN returns error โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ SELECT ok( @@ -179,6 +194,28 @@ SELECT ok( -- โ”€โ”€โ”€ 7. Whitespace trimming โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +-- โ”€โ”€โ”€ 6b. Explicit scan_country parameter (#923) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +SELECT is( + (public.api_record_scan('5901234123457', 'PL'))->>'scan_country', + 'PL', + 'explicit p_scan_country=PL is returned in response (#923)' +); + +SELECT is( + (public.api_record_scan('5901234123457', 'PL'))->>'product_country', + 'XX', + 'product_country reflects fixture product country XX (#923)' +); + +SELECT is( + (public.api_record_scan('5901234123457'))->>'scan_country', + NULL, + 'scan_country is NULL when no param and no auth (#923)' +); + +-- โ”€โ”€โ”€ 7 (cont). Whitespace trimming โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + SELECT is( (public.api_record_scan(' 5901234123457 '))->>'found', 'true', diff --git a/supabase/tests/schema_contracts.test.sql b/supabase/tests/schema_contracts.test.sql index b5665077..e36d5858 100644 --- a/supabase/tests/schema_contracts.test.sql +++ b/supabase/tests/schema_contracts.test.sql @@ -7,7 +7,7 @@ -- โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ BEGIN; -SELECT plan(300); +SELECT plan(302); -- โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• -- 1. Core data tables exist @@ -440,5 +440,11 @@ SELECT fk_ok('public', 'product_submissions', 'suggested_country', 'public', 'country_ref', 'country_code', 'product_submissions.suggested_country references country_ref(country_code)'); +-- โ”€โ”€โ”€ Country-aware RPC signatures (#923, epic #920) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +SELECT has_function('public', 'api_record_scan', ARRAY['text', 'text'], + 'api_record_scan(text, text) โ€” 2-param country-aware signature (#923)'); +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)'); + SELECT * FROM finish(); ROLLBACK;