From 48ffa51748362dfce5818cdccbccb27073e6fc49 Mon Sep 17 00:00:00 2001 From: pavlov346346-source Date: Thu, 17 Sep 2026 20:15:32 +0300 Subject: [PATCH] Preserve imported order history and saved order pricing --- docs/ORDER-HISTORY-IMPORT-20260917.md | 7 +++ docs/release-manifest.json | 2 +- package.json | 2 +- public/app-runtime.js | 7 +-- public/core/import-archive.js | 29 ++++++++++++ public/core/order-enhancements-v1775.js | 4 +- public/core/performance.js | 2 +- public/core/ux-fixes-v1764.js | 2 +- public/index.html | 16 ++----- public/service-worker.js | 8 ++-- .../20260917221500_import_order_history.sql | 46 +++++++++++++++++++ tests/order-import.spec.mjs | 33 +++++++++++++ tests/playwright.config.mjs | 2 +- tests/recovery/smoke.mjs | 4 ++ tests/release-check.mjs | 10 ++-- tests/static-security.mjs | 4 +- 16 files changed, 146 insertions(+), 32 deletions(-) create mode 100644 docs/ORDER-HISTORY-IMPORT-20260917.md create mode 100644 public/core/import-archive.js create mode 100644 supabase/migrations/20260917221500_import_order_history.sql create mode 100644 tests/order-import.spec.mjs diff --git a/docs/ORDER-HISTORY-IMPORT-20260917.md b/docs/ORDER-HISTORY-IMPORT-20260917.md new file mode 100644 index 0000000..b093149 --- /dev/null +++ b/docs/ORDER-HISTORY-IMPORT-20260917.md @@ -0,0 +1,7 @@ +# Preserving imported order history + +Imported orders can carry `autoCompletionDisabled: true`. Both server and local automation leave their status, prepayment and balance unchanged, so old dates cannot manufacture full payment. The import archive is a typed workspace storage value (`sunTelegramImportArchiveV1`); reads require both order and customer viewing permissions. The Orders screen exposes searchable original messages and images using text-only DOM rendering and validated image data URLs. Customer records and source files remain outside this repository. + +Opening an order now hydrates its delivery and discount controls before reading them back. The late order-pane patch retains shared pricing, and order enhancements use its item subtotal plus delivery once. Saved historical prices and promotion discounts survive open/save. + +Validation: deployment checks and database permission tests; 66 browser checks passed, 2 existing skips. A private rehearsal round-tripped the import, confirmed idempotency and payment preservation, and opened/saved every imported order while comparing names, phone numbers, addresses, notes, prices, payment and line counts. diff --git a/docs/release-manifest.json b/docs/release-manifest.json index 1039a36..1ec8f32 100644 --- a/docs/release-manifest.json +++ b/docs/release-manifest.json @@ -11,7 +11,7 @@ "serverReady": true, "workspaceAutoDiscovery": true, "invitesTemporarilyDisabled": false, - "pwaCache": "v84-20260917-company-branding", + "pwaCache": "v85-20260917-order-import", "fullOfferDescriptions": true, "dynamicOfferRows": true, "pdfOfferDescriptionFix": true, diff --git a/package.json b/package.json index 92a7cbd..50b8e13 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "version": "17.7.3", "type": "module", "scripts": { - "check:syntax": "node --check public/app-runtime.js && node --check public/service-worker.js && node --check public/legacy/bootstrap.js && node --check public/core/sun-safe.js && node --check public/core/account-center-v1780.js && node --check public/core/performance.js && node --check public/core/auth-security-v1774.js && node --check public/core/trial-promo-developer-v181.js && node --check public/core/order-enhancements-v1775.js && node --check public/core/data-layer-v1773.js && node --check public/core/server-automation-v1770.js && node --check public/core/hotfix-v1763.js && node --check public/core/ops-ux-v1762.js && node --check public/core/ux-fixes-v1764.js && node --check public/core/pdf-engine.js && node --check public/core/classic-offer-pdf-v1767.js && node --check public/core/signature-offer-pdf-v18.js && node --check public/core/developer-console-v1768.js && node --check public/core/offer-workspace-v1769.js && node --check public/core/brand-theme.js && node --check public/core/company-branding.js", + "check:syntax": "node --check public/app-runtime.js && node --check public/service-worker.js && node --check public/legacy/bootstrap.js && node --check public/core/sun-safe.js && node --check public/core/account-center-v1780.js && node --check public/core/performance.js && node --check public/core/auth-security-v1774.js && node --check public/core/trial-promo-developer-v181.js && node --check public/core/order-enhancements-v1775.js && node --check public/core/data-layer-v1773.js && node --check public/core/server-automation-v1770.js && node --check public/core/hotfix-v1763.js && node --check public/core/ops-ux-v1762.js && node --check public/core/ux-fixes-v1764.js && node --check public/core/pdf-engine.js && node --check public/core/classic-offer-pdf-v1767.js && node --check public/core/signature-offer-pdf-v18.js && node --check public/core/developer-console-v1768.js && node --check public/core/offer-workspace-v1769.js && node --check public/core/brand-theme.js && node --check public/core/company-branding.js && node --check public/core/import-archive.js", "test:static": "node tests/static-security.mjs && node tests/auth-security-v1774.mjs && node tests/employee-create-v1774.mjs && node tests/html-integrity-v1774.mjs && node tests/edge-security-v1774.mjs && node tests/branding-v1774.mjs && node tests/order-enhancements-v1775.mjs", "check:release": "node tests/release-check.mjs", "check:deploy": "npm run check:syntax && npm run test:static && npm run check:release && node tests/backend-cutover.mjs && npm run test:db", diff --git a/public/app-runtime.js b/public/app-runtime.js index 484d1de..b1dd43e 100644 --- a/public/app-runtime.js +++ b/public/app-runtime.js @@ -628,7 +628,9 @@ window.SUN_LEGACY_CATALOG_V175=[{"id":"1","name":"Фуршетный бокс updatePromoMeta(); } function updatePromoMeta(){const meta=$('promoCodeMeta'),select=$('promoCodeSelect');if(!meta||!select)return;const id=select.value;if(!id){meta.textContent='Выберите активный промокод из раздела «Рассылки».';return}if(id==='__snapshot__'){meta.textContent=`Условия сохранены в заказе: ${draft?.promoCode||''} · ${draft?.promoDiscountType==='amount'?money(draft?.promoDiscountValue||0):`${Number(draft?.promoDiscountValue||0).toLocaleString('ru-RU',{maximumFractionDigits:2})}%`}.`;return}const p=promoById(id);meta.textContent=p?`${p.code} · скидка ${promoDiscountText(p)} · действует ${p.startDate||'без даты'} — ${p.endDate||'без даты'}`:'Промокод недоступен.';} + let hydratingOrder=false; function captureDiscounts(){ + if(hydratingOrder)return; if(!draft)return;draft.manualDiscountValue=Math.max(0,Number($('manualDiscountValue')?.value||0));draft.manualDiscountType=$('manualDiscountType')?.value==='amount'?'amount':'percent'; const select=$('promoCodeSelect');if(!select)return;const id=String(select.value||''); if(!id){draft.promoId='';draft.promoCode='';draft.promoDiscountValue=0;draft.promoDiscountType='percent';draft.promoStartDate='';draft.promoEndDate='';return} @@ -636,7 +638,7 @@ window.SUN_LEGACY_CATALOG_V175=[{"id":"1","name":"Фуршетный бокс const p=promoById(id);if(!p||!promoActive(p)){renderPromoSelect();return} draft.promoId=String(p.id||'');draft.promoCode=String(p.code||'').trim();draft.promoDiscountValue=Math.max(0,Number(p.discountValue||0));draft.promoDiscountType=p.discountType==='amount'?'amount':'percent';draft.promoStartDate=String(p.startDate||'');draft.promoEndDate=String(p.endDate||'');updatePromoMeta(); } - function captureDelivery(){if(!draft)return;draft.deliveryCost=Math.max(0,Number($('deliveryCost')?.value||0));} + function captureDelivery(){if(!draft||hydratingOrder)return;draft.deliveryCost=Math.max(0,Number($('deliveryCost')?.value||0));} function syncDeliveryField(){ensureDiscountUI();if($('deliveryCost'))$('deliveryCost').value=Math.max(0,Number(draft?.deliveryCost||0));} function discountAmount(value,type,base){const n=Math.max(0,Number(value||0));return Math.min(base,type==='amount'?n:base*Math.min(100,n)/100)} function pricing(order=draft){const base=baseTotal(order),manual=discountAmount(order?.manualDiscountValue,order?.manualDiscountType,base),afterManual=Math.max(0,base-manual),promo=discountAmount(order?.promoDiscountValue,order?.promoDiscountType,afterManual),itemsTotal=Math.max(0,afterManual-promo),delivery=Math.max(0,Number(order?.deliveryCost||0)),total=itemsTotal+delivery,paid=Math.max(0,Number(order===draft?($('prepayment')?.value||order?.prepayment||0):(order?.prepayment||0))),balance=Math.max(0,total-paid);return {base,manual,promo,itemsTotal,delivery,total,paid,balance,discount:manual+promo}} @@ -685,7 +687,7 @@ window.SUN_LEGACY_CATALOG_V175=[{"id":"1","name":"Фуршетный бокс const previousSave=window.saveOrder; if(typeof previousSave==='function')window.saveOrder=function(...args){captureBuyerFromFields();captureDiscounts();captureDelivery();const result=previousSave.apply(this,args);renderPromoStats();return result}; const previousOpen=window.openOrder; - if(typeof previousOpen==='function')window.openOrder=function(...args){const r=previousOpen.apply(this,args);setTimeout(()=>{syncBuyerFields();syncDiscountFields();syncDeliveryField();setOrderPane('order');patchDocButtons();renderCatalogV5();},0);return r}; + if(typeof previousOpen==='function')window.openOrder=function(...args){hydratingOrder=true;let r;try{r=previousOpen.apply(this,args)}catch(error){hydratingOrder=false;throw error}setTimeout(()=>{try{syncBuyerFields();syncDiscountFields();syncDeliveryField();setOrderPane('order');patchDocButtons();renderCatalogV5()}finally{hydratingOrder=false;updateOrderSummary()}},0);return r}; const previousReset=window.resetDraft; if(typeof previousReset==='function')window.resetDraft=function(...args){const r=previousReset.apply(this,args);draft.buyerRequisites=buyerDefaults();draft.manualDiscountValue=0;draft.manualDiscountType='percent';draft.promoId='';draft.promoCode='';draft.promoDiscountValue=0;draft.promoDiscountType='percent';draft.promoStartDate='';draft.promoEndDate='';draft.deliveryCost=0;delete draft.qrImage;setTimeout(()=>{syncBuyerFields();syncDiscountFields();syncDeliveryField();setOrderPane('order');renderCatalogV5();},0);return r}; const previousStats=window.renderStats; @@ -5738,4 +5740,3 @@ window.SUN_LEGACY_CATALOG_V175=[{"id":"1","name":"Фуршетный бокс window.SunChatV31=window.SunChatV29; if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',boot,{once:true});else boot(); })(); - diff --git a/public/core/import-archive.js b/public/core/import-archive.js new file mode 100644 index 0000000..50a4036 --- /dev/null +++ b/public/core/import-archive.js @@ -0,0 +1,29 @@ +(()=>{ + 'use strict'; + if(window.CateriumImportArchive)return; + const KEY='sunTelegramImportArchiveV1'; + let dialog=null; + const read=()=>{try{return JSON.parse(localStorage.getItem(KEY)||'null')}catch(_){return null}}; + const node=(tag,text)=>{const e=document.createElement(tag);if(text!==undefined)e.textContent=String(text);return e}; + function close(){dialog?.remove();dialog=null} + function show(){ + const archive=read();if(!archive)return;close(); + dialog=node('dialog');dialog.setAttribute('aria-label','Архив заказов из Telegram');dialog.style.cssText='width:min(900px,94vw);max-height:88vh;overflow:auto;border:1px solid #ccd2d6;border-radius:16px;padding:24px'; + const top=node('div');top.style.cssText='display:flex;justify-content:space-between;gap:20px';top.append(node('h2','Исходный архив Telegram'));const exit=node('button','Закрыть');exit.onclick=close;top.append(exit);dialog.append(top); + dialog.append(node('p',`${archive.orderCount} заказов · ${archive.clientCount} клиентов. Цены и платежи сохранены по исходным сообщениям. Неуказанная оплата не подтверждает долг. Автоматическое закрытие импортированных заказов отключено.`)); + const search=node('input');search.type='search';search.placeholder='Номер заказа, имя, телефон или адрес';search.setAttribute('aria-label','Поиск в архиве');search.style.cssText='width:100%;padding:12px;margin:8px 0';dialog.append(search); + const list=node('div');dialog.append(list); + const render=()=>{list.replaceChildren();const q=search.value.toLocaleLowerCase('ru');for(const message of archive.messages||[]){if(q&&!`${message.id} ${message.text} ${message.transcription||''}`.toLocaleLowerCase('ru').includes(q))continue;const d=node('details');d.style.cssText='padding:10px 0;border-bottom:1px solid #ddd';d.append(node('summary',`${message.sentAt} · ${message.id} · ${(message.text||'Фотография').split('\n')[0]}`));const pre=node('pre',message.text);pre.style.cssText='white-space:pre-wrap;overflow-wrap:anywhere;font:inherit';d.append(pre);if(message.transcription)d.append(node('p',message.transcription));for(const a of archive.attachments||[]){if(a.messageId!==message.id||!/^data:image\/(?:jpeg|png|webp);base64,[A-Za-z0-9+/=]+$/.test(a.data||''))continue;const img=node('img');img.src=a.data;img.alt=a.href;img.loading='lazy';img.style.cssText='display:block;max-width:100%;max-height:80vh;object-fit:contain';d.append(img);}list.append(d);}}; + search.oninput=render;render();document.body.append(dialog);dialog.showModal();dialog.addEventListener('close',close); + } + function install(){ + const archive=read(),ordersView=document.getElementById('orders');let b=document.getElementById('cateriumTelegramArchive'); + if(!archive){b?.remove();return}if(!ordersView||b)return; + b=node('button','Исходный архив Telegram');b.id='cateriumTelegramArchive';b.type='button';b.className='outline';b.onclick=show; + const anchor=ordersView.querySelector('h1');if(anchor)anchor.insertAdjacentElement('afterend',b);else ordersView.prepend(b); + } + window.CateriumImportArchive={show,refresh:install}; + window.addEventListener('sun:cloud-state-applied',()=>{close();install()}); + window.addEventListener('sun:cloud-permissions-changed',()=>{close();install()}); + if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',install,{once:true});else install(); +})(); diff --git a/public/core/order-enhancements-v1775.js b/public/core/order-enhancements-v1775.js index 472e479..8b514a0 100644 --- a/public/core/order-enhancements-v1775.js +++ b/public/core/order-enhancements-v1775.js @@ -9,11 +9,11 @@ function activeLines(){return Array.isArray(draft?.lines)?draft.lines.filter(l=> function comments(){let rows=lineRows(),ls=activeLines();rows.forEach((row,i)=>{let l=ls[i];if(!l)return;let w=$('.sun-line-comment-v1775',row);if(!w){w=document.createElement('div');w.className='sun-line-comment-v1775';w.innerHTML='
';row.appendChild(w)}let btn=$('.sun-line-comment-btn-v1775',w),editor=$('.sun-line-comment-editor-v1775',w),input=$('input',w),save=$('.sun-line-comment-save-v1775',w);let current=safe(l.comment);btn.classList.toggle('has-comment',!!current);btn.textContent=current?'💬 Комментарий добавлен':'💬 Оставить комментарий';if(document.activeElement!==input&&input.value!==current)input.value=current;if(!btn.dataset.bound){btn.dataset.bound='1';btn.addEventListener('click',()=>{editor.classList.toggle('on');if(editor.classList.contains('on')){input.value=safe(l.comment);setTimeout(()=>input.focus(),0)}});input.addEventListener('input',()=>{l.comment=input.value.slice(0,300)});input.addEventListener('keydown',e=>{if(e.key==='Enter'){e.preventDefault();save.click()}});save.addEventListener('click',()=>{l.comment=input.value.trim().slice(0,300);btn.classList.toggle('has-comment',!!l.comment);btn.textContent=l.comment?'💬 Комментарий добавлен':'💬 Оставить комментарий';editor.classList.remove('on')})}})} function searchBox(){let catalog=$('#new .catalog');if(!catalog||$('#sunCatalogSearchV1775'))return;let i=document.createElement('input');i.id='sunCatalogSearchV1775';i.type='search';i.placeholder='Поиск по № бокса или названию';($('.catalog-head',catalog)||catalog.firstElementChild)?.insertAdjacentElement('afterend',i);i.addEventListener('input',()=>{let q=safe(i.value).toLowerCase().replace(/ё/g,'е');$$('#tiles .tile').forEach(t=>{let text=safe(t.textContent).toLowerCase().replace(/ё/g,'е');t.hidden=t.classList.contains('add')?!!q:!!(q&&!text.includes(q))})})} function delivery(){return Math.max(0,Number($('#orderDeliveryCost')?.value||draft?.costs?.delivery||draft?.deliveryCost||0))} -function financial(){let positions=Math.max(0,Number(typeof calcOrderTotal==='function'?calcOrderTotal():0)),d=delivery(),paid=Math.max(0,Number($('#prepayment')?.value||draft?.prepayment||0)),total=positions+d;return{positions,d,total,paid,balance:Math.max(0,total-paid)}} +function financial(){const p=window.sunOrderPricing?.(draft);let d=p?Number(p.delivery||0):delivery(),positions=p?Number(p.itemsTotal||0):Math.max(0,Number(typeof calcOrderTotal==='function'?calcOrderTotal():0)),paid=Math.max(0,Number($('#prepayment')?.value||draft?.prepayment||0)),total=positions+d;return{positions,d,total,paid,balance:Math.max(0,total-paid)}} function summary(){let h=$('.order-summary');if(!h)return;let f=financial();if($('#summaryTotal'))$('#summaryTotal').textContent=fmt(f.positions);let d=$('#sunSummaryDeliveryV1775');if(!d){d=document.createElement('p');d.id='sunSummaryDeliveryV1775';h.appendChild(d)}d.innerHTML=`Доставка${fmt(f.d)}`;let t=$('#sunSummaryGrandV1775');if(!t){t=document.createElement('p');t.id='sunSummaryGrandV1775';t.className='sun-summary-grand-v1775';h.appendChild(t)}t.innerHTML=`Итого${fmt(f.total)}`;if($('#orderTotal'))$('#orderTotal').value=Math.round(f.total);if($('#balance'))$('#balance').value=Math.round(f.balance);if($('#summaryBalance'))$('#summaryBalance').textContent=fmt(f.balance);draft.total=f.total;draft.balance=f.balance} function post(){styles();searchBox();comments();summary()} function wrap(n){let old=window[n];if(typeof old!=='function'||old.__commentsV1775)return;let fn=function(...a){let r=old.apply(this,a);queueMicrotask(post);setTimeout(post,50);return r};fn.__commentsV1775=true;window[n]=fn} styles();['render','openOrder','resetDraft','updateOrderSummary','saveOrder'].forEach(wrap);post(); let timer=0;new MutationObserver(ms=>{if(!ms.some(m=>m.addedNodes.length||m.removedNodes.length))return;clearTimeout(timer);timer=setTimeout(()=>{comments();searchBox()},30)}).observe(document.body,{childList:true,subtree:true}); document.addEventListener('input',e=>{if(e.target?.id==='orderDeliveryCost'||e.target?.id==='prepayment')summary()},true); -})(); \ No newline at end of file +})(); diff --git a/public/core/performance.js b/public/core/performance.js index 70df1c6..4666ce9 100644 --- a/public/core/performance.js +++ b/public/core/performance.js @@ -1,7 +1,7 @@ (()=>{ 'use strict'; const VERSION='17.7.3'; - const RELEASE='20260916-v18-1-1-template-whitelist-fix'; + const RELEASE='20260917-order-import'; 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(){ diff --git a/public/core/ux-fixes-v1764.js b/public/core/ux-fixes-v1764.js index e0378ff..76c525b 100644 --- a/public/core/ux-fixes-v1764.js +++ b/public/core/ux-fixes-v1764.js @@ -76,7 +76,7 @@ if(!canAutoWrite())return {changed:0,ids:[]}; const list=readOrders(),ids=[]; for(const order of list){ - if(!order||String(order.status||'').trim()==='Отменён'||isAutoCompleted(order))continue; + if(!order||order.autoCompletionDisabled===true||String(order.status||'').trim()==='Отменён'||isAutoCompleted(order))continue; const due=dueAt(order);if(!Number.isFinite(due)||nowMs - + diff --git a/public/service-worker.js b/public/service-worker.js index 52b2a34..5c7f0fd 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,7 +1,7 @@ -const CACHE='sun-catering-pwa-v84-20260917-company-branding'; -const VERSION='20260917-company-branding'; +const CACHE='sun-catering-pwa-v85-20260917-order-import'; +const VERSION='20260917-order-import'; const CORE=[ - './','./index.html',`./core/company-branding.js?v=${VERSION}`,`./core/signature-offer-pdf-v18.js?v=${VERSION}`,`./core/brand-theme.js?v=${VERSION}`, + './','./index.html',`./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/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}`, './offer-gallery/001.jpg','./offer-gallery/002.jpg', './catalog/001.jpg','./catalog/002.jpg','./catalog/003.jpg', @@ -9,7 +9,7 @@ const CORE=[ './offer-templates/thumb-light.jpg','./offer-templates/thumb-editorial-grid.jpg','./offer-templates/thumb-midnight-glass.jpg','./offer-templates/thumb-emerald-gold.jpg' ]; const CRITICAL_FRESH=new Set([ - '/core/company-branding.js','/core/brand-theme.js','/core/sun-safe.js','/core/performance.js','/core/account-center-v1780.js','/core/login-signature-v1776.js','/core/auth-security-v1774.js','/legacy/bootstrap.js','/app-runtime.js' + '/core/import-archive.js','/core/company-branding.js','/core/brand-theme.js','/core/sun-safe.js','/core/performance.js','/core/account-center-v1780.js','/core/login-signature-v1776.js','/core/auth-security-v1774.js','/legacy/bootstrap.js','/app-runtime.js' ]); self.addEventListener('install',event=>{ event.waitUntil(caches.open(CACHE).then(cache=>cache.addAll(CORE)).then(()=>self.skipWaiting())); diff --git a/supabase/migrations/20260917221500_import_order_history.sql b/supabase/migrations/20260917221500_import_order_history.sql new file mode 100644 index 0000000..3d1d909 --- /dev/null +++ b/supabase/migrations/20260917221500_import_order_history.sql @@ -0,0 +1,46 @@ +-- Imported history must preserve unknown or partial payments. +create or replace function public.sun_process_due_orders_internal(p_workspace uuid,p_now timestamptz default now()) +returns jsonb +language plpgsql +security definer +set search_path='public' as $$ +declare v_tz text; v_changed integer:=0; v_ids text[]:=array[]::text[]; r record; v_total numeric; v_iso text; v_orders jsonb; +begin + select timezone into v_tz from public.sun_workspaces where id=p_workspace; + if v_tz is null then return jsonb_build_object('changed',0,'ids','[]'::jsonb); end if; + v_iso:=to_char(p_now at time zone 'UTC','YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'); + for r in + select order_id,data,version from public.sun_v17_orders + where workspace_id=p_workspace + and lower(coalesce(data->>'status','')) not in ('отменён','отменен','отдан заказчику','завершён','завершен') + and coalesce(data->>'sunAutoCompletedAt','')='' + and coalesce(data->>'autoCompletionDisabled','false') <> 'true' + and public.sun_order_due_at(data,v_tz) is not null + and public.sun_order_due_at(data,v_tz) <= p_now + for update + loop + v_total:=case when coalesce(r.data->>'total','') ~ '^-?\d+(\.\d+)?$' then greatest(0,(r.data->>'total')::numeric) else 0 end; + update public.sun_v17_orders + set data=r.data||jsonb_build_object('prepayment',v_total,'balance',0,'status','Отдан заказчику','paymentStatus','paid','completedAt',coalesce(nullif(r.data->>'completedAt',''),v_iso),'paymentCompletedAt',coalesce(nullif(r.data->>'paymentCompletedAt',''),v_iso),'sunAutoCompletedAt',v_iso,'sunAutoCompletedV1770',true,'sunAutoCompletedVersion','17.7.0'),version=version+1,updated_at=p_now,updated_by=null + where workspace_id=p_workspace and order_id=r.order_id; + insert into public.sun_v17_change_events(workspace_id,entity,entity_key,operation,version,client_id,created_by) + values(p_workspace,'order',r.order_id,'upsert',r.version+1,'server:auto-complete',null); + v_changed:=v_changed+1;v_ids:=array_append(v_ids,r.order_id); + end loop; + if v_changed>0 then + select coalesce(jsonb_agg(o.data order by case when o.order_id ~ '^\d+$' then o.order_id::bigint else null end,o.order_id),'[]'::jsonb) into v_orders from public.sun_v17_orders o where o.workspace_id=p_workspace; + update public.sun_app_state s set payload=jsonb_set(coalesce(s.payload,'{}'::jsonb),'{storage,sunOrders}',jsonb_build_object('t','j','v',v_orders),true),revision=s.revision+1,updated_at=p_now,client_id='server:auto-complete' where s.workspace_id=p_workspace; + if not found then insert into public.sun_app_state(workspace_id,payload,revision,updated_at,client_id) values(p_workspace,jsonb_build_object('format','sun-cloud-v2','version',2,'storage',jsonb_build_object('sunOrders',jsonb_build_object('t','j','v',v_orders))),1,p_now,'server:auto-complete'); end if; + end if; + return jsonb_build_object('changed',v_changed,'ids',to_jsonb(v_ids),'workspace',p_workspace,'timezone',v_tz,'processed_at',v_iso); +end;$$; + +-- The archive contains both order and customer data. +do $migration$ +declare definition text; +begin + definition:=pg_get_functiondef('public.sun_can_read_storage_key(uuid,text)'::regprocedure); + definition:=replace(definition, 'when p_key=''sunOrders'' then', 'when p_key=''sunTelegramImportArchiveV1'' then public.sun_has_permission(p_workspace,''orders.view'') and public.sun_has_permission(p_workspace,''clients.view'') + when p_key=''sunOrders'' then'); + execute definition; +end $migration$; diff --git a/tests/order-import.spec.mjs b/tests/order-import.spec.mjs new file mode 100644 index 0000000..fe08926 --- /dev/null +++ b/tests/order-import.spec.mjs @@ -0,0 +1,33 @@ +import {test,expect} from '@playwright/test'; + +test('imported history keeps partial payments and delivery is charged once after save',async({page})=>{ + page.on('dialog',d=>d.accept()); + await page.goto('/index.html',{waitUntil:'domcontentloaded'}); + await page.waitForFunction(()=>window.__cateriumOrderEnhancementsV1775&&window.SunUXFixV1764&&window.sunOrderPricing); + const result=await page.evaluate(async()=>{ + const item={id:'import-fixture',name:'Исходная позиция',price:9900,category:0};boxes.push(item); + const order={id:900001,event:'Исторический заказ',date:'2026-09-10',time:'12:00',contact:'Тест',phone:'',address:'',note:'Источник сохранён',status:'Новый',lines:[{id:item.id,qty:2,price:3500}],total:7500,deliveryCost:1500,promoCode:'TEST',promoDiscountValue:1000,promoDiscountType:'amount',prepayment:2000,autoCompletionDisabled:true}; + orders.push(order);persist();window.openOrder(order.id);await new Promise(r=>setTimeout(r,150)); + const shown=Number(document.getElementById('orderTotal').value); + window.saveOrder();await new Promise(r=>setTimeout(r,150)); + window.SunUXFixV1764.autoCompleteOrders(Date.parse('2030-01-01T12:00:00Z'),{silent:true}); + const saved=orders.find(x=>x.id===order.id); + return {shown,total:saved.total,prepayment:saved.prepayment,status:saved.status,flag:saved.autoCompletionDisabled,rows:document.querySelectorAll('.sun-order-line-row').length}; + }); + expect(result).toEqual({shown:7500,total:7500,prepayment:2000,status:'Новый',flag:true,rows:1}); +}); + +test('private import archive safely shows messages and clears on workspace change',async({page})=>{ + await page.route('**/index.html',r=>r.fulfill({contentType:'text/html',body:'

