fix: clarify explicit zero prices in manual review

This commit is contained in:
Wyndham ARR committed 2026-10-08 21:22:01 +08:00
1 parent 9487634f83
commit 5acbec75fe
7 files changed
+98 -11

No files matched your search

+75
View File
@@ -0,0 +1,75 @@
// Actual UI price saving against a synthetic API, without live business writes.
const test = require('node:test');
const assert = require('node:assert/strict');
const {harness,response} = require('./helpers/arr_ui_harness.cjs');
const fixture=(price=null)=>({case_id:'fixture-case',case_status:'open',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}});
test('zero is a valid explicit price while blank and negative prices remain invalid',()=>{
const h=harness(()=>{throw new Error('unexpected request');});
for(const value of [0,'0',' 0 ','1500']) assert.equal(h.strictReviewPrice(value),true);
for(const value of [null,undefined,'',' ','-1',-1,'0.00','1.5','abc']) assert.equal(h.strictReviewPrice(value),false);
assert.equal(h.reviewPriceInput(0),'0');
assert.equal(h.reviewPriceInput('0'),'0');
assert.equal(h.reviewPriceInput(null),'');
});
test('zero is rendered as a saved value and permits continuation when all items are saved',()=>{
const h=harness(()=>{throw new Error('unexpected request');});
h.renderDailyPriceReview(fixture(0));
const html=h.element('#daily-price-review-body').innerHTML;
assert.match(html,/value="0"/);
assert.match(html,/daily-price-review-row is-complete/);
assert.match(html,/daily_review.saved/);
assert.equal(h.element('#daily-price-review-finalize').disabled,false);
h.renderDailyPriceReview(fixture(null));
assert.match(h.element('#daily-price-review-body').innerHTML,/value=""/);
assert.match(h.element('#daily-price-review-body').innerHTML,/daily_review.price_placeholder/);
assert.equal(h.element('#daily-price-review-body').innerHTML.includes('placeholder="0"'),false);
assert.equal(h.element('#daily-price-review-finalize').disabled,true);
});
test('explicit zero is saved as zero and survives reloading without becoming empty',async()=>{
let current=fixture();
const h=harness((url,options)=>{
if(options.method==='PATCH') {
assert.equal(url,'/api/jobs/fixture-job/review/items/1');
const decision=JSON.parse(options.body);
assert.deepEqual(decision,{case_id:'fixture-case',revision:1,real_price:'0'});
current={...fixture('0'),revision:2};
return response(200,{revision:2});
}
if(url.startsWith('/api/jobs/fixture-job/review?')) return response(200,current);
if(url.startsWith('/api/jobs?')) return response(200,[]);
throw new Error('unexpected request '+url);
});
h.state.dailyPriceReview=fixture();
h.state.dailyPriceReviewJobId='fixture-job';
const input={value:'0',focus(){}};
const row={dataset:{dailyPriceReviewItemId:'1'},querySelector:()=>input};
await h.saveDailyPriceReviewItem({closest:()=>row});
assert.equal(h.calls.filter(c=>c.method==='PATCH').length,1);
assert.equal(h.state.dailyPriceReview.items[0].real_price,'0');
assert.equal(h.state.dailyPriceReview.completed_items,1);
assert.equal(h.dailyPriceReviewCanFinalize(h.state.dailyPriceReview),true);
assert.equal(h.element('#daily-price-review-finalize').disabled,false);
assert.match(h.element('#daily-price-review-body').innerHTML,/value="0"/);
await h.loadDailyPriceReview();
assert.equal(h.state.dailyPriceReview.items[0].real_price,'0');
assert.equal(h.calls.filter(c=>c.method==='PATCH').length,1,'reload must not submit another decision');
});
test('an empty input cannot be silently saved as zero',async()=>{
const h=harness(()=>{throw new Error('unexpected request');});
h.state.dailyPriceReview=fixture();
h.state.dailyPriceReviewJobId='fixture-job';
const input={value:'',focus(){}};
const row={dataset:{dailyPriceReviewItemId:'1'},querySelector:()=>input};
await h.saveDailyPriceReviewItem({closest:()=>row});
assert.equal(h.calls.length,0);
assert.equal(h.state.dailyPriceReview.items[0].real_price,null);
assert.equal(h.dailyPriceReviewCanFinalize(h.state.dailyPriceReview),false);
});
+2 -2
View File
@@ -17,12 +17,12 @@ function harness(respond) {
};
const context=vm.createContext({Headers, console, Date, Intl, Uint8Array,
document:{hidden:false,querySelector:element},
window:{ARRI18n:{t:key=>key,text:value=>value,errorMessage:code=>code},crypto:webcrypto,
window:{ARRI18n:{t:key=>key,text:value=>value,errorMessage:code=>code,formatInteger:value=>String(value)},crypto:webcrypto,
setTimeout(){return 1;},clearTimeout(){},location:{replace(){}}},
localStorage:{setItem:(k,v)=>storage.set(k,v),getItem:k=>{storageReads.push(k);return storage.get(k);},removeItem:k=>storage.delete(k)},
fetch:async (url, options) => {calls.push({url,method:options.method||'GET',body:options.body,csrf:options.headers.get('X-ARR-CSRF')});return respond(url,options,calls);},
});
const exports='state, api, submitARRDownload, rememberARRIntent, acceptARRDownloadTask, handleARRDownloadDateChange, initARRDownload, loadARRDownloadTask, loadARRDataReview, renderARRDataReview, arrDataReviewCanFinalize, saveARRDataReviewItem, finalizeARRDataReview, trackARRDataReviewDraft, parseARRDataReviewValue, selectARRPendingReview';
const exports='state, api, submitARRDownload, rememberARRIntent, acceptARRDownloadTask, handleARRDownloadDateChange, initARRDownload, loadARRDownloadTask, loadARRDataReview, renderARRDataReview, arrDataReviewCanFinalize, saveARRDataReviewItem, finalizeARRDataReview, trackARRDataReviewDraft, parseARRDataReviewValue, selectARRPendingReview, strictReviewPrice, reviewPriceInput, renderDailyPriceReview, saveDailyPriceReviewItem, loadDailyPriceReview, dailyPriceReviewCanFinalize';
vm.runInContext(source.replace(boot,` globalThis.subject = {${exports}};\n})();`),context);
const subject=context.subject;
Object.assign(subject.state,{arrDownloadReady:true,arrDownloadLoaded:true,arrDownloadStorageKey:'test',arrDownloadUsername:'operator',csrf:'fixture'});