const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const vm = require('node:vm'); const {harness,response,scopedKey} = require('./helpers/arr_ui_harness.cjs'); const requestId='a'.repeat(32); const task={request_id:requestId,report_date:'2026-10-07',status:'needs_data_review',job_id:null,can_retry:false}; const item=(extra={})=>({item_id:'1:BLOCK_CODE',source_sequence:1,confirmation_no:'SYNTHETIC-001',room_no:'SYNTHETIC-ROOM',company_name:'Synthetic Company',rate_code:'SYNTHETIC',field:'BLOCK_CODE',field_label:'团队代码',reason_code:'MISSING',can_be_empty:true,value:null,confirmed:false,...extra}); const review=(items,revision=1)=>({request_id:requestId,report_date:task.report_date,status:'editing',revision,items,pending_count:items.filter(x=>!x.confirmed).length,total_count:items.length,can_finalize:items.every(x=>x.confirmed)}); const plain=value=>JSON.parse(JSON.stringify(value)); const priceTask={request_id:'c'.repeat(32),report_date:'2026-09-16',status:'needs_review', job_id:'fixture-price-september',can_retry:false}; const priceReview=(price=null)=>({case_id:'fixture-price-case',case_status:'open',business_date:priceTask.report_date,revision:1, completed_items:price===null?0:1,total_items:1, items:[{item_id:1,company_key:'Fixture Company',rate_code:'FIXTURE',effective_rate_amount:100, real_price:price,affected_records:1,affected_rooms:1,affected_room_nights:2,candidate_prices:[]}], pagination:{offset:0,limit:50,total:1}}); function pendingAPI(url,currentPrice=priceTask) { if(url===`/api/arr-downloads/${requestId}`) return response(200,task); if(url===`/api/arr-downloads/${priceTask.request_id}`) return response(200,currentPrice); if(url.endsWith('/data-review')) return response(200,review([item()])); if(url.includes('/review?')) return response(200,priceReview()); if(url.endsWith('/trace')) return response(200,{job:{job_id:priceTask.job_id, business_date:priceTask.report_date,status:currentPrice.status,active:currentPrice.status==='needs_review'},logs:[]}); if(url.startsWith('/api/jobs?')) return response(200,[]); throw new Error('unexpected request '+url); } test('cross-month field and price dates restore, stay clickable after polling, and open their own pages by GET',async()=>{ const h=harness(url=>url==='/api/arr-downloads' ? response(200,{context_id:'production',ready:true,default_date:priceTask.report_date, pending_reviews:[task,priceTask],pending_data_reviews:[task],latest_task:priceTask}) : pendingAPI(url)); h.storage.set(scopedKey(),JSON.stringify({request_id:priceTask.request_id,report_date:priceTask.report_date})); await h.initARRDownload(); assert.equal(h.state.arrDownloadPendingReviews.length,2); assert.equal(h.element('#arr-download-pending-title').textContent,'arr_download.pending_dates'); assert.match(h.element('#arr-download-pending-list').innerHTML,/2026-10-07.*arr_download.pending_date/); assert.match(h.element('#arr-download-pending-list').innerHTML,/2026-09-16.*arr_download.pending_price/); assert.doesNotMatch(h.element('#arr-download-pending-list').innerHTML,/ disabled/); await h.selectARRPendingReview(requestId); h.trackARRDataReviewDraft(h.row('1:BLOCK_CODE','OCTOBER-DRAFT').input); await h.selectARRPendingReview(priceTask.request_id); assert.equal(h.state.jobsMonth,'2026-09'); assert.equal(h.element('#jobs-history-month').value,'2026-09'); assert.equal(h.state.selectedJobId,priceTask.job_id); assert.equal(h.state.dailyPriceReviewJobId,priceTask.job_id); assert.equal(h.element('#daily-price-review-panel').hidden,false); assert.equal(h.element('#arr-data-review-panel').hidden,true); assert.equal(h.element('#manual-review-workspace').hidden,false); assert.equal(h.element('#manual-review-context').textContent,'2026-09-16 · manual_review.prices'); assert.equal(h.element('#arr-download-pending').hidden,false); assert.equal(h.element('#arr-download-date').value,'2026-09-16'); assert.doesNotMatch(h.element('#arr-download-pending-list').innerHTML,/ disabled/); await h.loadARRDownloadTask(); assert.doesNotMatch(h.element('#arr-download-pending-list').innerHTML,/ disabled/, 'a finished status poll must unlock the pending buttons again'); await h.selectARRPendingReview(requestId); assert.equal(h.element('#daily-price-review-panel').hidden,true); assert.equal(h.element('#arr-data-review-panel').hidden,false); assert.equal(h.element('#manual-review-workspace').hidden,false); assert.equal(h.element('#manual-review-context').textContent,'2026-10-07 · manual_review.fields'); assert.equal(h.element('#arr-download-pending').hidden,false); assert.equal(h.state.arrDataReviewDrafts['1:BLOCK_CODE'],'OCTOBER-DRAFT'); assert.equal(h.calls.every(call=>call.method==='GET'),true,'restoring reviews never downloads or changes prices'); }); test('history price entry uses the same workspace and keeps field drafts while switching bodies',async()=>{ const h=harness(url=>pendingAPI(url)); h.state.arrDownloadPendingReviews=[task,priceTask]; await h.acceptARRDownloadTask(task,{sync:false}); h.trackARRDataReviewDraft(h.row('1:BLOCK_CODE','FIELD-DRAFT').input); await h.openDailyPriceReview(priceTask.job_id,false); assert.equal(h.element('#manual-review-workspace').hidden,false); assert.equal(h.element('#arr-data-review-panel').hidden,true); assert.equal(h.element('#daily-price-review-panel').hidden,false); assert.equal(h.element('#manual-review-context').textContent,'2026-09-16 · manual_review.prices'); assert.equal(h.element('#arr-download-pending').hidden,false); h.renderARRDataReview(); h.renderDailyPriceReview(); assert.equal(h.element('#arr-data-review-panel').hidden,true,'routine renders cannot switch the selected body'); await h.selectARRPendingReview(requestId); assert.equal(h.element('#arr-data-review-panel').hidden,false); assert.equal(h.element('#daily-price-review-panel').hidden,true); assert.equal(h.state.arrDataReviewDrafts['1:BLOCK_CODE'],'FIELD-DRAFT'); assert.equal(h.calls.every(call=>call.method==='GET'),true); }); test('terminal reviews hide the workspace without hiding other dates requiring attention',async()=>{ const h=harness(url=>pendingAPI(url)); h.state.arrDownloadPendingReviews=[task,priceTask]; await h.acceptARRDownloadTask(task,{sync:false}); await h.openDailyPriceReview(priceTask.job_id,false); for(const case_status of ['completed','cancelled','failed']) { h.state.dailyPriceReview={...priceReview(),case_status}; h.renderDailyPriceReview(); assert.equal(h.element('#manual-review-workspace').hidden,true); assert.equal(h.element('#arr-data-review-panel').hidden,true); assert.equal(h.element('#daily-price-review-panel').hidden,true); assert.equal(h.element('#arr-download-pending').hidden,false); } h.state.arrDownloadTask={...task,status:'succeeded'}; h.state.manualReviewMode='fields'; h.renderARRDataReview(); assert.equal(h.element('#manual-review-workspace').hidden,true); }); test('opening fields directly cannot hide an unsaved price edit',async()=>{ const h=harness(url=>pendingAPI(url)); await h.acceptARRDownloadTask(task,{sync:false}); await h.openDailyPriceReview(priceTask.job_id,false); const input={value:'0',closest:()=>({dataset:{dailyPriceReviewItemId:'1'}})}; h.element('#daily-price-review-body').querySelectorAll=()=>[input]; const count=h.calls.length; await h.loadARRDataReview(); assert.equal(h.calls.length,count); assert.equal(h.element('#daily-price-review-panel').hidden,false); assert.equal(h.element('#arr-data-review-panel').hidden,true); assert.equal(input.value,'0'); }); test('a source context change clears the workspace and ignores an older price response',async()=>{ let finishPrice; const h=harness(url=>{ if(url.includes('/review?')) return new Promise(resolve=>{finishPrice=()=>resolve(response(200,priceReview()));}); if(url==='/api/arr-downloads') return response(200,{context_id:'new-context',ready:true, default_date:'2026-09-17',pending_reviews:[],latest_task:null}); throw new Error('unexpected request '+url); }); h.state.arrDownloadContextId='old-context'; h.state.arrDownloadStorageKey=scopedKey('old-context'); h.state.dailyPriceReview=priceReview(); h.state.dailyPriceReviewJobId=priceTask.job_id; h.state.dailyPriceReviewOffset=50; const loading=h.loadDailyPriceReview(); await h.initARRDownload(); assert.equal(h.state.manualReviewMode,''); assert.equal(h.state.dailyPriceReview,null); assert.equal(h.state.dailyPriceReviewJobId,''); assert.equal(h.state.dailyPriceReviewOffset,0); assert.equal(h.element('#manual-review-workspace').hidden,true); finishPrice(); await loading; assert.equal(h.state.dailyPriceReview,null); assert.equal(h.element('#manual-review-workspace').hidden,true); assert.equal(h.calls.every(call=>call.method==='GET'),true); }); test('an authoritative empty pending list overrides legacy and stale current review state',async()=>{ let cleared=false; const h=harness(url=>url==='/api/arr-downloads' ? response(200,{context_id:'production',ready:true,default_date:priceTask.report_date, pending_reviews:cleared?[]:[priceTask],pending_data_reviews:[task],latest_task:priceTask}) : pendingAPI(url)); h.storage.set(scopedKey(),JSON.stringify({request_id:priceTask.request_id,report_date:priceTask.report_date})); await h.initARRDownload(); assert.deepEqual(plain(h.state.arrDownloadPendingReviews).map(x=>x.request_id),[priceTask.request_id]); cleared=true; await h.initARRDownload(); assert.equal(h.state.arrDownloadTask.status,'needs_review','the fixture deliberately returns an older task snapshot'); assert.equal(h.state.arrDownloadPendingReviews.length,0); assert.equal(h.element('#arr-download-pending').hidden,true); assert.equal(h.calls.every(call=>call.method==='GET'),true); }); test('a source request can return as a price review after processing with the same request id',async()=>{ const h=harness(()=>response(200,review([item()]))); await h.acceptARRDownloadTask(task,{sync:false}); await h.acceptARRDownloadTask({...task,status:'processing'},{sync:false}); assert.equal(h.state.arrDownloadPendingReviews.length,0); await h.acceptARRDownloadTask({...task,status:'needs_review',job_id:'same-request-price-job'},{sync:false}); assert.equal(h.state.arrDownloadPendingReviews.length,1); assert.equal(h.state.arrDownloadPendingReviews[0].status,'needs_review'); assert.match(h.element('#arr-download-pending-list').innerHTML,/arr_download.pending_price/); }); for(const status of ['succeeded','failed']) { test(`a stale price date whose authoritative task is ${status} leaves the pending list without reopening prices`,async()=>{ const h=harness(url=>pendingAPI(url,{...priceTask,status})); h.state.arrDownloadPendingReviews=[priceTask]; await h.selectARRPendingReview(priceTask.request_id); assert.equal(h.state.arrDownloadTask.status,status); assert.equal(h.state.arrDownloadPendingReviews.length,0); assert.equal(h.element('#arr-download-pending').hidden,true); assert.equal(h.element('#manual-review-workspace').hidden,true); assert.equal(h.element('#daily-price-review-panel').hidden,true); assert.equal(h.calls.some(call=>call.url.includes('/review?')),false); assert.equal(h.calls.every(call=>call.method==='GET'),true); }); } test('unsaved prices prevent a pending date switch without clearing the current price input',async()=>{ const h=harness(()=>{throw new Error('switch must not send a request');}); h.state.arrDownloadPendingReviews=[task,priceTask]; h.state.arrDownloadTask=priceTask; h.state.dailyPriceReviewJobId=priceTask.job_id; h.state.dailyPriceReview=priceReview(); const input={value:'0',closest:()=>({dataset:{dailyPriceReviewItemId:'1'}})}; h.element('#daily-price-review-body').querySelectorAll=()=>[input]; assert.equal(h.dailyPriceReviewHasUnsavedChanges(),true); await h.selectARRPendingReview(requestId); assert.equal(h.calls.length,0); assert.equal(h.state.arrDownloadTask.request_id,priceTask.request_id); assert.equal(input.value,'0'); assert.equal(h.element('#daily-price-review-message').textContent,'daily_review.save_before_switch'); h.state.dailyPriceReview=priceReview('0'); assert.equal(h.dailyPriceReviewHasUnsavedChanges(),false,'a saved zero is not an unsaved draft'); }); for(const operation of ['finalize','cancel']) { test(`price ${operation} refreshes the same request and removes its terminal pending date`,async()=>{ const h=harness((url,options)=>{ if(options.method==='POST') { assert.equal(url,`/api/jobs/${priceTask.job_id}/review/${operation}`); return response(200,{status:operation==='finalize'?'succeeded':'cancelled'}); } return pendingAPI(url,{...priceTask,status:operation==='finalize'?'succeeded':'failed'}); }); h.state.arrDownloadPendingReviews=[priceTask]; h.state.arrDownloadTask=priceTask; h.state.dailyPriceReviewJobId=priceTask.job_id; h.state.dailyPriceReview=priceReview('0'); if(operation==='finalize') await h.finalizeDailyPriceReview(); else await h.confirmDailyPriceReviewCancel(); assert.equal(h.state.arrDownloadPendingReviews.length,0); assert.equal(h.element('#arr-download-pending').hidden,true); assert.equal(h.calls.filter(call=>call.method==='POST').length,1); assert.equal(h.calls.some(call=>call.url===`/api/arr-downloads/${priceTask.request_id}`),true); assert.equal(h.calls.some(call=>call.url==='/api/arr-downloads'),false,'no replacement download is created'); assert.equal(h.state.arrDownloadTask.status,operation==='finalize'?'succeeded':'failed'); }); } test('pending date labels have Chinese, English and Thai translations',()=>{ const context=vm.createContext({window:{},document:{readyState:'loading',addEventListener(){}}}); vm.runInContext(fs.readFileSync(path.resolve(__dirname,'../../arr_web/static/i18n.js'),'utf8'),context); const catalog=context.window.ARRI18n.CATALOG; for(const key of ['arr_download.pending_dates','arr_download.pending_date','arr_download.pending_price','daily_review.save_before_switch', 'manual_review.title','manual_review.fields','manual_review.prices','manual_review.context']) { assert.equal(catalog[key].length,3); catalog[key].forEach(text=>assert.equal(typeof text==='string' && text.length>0,true)); } assert.equal(catalog['arr_download.pending_dates'][0],'需处理日期'); assert.equal(catalog['manual_review.title'][0],'人工核对'); assert.equal(catalog['arr_download.pending_price'][0],'待复核价格'); }); test('cancelled source rows need no fields and the exclusion stays visible before continuation',async()=>{ const h=harness(()=>response(200,{...review([]),excluded_cancelled_count:6})); await h.acceptARRDownloadTask(task,{sync:false}); assert.equal(h.element('#arr-data-review-body').innerHTML,''); assert.match(h.element('#arr-data-review-progress').textContent,/data_review\.excluded_cancelled/); assert.equal(h.element('#arr-data-review-finalize').disabled,false); assert.equal(h.calls.every(call=>call.method==='GET'),true); }); test('PM exclusions stay visible without requiring field completion',async()=>{ const h=harness(()=>response(200,{...review([]),excluded_cancelled_count:6,excluded_pm_count:19})); await h.acceptARRDownloadTask(task,{sync:false}); const progress=h.element('#arr-data-review-progress').textContent; assert.match(progress,/data_review\.excluded_cancelled/); assert.match(progress,/data_review\.excluded_pm/); assert.equal(h.element('#arr-data-review-body').innerHTML,''); assert.equal(h.element('#arr-data-review-finalize').disabled,false); assert.equal(h.calls.every(call=>call.method==='GET'),true); }); test('removed cancelled items clear obsolete drafts but retain edits still in the review',async()=>{ let current=review([item(),item({item_id:'2:DISP_ROOM_NO',source_sequence:2,field:'DISP_ROOM_NO',can_be_empty:false})]); const h=harness(()=>response(200,current)); await h.acceptARRDownloadTask(task,{sync:false}); h.state.arrDataReviewDrafts['1:BLOCK_CODE']='CANCELLED-DRAFT'; h.state.arrDataReviewDrafts['2:DISP_ROOM_NO']='ROOM-DRAFT'; current=review([current.items[1]],2); await h.loadARRDataReview(requestId); assert.equal(h.state.arrDataReviewDrafts['1:BLOCK_CODE'],undefined); assert.equal(h.state.arrDataReviewDrafts['2:DISP_ROOM_NO'],'ROOM-DRAFT'); current={...review([],3),excluded_cancelled_count:2}; await h.loadARRDataReview(requestId); assert.equal(Object.keys(h.state.arrDataReviewDrafts).length,0); assert.equal(h.element('#arr-data-review-finalize').disabled,false); }); for (const fixture of [ {name:'missing room retains the original required rule', field:'DISP_ROOM_NO', can_be_empty:false, source_state:'missing', reason_code:'calendar_missing_rooms', expected:'room_missing_in_scope'}, {name:'failed room lookup is not described as confirmed absence', field:'DISP_ROOM_NO', can_be_empty:false, source_state:'failed', reason_code:'calendar_missing_rooms', expected:'fetch_failed'}, {name:'conflicting room evidence is not described as confirmed absence', field:'DISP_ROOM_NO', can_be_empty:false, source_state:'ambiguous', reason_code:'calendar_missing_rooms', expected:'conflict_value'}, {name:'missing block is optional information awaiting confirmation', field:'BLOCK_CODE', can_be_empty:true, source_state:'missing', reason_code:'missing_reservation_block', expected:'source_unconfirmed'}, {name:'missing packages are optional information awaiting confirmation', field:'PRODUCTS', can_be_empty:true, source_state:'missing', reason_code:'missing_reservation_packages', expected:'source_unconfirmed'}, {name:'failed source state takes precedence over a missing reason', source_state:'failed', reason_code:'missing_reservation_block', expected:'fetch_failed'}, {name:'ambiguous source state takes precedence over an invalid reason', source_state:'ambiguous', reason_code:'invalid_package_code', expected:'conflict_value'}, {name:'older HTTP failure snapshots remain failures', reason_code:'http_failure', expected:'fetch_failed'}, {name:'invalid response JSON is a read failure rather than a field format error', reason_code:'invalid_json', expected:'fetch_failed'}, {name:'a block disagreement is shown as conflicting information', reason_code:'block_code_disagreement', expected:'conflict_value'}, {name:'a changed block association is shown as conflicting information', reason_code:'block_association_changed', expected:'conflict_value'}, {name:'malformed package code has a separate format explanation', reason_code:'invalid_package_code', expected:'invalid_value'}, {name:'invalid nights retain a separate format explanation', field:'DEPARTURE', can_be_empty:false, reason_code:'XML_NEGATIVE_NIGHTS', expected:'invalid_value'}, {name:'confirmed decisions show the saved confirmation instead of the original problem', source_state:'failed', reason_code:'http_failure', confirmed:true, value:'VERIFIED', expected:'confirmed_value'}, ]) { test(`field review explanation: ${fixture.name}`,async()=>{ const source=item(fixture); const original=plain(source); const h=harness(()=>response(200,review([source]))); await h.acceptARRDownloadTask(task,{sync:false}); const markup=h.element('#arr-data-review-body').innerHTML; assert.match(markup,new RegExp(`data_review\\.${fixture.expected}\\b`)); assert.match(markup,new RegExp(`data_review\\.${source.can_be_empty?'optional':'required'}_field_rule\\b`)); assert.equal(markup.includes('data_review.missing_value'),false); assert.equal(markup.includes('data-arr-data-review-empty'),source.can_be_empty); assert.deepEqual(plain(source),original,'rendering explanations never changes a source or confirmation'); assert.equal(h.calls.every(call=>call.method==='GET'),true); }); } test('review explanations have distinct Chinese, English and Thai copy for required, optional and failed reads',()=>{ const context=vm.createContext({window:{},document:{readyState:'loading',addEventListener(){}}}); vm.runInContext(fs.readFileSync(path.resolve(__dirname,'../../arr_web/static/i18n.js'),'utf8'),context); const catalog=context.window.ARRI18n.CATALOG; const keys=['required_field_rule','optional_field_rule','required_value_missing','room_missing_in_scope','source_unconfirmed', 'fetch_failed','conflict_value','invalid_value']; for(const key of keys) { const translations=catalog[`data_review.${key}`]; assert.equal(translations.length,3); translations.forEach(text=>assert.equal(typeof text==='string' && text.length>0,true)); } assert.match(catalog['data_review.required_field_rule'][0],/原处理规则必填/); assert.match(catalog['data_review.optional_field_rule'][0],/允许为空/); assert.match(catalog['data_review.room_missing_in_scope'][0],/费率在处理范围内/); assert.match(catalog['data_review.source_unconfirmed'][0],/确认无此信息/); assert.match(catalog['data_review.fetch_failed'][0],/不能直接认定没有此信息/); assert.equal(catalog['data_review.fetch_failed'][0].includes('刷新'),false, 'refreshing the review must not imply a retry of Oracle acquisition'); }); test('source review opens before a job exists and another date can start its own download',async()=>{ const h=harness((url,options)=>options.method==='POST' ? response(202,{...JSON.parse(options.body),status:'queued',job_id:null,can_retry:false}) : response(200,review([item()]))); await h.acceptARRDownloadTask(task,{sync:false}); assert.equal(h.element('#arr-download-data-review').hidden,false); assert.equal(h.element('#arr-data-review-panel').hidden,false); assert.equal(h.element('#arr-download-review').hidden,true); assert.equal(h.element('#arr-data-review-finalize').disabled,true); h.element('#arr-download-date').value='2026-09-17'; h.handleARRDownloadDateChange(); assert.equal(h.state.arrDownloadIntent.request_id,requestId,'choosing a date alone keeps the earlier task'); assert.equal(h.element('#arr-download-button').textContent,'arr_download.start'); assert.equal(h.element('#arr-download-button').disabled,false); await h.submit(); assert.notEqual(h.state.arrDownloadIntent.request_id,requestId); assert.equal(h.state.arrDownloadTask.report_date,'2026-09-17'); assert.equal(h.element('#arr-download-date').value,'2026-09-17'); const post=h.calls.find(call=>call.method==='POST'); assert.equal(post.url,'/api/arr-downloads'); assert.equal(JSON.parse(post.body).report_date,'2026-09-17'); assert.equal(h.state.arrDownloadPendingReviews[0].request_id,requestId); assert.equal(h.element('#arr-data-review-panel').hidden,true); }); test('submitting the pending review date only opens that review without another download',async()=>{ const h=harness(()=>response(200,review([item()]))); await h.acceptARRDownloadTask(task,{sync:false}); assert.equal(h.element('#arr-download-button').textContent,'arr_download.complete_data'); await h.submit(); assert.equal(h.state.arrDownloadIntent.request_id,requestId); assert.equal(h.calls.every(call=>call.method==='GET'),true); }); test('pending dates and unsaved drafts remain separate when moving between two field reviews',async()=>{ const otherId='b'.repeat(32); const otherTask={...task,request_id:otherId,report_date:'2026-09-17'}; const h=harness(url=>url.endsWith('/data-review') ? response(200,{...review([item()]),request_id:url.includes(otherId)?otherId:requestId}) : response(200,url.endsWith(otherId)?otherTask:task)); await h.acceptARRDownloadTask(task,{sync:false}); h.trackARRDataReviewDraft(h.row('1:BLOCK_CODE','OCTOBER-DRAFT').input); await h.acceptARRDownloadTask(otherTask,{sync:false}); assert.equal(h.state.arrDownloadPendingReviews.length,2); assert.equal(h.state.arrDataReviewDrafts['1:BLOCK_CODE'],undefined); h.trackARRDataReviewDraft(h.row('1:BLOCK_CODE','SEPTEMBER-DRAFT').input); await h.selectARRPendingReview(requestId); assert.equal(h.state.arrDownloadTask.request_id,requestId); assert.equal(h.element('#arr-download-date').value,'2026-10-07'); assert.equal(h.state.arrDataReviewDrafts['1:BLOCK_CODE'],'OCTOBER-DRAFT'); await h.selectARRPendingReview(otherId); assert.equal(h.state.arrDataReviewDrafts['1:BLOCK_CODE'],'SEPTEMBER-DRAFT'); assert.equal(h.calls.every(call=>call.method==='GET'),true,'returning to a review never re-fetches Oracle'); }); test('pending dates restore from the server even when the latest task is no longer a review',async()=>{ const h=harness(url=>url==='/api/arr-downloads' ? response(200,{context_id:'production',ready:true,default_date:'2026-10-07', pending_data_reviews:[task],latest_task:{...task,request_id:'b'.repeat(32),report_date:'2026-09-17',status:'failed'}}) : response(200,url.endsWith('/data-review')?review([item()]):task)); await h.initARRDownload(); assert.equal(h.state.arrDownloadTask,null); assert.equal(h.element('#arr-download-pending').hidden,false); assert.match(h.element('#arr-download-pending-list').innerHTML,/2026-10-07/); await h.selectARRPendingReview(requestId); assert.equal(h.state.arrDownloadTask.request_id,requestId); assert.equal(h.state.arrDataReview.pending_count,1); }); test('an uncertain new date cannot be abandoned by opening an earlier field review',async()=>{ const h=harness(()=>{throw new Error('unexpected request');}); h.state.arrDownloadPendingReviews=[task]; h.rememberARRIntent({request_id:'b'.repeat(32),report_date:'2026-09-17'}); h.state.arrDownloadDisconnected=true; await h.selectARRPendingReview(requestId); assert.equal(h.calls.length,0); assert.equal(h.state.arrDownloadIntent.report_date,'2026-09-17'); }); test('a completed pending review can still be finalized after reopening it',async()=>{ const h=harness(url=>response(200,url.endsWith('/data-review') ? review([item({confirmed:true,value:'VERIFIED'})]) : task)); h.state.arrDownloadPendingReviews=[task]; await h.selectARRPendingReview(requestId); assert.equal(h.state.arrDownloadBusy,false); assert.equal(h.arrDataReviewCanFinalize(),true); assert.equal(h.element('#arr-data-review-finalize').disabled,false); }); test('lost submission for another date retains both its uncertain intent and the earlier pending review',async()=>{ const h=harness((url,options)=>{ if(options.method==='POST') throw new TypeError('lost response'); if(url.endsWith('/data-review')) return response(200,review([item()])); return response(404,null,'ARR_DOWNLOAD_NOT_FOUND'); }); await h.acceptARRDownloadTask(task,{sync:false}); h.element('#arr-download-date').value='2026-09-17'; h.handleARRDownloadDateChange(); await h.submit(); const intent=plain(h.state.arrDownloadIntent); assert.equal(intent.report_date,'2026-09-17'); assert.notEqual(intent.request_id,requestId); assert.equal(h.state.arrDownloadTask.status,'not_received'); assert.equal(h.state.arrDownloadPendingReviews[0].request_id,requestId); await h.selectARRPendingReview(requestId); assert.deepEqual(plain(h.state.arrDownloadIntent),intent); }); test('incomplete review cannot finalize even when server flag says it can',async()=>{ const h=harness(()=>response(200,{...review([item()]),can_finalize:true,pending_count:0})); await h.acceptARRDownloadTask(task,{sync:false}); await h.finalizeARRDataReview(); assert.equal(h.arrDataReviewCanFinalize(),false); assert.equal(h.calls.length,1); assert.equal(h.element('#arr-data-review-message').textContent,'data_review.complete_first'); }); test('blank text is not silently confirmed; explicit empty saves its revision and reloads the review',async()=>{ let current=review([item()]); const h=harness((url,options)=>{ if(options.method==='POST') { assert.equal(url,`/api/arr-downloads/${requestId}/data-review/items/1%3ABLOCK_CODE`); assert.deepEqual(JSON.parse(options.body),{revision:1,value:''}); current=review([item({confirmed:true,value:''})],2); return response(200,{revision:2}); } return response(200,current); }); await h.acceptARRDownloadTask(task,{sync:false}); const row=h.row('1:BLOCK_CODE',''); await h.saveARRDataReviewItem(row.button); assert.equal(h.calls.length,1); assert.equal(h.element('#arr-data-review-message').textContent,'data_review.use_empty'); await h.saveARRDataReviewItem(row.button,{empty:true}); assert.equal(h.calls.length,3); assert.equal(h.state.arrDataReview.revision,2); assert.equal(h.arrDataReviewCanFinalize(),true); assert.equal(h.element('#arr-data-review-finalize').disabled,false); }); test('products are an ordered code array and explicit empty products are an empty array',()=>{ const h=harness(()=>{throw new Error('no request');}); const product=item({field:'PRODUCTS'}); assert.deepEqual(plain(h.parseARRDataReviewValue(product,'BREAKFAST\nDINNER\nBREAKFAST',false)),['BREAKFAST','DINNER','BREAKFAST']); assert.deepEqual(plain(h.parseARRDataReviewValue(product,'ignored',true)),[]); assert.equal(h.parseARRDataReviewValue(item({field:'EFFECTIVE_RATE_AMOUNT'}),'900.000000000001',false),'900.000000000001'); assert.equal(h.parseARRDataReviewValue(item({field:'ADULTS'}),'0',false),'0'); assert.throws(()=>h.parseARRDataReviewValue(item({field:'ADULTS'}),'1.5',false)); assert.throws(()=>h.parseARRDataReviewValue(item({field:'ARRIVAL'}),'2026-02-30',false)); }); test('revision conflict reloads the current revision before any next save',async()=>{ let reads=0; const h=harness((url,options)=>options.method==='POST' ? response(409,null,'ARR_DATA_REVIEW_CONFLICT') : response(200,review([item()],++reads===1 ? 1 : 5))); await h.acceptARRDownloadTask(task,{sync:false}); const row=h.row('1:BLOCK_CODE','VERIFIED'); h.trackARRDataReviewDraft(row.input); await h.saveARRDataReviewItem(row.button); assert.equal(h.state.arrDataReview.revision,5); assert.equal(h.state.arrDataReviewDrafts['1:BLOCK_CODE'],'VERIFIED'); assert.equal(h.element('#arr-data-review-message').textContent,'data_review.conflict'); assert.equal(h.state.arrDataReviewMutating,false); }); test('unconfirmed source values are never prefilled and all labels and stored values are escaped',async()=>{ const h=harness(()=>response(200,review([ item({company_name:'',value:'GUESSED-BLOCK'}), item({item_id:'2:RES_COMMENT',field:'RES_COMMENT',confirmed:true,value:''}), ]))); await h.acceptARRDownloadTask(task,{sync:false}); const markup=h.element('#arr-data-review-body').innerHTML; assert.equal(markup.includes('GUESSED-BLOCK'),false); assert.equal(markup.includes(''),false); assert.match(markup,/<img/); assert.match(markup,/<\/textarea>/); }); test('unsaved edits of confirmed fields block finalization until explicitly saved',async()=>{ const h=harness(()=>response(200,review([item({confirmed:true,value:'FIRST'})]))); await h.acceptARRDownloadTask(task,{sync:false}); assert.equal(h.arrDataReviewCanFinalize(),true); const row=h.row('1:BLOCK_CODE','SECOND'); h.trackARRDataReviewDraft(row.input); await h.finalizeARRDataReview(); assert.equal(h.arrDataReviewCanFinalize(),false); assert.equal(h.calls.length,1); row.input.value='FIRST'; h.trackARRDataReviewDraft(row.input); assert.equal(h.arrDataReviewCanFinalize(),true); }); test('lost save response re-reads durable confirmation rather than resubmitting the item',async()=>{ let current=review([item()]); const h=harness((url,options)=>{ if(options.method==='POST') { current=review([item({value:'VERIFIED',confirmed:true})],2); throw new TypeError('lost response'); } return response(200,current); }); await h.acceptARRDownloadTask(task,{sync:false}); const row=h.row('1:BLOCK_CODE','VERIFIED'); h.trackARRDataReviewDraft(row.input); await h.saveARRDataReviewItem(row.button); assert.equal(h.calls.filter(call=>call.method==='POST').length,1); assert.equal(h.state.arrDataReview.revision,2); assert.equal(h.arrDataReviewCanFinalize(),true); }); test('finalize uses the same request and a lost response reconciles processing without a new download',async()=>{ let finalized=false; const h=harness((url,options)=>{ if(options.method==='POST') { assert.equal(url,`/api/arr-downloads/${requestId}/data-review/finalize`); assert.deepEqual(JSON.parse(options.body),{revision:7}); finalized=true; throw new TypeError('lost finalize response'); } if(url===`/api/arr-downloads/${requestId}`) return response(200,{...task,status:'processing'}); return response(200,review([item({confirmed:true,value:''})],7)); }); await h.acceptARRDownloadTask(task,{sync:false}); await h.finalizeARRDataReview(); assert.equal(finalized,true); assert.equal(h.state.arrDownloadTask.status,'processing'); assert.equal(h.state.arrDownloadIntent.request_id,requestId); assert.equal(h.state.arrDataReview,null); assert.equal(h.state.arrDataReviewFinalizing,false); assert.equal(h.calls.filter(call=>call.method==='POST').length,1); assert.equal(h.calls.some(call=>call.url==='/api/arr-downloads'),false); }); test('an uncertain frozen finalize stays locked when the task and review cannot be reconciled',async()=>{ let finalizing=false; const h=harness((url,options)=>{ if(options.method==='POST') {finalizing=true;throw new TypeError('offline');} if(finalizing) throw new TypeError('offline'); return response(200,review([item({confirmed:true,value:''})])); }); await h.acceptARRDownloadTask(task,{sync:false}); await h.finalizeARRDataReview(); assert.equal(h.state.arrDataReviewFinalizing,true); assert.equal(h.state.arrDataReviewDisconnected,true); assert.equal(h.element('#arr-data-review-finalize').disabled,true); await h.submit(); assert.equal(h.calls.filter(call=>call.method==='POST').length,1); });