Заказы

'})); + await page.goto('/index.html'); + await page.evaluate(()=>localStorage.setItem('sunTelegramImportArchiveV1',JSON.stringify({orderCount:1,clientCount:1,messages:[{id:'message1',sentAt:'01.01.2026',text:'Заказ \nАдрес: тест'}],attachments:[]}))); + await page.addScriptTag({url:'/core/import-archive.js'}); + await page.getByRole('button',{name:'Исходный архив Telegram'}).click(); + await page.getByRole('searchbox').fill('Адрес'); + await page.locator('summary').click(); + await expect(page.locator('dialog pre')).toContainText('{localStorage.removeItem('sunTelegramImportArchiveV1');window.dispatchEvent(new Event('sun:cloud-state-applied'))}); + expect(await page.locator('dialog').count()).toBe(0); + expect(await page.locator('#cateriumTelegramArchive').count()).toBe(0); +}); diff --git a/tests/playwright.config.mjs b/tests/playwright.config.mjs index d6ebd0c..b007e96 100644 --- a/tests/playwright.config.mjs +++ b/tests/playwright.config.mjs @@ -2,7 +2,7 @@ import { defineConfig, devices } from '@playwright/test'; import {fileURLToPath} from 'node:url'; export default defineConfig({ testDir:'.', - testMatch:['app.spec.mjs','theme-startup.spec.mjs','company-branding.spec.mjs'], + testMatch:['app.spec.mjs','theme-startup.spec.mjs','company-branding.spec.mjs','order-import.spec.mjs'], timeout:30000, use:{baseURL:'http://127.0.0.1:4173'}, webServer:{command:'npx http-server public -p 4173 -c-1',cwd:fileURLToPath(new URL('../',import.meta.url)),port:4173,reuseExistingServer:true}, diff --git a/tests/recovery/smoke.mjs b/tests/recovery/smoke.mjs index 60b1b53..0c5d044 100644 --- a/tests/recovery/smoke.mjs +++ b/tests/recovery/smoke.mjs @@ -43,7 +43,11 @@ await q('select * from public.sun_v17_clients_snapshot_v1773($1)',[ws]); await q("select public.sun_v17_create_backup($1,'daily','test')",[ws]); assert.equal((await q('select * from public.sun_v17_list_backups($1)',[ws])).rows.length,1); await q('select public.sun_v17_prune_backups($1)',[ws]); +payload.storage.sunOrders.v.push({id:77,date:'2025-01-01',time:'12:00',status:'Новый',total:7500,prepayment:2000,balance:5500,autoCompletionDisabled:true}); +row=await one('select * from public.sun_save_app_state_v17($1,$2,$3,$4)',[ws,payload,'import-test',row.revision]); await q('select public.sun_run_order_automation($1)',[ws]); +const historical=(await one('select public.sun_v17_entity_snapshot($1) as s',[ws])).s.orders.find(o=>String(o.id)==='77').data; +assert.equal(historical.prepayment,2000);assert.equal(historical.status,'Новый');assert.equal(historical.balance,5500); await q('select public.caterium_account_snapshot($1)',[ws]); await q('select * from public.sun_list_workspace_members($1)',[ws]); await q("select public.sun_employee_prepare_v28($1,'test3@example.invalid','Viewer','viewer')",[ws]); diff --git a/tests/release-check.mjs b/tests/release-check.mjs index 84db0a9..b976ce0 100644 --- a/tests/release-check.mjs +++ b/tests/release-check.mjs @@ -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('v84-20260917-company-branding')&&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('20260917-company-branding')&&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('v85-20260917-order-import')&&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('20260917-order-import')&&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('v84-20260917-company-branding'),'release manifest points to current PWA cache'); +check(String(releaseManifest.pwaCache||'').includes('v85-20260917-order-import'),'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=12000')&&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('20260917-company-branding'),'index cache bust is v17.7.3'); -check(sw.includes('v84-20260917-company-branding')&&sw.includes('data-layer-v1773.js')&&sw.includes('server-automation-v1770.js'),'PWA caches v17.7.3 client foundation modules'); +check(index.includes('20260917-order-import'),'index cache bust is v17.7.3'); +check(sw.includes('v85-20260917-order-import')&&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'); diff --git a/tests/static-security.mjs b/tests/static-security.mjs index 60b4c01..699d357 100644 --- a/tests/static-security.mjs +++ b/tests/static-security.mjs @@ -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('20260917-company-branding')||!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('20260917-company-branding')||!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('20260917-order-import')||!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('20260917-order-import')||!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');