fix: invalidate preparation approval for changed portfolio products
This commit is contained in:
@@ -944,3 +944,10 @@ Taslak türü seçicisine erişilebilir ad eklendi. Üretim derlemesi geçti. Ö
|
|||||||
- Oturum geri yükleme sırasında 5xx, bozuk yanıt ve zaman aşımı saklı oturumu silmez. Açık 401/403 yetki reddi oturumu temizler. Yanıt beklerken yeni hesaba geçilmişse eski yanıt yeni hesabı değiştiremez. Sunucu tarafındaki yetkilendirme geçerliliğini korur.
|
- Oturum geri yükleme sırasında 5xx, bozuk yanıt ve zaman aşımı saklı oturumu silmez. Açık 401/403 yetki reddi oturumu temizler. Yanıt beklerken yeni hesaba geçilmişse eski yanıt yeni hesabı değiştiremez. Sunucu tarafındaki yetkilendirme geçerliliğini korur.
|
||||||
- 215 otomatik test geçti; yeni testler yapılandırma ayrımı, iptal, geçersiz giriş, yarım depolama, 401/403, 5xx/bozuk yanıt, hesap değişimi ve takılmış yanıt gövdesinin zaman aşımını kapsar. Üretim derlemesi geçti; büyük paket uyarısı sürüyor.
|
- 215 otomatik test geçti; yeni testler yapılandırma ayrımı, iptal, geçersiz giriş, yarım depolama, 401/403, 5xx/bozuk yanıt, hesap değişimi ve takılmış yanıt gövdesinin zaman aşımını kapsar. Üretim derlemesi geçti; büyük paket uyarısı sürüyor.
|
||||||
- Gerçek Google OAuth hesabıyla giriş, SDK yükleme hatasının tarayıcı kabulü ve tüm mobil giriş etkileşimleri henüz doğrulanmadı. Uygulama Google hesabı kullanıyor; bağımsız parola sıfırlama akışı bulunmuyor. Canlıya yayın yapılmadı.
|
- Gerçek Google OAuth hesabıyla giriş, SDK yükleme hatasının tarayıcı kabulü ve tüm mobil giriş etkileşimleri henüz doğrulanmadı. Uygulama Google hesabı kullanıyor; bağımsız parola sıfırlama akışı bulunmuyor. Canlıya yayın yapılmadı.
|
||||||
|
|
||||||
|
### Portföy değiştiğinde eski ürün onaylarının engellenmesi — 6 Eylül 2026
|
||||||
|
- Seçim anındaki portföy kopyası güncel kaydın kimliği, ürün grubu, marka, model, kapsam, ÜTS beyanı/açıklaması ve servis kapasitesiyle karşılaştırılır. Kayıt silinmişse, aynı kimlik birden fazla kayıtla eşleşiyorsa veya beyan değişmişse eski uygunluk işaretleri çıktıya izin vermez. Elle girilen kapsamın mevcut kontrol akışı korunur.
|
||||||
|
- Ürün kontrolünde değişiklik uyarısı ve güncel ürünü alıp kontrolleri sıfırlama düğmesi eklendi. Önceki kayıt otomatik değiştirilmez; kullanıcı güncel seçimi aldığında teknik kaynak, uygunluk ve dört sağlık kontrolü yeniden doldurulur. Hazırlık özeti aynı engeli gösterir.
|
||||||
|
- Bağlı portföy kullanan çıktı işlemi sunucudaki firma profilini yeniden okur. Excel oluşturmanın ardından portföy tekrar okunur; yerel profil değişimi de bekleyen çıktı işlemini geçersiz kılar. Profil isteği başarısızsa çıktı tamamlanmaz.
|
||||||
|
- 217 otomatik test ve üretim derlemesi geçti. Testler yedi beyan alanındaki değişiklikleri, silinmiş/belirsiz kimliği, eksik profil/kopyayı, ilgisiz metadata değişimini ve tamamen onaylı bir ürünün eski portföyle engellenmesini kapsar. Yeniden seçimden sonra beş uygunluk kontrolünün tekrar gerektiği doğrulandı.
|
||||||
|
- Yeni düğmenin tarayıcı etkileşimi ve iki gerçek oturumda profil değiştirirken çıktı denemesi henüz kabul edilmedi. Profil sunucudan okunduğu andaki durumu temsil eder; sunucu kilidi veya resmi ürün kayıt doğrulaması değildir. Canlıya yayın yapılmadı.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { preparationDeadlineProblem } from '../services/preparationDeadline';
|
|||||||
import { bidValidityForForm, bidTypeForForm } from '../services/bidTerms';
|
import { bidValidityForForm, bidTypeForForm } from '../services/bidTerms';
|
||||||
import { createBidWorkbook, downloadBidWorkbook } from '../services/bidWorkbook';
|
import { createBidWorkbook, downloadBidWorkbook } from '../services/bidWorkbook';
|
||||||
import { getToken } from '../services/auth';
|
import { getToken } from '../services/auth';
|
||||||
import { listDocuments, downloadDocument, validateDocumentTextReview } from '../services/account';
|
import { getAllUserData, listDocuments, downloadDocument, validateDocumentTextReview } from '../services/account';
|
||||||
import { tenderDateKey } from '../services/tenderCalendar';
|
import { tenderDateKey } from '../services/tenderCalendar';
|
||||||
import React, { useState, useMemo, useEffect, useRef, useId } from 'react';
|
import React, { useState, useMemo, useEffect, useRef, useId } from 'react';
|
||||||
import { bidRowTotal, validateBidRows, bidTotal, bidAmount } from '../services/bidValidation';
|
import { bidRowTotal, validateBidRows, bidTotal, bidAmount } from '../services/bidValidation';
|
||||||
@@ -115,7 +115,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender, initialFocu
|
|||||||
|
|
||||||
const kalemToplami = useMemo(() => bidTotal(satirlar), [satirlar]);
|
const kalemToplami = useMemo(() => bidTotal(satirlar), [satirlar]);
|
||||||
const latestDraft = useRef('');
|
const latestDraft = useRef('');
|
||||||
latestDraft.current = JSON.stringify([firma,teklifTuru,gecerlilikGun,kisim,goturuBedel,fiyatlar,sourceSnapshot,reviewedSource,checks,manualDocuments,partialBid,itemSource]);
|
latestDraft.current = JSON.stringify([firma,teklifTuru,gecerlilikGun,kisim,goturuBedel,fiyatlar,sourceSnapshot,reviewedSource,checks,manualDocuments,partialBid,itemSource,userProfile.scrapedCapabilities?.healthPortfolio]);
|
||||||
const mounted = useRef(true);
|
const mounted = useRef(true);
|
||||||
const printWindow = useRef(null);
|
const printWindow = useRef(null);
|
||||||
const [printing,setPrinting] = useState(false);
|
const [printing,setPrinting] = useState(false);
|
||||||
@@ -137,7 +137,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender, initialFocu
|
|||||||
const [deadlineNow,setDeadlineNow] = useState(() => new Date());
|
const [deadlineNow,setDeadlineNow] = useState(() => new Date());
|
||||||
useEffect(() => {const timer=setInterval(()=>setDeadlineNow(new Date()),30000);return ()=>clearInterval(timer);},[]);
|
useEffect(() => {const timer=setInterval(()=>setDeadlineNow(new Date()),30000);return ()=>clearInterval(timer);},[]);
|
||||||
const deadlineProblem = preparationDeadlineProblem(tender,deadlineNow);
|
const deadlineProblem = preparationDeadlineProblem(tender,deadlineNow);
|
||||||
const kontrolEksikleri = preparationBlockers(lotSelection.rows, documents, checks, {...vault,deadlineDate:tenderDateKey(tender),tenderIkn:tender.ikn});
|
const kontrolEksikleri = preparationBlockers(lotSelection.rows, documents, checks, {...vault,healthPortfolio:userProfile.scrapedCapabilities?.healthPortfolio || [],deadlineDate:tenderDateKey(tender),tenderIkn:tender.ikn});
|
||||||
const analizEksik = effectiveResults.sourceReview?.status === 'partial';
|
const analizEksik = effectiveResults.sourceReview?.status === 'partial';
|
||||||
const hazir = !deadlineProblem && eksikler.length === 0 && toplam > 0 && !kalemEksik && !turCeliskisi && !sureEksik && !kaynakKontrolGerekli && kontrolEksikleri.length === 0 && lotSelection.errors.length === 0 && !analizEksik && !sourceOptions.needsSelection;
|
const hazir = !deadlineProblem && eksikler.length === 0 && toplam > 0 && !kalemEksik && !turCeliskisi && !sureEksik && !kaynakKontrolGerekli && kontrolEksikleri.length === 0 && lotSelection.errors.length === 0 && !analizEksik && !sourceOptions.needsSelection;
|
||||||
|
|
||||||
@@ -181,12 +181,14 @@ export const BidPackageBuilder = ({ results, tender: selectedTender, initialFocu
|
|||||||
// Open during the click; async validation must not lose popup activation.
|
// Open during the click; async validation must not lose popup activation.
|
||||||
printWindow.current = belgeYazdir('Dosya kontrolü', '<p>Belgeler kontrol ediliyor…</p>');
|
printWindow.current = belgeYazdir('Dosya kontrolü', '<p>Belgeler kontrol ediliyor…</p>');
|
||||||
setPrinting(true);
|
setPrinting(true);
|
||||||
const currentFiles = await listDocuments();
|
const linkedPortfolio=lotSelection.rows.some(row=>checks.products?.[row.priceKey]?.portfolioId);
|
||||||
|
const readPortfolio=async()=>linkedPortfolio ? (await getAllUserData()).firma_profil?.scrapedCapabilities?.healthPortfolio || [] : [];
|
||||||
|
const [currentFiles,currentPortfolio] = await Promise.all([listDocuments(),readPortfolio()]);
|
||||||
const reviewedText = effectiveResults.sourceReview?.humanReview;
|
const reviewedText = effectiveResults.sourceReview?.humanReview;
|
||||||
if (reviewedText) await validateDocumentTextReview(reviewedText,tender.ikn);
|
if (reviewedText) await validateDocumentTextReview(reviewedText,tender.ikn);
|
||||||
if (!mounted.current || session !== getToken() || submitted !== latestDraft.current)
|
if (!mounted.current || session !== getToken() || submitted !== latestDraft.current)
|
||||||
throw new Error('Form, kaynak veya oturum değişti. Güncel bilgilerle yeniden çıktı oluşturun.');
|
throw new Error('Form, kaynak veya oturum değişti. Güncel bilgilerle yeniden çıktı oluşturun.');
|
||||||
const currentVault = {loading:false,error:null,documents:currentFiles.belgeler || [],deadlineDate:tenderDateKey(tender),tenderIkn:tender.ikn};
|
const currentVault = {healthPortfolio:currentPortfolio,loading:false,error:null,documents:currentFiles.belgeler || [],deadlineDate:tenderDateKey(tender),tenderIkn:tender.ikn};
|
||||||
setVault(currentVault);
|
setVault(currentVault);
|
||||||
const fileErrors = preparationBlockers(lotSelection.rows, documents, checks, currentVault);
|
const fileErrors = preparationBlockers(lotSelection.rows, documents, checks, currentVault);
|
||||||
if (fileErrors.length) throw new Error(fileErrors.join(' '));
|
if (fileErrors.length) throw new Error(fileErrors.join(' '));
|
||||||
@@ -196,6 +198,9 @@ export const BidPackageBuilder = ({ results, tender: selectedTender, initialFocu
|
|||||||
if (tip === 'excel') {
|
if (tip === 'excel') {
|
||||||
const buffer = await createBidWorkbook(f,satirlar);
|
const buffer = await createBidWorkbook(f,satirlar);
|
||||||
if (reviewedText) await validateDocumentTextReview(reviewedText,tender.ikn);
|
if (reviewedText) await validateDocumentTextReview(reviewedText,tender.ikn);
|
||||||
|
const finalPortfolio=await readPortfolio();
|
||||||
|
const finalErrors=preparationBlockers(lotSelection.rows,documents,checks,{...currentVault,healthPortfolio:finalPortfolio});
|
||||||
|
if(finalErrors.length)throw new Error(finalErrors.join(' '));
|
||||||
if (!mounted.current || session !== getToken() || submitted !== latestDraft.current)
|
if (!mounted.current || session !== getToken() || submitted !== latestDraft.current)
|
||||||
throw new Error('Form veya oturum değişti; Excel çıktısını yeniden oluşturun.');
|
throw new Error('Form veya oturum değişti; Excel çıktısını yeniden oluşturun.');
|
||||||
const finalDeadlineProblem=preparationDeadlineProblem(tender);
|
const finalDeadlineProblem=preparationDeadlineProblem(tender);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import {portfolioSelectionProblem} from '../services/portfolioRevision';
|
||||||
import {useApp} from '../context/AppContext';
|
import {useApp} from '../context/AppContext';
|
||||||
import {portfolioProductText,selectPortfolioProduct} from '../services/healthPortfolio';
|
import {portfolioProductText,selectPortfolioProduct} from '../services/healthPortfolio';
|
||||||
import React, {useState} from 'react';
|
import React, {useState} from 'react';
|
||||||
@@ -57,6 +58,8 @@ export function PreparationChecks({rows, documents, manualDocuments, setManualDo
|
|||||||
</div>
|
</div>
|
||||||
{rows.map(row => {
|
{rows.map(row => {
|
||||||
const c=checks.products?.[row.priceKey] || {};
|
const c=checks.products?.[row.priceKey] || {};
|
||||||
|
const portfolioProblem=portfolioSelectionProblem(c,portfolio);
|
||||||
|
const currentProduct=portfolio.find(item=>item.id===c.portfolioId);
|
||||||
return <details key={row.priceKey} className="border rounded-xl p-3">
|
return <details key={row.priceKey} className="border rounded-xl p-3">
|
||||||
<summary className="cursor-pointer text-xs font-bold">{row.name} — ürün/hizmet ve yükümlülük kontrolü</summary>
|
<summary className="cursor-pointer text-xs font-bold">{row.name} — ürün/hizmet ve yükümlülük kontrolü</summary>
|
||||||
<div className="space-y-2 mt-3">
|
<div className="space-y-2 mt-3">
|
||||||
@@ -68,6 +71,10 @@ export function PreparationChecks({rows, documents, manualDocuments, setManualDo
|
|||||||
{portfolio.filter(item=>portfolioProductText(item)).map(item=><option key={item.id} value={item.id}>{portfolioProductText(item)}</option>)}
|
{portfolio.filter(item=>portfolioProductText(item)).map(item=><option key={item.id} value={item.id}>{portfolioProductText(item)}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
{portfolioProblem&&<div role="alert" className="text-xs text-red-700 border border-red-200 p-2 rounded">
|
||||||
|
<p>{portfolioProblem}</p>
|
||||||
|
{currentProduct&&<button type="button" className="underline mt-2" onClick={()=>updateProduct(row.priceKey,selectPortfolioProduct(currentProduct))}>Güncel ürün bilgilerini al ve kontrolleri sıfırla</button>}
|
||||||
|
</div>}
|
||||||
{c.portfolioSnapshot&&<p className="text-xs text-slate-600">Seçim anındaki firma beyanı: {c.portfolioSnapshot.kind==='device'?'Cihaz ve servis':c.portfolioSnapshot.kind==='staffedService'?'Personelli hizmet':'Sarf / test ürünü'} · ÜTS: {{declared:'Kayıt mevcut — firma beyanı',notApplicable:'Uygulanmıyor — firma beyanı',unknown:'Kontrol gerekli'}[c.portfolioSnapshot.utsStatus] || 'Kontrol gerekli'} · {c.portfolioSnapshot.utsReference || 'Kayıt açıklaması yok'} · Servis: {c.portfolioSnapshot.serviceCapacity || 'Belirtilmedi'}. İhale koşullarını aşağıda ayrıca doğrulayın.</p>}
|
{c.portfolioSnapshot&&<p className="text-xs text-slate-600">Seçim anındaki firma beyanı: {c.portfolioSnapshot.kind==='device'?'Cihaz ve servis':c.portfolioSnapshot.kind==='staffedService'?'Personelli hizmet':'Sarf / test ürünü'} · ÜTS: {{declared:'Kayıt mevcut — firma beyanı',notApplicable:'Uygulanmıyor — firma beyanı',unknown:'Kontrol gerekli'}[c.portfolioSnapshot.utsStatus] || 'Kontrol gerekli'} · {c.portfolioSnapshot.utsReference || 'Kayıt açıklaması yok'} · Servis: {c.portfolioSnapshot.serviceCapacity || 'Belirtilmedi'}. İhale koşullarını aşağıda ayrıca doğrulayın.</p>}
|
||||||
<label className="block text-xs">Teklif edilen marka/model veya hizmet kapsamı<input className={fieldClass} value={c.product || ''} onChange={e=>updateProduct(row.priceKey,{...selectPortfolioProduct(null),product:e.target.value})}/></label>
|
<label className="block text-xs">Teklif edilen marka/model veya hizmet kapsamı<input className={fieldClass} value={c.product || ''} onChange={e=>updateProduct(row.priceKey,{...selectPortfolioProduct(null),product:e.target.value})}/></label>
|
||||||
<label className="block text-xs">Teknik doküman/madde ve uygunluk dayanağı<input className={fieldClass} value={c.source || ''} onChange={e=>updateProduct(row.priceKey,{source:e.target.value})}/></label>
|
<label className="block text-xs">Teknik doküman/madde ve uygunluk dayanağı<input className={fieldClass} value={c.source || ''} onChange={e=>updateProduct(row.priceKey,{source:e.target.value})}/></label>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export function PreparationOverview({tenders,now}) {
|
|||||||
return <Overview key={`${authUser?.email}:${getToken()}`} tenders={tenders} now={now}/>;
|
return <Overview key={`${authUser?.email}:${getToken()}`} tenders={tenders} now={now}/>;
|
||||||
}
|
}
|
||||||
function Overview({tenders,now}) {
|
function Overview({tenders,now}) {
|
||||||
const {setActiveTenderDetail}=useApp();
|
const {setActiveTenderDetail,userProfile}=useApp();
|
||||||
const [vault,setVault]=useState({loading:true,error:null,documents:[]}),[refresh,setRefresh]=useState(0);
|
const [vault,setVault]=useState({loading:true,error:null,documents:[]}),[refresh,setRefresh]=useState(0);
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
let cancelled=false;
|
let cancelled=false;
|
||||||
@@ -29,7 +29,7 @@ function Overview({tenders,now}) {
|
|||||||
<div className="overflow-x-auto"><table className="w-full text-sm min-w-[700px]">
|
<div className="overflow-x-auto"><table className="w-full text-sm min-w-[700px]">
|
||||||
<thead><tr className="text-left border-b"><th className="p-2">İhale</th><th className="p-2">Sorumlu</th><th className="p-2">Belge eksikleri</th><th className="p-2">Sıradaki kontrol</th></tr></thead>
|
<thead><tr className="text-left border-b"><th className="p-2">İhale</th><th className="p-2">Sorumlu</th><th className="p-2">Belge eksikleri</th><th className="p-2">Sıradaki kontrol</th></tr></thead>
|
||||||
<tbody>{tenders.map(tender=>{
|
<tbody>{tenders.map(tender=>{
|
||||||
const summary=preparationSummary(tender,vault,now);
|
const summary=preparationSummary(tender,{...vault,healthPortfolio:userProfile.scrapedCapabilities?.healthPortfolio || []},now);
|
||||||
return <tr key={tender.id} className="border-b align-top">
|
return <tr key={tender.id} className="border-b align-top">
|
||||||
<td className="p-2"><button onClick={()=>open(tender,'source')} className="text-blue-800 underline text-left">{tender.ikn} · {tender.baslik}</button><p className="text-xs mt-1">{summary.issues.length ? `${summary.issues.length} kontrol eksik` : 'Kayıtlı kontroller tamam; son kontrol gerekli'}</p></td>
|
<td className="p-2"><button onClick={()=>open(tender,'source')} className="text-blue-800 underline text-left">{tender.ikn} · {tender.baslik}</button><p className="text-xs mt-1">{summary.issues.length ? `${summary.issues.length} kontrol eksik` : 'Kayıtlı kontroller tamam; son kontrol gerekli'}</p></td>
|
||||||
<td className="p-2">{summary.owner}</td>
|
<td className="p-2">{summary.owner}</td>
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
// Compare declared product facts only; field order and unrelated metadata do not
|
||||||
|
// invalidate a review. Missing/duplicate identities cannot establish provenance.
|
||||||
|
export function portfolioRevision(item) {
|
||||||
|
if (!item || typeof item !== 'object') return null;
|
||||||
|
return JSON.stringify(['id','group','brand','model','kind','utsStatus','utsReference','serviceCapacity'].map(key=>String(item[key] ?? '')));
|
||||||
|
}
|
||||||
|
export function portfolioSelectionProblem(check, portfolio) {
|
||||||
|
if (!check?.portfolioId) return null;
|
||||||
|
if (!Array.isArray(portfolio)) return 'Firma portföyü doğrulanamadı; güncel profili kontrol edin.';
|
||||||
|
const matches=portfolio.filter(item=>item?.id===check.portfolioId);
|
||||||
|
if (matches.length!==1) return 'Seçilen portföy kaydı kaldırılmış veya kimliği belirsiz; ürünü yeniden seçin.';
|
||||||
|
if (check.portfolioSnapshot?.id!==check.portfolioId || portfolioRevision(check.portfolioSnapshot)!==portfolioRevision(matches[0]))
|
||||||
|
return 'Firma portföyündeki ürün bilgileri değişti; güncel ürünü seçip uygunluk kontrollerini yeniden yapın.';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import {portfolioSelectionProblem} from './portfolioRevision.js';
|
||||||
export const HEALTH_CHECKS = [
|
export const HEALTH_CHECKS = [
|
||||||
['uts', 'ÜTS / ürün kaydı'],
|
['uts', 'ÜTS / ürün kaydı'],
|
||||||
['sample', 'Numune / demonstrasyon'],
|
['sample', 'Numune / demonstrasyon'],
|
||||||
@@ -37,6 +38,8 @@ export function preparationBlockers(rows, documents, checks = {}, vault) {
|
|||||||
}
|
}
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const c = checks.products?.[row.priceKey] || {};
|
const c = checks.products?.[row.priceKey] || {};
|
||||||
|
const portfolioProblem=portfolioSelectionProblem(c,vault?.healthPortfolio);
|
||||||
|
if(portfolioProblem)errors.push(`${row.name}: ${portfolioProblem}`);
|
||||||
if (c.status !== 'suitable' || !c.product?.trim() || !c.source?.trim())
|
if (c.status !== 'suitable' || !c.product?.trim() || !c.source?.trim())
|
||||||
errors.push(`${row.name}: ürün/hizmet uygunluğu ve teknik kaynak eksik veya uygun değil.`);
|
errors.push(`${row.name}: ürün/hizmet uygunluğu ve teknik kaynak eksik veya uygun değil.`);
|
||||||
for (const [key, label] of HEALTH_CHECKS) {
|
for (const [key, label] of HEALTH_CHECKS) {
|
||||||
|
|||||||
@@ -10,3 +10,31 @@ test('portfolio selection preserves declaration snapshot without approving tende
|
|||||||
for(const [key] of HEALTH_CHECKS)assert.deepEqual(check[key],{status:'pending',evidence:''});
|
for(const [key] of HEALTH_CHECKS)assert.deepEqual(check[key],{status:'pending',evidence:''});
|
||||||
const manual=selectPortfolioProduct(null);assert.equal(manual.portfolioId,null);assert.equal(manual.portfolioSnapshot,null);
|
const manual=selectPortfolioProduct(null);assert.equal(manual.portfolioId,null);assert.equal(manual.portfolioSnapshot,null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('portfolio revisions reject changed, missing and ambiguous declarations',async()=>{
|
||||||
|
const {portfolioSelectionProblem}=await import('../src/services/portfolioRevision.js');
|
||||||
|
const item={id:'device',group:'Analiz',brand:'QA',model:'M1',kind:'device',utsStatus:'declared',utsReference:'TEST',serviceCapacity:'2 uzman'};
|
||||||
|
const check=selectPortfolioProduct(item);
|
||||||
|
assert.equal(portfolioSelectionProblem(check,[{...item,updatedAt:'later'}]),null);
|
||||||
|
for(const key of ['group','brand','model','kind','utsStatus','utsReference','serviceCapacity'])
|
||||||
|
assert.match(portfolioSelectionProblem(check,[{...item,[key]:'changed'}]),/değişti/,key);
|
||||||
|
assert.match(portfolioSelectionProblem(check,[]),/kaldırılmış/);
|
||||||
|
assert.match(portfolioSelectionProblem(check,[item,item]),/belirsiz/);
|
||||||
|
assert.match(portfolioSelectionProblem(check,undefined),/doğrulanamadı/);
|
||||||
|
assert.match(portfolioSelectionProblem({...check,portfolioSnapshot:null},[item]),/değişti/);
|
||||||
|
assert.equal(portfolioSelectionProblem(selectPortfolioProduct(null),undefined),null);
|
||||||
|
});
|
||||||
|
test('fully approved product is blocked when its portfolio changes; reselecting requires new evidence',async()=>{
|
||||||
|
const {preparationBlockers}=await import('../src/services/preparationChecks.js');
|
||||||
|
const item={id:'kit',group:'Test kiti',model:'M1',kind:'consumable'};
|
||||||
|
const approved={...selectPortfolioProduct(item),status:'suitable',source:'Teknik madde 2',
|
||||||
|
...Object.fromEntries(HEALTH_CHECKS.map(([key])=>[key,{status:'verified',evidence:'Kontrol belgesi 1'}]))};
|
||||||
|
const rows=[{priceKey:'p1',name:'Kit'}],documents=[{document:'İsteğe bağlı',mandatory:false}];
|
||||||
|
const checks={products:{p1:approved},documents:{'["İsteğe bağlı",false,""]':{status:'notApplicable',evidence:'Şartname 4'}}};
|
||||||
|
assert.deepEqual(preparationBlockers(rows,documents,checks,{healthPortfolio:[item]}),[]);
|
||||||
|
const changed={...item,model:'M2'};
|
||||||
|
assert.ok(preparationBlockers(rows,documents,checks,{healthPortfolio:[changed]}).some(e=>e.includes('değişti')));
|
||||||
|
const refreshed={...checks,products:{p1:selectPortfolioProduct(changed)}};
|
||||||
|
const errors=preparationBlockers(rows,documents,refreshed,{healthPortfolio:[changed]});
|
||||||
|
assert.equal(errors.length,5);assert.ok(errors.every(e=>!e.includes('değişti')));
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user