Fill empty trial tabs with premium, drinks, tableware, extras and delivery
This commit is contained in:
parent
18ff2b9c30
commit
dac1a1de72
@ -11,7 +11,7 @@
|
||||
"serverReady": true,
|
||||
"workspaceAutoDiscovery": true,
|
||||
"invitesTemporarilyDisabled": false,
|
||||
"pwaCache": "v97-20260918-trial-banquet",
|
||||
"pwaCache": "v98-20260918-trial-extras",
|
||||
"fullOfferDescriptions": true,
|
||||
"dynamicOfferRows": true,
|
||||
"pdfOfferDescriptionFix": true,
|
||||
|
||||
16
ops/demo/build-trial-extras.mjs
Normal file
16
ops/demo/build-trial-extras.mjs
Normal file
@ -0,0 +1,16 @@
|
||||
import fs from 'node:fs';
|
||||
import assert from 'node:assert/strict';
|
||||
import {extrasDemo} from './trial-extras-data.mjs';
|
||||
const root=new URL('../../',import.meta.url);
|
||||
const original=fs.readFileSync(new URL('supabase/migrations/20260918060000_trial_banquet.sql',root),'utf8');
|
||||
const block=name=>{const start=original.indexOf(`create or replace function public.${name}(`);assert.ok(start>=0);return original.slice(start,original.indexOf('end $fn$;',start)+10)};
|
||||
let status=block('caterium_trial_demo_status');
|
||||
const old="and coalesce(v_storage#>>'{sunTrialDemoV1,v,banquetVersion}','')<>'1'";
|
||||
assert.ok(status.includes(old));status=status.replace(old,"and (coalesce(v_storage#>>'{sunTrialDemoV1,v,banquetVersion}','')<>'1' or coalesce(v_storage#>>'{sunTrialDemoV1,v,extrasVersion}','')<>'1')");
|
||||
let banquet=block('caterium_install_trial_banquet');
|
||||
banquet=banquet.replace("if not (v_status->>'canUpgrade')::boolean then return v_status; 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;");
|
||||
const install=block('caterium_install_trial_demo').replaceAll('return public.caterium_install_trial_banquet(p_workspace);','perform public.caterium_install_trial_banquet(p_workspace); return public.caterium_install_trial_extras(p_workspace);');
|
||||
const template=fs.readFileSync(new URL('trial-extras.sql',import.meta.url),'utf8');
|
||||
fs.writeFileSync(new URL('public/demo/extras-v1.json',root),JSON.stringify(extrasDemo,null,2)+'\n');
|
||||
fs.writeFileSync(new URL('supabase/migrations/20260918083000_trial_extras.sql',root),template.replace('__EXTRAS_JSON__',JSON.stringify(extrasDemo)).replace('__STATUS_FUNCTION__',status).replace('__BANQUET_FUNCTION__',banquet).replace('__INSTALL_FUNCTION__',install));
|
||||
console.log(`Built ${extrasDemo.items.length} items across five trial categories and ${extrasDemo.stock.length} stock products.`);
|
||||
36
ops/demo/trial-extras-data.mjs
Normal file
36
ops/demo/trial-extras-data.mjs
Normal file
@ -0,0 +1,36 @@
|
||||
// Fictional items for empty trial tabs. Native category IDs must not follow visual order.
|
||||
import {boxes} from './trial-demo-data.mjs';
|
||||
const round=n=>Math.round(n*1000)/1000;
|
||||
const mixes=[
|
||||
['salmon-caprese','Премиум-сет «Лосось и капрезе»',3500,18,['salmon-cream','caprese']],
|
||||
['meat-cheese','Премиум-сет «Мясо и сыры»',3900,0,['meat-assortment','cheese-fruit']],
|
||||
['mini-buffet','Премиум-сет «Мини-фуршет»',2900,22,['bruschetta-tomato','mushroom-tartlet']]
|
||||
];
|
||||
const premium=mixes.map(([key,name,price,pieces,sourceIds],index)=>{
|
||||
const sources=sourceIds.map(id=>boxes.find(b=>b.id===`demo-v1-${id}`)),map=new Map();
|
||||
for(const b of sources)for(const r of b.ttk.rows){const old=map.get(r.productId)||{...r,gross:0,net:0};old.gross=round(old.gross+r.gross/2);old.net=round(old.net+r.net/2);map.set(r.productId,old)}
|
||||
const rows=[...map.values()],outputGrams=Math.round(rows.filter(r=>r.unit==='кг').reduce((s,r)=>s+r.net*1000,0));
|
||||
return {id:`demo-extras-v1-${key}`,name,category:5,catalogSection:'Демонстрационные премиум-сеты',price,pieces,weight:`${outputGrams} г`,photo:'',demo:true,
|
||||
composition:sources.map(b=>'Половина стандартного бокса: '+b.name),ingredients:rows.map(r=>[r.name,r.gross,r.unit]),
|
||||
ttk:{number:`ДЕМО-П${index+1}`,basis:'На 1 готовый премиум-сет',outputGrams,pieces,rows,
|
||||
steps:['Приготовить две мини-подборки в половинном объёме по указанным нормам.',...sources.flatMap(b=>b.ttk.steps.map(s=>b.name+': '+s)),'Уложить обе подборки в одну общую коробку.'],
|
||||
allergens:[...new Set(sources.flatMap(b=>b.ttk.allergens))],ingredientCost:Math.round(rows.reduce((s,r)=>s+r.gross*r.unitCost,0)*100)/100,note:'Учебный сет и примерные цены. Все количества в таблице рассчитаны на один сет.'}};
|
||||
});
|
||||
const specs=[
|
||||
// key, category, sale name, sale price, stock name, purchase cost, unit, supplier, description
|
||||
['water',3,'Вода негазированная, 0,5 л',90,'Демо: вода негазированная 0,5 л',35,'шт.','grocery','Одна бутылка 0,5 л'],
|
||||
['sparkling',3,'Вода газированная, 0,5 л',100,'Демо: вода газированная 0,5 л',40,'шт.','grocery','Одна бутылка 0,5 л'],
|
||||
['juice',3,'Яблочный сок, 1 л',220,'Демо: сок яблочный 1 л',110,'шт.','grocery','Одна упаковка 1 л'],
|
||||
['mors',3,'Клюквенный морс, 1 л',280,'Демо: морс клюквенный 1 л',140,'шт.','grocery','Одна бутылка готового морса 1 л'],
|
||||
['plate',1,'Тарелка одноразовая, 1 шт.',25,'Демо: тарелка одноразовая',12,'шт.','pack','Одна сервировочная тарелка'],
|
||||
['fork',1,'Вилка одноразовая, 1 шт.',10,'Демо: вилка одноразовая',4,'шт.','pack','Одна вилка'],
|
||||
['glass',1,'Стакан одноразовый, 250 мл',15,'Демо: стакан одноразовый 250 мл',6,'шт.','pack','Один стакан'],
|
||||
['napkin',1,'Салфетка сервировочная, 1 шт.',10,'Демо: салфетка сервировочная',3,'шт.','pack','Одна бумажная салфетка'],
|
||||
['ice',2,'Лёд пищевой, пакет 1 кг',180,'Демо: лёд пищевой',70,'кг','grocery','Один пакет пищевого льда, 1 кг'],
|
||||
['tablecloth',2,'Скатерть одноразовая, 1 шт.',250,'Демо: скатерть одноразовая',110,'шт.','pack','Одна одноразовая скатерть 120 × 180 см'],
|
||||
['serving-kit',2,'Набор для подачи: щипцы и лопатка',190,'Демо: набор щипцы и лопатка',85,'шт.','pack','Один комплект одноразовых приборов для подачи']
|
||||
];
|
||||
const stock=specs.map(([key,category,name,price,productName,cost,unit,supplier])=>({id:`demo-extras-v1-stock-${key}`,name:productName,cost,unit,qty:0,min:unit==='кг'?1:10,supplierId:`demo-v1-supplier-${supplier}`,lastPurchaseDate:'',lastPurchasePrice:0}));
|
||||
const packaged=specs.map(([key,category,name,price,productName,cost,unit,supplier,description])=>({id:`demo-extras-v1-${key}`,name,category,catalogSection:({1:'Одноразовая посуда',2:'Для сервировки',3:'Безалкогольные напитки'})[category],price,pieces:1,weight:key==='ice'?'1 кг':'',photo:'',composition:[description],ingredients:[[productName,1,unit]],demo:true}));
|
||||
const delivery=[['city','Доставка по городу',600,'Учебный тариф за один адрес в пределах города.'],['outer','Доставка за город',1200,'Учебный тариф за один адрес в ближайшем пригороде.']].map(([key,name,price,note])=>({id:`demo-extras-v1-delivery-${key}`,name,category:4,catalogSection:'Учебные тарифы доставки',price,pieces:0,weight:'',photo:'',composition:[note],ingredients:[],demo:true}));
|
||||
export const extrasDemo={version:1,items:[...premium,...packaged,...delivery],stock};
|
||||
48
ops/demo/trial-extras.sql
Normal file
48
ops/demo/trial-extras.sql
Normal file
@ -0,0 +1,48 @@
|
||||
-- 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$__EXTRAS_JSON__$demo$::jsonb $fn$;
|
||||
revoke all on function public.caterium_trial_extras_seed_v1() from public,anon,authenticated;
|
||||
|
||||
__STATUS_FUNCTION__
|
||||
|
||||
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;
|
||||
|
||||
__BANQUET_FUNCTION__
|
||||
__INSTALL_FUNCTION__
|
||||
notify pgrst,'reload schema';
|
||||
commit;
|
||||
@ -13,7 +13,7 @@
|
||||
"test:e2e": "playwright test --config=tests/playwright.config.mjs",
|
||||
"test": "npm run check:deploy",
|
||||
"deploy:cloudflare-backup": "wrangler deploy",
|
||||
"build:demo": "node ops/demo/build-trial-demo.mjs && node ops/demo/build-trial-banquet.mjs"
|
||||
"build:demo": "node ops/demo/build-trial-demo.mjs && node ops/demo/build-trial-banquet.mjs && node ops/demo/build-trial-extras.mjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electric-sql/pglite": "0.5.8",
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
(()=>{
|
||||
'use strict';
|
||||
const VERSION='17.7.3';
|
||||
const RELEASE='20260918-trial-banquet';
|
||||
const RELEASE='20260918-trial-extras';
|
||||
|
||||
const hasStoredSession=()=>{try{return Object.keys(localStorage).some(k=>/^sb-.*-auth-token$/i.test(k)&&String(localStorage.getItem(k)||'').length>20)}catch(_){return false}};
|
||||
function installAuthBoot(){
|
||||
|
||||
@ -29,17 +29,17 @@
|
||||
}
|
||||
function render(){
|
||||
const host=$('new');if(!host)return;
|
||||
const items=catalog().filter(b=>b.demo&&b.ttk),active=installed()&&items.length>0;
|
||||
const demoItems=catalog().filter(b=>b.demo),items=demoItems.filter(b=>b.ttk),active=installed()&&demoItems.length>0;
|
||||
if(!signedIn()||cloud()?.isSupportMode?.()||(!active&&!status?.canInstall)){$('ctTrialDemo')?.remove();return;}
|
||||
let card=$('ctTrialDemo');if(!card){card=document.createElement('section');card.id='ctTrialDemo';card.className='card';host.prepend(card);}
|
||||
const signature=JSON.stringify({active,items:items.map(x=>[x.id,x.name]),upgrade:status?.canUpgrade,can:writable()});
|
||||
const signature=JSON.stringify({active,items:demoItems.map(x=>[x.id,x.name]),upgrade:status?.canUpgrade,can:writable()});
|
||||
if(card.dataset.signature===signature)return;card.dataset.signature=signature;
|
||||
card.innerHTML=active?`<div class="ct-demo-heading"><div><strong>Демонстрационная база</strong><p>${items.filter(b=>Number(b.category)!==6).length} боксов с фото · ${items.filter(b=>Number(b.category)===6).length} банкетных блюд · ТТК, учебные остатки и поставщики</p></div><span class="ct-demo-badge">Можно пробовать</span></div>
|
||||
card.innerHTML=active?`<div class="ct-demo-heading"><div><strong>Демонстрационная база</strong><p>${demoItems.length} тестовых позиций · готовые составы, учебные цены и поставщики</p></div><span class="ct-demo-badge">Можно пробовать</span></div>
|
||||
<p>Создайте пробный заказ, посмотрите нехватку в закупках, оформите приход на складе и спишите продукты по заказу. Все данные можно редактировать.</p>
|
||||
<div class="ct-demo-actions"><button class="primary" data-demo-order ${writable()?'':'disabled'}>Создать пробный заказ</button><button class="outline" data-demo-go="stock">Склад</button><button class="outline" data-demo-go="shopping">Закупки</button></div>
|
||||
${status?.canUpgrade?'<button class="outline" data-demo-install>Добавить 18 банкетных блюд</button>':''}
|
||||
<details><summary>Технологические карты боксов и блюд</summary><div class="ct-demo-actions"><label>Бокс или блюдо<select id="ctDemoBox">${[...new Set(items.map(b=>Number(b.category)===6?'Банкетное меню':'Боксы'))].map(label=>`<optgroup label="${label}">${items.filter(b=>(Number(b.category)===6?'Банкетное меню':'Боксы')===label).map(b=>`<option value="${esc(b.id)}">${esc(b.name)}</option>`).join('')}</optgroup>`).join('')}</select></label><button class="outline" data-demo-ttk>Открыть ТТК</button></div></details><p id="ctDemoMessage" role="status" aria-live="polite"></p>`:
|
||||
`<div class="ct-demo-heading"><div><strong>Попробуйте приложение на готовом меню</strong><p>10 боксов с фото, 18 банкетных блюд с ТТК, 52 продукта, 5 учебных поставщиков и начальные остатки.</p></div></div><button class="primary" data-demo-install>Загрузить демо</button><p id="ctDemoMessage" role="status" aria-live="polite"></p>`;
|
||||
${status?.canUpgrade?'<button class="outline" data-demo-install>Дополнить пустые вкладки демо</button>':''}
|
||||
<details><summary>Технологические карты боксов и блюд</summary><div class="ct-demo-actions"><label>Бокс или блюдо<select id="ctDemoBox">${[...new Set(items.map(b=>Number(b.category)===6?'Банкетное меню':Number(b.category)===5?'Премиум':'Боксы'))].map(label=>`<optgroup label="${label}">${items.filter(b=>(Number(b.category)===6?'Банкетное меню':Number(b.category)===5?'Премиум':'Боксы')===label).map(b=>`<option value="${esc(b.id)}">${esc(b.name)}</option>`).join('')}</optgroup>`).join('')}</select></label><button class="outline" data-demo-ttk>Открыть ТТК</button></div></details><p id="ctDemoMessage" role="status" aria-live="polite"></p>`:
|
||||
`<div class="ct-demo-heading"><div><strong>Попробуйте приложение на готовом меню</strong><p>Боксы и премиум-сеты, 18 банкетных блюд, напитки, посуда, дополнения и доставка. Учебные цены, составы и поставщики для проверки заказа, склада и закупки.</p></div></div><button class="primary" data-demo-install>Загрузить демо</button><p id="ctDemoMessage" role="status" aria-live="polite"></p>`;
|
||||
card.querySelector('[data-demo-install]')?.addEventListener('click',install);
|
||||
card.querySelector('[data-demo-order]')?.addEventListener('click',createOrder);
|
||||
card.querySelector('[data-demo-ttk]')?.addEventListener('click',()=>showTTK($('ctDemoBox').value));
|
||||
@ -48,7 +48,7 @@
|
||||
async function install(){
|
||||
if(busy)return;busy=true;const btn=$('ctTrialDemo')?.querySelector('[data-demo-install]');if(btn)btn.disabled=true;
|
||||
message('Загружаю демонстрационную базу…');
|
||||
try{await cloud().installTrialDemo();status=null;statusWorkspace='';render();message('Готово. Выберите боксы или откройте «Банкетное меню».');await refreshStatus();}
|
||||
try{await cloud().installTrialDemo();status=null;statusWorkspace='';render();message('Готово. Выберите позиции в нужной вкладке каталога.');await refreshStatus();}
|
||||
catch(error){message(error?.message||'Не удалось загрузить демо. Повторите попытку.');}
|
||||
finally{busy=false;if(btn?.isConnected)btn.disabled=false;}
|
||||
}
|
||||
|
||||
983
public/demo/extras-v1.json
Normal file
983
public/demo/extras-v1.json
Normal file
@ -0,0 +1,983 @@
|
||||
{
|
||||
"version": 1,
|
||||
"items": [
|
||||
{
|
||||
"id": "demo-extras-v1-salmon-caprese",
|
||||
"name": "Премиум-сет «Лосось и капрезе»",
|
||||
"category": 5,
|
||||
"catalogSection": "Демонстрационные премиум-сеты",
|
||||
"price": 3500,
|
||||
"pieces": 18,
|
||||
"weight": "710 г",
|
||||
"photo": "",
|
||||
"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": 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": 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": "",
|
||||
"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": "",
|
||||
"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": "",
|
||||
"composition": [
|
||||
"Одна упаковка 1 л"
|
||||
],
|
||||
"ingredients": [
|
||||
[
|
||||
"Демо: сок яблочный 1 л",
|
||||
1,
|
||||
"шт."
|
||||
]
|
||||
],
|
||||
"demo": true
|
||||
},
|
||||
{
|
||||
"id": "demo-extras-v1-mors",
|
||||
"name": "Клюквенный морс, 1 л",
|
||||
"category": 3,
|
||||
"catalogSection": "Безалкогольные напитки",
|
||||
"price": 280,
|
||||
"pieces": 1,
|
||||
"weight": "",
|
||||
"photo": "",
|
||||
"composition": [
|
||||
"Одна бутылка готового морса 1 л"
|
||||
],
|
||||
"ingredients": [
|
||||
[
|
||||
"Демо: морс клюквенный 1 л",
|
||||
1,
|
||||
"шт."
|
||||
]
|
||||
],
|
||||
"demo": true
|
||||
},
|
||||
{
|
||||
"id": "demo-extras-v1-plate",
|
||||
"name": "Тарелка одноразовая, 1 шт.",
|
||||
"category": 1,
|
||||
"catalogSection": "Одноразовая посуда",
|
||||
"price": 25,
|
||||
"pieces": 1,
|
||||
"weight": "",
|
||||
"photo": "",
|
||||
"composition": [
|
||||
"Одна сервировочная тарелка"
|
||||
],
|
||||
"ingredients": [
|
||||
[
|
||||
"Демо: тарелка одноразовая",
|
||||
1,
|
||||
"шт."
|
||||
]
|
||||
],
|
||||
"demo": true
|
||||
},
|
||||
{
|
||||
"id": "demo-extras-v1-fork",
|
||||
"name": "Вилка одноразовая, 1 шт.",
|
||||
"category": 1,
|
||||
"catalogSection": "Одноразовая посуда",
|
||||
"price": 10,
|
||||
"pieces": 1,
|
||||
"weight": "",
|
||||
"photo": "",
|
||||
"composition": [
|
||||
"Одна вилка"
|
||||
],
|
||||
"ingredients": [
|
||||
[
|
||||
"Демо: вилка одноразовая",
|
||||
1,
|
||||
"шт."
|
||||
]
|
||||
],
|
||||
"demo": true
|
||||
},
|
||||
{
|
||||
"id": "demo-extras-v1-glass",
|
||||
"name": "Стакан одноразовый, 250 мл",
|
||||
"category": 1,
|
||||
"catalogSection": "Одноразовая посуда",
|
||||
"price": 15,
|
||||
"pieces": 1,
|
||||
"weight": "",
|
||||
"photo": "",
|
||||
"composition": [
|
||||
"Один стакан"
|
||||
],
|
||||
"ingredients": [
|
||||
[
|
||||
"Демо: стакан одноразовый 250 мл",
|
||||
1,
|
||||
"шт."
|
||||
]
|
||||
],
|
||||
"demo": true
|
||||
},
|
||||
{
|
||||
"id": "demo-extras-v1-napkin",
|
||||
"name": "Салфетка сервировочная, 1 шт.",
|
||||
"category": 1,
|
||||
"catalogSection": "Одноразовая посуда",
|
||||
"price": 10,
|
||||
"pieces": 1,
|
||||
"weight": "",
|
||||
"photo": "",
|
||||
"composition": [
|
||||
"Одна бумажная салфетка"
|
||||
],
|
||||
"ingredients": [
|
||||
[
|
||||
"Демо: салфетка сервировочная",
|
||||
1,
|
||||
"шт."
|
||||
]
|
||||
],
|
||||
"demo": true
|
||||
},
|
||||
{
|
||||
"id": "demo-extras-v1-ice",
|
||||
"name": "Лёд пищевой, пакет 1 кг",
|
||||
"category": 2,
|
||||
"catalogSection": "Для сервировки",
|
||||
"price": 180,
|
||||
"pieces": 1,
|
||||
"weight": "1 кг",
|
||||
"photo": "",
|
||||
"composition": [
|
||||
"Один пакет пищевого льда, 1 кг"
|
||||
],
|
||||
"ingredients": [
|
||||
[
|
||||
"Демо: лёд пищевой",
|
||||
1,
|
||||
"кг"
|
||||
]
|
||||
],
|
||||
"demo": true
|
||||
},
|
||||
{
|
||||
"id": "demo-extras-v1-tablecloth",
|
||||
"name": "Скатерть одноразовая, 1 шт.",
|
||||
"category": 2,
|
||||
"catalogSection": "Для сервировки",
|
||||
"price": 250,
|
||||
"pieces": 1,
|
||||
"weight": "",
|
||||
"photo": "",
|
||||
"composition": [
|
||||
"Одна одноразовая скатерть 120 × 180 см"
|
||||
],
|
||||
"ingredients": [
|
||||
[
|
||||
"Демо: скатерть одноразовая",
|
||||
1,
|
||||
"шт."
|
||||
]
|
||||
],
|
||||
"demo": true
|
||||
},
|
||||
{
|
||||
"id": "demo-extras-v1-serving-kit",
|
||||
"name": "Набор для подачи: щипцы и лопатка",
|
||||
"category": 2,
|
||||
"catalogSection": "Для сервировки",
|
||||
"price": 190,
|
||||
"pieces": 1,
|
||||
"weight": "",
|
||||
"photo": "",
|
||||
"composition": [
|
||||
"Один комплект одноразовых приборов для подачи"
|
||||
],
|
||||
"ingredients": [
|
||||
[
|
||||
"Демо: набор щипцы и лопатка",
|
||||
1,
|
||||
"шт."
|
||||
]
|
||||
],
|
||||
"demo": true
|
||||
},
|
||||
{
|
||||
"id": "demo-extras-v1-delivery-city",
|
||||
"name": "Доставка по городу",
|
||||
"category": 4,
|
||||
"catalogSection": "Учебные тарифы доставки",
|
||||
"price": 600,
|
||||
"pieces": 0,
|
||||
"weight": "",
|
||||
"photo": "",
|
||||
"composition": [
|
||||
"Учебный тариф за один адрес в пределах города."
|
||||
],
|
||||
"ingredients": [],
|
||||
"demo": true
|
||||
},
|
||||
{
|
||||
"id": "demo-extras-v1-delivery-outer",
|
||||
"name": "Доставка за город",
|
||||
"category": 4,
|
||||
"catalogSection": "Учебные тарифы доставки",
|
||||
"price": 1200,
|
||||
"pieces": 0,
|
||||
"weight": "",
|
||||
"photo": "",
|
||||
"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
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@ -1,5 +1,5 @@
|
||||
const CACHE='sun-catering-pwa-v97-20260918-trial-banquet';
|
||||
const VERSION='20260918-trial-banquet';
|
||||
const CACHE='sun-catering-pwa-v98-20260918-trial-extras';
|
||||
const VERSION='20260918-trial-extras';
|
||||
const CORE=[
|
||||
'./','./index.html',`./core/proposal-layout.js?v=${VERSION}`,'./fonts/Manrope.ttf','./fonts/PlayfairDisplay.ttf','./fonts/PlayfairDisplay-Italic.ttf',`./core/trial-demo.js?v=${VERSION}`,`./core/cloud-transport.js?v=${VERSION}`,`./core/banquet-menu.js?v=${VERSION}`,`./core/access-policy.js?v=${VERSION}`,`./core/import-archive.js?v=${VERSION}`,`./core/company-branding.js?v=${VERSION}`,`./core/signature-offer-pdf-v18.js?v=${VERSION}`,`./core/brand-theme.js?v=${VERSION}`,
|
||||
`./core/sun-safe.js?v=${VERSION}`,`./core/performance.js?v=${VERSION}`,`./core/account-center-v1780.js?v=${VERSION}`,`./core/login-signature-v1776.js?v=${VERSION}`,`./core/login-signature-v1776.css?v=${VERSION}`,`./core/data-layer-v1773.js?v=${VERSION}`,`./core/server-automation-v1770.js?v=${VERSION}`,`./core/hotfix-v1763.js?v=${VERSION}`,`./core/ops-ux-v1762.js?v=${VERSION}`,`./core/ux-fixes-v1764.js?v=${VERSION}`,`./core/pdf-engine.js?v=${VERSION}`,`./core/classic-offer-pdf-v1767.js?v=${VERSION}`,`./core/developer-console-v1768.js?v=${VERSION}`,`./core/offer-workspace-v1769.js?v=${VERSION}`,`./core/auth-security-v1774.js?v=${VERSION}`,`./core/order-enhancements-v1775.js?v=${VERSION}`,`./legacy/bootstrap.js?v=${VERSION}`,`./app-runtime.js?v=${VERSION}`,
|
||||
|
||||
134
supabase/migrations/20260918083000_trial_extras.sql
Normal file
134
supabase/migrations/20260918083000_trial_extras.sql
Normal file
File diff suppressed because one or more lines are too long
@ -61,21 +61,23 @@ await actor(ids[2]);const foreign=(await one('select public.sun_create_workspace
|
||||
assert.equal((await one('select * from public.sun_fetch_app_state($1)',[foreign])).payload.storage.sunPdfBrandLogoV1,undefined);
|
||||
const demoRow=await one('select * from public.sun_fetch_app_state($1)',[foreign]);
|
||||
const training=demoRow.payload.storage;
|
||||
assert.equal(training.sunBoxes.v.length,28);assert.equal(training.sunStock.v.length,52);assert.equal(training.sunSuppliers.v.length,5);
|
||||
assert.equal(training.sunBoxes.v.length,44);assert.equal(training.sunStock.v.length,63);assert.equal(training.sunSuppliers.v.length,5);
|
||||
assert.deepEqual([0,1,2,3,4,5,6].map(c=>training.sunBoxes.v.filter(b=>b.category===c).length),[10,4,3,4,2,3,18]);
|
||||
assert.equal(training.sunBoxes.v.filter(b=>b.category===6).length,18);assert.equal(training.sunTrialDemoV1.v.banquetVersion,1);
|
||||
assert.equal(training.sunOrders,undefined);assert.equal(training.sunTrialDemoV1.v.version,1);
|
||||
assert.equal((await one('select public.sun_v17_entity_snapshot($1) as s',[foreign])).s.catalog.length,28);
|
||||
assert.equal((await one('select public.sun_v17_entity_snapshot($1) as s',[foreign])).s.catalog.length,44);
|
||||
assert.ok(training.sunStockMoves.v.length>0);assert.ok(training.sunStock.v.some(p=>p.qty===0));
|
||||
for(const b of training.sunBoxes.v){assert.ok(b.ttk.ingredientCost>0);assert.equal(b.ttk.outputGrams,Math.round(b.ttk.rows.filter(r=>r.unit==='кг').reduce((s,r)=>s+r.net*1000,b.ttk.waterGrams||0)));for(const r of b.ttk.rows){assert.ok(r.gross>=r.net);assert.ok(training.sunStock.v.some(p=>p.id===r.productId&&p.name===r.name&&p.unit===r.unit&&p.cost===r.unitCost));}}
|
||||
for(const b of training.sunBoxes.v){for(const [name,qty,unit] of b.ingredients){assert.ok(qty>0);assert.ok(training.sunStock.v.some(p=>p.name===name&&p.unit===unit));}if(!b.ttk)continue;assert.ok(b.ttk.ingredientCost>0);assert.equal(b.ttk.outputGrams,Math.round(b.ttk.rows.filter(r=>r.unit==='кг').reduce((s,r)=>s+r.net*1000,b.ttk.waterGrams||0)));for(const r of b.ttk.rows){assert.ok(r.gross>=r.net);assert.ok(training.sunStock.v.some(p=>p.id===r.productId&&p.name===r.name&&p.unit===r.unit&&p.cost===r.unitCost));}}
|
||||
await q('select public.caterium_install_trial_demo($1)',[foreign]);
|
||||
assert.equal((await one('select * from public.sun_fetch_app_state($1)',[foreign])).revision,demoRow.revision,'idempotent demo install does not overwrite');
|
||||
// Model an already used v1 trial: no banquet yet, edited prices, real balances and an order.
|
||||
const legacy=structuredClone(demoRow.payload);
|
||||
legacy.storage.sunBoxes.v=legacy.storage.sunBoxes.v.filter(b=>b.category!==6);
|
||||
legacy.storage.sunBoxes.v=legacy.storage.sunBoxes.v.filter(b=>b.category===0);
|
||||
legacy.storage.sunBoxes.v[0].price=4321;
|
||||
legacy.storage.sunStock.v=legacy.storage.sunStock.v.filter(p=>!p.id.startsWith('demo-banquet-'));
|
||||
legacy.storage.sunStock.v=legacy.storage.sunStock.v.filter(p=>p.id.startsWith('demo-v1-stock-'));
|
||||
legacy.storage.sunStock.v[0].qty=7.123;
|
||||
delete legacy.storage.sunTrialDemoV1.v.banquetVersion;
|
||||
delete legacy.storage.sunTrialDemoV1.v.extrasVersion;
|
||||
legacy.storage.sunOrders={t:'j',v:[{id:91,event:'Keep my order',date:'2099-12-20',time:'12:00',status:'Новый',lines:[],total:4321,prepayment:100,balance:4221,autoCompletionDisabled:true}]};
|
||||
let upgraded=await one('select * from public.sun_save_app_state_v17($1,$2,$3,$4)',[foreign,legacy,'legacy-trial-fixture',demoRow.revision]);
|
||||
assert.equal((await one('select public.caterium_trial_demo_status($1) as s',[foreign])).s.canUpgrade,true);
|
||||
@ -84,18 +86,38 @@ upgraded=await one('select * from public.sun_fetch_app_state($1)',[foreign]);
|
||||
assert.deepEqual(upgraded.payload.storage.sunBoxes.v.slice(0,10),legacy.storage.sunBoxes.v);
|
||||
assert.deepEqual(upgraded.payload.storage.sunStock.v.slice(0,43),legacy.storage.sunStock.v);
|
||||
for(const key of ['sunOrders','sunStockMoves','sunSuppliers'])assert.deepEqual(upgraded.payload.storage[key],legacy.storage[key]);
|
||||
assert.equal(upgraded.payload.storage.sunBoxes.v.length,28);
|
||||
assert.equal(upgraded.payload.storage.sunBoxes.v.length,44);
|
||||
await q('select public.caterium_install_trial_banquet($1)',[foreign]);
|
||||
assert.equal((await one('select * from public.sun_fetch_app_state($1)',[foreign])).revision,upgraded.revision);
|
||||
// Existing custom drink/ware tabs must remain untouched, with no unused demo stock.
|
||||
const partial=structuredClone(upgraded.payload);
|
||||
partial.storage.sunBoxes.v=partial.storage.sunBoxes.v.filter(b=>!b.id.startsWith('demo-extras-'));
|
||||
partial.storage.sunStock.v=partial.storage.sunStock.v.filter(p=>!p.id.startsWith('demo-extras-'));
|
||||
delete partial.storage.sunTrialDemoV1.v.extrasVersion;
|
||||
const custom=[{id:'own-drink',name:'My drink',category:3,price:123,ingredients:[]},{id:'own-plate',name:'My plate',category:1,price:456,ingredients:[]}];
|
||||
partial.storage.sunBoxes.v.push(...custom);
|
||||
await q('select * from public.sun_save_app_state_v17($1,$2,$3,$4)',[foreign,partial,'partly-filled-trial',upgraded.revision]);
|
||||
await q('select public.caterium_install_trial_demo($1)',[foreign]);
|
||||
upgraded=await one('select * from public.sun_fetch_app_state($1)',[foreign]);
|
||||
assert.equal(upgraded.payload.storage.sunBoxes.v.length,38);
|
||||
assert.deepEqual(upgraded.payload.storage.sunBoxes.v.filter(b=>b.category===1||b.category===3),custom);
|
||||
assert.equal(upgraded.payload.storage.sunStock.v.length,55);
|
||||
assert.deepEqual(upgraded.payload.storage.sunBoxes.v.slice(0,30),partial.storage.sunBoxes.v);
|
||||
assert.deepEqual(upgraded.payload.storage.sunStock.v.slice(0,52),partial.storage.sunStock.v);
|
||||
assert.equal(upgraded.payload.storage.sunTrialDemoV1.v.banquetInstalledAt,partial.storage.sunTrialDemoV1.v.banquetInstalledAt,'already upgraded banquet untouched');
|
||||
await q('select public.caterium_install_trial_extras($1)',[foreign]);
|
||||
assert.equal((await one('select * from public.sun_fetch_app_state($1)',[foreign])).revision,upgraded.revision);
|
||||
const filled=structuredClone(upgraded.payload);delete filled.storage.sunTrialDemoV1;
|
||||
await q('select * from public.sun_save_app_state_v17($1,$2,$3,$4)',[foreign,filled,'test-existing-catalog',upgraded.revision]);
|
||||
await denied('select public.caterium_install_trial_banquet($1)',[foreign],/Сначала установите/);
|
||||
await denied('select public.caterium_install_trial_extras($1)',[foreign],/Сначала установите/);
|
||||
await denied('select public.caterium_install_trial_demo($1)',[foreign],/уже заполнены/);
|
||||
assert.equal((await one('select public.caterium_my_sidebar_brand() as b')).b.variant,'caterium');
|
||||
await denied('select * from public.sun_fetch_app_state($1)',[ws],/Access denied/);
|
||||
await denied('select public.caterium_trial_demo_status($1)',[ws],/Access denied/);
|
||||
await denied('select public.caterium_install_trial_demo($1)',[ws],/Administrator membership/);
|
||||
await denied('select public.caterium_install_trial_banquet($1)',[ws],/Administrator membership/);
|
||||
await denied('select public.caterium_install_trial_extras($1)',[ws],/Administrator membership/);
|
||||
await denied('select * from public.sun_v17_clients_snapshot_v1773($1)',[ws],/Access denied/);
|
||||
await denied('select public.sun_dev_dashboard()',[],/administrator/);
|
||||
await actor(ids[3]);
|
||||
@ -112,6 +134,7 @@ await actor(ids[1]);
|
||||
assert.equal((await one('select public.caterium_trial_demo_status($1) as s',[ws])).s.eligible,false,'Solnce company excluded');
|
||||
await denied('select public.caterium_install_trial_demo($1)',[ws],/Демонстрационная база доступна/);
|
||||
await denied('select public.caterium_install_trial_banquet($1)',[ws],/Демонстрационная база доступна/);
|
||||
await denied('select public.caterium_install_trial_extras($1)',[ws],/Демонстрационная база доступна/);
|
||||
await db.exec('reset role');
|
||||
const devWs=(await one("insert into public.sun_workspaces(name,created_by) values('Own developer test',$1) returning id",[ids[0]])).id;
|
||||
await q("insert into public.sun_workspace_members(workspace_id,user_id,role,is_active,permissions) values($1,$2,'admin',true,public.sun_role_default_permissions('admin'))",[devWs,ids[0]]);
|
||||
@ -122,7 +145,8 @@ await actor(ids[0]);
|
||||
await q('select public.caterium_install_trial_demo($1)',[devWs]);
|
||||
const seeded=(await one('select * from public.sun_fetch_app_state($1)',[devWs])).payload.storage;
|
||||
assert.deepEqual(seeded.sunClientLoyaltyV1,preserved.storage.sunClientLoyaltyV1);assert.deepEqual(seeded.sunCompanyProfileV1,preserved.storage.sunCompanyProfileV1);
|
||||
assert.equal(seeded.sunBoxes.v.length,28,'developer own company works without developer-panel MFA');
|
||||
assert.equal(seeded.sunBoxes.v.length,44,'developer own company works without developer-panel MFA');
|
||||
await db.exec('reset role; set role anon');await denied('select public.caterium_install_trial_extras($1)',[devWs],/permission denied/);await denied('select public.caterium_trial_extras_seed_v1()',[],/permission denied/);
|
||||
await db.exec('reset role; set role anon');await denied('select public.caterium_install_trial_banquet($1)',[devWs],/permission denied/);await denied('select public.caterium_trial_banquet_seed_v1()',[],/permission denied/);
|
||||
await db.exec('reset role; set role anon');await denied('select public.caterium_install_trial_demo($1)',[devWs],/permission denied/);await denied('select public.caterium_trial_demo_seed_v1()',[],/permission denied/);
|
||||
console.log('PASS demo: trial auto seed, 10 box + 18 banquet TTKs, stock links, additive upgrade preserving orders/prices/balances, idempotency, populated catalog guard, Solnce exclusion, own developer account and preserved clients');
|
||||
|
||||
@ -14,8 +14,8 @@ check(!index.includes('offer-gallery-data.js'),'blocking Base64 gallery absent')
|
||||
check((runtime.match(/\/Type \/Catalog/g)||[]).length===0,'runtime contains no PDF binary writer');
|
||||
check(read('core/pdf-engine.js').includes('595.28')&&read('core/pdf-engine.js').includes('841.89'),'PDF engine uses A4 MediaBox');
|
||||
check([...index.matchAll(/@page\{([^}]*)\}/g)].every(m=>/size:A4/i.test(m[1])),'compact @page rules use A4');
|
||||
check(sw.includes('v97-20260918-trial-banquet')&&sw.includes('data-layer-v1773.js')&&sw.includes('server-automation-v1770.js')&&sw.includes('offer-workspace-v1769.js'),'service worker cache is v17.7.3');
|
||||
check(index.includes('20260918-trial-banquet')&&index.includes('classic-offer-pdf-v1767.js')&&!index.includes('20260907-v17-6-0-stability-security'),'index cache-busting points to v17.7.3');
|
||||
check(sw.includes('v98-20260918-trial-extras')&&sw.includes('data-layer-v1773.js')&&sw.includes('server-automation-v1770.js')&&sw.includes('offer-workspace-v1769.js'),'service worker cache is v17.7.3');
|
||||
check(index.includes('20260918-trial-extras')&&index.includes('classic-offer-pdf-v1767.js')&&!index.includes('20260907-v17-6-0-stability-security'),'index cache-busting points to v17.7.3');
|
||||
check(performance.includes('SunAttachmentGuard')&&performance.includes('TARGET=2*1024*1024'),'chat photo auto-compression is versioned');
|
||||
check(performance.includes("rpc('sun_dev_dashboard')")&&performance.includes('server_size')&&performance.includes('storage_size'),'Developer Console server/storage counters are versioned');
|
||||
check(performance.includes('MEMORY_REFRESH_MS=30000')&&performance.includes('MEMORY_TIMEOUT_MS=8000')&&performance.includes('memoryPromise'),'Developer Console memory refresh is bounded');
|
||||
@ -39,7 +39,7 @@ check(!/sb_secret_[A-Za-z0-9_-]{20,}|service_role\s*[:=]\s*["'][A-Za-z0-9._-]{30
|
||||
check(lock.version===pkg.version&&lock.packages?.['']?.version===pkg.version,'package.json and package-lock.json versions match');
|
||||
check(releaseManifest.version===`v${pkg.version}`,'release manifest version matches package.json');
|
||||
check(releaseManifest.channel==='production','release manifest channel is production');
|
||||
check(String(releaseManifest.pwaCache||'').includes('v97-20260918-trial-banquet'),'release manifest points to current PWA cache');
|
||||
check(String(releaseManifest.pwaCache||'').includes('v98-20260918-trial-extras'),'release manifest points to current PWA cache');
|
||||
check(['17.6.2','17.6.3','17.6.4','17.6.5','17.6.6','17.6.7','17.6.8','17.6.9','17.7.0','17.7.1','17.7.2','17.7.3'].every(v=>fs.existsSync(path.join(root,`docs/releases/V${v}-CHANGES.txt`))),'release notes exist through v17.7.3');
|
||||
check(runtime.includes('CLOUD_RPC_TIMEOUT_MS=45000')&&runtime.includes('CLOUD_CONFLICT_MAX_RETRIES=4')&&runtime.includes('retryCount'),'cloud sync has timeout and capped exponential conflict retries');
|
||||
check(runtime.includes("const VERSION = '17.7.3'")&&runtime.includes('ERROR_DEDUPE_MS=5*60*1000')&&runtime.includes('mirrorBusy=false')&&runtime.includes('backupBusy=false'),'stability logger uses current version, dedupe and single-flight guards');
|
||||
@ -65,8 +65,8 @@ check(offerWorkspace.includes('PDF и предпросмотр')&&offerWorkspace
|
||||
check(offerWorkspace.includes('SunClassicOfferPDFV1767')&&offerWorkspace.includes('finalGallery=galleryFor'),'custom gallery is injected into PDF renderer');
|
||||
check(releaseManifest.offerWorkspaceTabs===true&&releaseManifest.offerTemplatesSeparateTab===true&&releaseManifest.offerTwoCustomGalleryPhotos===true,'release manifest records offer workspace changes');
|
||||
check(pkg.version==='17.7.3','package version is v17.7.3');
|
||||
check(index.includes('20260918-trial-banquet'),'index cache bust is v17.7.3');
|
||||
check(sw.includes('v97-20260918-trial-banquet')&&sw.includes('data-layer-v1773.js')&&sw.includes('server-automation-v1770.js'),'PWA caches v17.7.3 client foundation modules');
|
||||
check(index.includes('20260918-trial-extras'),'index cache bust is v17.7.3');
|
||||
check(sw.includes('v98-20260918-trial-extras')&&sw.includes('data-layer-v1773.js')&&sw.includes('server-automation-v1770.js'),'PWA caches v17.7.3 client foundation modules');
|
||||
check(fs.existsSync(path.join(root,'public/core/data-layer-v1773.js'))&&fs.existsSync(path.join(root,'public/core/server-automation-v1770.js')),'data layer and server automation modules exist');
|
||||
check(ux.includes('CateriumServerAutomationV1770?.enabled'),'cloud browser auto completion is disabled when server automation is active');
|
||||
check(runtime.includes("const VERSION = '17.7.3'")&&runtime.includes("v17.7.3 Clients Server Read"),'stability logger reports v17.7.3');
|
||||
|
||||
@ -28,8 +28,8 @@ if(current!==113)fail(`current catalog photo count ${current}, expected 113`);el
|
||||
if(legacyCount!==60)fail(`legacy catalog photo count ${legacyCount}, expected 60`);else ok('60 legacy catalog photos');
|
||||
const gallery=fs.readdirSync(path.join(pub,'offer-gallery')).filter(x=>/\.jpg$/i.test(x));
|
||||
if(gallery.length!==2)fail(`offer gallery contains ${gallery.length} jpg files, expected 2`);else ok('offer gallery trimmed');
|
||||
if(!sw.includes('20260918-trial-banquet')||!sw.includes('login-signature-v1776.js')||!sw.includes('data-layer-v1773.js')||!sw.includes('server-automation-v1770.js')||!sw.includes('offer-workspace-v1769.js')||sw.includes('offer-gallery-data.js'))fail('service worker cache is stale');else ok('PWA cache updated for login refresh');
|
||||
if(html.includes('20260907-v17-6-0-stability-security')||html.includes('20260909-v17-7-3-clients-server-read')||!html.includes('20260918-trial-banquet')||!html.includes('classic-offer-pdf-v1767.js'))fail('index still serves stale core asset version');else ok('index cache-busting is current');
|
||||
if(!sw.includes('20260918-trial-extras')||!sw.includes('login-signature-v1776.js')||!sw.includes('data-layer-v1773.js')||!sw.includes('server-automation-v1770.js')||!sw.includes('offer-workspace-v1769.js')||sw.includes('offer-gallery-data.js'))fail('service worker cache is stale');else ok('PWA cache updated for login refresh');
|
||||
if(html.includes('20260907-v17-6-0-stability-security')||html.includes('20260909-v17-7-3-clients-server-read')||!html.includes('20260918-trial-extras')||!html.includes('classic-offer-pdf-v1767.js'))fail('index still serves stale core asset version');else ok('index cache-busting is current');
|
||||
if(!performance.includes('SunAttachmentGuard')||!performance.includes('MAX_SIDE=2048'))fail('chat photo compression guard missing');else ok('chat photo compression guard present');
|
||||
if(!performance.includes("rpc('sun_dev_dashboard')")||!performance.includes('storage_size')||!performance.includes('server_size'))fail('Developer Console memory counters missing');else ok('Developer Console memory counters present');
|
||||
if(performance.includes('records.forEach(r=>r.addedNodes.forEach(n=>{if(n.nodeType===1)scan(n)}));enhanceDeveloperMemory()'))fail('Developer Console memory refresh is still coupled to MutationObserver');else ok('Developer Console memory refresh loop removed');
|
||||
|
||||
@ -2,7 +2,8 @@ import {test,expect} from '@playwright/test';
|
||||
import fs from 'node:fs';
|
||||
import {demo} from '../ops/demo/trial-demo-data.mjs';
|
||||
import {banquetDemo} from '../ops/demo/trial-banquet-data.mjs';
|
||||
const completeDemo={...demo,boxes:[...demo.boxes,...banquetDemo.banquet],stock:[...demo.stock,...banquetDemo.stock]};
|
||||
import {extrasDemo} from '../ops/demo/trial-extras-data.mjs';
|
||||
const completeDemo={...demo,boxes:[...demo.boxes,...banquetDemo.banquet,...extrasDemo.items],stock:[...demo.stock,...banquetDemo.stock,...extrasDemo.stock]};
|
||||
|
||||
test('installed app serves old trial PNG links as cached lightweight images even offline',async({page,context})=>{
|
||||
await page.route('**://api.caterium.ru/**',r=>r.abort());await page.route('**://*.supabase.co/**',r=>r.abort());
|
||||
@ -87,6 +88,32 @@ test('trial banquet categories, per-guest TTK, shopping and stock work together'
|
||||
expect(await page.evaluate(()=>JSON.parse(localStorage.sunStock).find(p=>p.id==='demo-banquet-v1-stock-potato').qty)).toBeCloseTo(.32,3);
|
||||
});
|
||||
|
||||
test('trial extras populate every tab and supplies reach shopping and stock',async({page})=>{
|
||||
await ready(page);
|
||||
for(const [label,count] of [['Премиум',3],['Напитки',4],['Посуда',4],['Дополнения',3],['Доставка',2]]){
|
||||
await page.getByRole('button',{name:label,exact:true}).click();
|
||||
await expect(page.locator('#tiles .tile:not(.add)')).toHaveCount(count);
|
||||
}
|
||||
await page.getByRole('button',{name:'Напитки',exact:true}).click();
|
||||
await page.locator('#tiles .tile').filter({hasText:'Вода негазированная'}).click();
|
||||
await page.locator('#tiles .tile').filter({hasText:'Вода негазированная'}).click();
|
||||
await page.getByRole('button',{name:'Посуда',exact:true}).click();
|
||||
await page.locator('#tiles .tile').filter({hasText:'Тарелка одноразовая'}).click();
|
||||
await page.getByRole('button',{name:'Доставка',exact:true}).click();
|
||||
await page.locator('#tiles .tile').filter({hasText:'Доставка по городу'}).click();
|
||||
expect(await page.evaluate(()=>window.sunBaseOrderTotal(draft))).toBe(805);
|
||||
await page.evaluate(()=>{const day=new Date();day.setDate(day.getDate()+1);draft.event='Демо с напитками';draft.date=`${day.getFullYear()}-${String(day.getMonth()+1).padStart(2,'0')}-${String(day.getDate()).padStart(2,'0')}`;draft.time='12:00';draft.autoCompletionDisabled=true;document.getElementById('date').value=draft.date;document.getElementById('time').value=draft.time;window.saveOrder()});
|
||||
await page.getByRole('button',{name:'Закупки',exact:true}).first().click();
|
||||
await expect(page.locator('[data-shop-key="демо: вода негазированная 0,5 л|шт."]')).toContainText('2');
|
||||
await expect(page.locator('[data-shop-key="демо: тарелка одноразовая|шт."]')).toContainText('1');
|
||||
expect((await page.locator('[data-shop-key]').allTextContents()).every(text=>!text.includes('Доставка по городу'))).toBe(true);
|
||||
await page.getByRole('button',{name:'Склад',exact:true}).first().click();
|
||||
await page.evaluate(()=>window.stockMovement('demo-extras-v1-stock-water','in'));
|
||||
await page.locator('#stockMoveQty').fill('5');await page.locator('#stockSaveMove').click();
|
||||
await page.evaluate(()=>window.writeOffOrderStock(orders.at(-1).id));
|
||||
expect(await page.evaluate(()=>JSON.parse(localStorage.sunStock).find(p=>p.id==='demo-extras-v1-stock-water').qty)).toBe(3);
|
||||
});
|
||||
|
||||
test('owner onboarding preserves server demo when saving the company profile',async({page})=>{
|
||||
await page.route('**/index.html',r=>r.fulfill({contentType:'text/html',body:'<!doctype html><html><body></body></html>'}));await page.goto('/index.html');
|
||||
await page.addScriptTag({url:'/core/sun-safe.js'});await page.addScriptTag({url:'/core/cloud-transport.js'});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user