caterium-app/supabase/migrations/20260918083000_trial_extras.sql
pavlov346346-source 40b5c146c2 feat: add photos for the 34 training-catalog items that had none
The banquet menu (18 dishes) and extras/supplies/delivery items (16)
in the training catalog were seeded with photo:'', which the catalog
tiles render as a plain red placeholder square. Sourced a real photo
for each item (mostly Pexels, free license, picked and visually
reviewed one by one to avoid mismatches -- several first search hits
were wrong: a live chicken for "chicken caesar salad", a person in
frame, branded bottles/cups) and two AI-generated (banquet-caprese,
banquet-roastbeef). Converted to square 1024x1024 WebP with sharp.

Wires the paths through the actual source of truth: ops/demo/trial-
banquet-data.mjs and trial-extras-data.mjs now set photo to the real
path, then ops/demo/build-trial-*.mjs regenerated public/demo/*.json
and the matching Supabase seed-function migrations, so the client's
static JSON fetch and the server-side company-provisioning function
stay in sync.

Bumped the demo-catalog fetch's own cache-busting query string in
training-catalog.js (and its script-tag/precache version), since it's
fetched by the client with a separate version from the outer script
tags -- the same stale-cache class of bug fixed earlier this session.

Verified: all 34 referenced files resolve (200) and decode as real
1024x1024 images; confirmed via the actual rendered catalog tiles
(#tiles img[src]) that the extras category -- where the user's
red-square screenshot was taken -- now serves the real photo paths.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 17:00:45 +03:00

132 lines
30 KiB
PL/PgSQL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- Generated by ops/demo/build-trial-extras.mjs. Fill empty trial categories only.
begin;
create or replace function public.caterium_trial_extras_seed_v1()
returns jsonb language sql immutable security definer set search_path=public,pg_temp
as $fn$ select $demo${"version":1,"items":[{"id":"demo-extras-v1-salmon-caprese","name":"Премиум-сет «Лосось и капрезе»","category":5,"catalogSection":"Демонстрационные премиум-сеты","price":3500,"pieces":18,"weight":"710 г","photo":"demo/images/extras-salmon-caprese.webp","demo":true,"composition":["Половина стандартного бокса: Канапе с лососем и сливочным сыром","Половина стандартного бокса: Канапе «Капрезе»"],"ingredients":[["Хлеб ржаной",0.12,"кг"],["Лосось слабосолёный",0.12,"кг"],["Сыр сливочный",0.08,"кг"],["Огурцы",0.042,"кг"],["Укроп",0.005,"кг"],["Коробка для кейтеринга",1,"шт."],["Моцарелла мини",0.18,"кг"],["Томаты черри",0.154,"кг"],["Базилик",0.011,"кг"],["Масло оливковое",0.02,"кг"],["Шпажка бамбуковая",10,"шт."]],"ttk":{"number":"ДЕМО-П1","basis":"На 1 готовый премиум-сет","outputGrams":710,"pieces":18,"rows":[{"productId":"demo-v1-stock-rye","name":"Хлеб ржаной","unit":"кг","gross":0.12,"net":0.12,"unitCost":220},{"productId":"demo-v1-stock-salmon","name":"Лосось слабосолёный","unit":"кг","gross":0.12,"net":0.12,"unitCost":2400},{"productId":"demo-v1-stock-cream","name":"Сыр сливочный","unit":"кг","gross":0.08,"net":0.08,"unitCost":850},{"productId":"demo-v1-stock-cucumber","name":"Огурцы","unit":"кг","gross":0.042,"net":0.036,"unitCost":220},{"productId":"demo-v1-stock-dill","name":"Укроп","unit":"кг","gross":0.005,"net":0.004,"unitCost":700},{"productId":"demo-v1-stock-box","name":"Коробка для кейтеринга","unit":"шт.","gross":1,"net":1,"unitCost":65},{"productId":"demo-v1-stock-mozzarella","name":"Моцарелла мини","unit":"кг","gross":0.18,"net":0.18,"unitCost":950},{"productId":"demo-v1-stock-cherry","name":"Томаты черри","unit":"кг","gross":0.154,"net":0.14,"unitCost":450},{"productId":"demo-v1-stock-basil","name":"Базилик","unit":"кг","gross":0.011,"net":0.01,"unitCost":1500},{"productId":"demo-v1-stock-oil","name":"Масло оливковое","unit":"кг","gross":0.02,"net":0.02,"unitCost":1100},{"productId":"demo-v1-stock-skewer","name":"Шпажка бамбуковая","unit":"шт.","gross":10,"net":10,"unitCost":2}],"steps":["Приготовить две мини-подборки в половинном объёме по указанным нормам.","Канапе с лососем и сливочным сыром: Вырезать из ржаного хлеба 16 основ.","Канапе с лососем и сливочным сыром: Нанести сливочный сыр, добавить ломтики огурца и лосося.","Канапе с лососем и сливочным сыром: Украсить укропом и уложить в коробку.","Канапе «Капрезе»: Подготовить черри и базилик, обсушить моцареллу.","Канапе «Капрезе»: Собрать 20 шпажек, распределив ингредиенты поровну.","Канапе «Капрезе»: Добавить оливковое масло, уложить в коробку.","Уложить обе подборки в одну общую коробку."],"allergens":["Рыба","Молоко","Глютен"],"ingredientCost":758.94,"note":"Учебный сет и примерные цены. Все количества в таблице рассчитаны на один сет."}},{"id":"demo-extras-v1-meat-cheese","name":"Премиум-сет «Мясо и сыры»","category":5,"catalogSection":"Демонстрационные премиум-сеты","price":3900,"pieces":0,"weight":"750 г","photo":"demo/images/extras-meat-cheese.webp","demo":true,"composition":["Половина стандартного бокса: Мясное ассорти с корнишонами","Половина стандартного бокса: Сырный бокс с виноградом и орехами"],"ingredients":[["Салями",0.09,"кг"],["Ростбиф готовый",0.11,"кг"],["Ветчина из индейки",0.075,"кг"],["Корнишоны",0.045,"кг"],["Маслины без косточек",0.023,"кг"],["Горчица",0.015,"кг"],["Соусник",1,"шт."],["Коробка для кейтеринга",1,"шт."],["Сыр бри",0.1,"кг"],["Сыр гауда",0.09,"кг"],["Сыр с голубой плесенью",0.06,"кг"],["Виноград",0.088,"кг"],["Орех грецкий очищенный",0.025,"кг"],["Мёд",0.02,"кг"],["Крекер",0.025,"кг"]],"ttk":{"number":"ДЕМО-П2","basis":"На 1 готовый премиум-сет","outputGrams":750,"pieces":0,"rows":[{"productId":"demo-v1-stock-salami","name":"Салями","unit":"кг","gross":0.09,"net":0.09,"unitCost":1400},{"productId":"demo-v1-stock-beef","name":"Ростбиф готовый","unit":"кг","gross":0.11,"net":0.11,"unitCost":2000},{"productId":"demo-v1-stock-turkey","name":"Ветчина из индейки","unit":"кг","gross":0.075,"net":0.075,"unitCost":1000},{"productId":"demo-v1-stock-pickle","name":"Корнишоны","unit":"кг","gross":0.045,"net":0.04,"unitCost":400},{"productId":"demo-v1-stock-olive","name":"Маслины без косточек","unit":"кг","gross":0.023,"net":0.02,"unitCost":650},{"productId":"demo-v1-stock-mustard","name":"Горчица","unit":"кг","gross":0.015,"net":0.015,"unitCost":350},{"productId":"demo-v1-stock-sauce-cup","name":"Соусник","unit":"шт.","gross":1,"net":1,"unitCost":7},{"productId":"demo-v1-stock-box","name":"Коробка для кейтеринга","unit":"шт.","gross":1,"net":1,"unitCost":65},{"productId":"demo-v1-stock-brie","name":"Сыр бри","unit":"кг","gross":0.1,"net":0.1,"unitCost":1600},{"productId":"demo-v1-stock-gouda","name":"Сыр гауда","unit":"кг","gross":0.09,"net":0.09,"unitCost":900},{"productId":"demo-v1-stock-blue","name":"Сыр с голубой плесенью","unit":"кг","gross":0.06,"net":0.06,"unitCost":1700},{"productId":"demo-v1-stock-grape","name":"Виноград","unit":"кг","gross":0.088,"net":0.08,"unitCost":420},{"productId":"demo-v1-stock-walnut","name":"Орех грецкий очищенный","unit":"кг","gross":0.025,"net":0.025,"unitCost":1000},{"productId":"demo-v1-stock-honey","name":"Мёд","unit":"кг","gross":0.02,"net":0.02,"unitCost":800},{"productId":"demo-v1-stock-cracker","name":"Крекер","unit":"кг","gross":0.025,"net":0.025,"unitCost":500}],"steps":["Приготовить две мини-подборки в половинном объёме по указанным нормам.","Мясное ассорти с корнишонами: Нарезать готовые мясные продукты тонкими ломтиками.","Мясное ассорти с корнишонами: Обсушить корнишоны и маслины, переложить горчицу в соусник.","Мясное ассорти с корнишонами: Разложить ассорти секциями, добавить гарниры.","Сырный бокс с виноградом и орехами: Нарезать сыры порционными кусочками.","Сырный бокс с виноградом и орехами: Подготовить виноград, переложить мёд в соусник.","Сырный бокс с виноградом и орехами: Разложить сыры, виноград, орехи и крекер отдельными секциями.","Уложить обе подборки в одну общую коробку."],"allergens":["Горчица","Молоко","Орехи","Глютен"],"ingredientCost":964.66,"note":"Учебный сет и примерные цены. Все количества в таблице рассчитаны на один сет."}},{"id":"demo-extras-v1-mini-buffet","name":"Премиум-сет «Мини-фуршет»","category":5,"catalogSection":"Демонстрационные премиум-сеты","price":2900,"pieces":22,"weight":"820 г","photo":"demo/images/extras-mini-buffet.webp","demo":true,"composition":["Половина стандартного бокса: Брускетты с томатами и базиликом","Половина стандартного бокса: Тарталетки с грибами и сыром"],"ingredients":[["Багет",0.24,"кг"],["Томаты",0.165,"кг"],["Базилик",0.013,"кг"],["Масло оливковое",0.028,"кг"],["Коробка для кейтеринга",1,"шт."],["Тарталетки готовые",0.1,"кг"],["Шампиньоны",0.22,"кг"],["Сливки",0.05,"кг"],["Сыр гауда",0.06,"кг"],["Лук репчатый",0.044,"кг"]],"ttk":{"number":"ДЕМО-П3","basis":"На 1 готовый премиум-сет","outputGrams":820,"pieces":22,"rows":[{"productId":"demo-v1-stock-baguette","name":"Багет","unit":"кг","gross":0.24,"net":0.24,"unitCost":240},{"productId":"demo-v1-stock-tomato","name":"Томаты","unit":"кг","gross":0.165,"net":0.15,"unitCost":260},{"productId":"demo-v1-stock-basil","name":"Базилик","unit":"кг","gross":0.013,"net":0.012,"unitCost":1500},{"productId":"demo-v1-stock-oil","name":"Масло оливковое","unit":"кг","gross":0.028,"net":0.028,"unitCost":1100},{"productId":"demo-v1-stock-box","name":"Коробка для кейтеринга","unit":"шт.","gross":1,"net":1,"unitCost":65},{"productId":"demo-v1-stock-tartlet","name":"Тарталетки готовые","unit":"кг","gross":0.1,"net":0.1,"unitCost":750},{"productId":"demo-v1-stock-mushroom","name":"Шампиньоны","unit":"кг","gross":0.22,"net":0.15,"unitCost":340},{"productId":"demo-v1-stock-cooking-cream","name":"Сливки","unit":"кг","gross":0.05,"net":0.05,"unitCost":500},{"productId":"demo-v1-stock-gouda","name":"Сыр гауда","unit":"кг","gross":0.06,"net":0.06,"unitCost":900},{"productId":"demo-v1-stock-onion","name":"Лук репчатый","unit":"кг","gross":0.044,"net":0.03,"unitCost":80}],"steps":["Приготовить две мини-подборки в половинном объёме по указанным нормам.","Брускетты с томатами и базиликом: Нарезать багет на 24 одинаковых ломтика и подсушить.","Брускетты с томатами и базиликом: Подготовить томаты и базилик, смешать с маслом.","Брускетты с томатами и базиликом: Распределить начинку по ломтикам, уложить в коробку.","Тарталетки с грибами и сыром: Подготовить и обжарить лук с грибами на масле; в расчёте учтены потери массы.","Тарталетки с грибами и сыром: Добавить сливки и распределить начинку по 20 тарталеткам.","Тарталетки с грибами и сыром: Добавить сыр, запечь до расплавления и упаковать после охлаждения.","Уложить обе подборки в одну общую коробку."],"allergens":["Глютен","Молоко"],"ingredientCost":448.12,"note":"Учебный сет и примерные цены. Все количества в таблице рассчитаны на один сет."}},{"id":"demo-extras-v1-water","name":"Вода негазированная, 0,5 л","category":3,"catalogSection":"Безалкогольные напитки","price":90,"pieces":1,"weight":"","photo":"demo/images/extras-water.webp","composition":["Одна бутылка 0,5 л"],"ingredients":[["Демо: вода негазированная 0,5 л",1,"шт."]],"demo":true},{"id":"demo-extras-v1-sparkling","name":"Вода газированная, 0,5 л","category":3,"catalogSection":"Безалкогольные напитки","price":100,"pieces":1,"weight":"","photo":"demo/images/extras-sparkling.webp","composition":["Одна бутылка 0,5 л"],"ingredients":[["Демо: вода газированная 0,5 л",1,"шт."]],"demo":true},{"id":"demo-extras-v1-juice","name":"Яблочный сок, 1 л","category":3,"catalogSection":"Безалкогольные напитки","price":220,"pieces":1,"weight":"","photo":"demo/images/extras-juice.webp","composition":["Одна упаковка 1 л"],"ingredients":[["Демо: сок яблочный 1 л",1,"шт."]],"demo":true},{"id":"demo-extras-v1-mors","name":"Клюквенный морс, 1 л","category":3,"catalogSection":"Безалкогольные напитки","price":280,"pieces":1,"weight":"","photo":"demo/images/extras-mors.webp","composition":["Одна бутылка готового морса 1 л"],"ingredients":[["Демо: морс клюквенный 1 л",1,"шт."]],"demo":true},{"id":"demo-extras-v1-plate","name":"Тарелка одноразовая, 1 шт.","category":1,"catalogSection":"Одноразовая посуда","price":25,"pieces":1,"weight":"","photo":"demo/images/extras-plate.webp","composition":["Одна сервировочная тарелка"],"ingredients":[["Демо: тарелка одноразовая",1,"шт."]],"demo":true},{"id":"demo-extras-v1-fork","name":"Вилка одноразовая, 1 шт.","category":1,"catalogSection":"Одноразовая посуда","price":10,"pieces":1,"weight":"","photo":"demo/images/extras-fork.webp","composition":["Одна вилка"],"ingredients":[["Демо: вилка одноразовая",1,"шт."]],"demo":true},{"id":"demo-extras-v1-glass","name":"Стакан одноразовый, 250 мл","category":1,"catalogSection":"Одноразовая посуда","price":15,"pieces":1,"weight":"","photo":"demo/images/extras-glass.webp","composition":["Один стакан"],"ingredients":[["Демо: стакан одноразовый 250 мл",1,"шт."]],"demo":true},{"id":"demo-extras-v1-napkin","name":"Салфетка сервировочная, 1 шт.","category":1,"catalogSection":"Одноразовая посуда","price":10,"pieces":1,"weight":"","photo":"demo/images/extras-napkin.webp","composition":["Одна бумажная салфетка"],"ingredients":[["Демо: салфетка сервировочная",1,"шт."]],"demo":true},{"id":"demo-extras-v1-ice","name":"Лёд пищевой, пакет 1 кг","category":2,"catalogSection":"Для сервировки","price":180,"pieces":1,"weight":"1 кг","photo":"demo/images/extras-ice.webp","composition":["Один пакет пищевого льда, 1 кг"],"ingredients":[["Демо: лёд пищевой",1,"кг"]],"demo":true},{"id":"demo-extras-v1-tablecloth","name":"Скатерть одноразовая, 1 шт.","category":2,"catalogSection":"Для сервировки","price":250,"pieces":1,"weight":"","photo":"demo/images/extras-tablecloth.webp","composition":["Одна одноразовая скатерть 120 × 180 см"],"ingredients":[["Демо: скатерть одноразовая",1,"шт."]],"demo":true},{"id":"demo-extras-v1-serving-kit","name":"Набор для подачи: щипцы и лопатка","category":2,"catalogSection":"Для сервировки","price":190,"pieces":1,"weight":"","photo":"demo/images/extras-serving-kit.webp","composition":["Один комплект одноразовых приборов для подачи"],"ingredients":[["Демо: набор щипцы и лопатка",1,"шт."]],"demo":true},{"id":"demo-extras-v1-delivery-city","name":"Доставка по городу","category":4,"catalogSection":"Учебные тарифы доставки","price":600,"pieces":0,"weight":"","photo":"demo/images/extras-delivery-city.webp","composition":["Учебный тариф за один адрес в пределах города."],"ingredients":[],"demo":true},{"id":"demo-extras-v1-delivery-outer","name":"Доставка за город","category":4,"catalogSection":"Учебные тарифы доставки","price":1200,"pieces":0,"weight":"","photo":"demo/images/extras-delivery-outer.webp","composition":["Учебный тариф за один адрес в ближайшем пригороде."],"ingredients":[],"demo":true}],"stock":[{"id":"demo-extras-v1-stock-water","name":"Демо: вода негазированная 0,5 л","cost":35,"unit":"шт.","qty":0,"min":10,"supplierId":"demo-v1-supplier-grocery","lastPurchaseDate":"","lastPurchasePrice":0},{"id":"demo-extras-v1-stock-sparkling","name":"Демо: вода газированная 0,5 л","cost":40,"unit":"шт.","qty":0,"min":10,"supplierId":"demo-v1-supplier-grocery","lastPurchaseDate":"","lastPurchasePrice":0},{"id":"demo-extras-v1-stock-juice","name":"Демо: сок яблочный 1 л","cost":110,"unit":"шт.","qty":0,"min":10,"supplierId":"demo-v1-supplier-grocery","lastPurchaseDate":"","lastPurchasePrice":0},{"id":"demo-extras-v1-stock-mors","name":"Демо: морс клюквенный 1 л","cost":140,"unit":"шт.","qty":0,"min":10,"supplierId":"demo-v1-supplier-grocery","lastPurchaseDate":"","lastPurchasePrice":0},{"id":"demo-extras-v1-stock-plate","name":"Демо: тарелка одноразовая","cost":12,"unit":"шт.","qty":0,"min":10,"supplierId":"demo-v1-supplier-pack","lastPurchaseDate":"","lastPurchasePrice":0},{"id":"demo-extras-v1-stock-fork","name":"Демо: вилка одноразовая","cost":4,"unit":"шт.","qty":0,"min":10,"supplierId":"demo-v1-supplier-pack","lastPurchaseDate":"","lastPurchasePrice":0},{"id":"demo-extras-v1-stock-glass","name":"Демо: стакан одноразовый 250 мл","cost":6,"unit":"шт.","qty":0,"min":10,"supplierId":"demo-v1-supplier-pack","lastPurchaseDate":"","lastPurchasePrice":0},{"id":"demo-extras-v1-stock-napkin","name":"Демо: салфетка сервировочная","cost":3,"unit":"шт.","qty":0,"min":10,"supplierId":"demo-v1-supplier-pack","lastPurchaseDate":"","lastPurchasePrice":0},{"id":"demo-extras-v1-stock-ice","name":"Демо: лёд пищевой","cost":70,"unit":"кг","qty":0,"min":1,"supplierId":"demo-v1-supplier-grocery","lastPurchaseDate":"","lastPurchasePrice":0},{"id":"demo-extras-v1-stock-tablecloth","name":"Демо: скатерть одноразовая","cost":110,"unit":"шт.","qty":0,"min":10,"supplierId":"demo-v1-supplier-pack","lastPurchaseDate":"","lastPurchasePrice":0},{"id":"demo-extras-v1-stock-serving-kit","name":"Демо: набор щипцы и лопатка","cost":85,"unit":"шт.","qty":0,"min":10,"supplierId":"demo-v1-supplier-pack","lastPurchaseDate":"","lastPurchasePrice":0}]}$demo$::jsonb $fn$;
revoke all on function public.caterium_trial_extras_seed_v1() from public,anon,authenticated;
create or replace function public.caterium_trial_demo_status(p_workspace uuid)
returns jsonb language plpgsql stable security definer set search_path=public,pg_temp
as $fn$
declare
v_storage jsonb; v_empty boolean:=true; v_key text; v_value jsonb;
v_allowed boolean; v_installed boolean;
begin
if auth.uid() is null or public.sun_member_role(p_workspace) is null then raise exception 'Access denied'; end if;
select coalesce(payload->'storage','{}'::jsonb) into v_storage from public.sun_app_state where workspace_id=p_workspace;
v_installed:=coalesce(v_storage#>>'{sunTrialDemoV1,v,version}','')='1';
foreach v_key in array array['sunBoxes','sunStock','sunSuppliers','sunStockMoves'] loop
v_value:=v_storage#>array[v_key,'v'];
if v_value is not null and v_value<>'[]'::jsonb then v_empty:=false; end if;
if v_storage ? v_key and (jsonb_typeof(v_storage->v_key)<>'object' or v_value is null) then v_empty:=false; end if;
end loop;
v_allowed:=public.sun_member_role(p_workspace)='admin'
and public.sun_subscription_access_mode(p_workspace)='full'
and (exists(select 1 from public.sun_workspace_subscriptions where workspace_id=p_workspace and status='trialing') or public.sun_is_platform_admin())
and public.sun_workspace_has_feature(p_workspace,'catalog_edit')
and public.sun_workspace_has_feature(p_workspace,'stock')
and public.sun_workspace_has_feature(p_workspace,'suppliers')
and not exists(select 1 from public.sun_workspace_members m join public.caterium_sidebar_brand_assignment a on a.user_id=m.user_id where m.workspace_id=p_workspace);
return jsonb_build_object('eligible',coalesce(v_allowed,false),'installed',v_installed,'empty',v_empty,'canInstall',coalesce(v_allowed,false) and v_empty and not v_installed,'canUpgrade',coalesce(v_allowed,false) and v_installed and (coalesce(v_storage#>>'{sunTrialDemoV1,v,banquetVersion}','')<>'1' or coalesce(v_storage#>>'{sunTrialDemoV1,v,extrasVersion}','')<>'1'));
end $fn$;
create or replace function public.caterium_install_trial_extras(p_workspace uuid)
returns jsonb language plpgsql security definer set search_path=public,pg_temp
as $fn$
declare
v_state public.sun_app_state%rowtype; v_status jsonb; v_storage jsonb; v_seed jsonb;
v_catalog jsonb; v_stock jsonb; v_added jsonb; v_products jsonb; v_marker jsonb;
begin
if auth.uid() is null or public.sun_member_role(p_workspace) is distinct from 'admin' then raise exception 'Administrator membership required'; end if;
perform 1 from public.sun_workspaces where id=p_workspace for update;
v_status:=public.caterium_trial_demo_status(p_workspace);
if not (v_status->>'eligible')::boolean then raise exception 'Демонстрационная база доступна в пробной или собственной тестовой компании разработчика'; end if;
if not (v_status->>'installed')::boolean then raise exception 'Сначала установите демонстрационную базу'; end if;
select * into v_state from public.sun_app_state where workspace_id=p_workspace for update;
v_storage:=v_state.payload->'storage';
if coalesce(v_storage#>>'{sunTrialDemoV1,v,extrasVersion}','')='1' then return v_status; end if;
v_catalog:=coalesce(v_storage#>'{sunBoxes,v}','[]'::jsonb);v_stock:=coalesce(v_storage#>'{sunStock,v}','[]'::jsonb);
if jsonb_typeof(v_catalog)<>'array' or jsonb_typeof(v_stock)<>'array' then raise exception 'Неверный формат каталога или склада. Данные сохранены без изменений'; end if;
v_seed:=public.caterium_trial_extras_seed_v1();
select coalesce(jsonb_agg(n order by ord),'[]'::jsonb) into v_added
from jsonb_array_elements(v_seed->'items') with ordinality as x(n,ord)
where not exists(select 1 from jsonb_array_elements(v_catalog) old where coalesce(old->>'category','0')=n->>'category' or old->>'id'=n->>'id');
select coalesce(jsonb_agg(n order by ord),'[]'::jsonb) into v_products
from jsonb_array_elements(v_seed->'stock') with ordinality as x(n,ord)
where not exists(select 1 from jsonb_array_elements(v_stock) old where old->>'id'=n->>'id' or (old->>'name'=n->>'name' and old->>'unit'=n->>'unit'))
and exists(select 1 from jsonb_array_elements(v_added) i cross join lateral jsonb_array_elements(i->'ingredients') r where r->>0=n->>'name' and r->>2=n->>'unit');
v_marker:=(v_storage#>'{sunTrialDemoV1,v}')||jsonb_build_object('extrasVersion',1,'extrasInstalledAt',now());
v_storage:=v_storage||jsonb_build_object('sunBoxes',jsonb_build_object('t','j','v',v_catalog||v_added),'sunStock',jsonb_build_object('t','j','v',v_stock||v_products),'sunTrialDemoV1',jsonb_build_object('t','j','v',v_marker));
perform public.sun_save_app_state_v17(p_workspace,jsonb_set(v_state.payload,'{storage}',v_storage),'trial-extras-v1',v_state.revision);
insert into public.sun_platform_audit_events(actor_user_id,action,target_workspace_id,details)
values(auth.uid(),'trial_demo.extras_install',p_workspace,jsonb_build_object('version',1,'items',jsonb_array_length(v_added)));
return public.caterium_trial_demo_status(p_workspace);
end $fn$;
revoke all on function public.caterium_install_trial_extras(uuid) from public,anon;
grant execute on function public.caterium_install_trial_extras(uuid) to authenticated;
create or replace function public.caterium_install_trial_banquet(p_workspace uuid)
returns jsonb language plpgsql security definer set search_path=public,pg_temp
as $fn$
declare
v_state public.sun_app_state%rowtype; v_status jsonb; v_storage jsonb;
v_seed jsonb; v_key text; v_items jsonb; v_added jsonb; v_marker jsonb;
begin
if auth.uid() is null or public.sun_member_role(p_workspace) is distinct from 'admin' then raise exception 'Administrator membership required'; end if;
perform 1 from public.sun_workspaces where id=p_workspace for update;
v_status:=public.caterium_trial_demo_status(p_workspace);
if not (v_status->>'eligible')::boolean then raise exception 'Демонстрационная база доступна в пробной или собственной тестовой компании разработчика'; end if;
if not (v_status->>'installed')::boolean then raise exception 'Сначала установите демонстрационную базу'; end if;
if coalesce((select payload#>>'{storage,sunTrialDemoV1,v,banquetVersion}' from public.sun_app_state where workspace_id=p_workspace),'')='1' then return v_status; end if;
select * into v_state from public.sun_app_state where workspace_id=p_workspace for update;
v_storage:=v_state.payload->'storage'; v_seed:=public.caterium_trial_banquet_seed_v1();
foreach v_key in array array['sunBoxes','sunStock'] loop
v_items:=coalesce(v_storage#>array[v_key,'v'],'[]'::jsonb);
if jsonb_typeof(v_items)<>'array' then raise exception 'Неверный формат каталога или склада. Данные сохранены без изменений'; end if;
-- Keep every existing row byte-for-byte, including edited demo prices and balances.
select coalesce(jsonb_agg(n),'[]'::jsonb) into v_added
from jsonb_array_elements(v_seed->case when v_key='sunBoxes' then 'banquet' else 'stock' end) n
where not exists(select 1 from jsonb_array_elements(v_items) old where old->>'id'=n->>'id');
v_storage:=v_storage||jsonb_build_object(v_key,jsonb_build_object('t','j','v',v_items||v_added));
end loop;
v_marker:=(v_storage#>'{sunTrialDemoV1,v}')||jsonb_build_object('banquetVersion',1,'banquetInstalledAt',now());
v_storage:=jsonb_set(v_storage,'{sunTrialDemoV1,v}',v_marker);
perform public.sun_save_app_state_v17(p_workspace,jsonb_set(v_state.payload,'{storage}',v_storage),'trial-banquet-v1',v_state.revision);
insert into public.sun_platform_audit_events(actor_user_id,action,target_workspace_id,details)
values(auth.uid(),'trial_demo.banquet_install',p_workspace,jsonb_build_object('version',1,'dishes',18));
return public.caterium_trial_demo_status(p_workspace);
end $fn$;
create or replace function public.caterium_install_trial_demo(p_workspace uuid)
returns jsonb language plpgsql security definer set search_path=public,pg_temp
as $fn$
declare
v_state public.sun_app_state%rowtype; v_status jsonb; v_seed jsonb; v_storage jsonb; v_moves jsonb; v_marker jsonb;
begin
if auth.uid() is null or public.sun_member_role(p_workspace) is distinct from 'admin' then raise exception 'Administrator membership required'; end if;
perform 1 from public.sun_workspaces where id=p_workspace for update;
v_status:=public.caterium_trial_demo_status(p_workspace);
if not (v_status->>'eligible')::boolean then raise exception 'Демонстрационная база доступна в пробной или собственной тестовой компании разработчика'; end if;
if (v_status->>'installed')::boolean then perform public.caterium_install_trial_banquet(p_workspace); return public.caterium_install_trial_extras(p_workspace); end if;
if not (v_status->>'empty')::boolean then raise exception 'Каталог, склад или поставщики уже заполнены. Данные сохранены без изменений'; end if;
select * into v_state from public.sun_app_state where workspace_id=p_workspace for update;
if not found then raise exception 'Рабочая база не найдена'; end if;
v_seed:=public.caterium_trial_demo_seed_v1();
select coalesce(jsonb_agg(jsonb_build_object('id','demo-v1-opening-'||(p->>'id'),'productId',p->>'id','productName',p->>'name','unit',p->>'unit',
'type','set','qty',p->'qty','setTo',p->'qty','cost',p->'cost','date',current_date::text,'createdAt',now(),
'supplierId',p->>'supplierId','note','Учебный начальный остаток')),'[]'::jsonb) into v_moves
from jsonb_array_elements(v_seed->'stock') p where (p->>'qty')::numeric>0;
v_marker:=jsonb_build_object('version',1,'batch','trial-demo-v1','installedAt',now(),'scenario',v_seed->'scenario');
v_storage:=coalesce(v_state.payload->'storage','{}'::jsonb)||jsonb_build_object(
'sunBoxes',jsonb_build_object('t','j','v',v_seed->'boxes'),
'sunStock',jsonb_build_object('t','j','v',v_seed->'stock'),
'sunSuppliers',jsonb_build_object('t','j','v',v_seed->'suppliers'),
'sunStockMoves',jsonb_build_object('t','j','v',v_moves),
'sunTrialDemoV1',jsonb_build_object('t','j','v',v_marker));
perform public.sun_save_app_state_v17(p_workspace,jsonb_set(v_state.payload,'{storage}',v_storage),'trial-demo-v1',v_state.revision);
insert into public.sun_platform_audit_events(actor_user_id,action,target_workspace_id,details)
values(auth.uid(),'trial_demo.install',p_workspace,jsonb_build_object('version',1,'boxes',10));
perform public.caterium_install_trial_banquet(p_workspace); return public.caterium_install_trial_extras(p_workspace);
end $fn$;
notify pgrst,'reload schema';
commit;