2026-10-09 00:45:21 +02:00
const { test } = require ( 'node:test' ) ;
const assert = require ( 'node:assert/strict' ) ;
const vm = require ( 'node:vm' ) ;
const fs = require ( 'node:fs' ) ;
const html = fs . readFileSync ( 'app/economy/frontend/time_queue.html' , 'utf8' ) ;
function fixture ( rows = [ ] ) {
2026-10-09 01:21:12 +02:00
const controls = { } ; for ( const id of [ 'approveBtn' , 'stickyApproveBtn' , 'settlementMethod' , 'prepaidCard' , 'agreementId' , 'workflowLineMode' , 'workflowLineModeHost' , 'setMethodBtn' , 'previewBtn' , 'stickyPreviewBtn' , 'selectionText' , 'previewPanel' , 'settleBtn' , 'filterMethod' , 'filterCustomer' , 'filterStatus' , 'filterCaseState' , 'filterBillable' , 'filterQuery' ] ) controls [ id ] = { value : '' , disabled : false , classList : { add ( ) { } , toggle ( ) { } } } ;
2026-10-09 00:45:21 +02:00
controls . workflowLineMode . value = 'separate' ;
2026-10-09 01:21:12 +02:00
const state = { scope : 'active' , items : rows , selected : new Set ( rows . map ( x => x . id ) ) , triage : 'all' , preview : { valid : true } , previewPayload : { ids : [ 1 ] } , previewFingerprint : 'old' } ;
const ctx = { state , $ : id => controls [ id ] , norm : v => ( { fixed _price : 'subscription' , prepaid _card : 'prepaid' } ) [ v ] || v || 'draft' , ids : ( ) => [ ... state . selected ] , selectedRows : ( ) => rows , confirm : ( ) => true , Number , JSON } ;
vm . createContext ( ctx ) ; vm . runInContext ( html . slice ( html . indexOf ( ' function actionable(' ) , html . indexOf ( ' function status(' ) ) , ctx ) ;
2026-10-09 00:45:21 +02:00
return { ctx , controls , state } ;
}
test ( 'workflow stale, absent or manual review never inherits invoice fallback' , ( ) => {
const { ctx } = fixture ( ) ;
assert . equal ( ctx . effectiveMethod ( { workflow _active : true , billing _method : 'invoice' } ) , 'draft' ) ;
assert . equal ( ctx . effectiveMethod ( { workflow _active : true , workflow _stale : true , workflow _decision : { method : 'invoice' , status : 'decided' } } ) , 'draft' ) ;
assert . equal ( ctx . effectiveMethod ( { workflow _active : true , workflow _decision : { method : 'invoice' , status : 'manual_review' } } ) , 'manual_review' ) ;
assert . match ( ctx . reviewIssue ( { workflow _active : true , workflow _stale : true } ) , /Genberegn/ ) ;
} ) ;
test ( 'readiness flags missing card/agreement and accepts concrete workflow selection' , ( ) => {
const { ctx } = fixture ( ) ; const base = { original _hours : 1 , customer _id : 9 , status : 'pending' } ;
assert . match ( ctx . reviewIssue ( { ... base , billing _method : 'prepaid' } ) , /klippekort/ ) ;
assert . match ( ctx . reviewIssue ( { ... base , billing _method : 'fixed_price' } ) , /aftale/ ) ;
assert . equal ( ctx . reviewIssue ( { ... base , workflow _active : true , workflow _decision : { method : 'subscription' , status : 'decided' } , fixed _price _agreement _id : 2 } ) , '' ) ;
assert . match ( ctx . reviewIssue ( { ... base , billing _method : 'invoice' , billable : false } ) , /ikke-fakturerbar/ ) ;
} ) ;
test ( 'mixed selection disables bulk overrides and preview instead of leaking configuration' , ( ) => {
const { ctx , controls } = fixture ( [ { id : 1 , workflow _active : true } , { id : 2 , workflow _active : false } ] ) ;
controls . settlementMethod . value = 'invoice' ; controls . prepaidCard . value = '7' ; ctx . syncWorkflowControls ( ) ;
assert . equal ( controls . setMethodBtn . disabled , true ) ; assert . equal ( controls . previewBtn . disabled , true ) ; assert . equal ( controls . stickyPreviewBtn . disabled , true ) ; assert . equal ( controls . settlementMethod . value , '' ) ; assert . equal ( controls . prepaidCard . value , '' ) ; assert . match ( controls . selectionText . textContent , /hver for sig/ ) ;
} ) ;
test ( 'preview fingerprint tracks billing inputs, filters, selection and line grouping' , ( ) => {
const { ctx , controls , state } = fixture ( [ { id : 1 , workflow _active : true } ] ) ; const initial = ctx . fingerprint ( ) ;
controls . workflowLineMode . value = 'combined' ; assert . notEqual ( ctx . fingerprint ( ) , initial ) ;
const grouped = ctx . fingerprint ( ) ; controls . filterQuery . value = 'ABA' ; assert . notEqual ( ctx . fingerprint ( ) , grouped ) ;
const filtered = ctx . fingerprint ( ) ; state . selected . add ( 2 ) ; assert . notEqual ( ctx . fingerprint ( ) , filtered ) ;
const selected = ctx . fingerprint ( ) ; controls . agreementId . value = '42' ; assert . notEqual ( ctx . fingerprint ( ) , selected ) ;
ctx . invalidatePreview ( ) ; assert . equal ( state . preview , null ) ; assert . equal ( state . previewPayload , null ) ; assert . equal ( controls . settleBtn . disabled , true ) ;
} ) ;
test ( 'settlement sends frozen preview request, never a mutated billing configuration' , async ( ) => {
2026-10-09 01:21:12 +02:00
const { ctx , controls , state } = fixture ( [ { id : 1 } ] ) ; let sent = null , sentUrl = null ; ctx . message = ( ) => { } ; ctx . api = async ( url , options ) => { sentUrl = url ; sent = JSON . parse ( options . body ) ; return { } } ; ctx . loadCustomers = async ( ) => { } ; ctx . load = async ( ) => { } ;
state . preview = { valid : true } ; state . previewPayload = { ids : [ 1 ] , line _mode : 'separate' , snapshots : [ { id : 1 , updated _at : 'reviewed' } ] } ; state . previewFingerprint = ctx . fingerprint ( ) ;
2026-10-09 00:45:21 +02:00
vm . runInContext ( html . slice ( html . indexOf ( ' async function settle(){' ) , html . indexOf ( ' function selectionChanged()' ) ) , ctx ) ;
2026-10-09 01:21:12 +02:00
await ctx . settle ( ) ; assert . match ( sentUrl , /send-reviewed$/ ) ; assert . equal ( sent . snapshots [ 0 ] . updated _at , 'reviewed' ) ; assert . equal ( sent . line _mode , 'separate' ) ; assert . deepEqual ( sent . ids , [ 1 ] ) ;
2026-10-09 00:45:21 +02:00
state . preview = { valid : true } ; state . previewPayload = { ids : [ 1 ] , billing _method : 'invoice' } ; state . previewFingerprint = ctx . fingerprint ( ) ; controls . agreementId . value = '99' ; sent = null ;
await ctx . settle ( ) ; assert . equal ( sent , null ) ; assert . equal ( state . preview , null ) ;
} ) ;
test ( 'triage and select-ready respect the visible billing filter' , ( ) => {
2026-10-09 01:21:12 +02:00
const rows = [ { id : 1 , can _approve : true , billing _method : 'invoice' , original _hours : 1 , customer _id : 9 } , { id : 2 , can _approve : true , billing _method : 'draft' , original _hours : 1 } , { id : 3 , can _approve : true , billing _method : 'prepaid' , original _hours : 1 , prepaid _card _id : 8 } ] ;
2026-10-09 00:45:21 +02:00
const { ctx , controls , state } = fixture ( rows ) ; vm . runInContext ( html . slice ( html . indexOf ( ' function methodFilteredItems(' ) , html . indexOf ( ' function render(){' ) ) , ctx ) ;
state . triage = 'action' ; assert . deepEqual ( Array . from ( ctx . visibleItems ( ) , x => x . id ) , [ 2 ] ) ; state . triage = 'ready' ; assert . deepEqual ( Array . from ( ctx . visibleItems ( ) , x => x . id ) , [ 1 , 3 ] ) ;
controls . filterMethod . value = 'prepaid' ; ctx . selectionChanged = ( ) => { } ; vm . runInContext ( html . slice ( html . indexOf ( "$('selectReadyBtn').onclick=" ) , html . indexOf ( ";['workflowLineMode'" , html . indexOf ( "$('selectReadyBtn').onclick=" ) ) ) , Object . assign ( ctx , { $ : id => id === 'selectReadyBtn' ? ( controls . selectReadyBtn ? ? = { } ) : controls [ id ] } ) ) ; controls . selectReadyBtn . onclick ( ) ; assert . deepEqual ( [ ... state . selected ] , [ 3 ] ) ;
} ) ;
test ( 'case tags escape names without inserting customer-controlled styles' , ( ) => {
const { ctx } = fixture ( ) ; ctx . esc = v => String ( v ) . replaceAll ( '<' , '<' ) . replaceAll ( '>' , '>' ) . replaceAll ( '"' , '"' ) ;
const out = ctx . caseTags ( { case _tags : [ { name : 'ABA <script>' , color : 'red;position:fixed' } ] } ) ; assert . match ( out , /ABA <script>/ ) ; assert . ok ( ! out . includes ( 'position:fixed' ) ) ; assert . match ( out , /bi-tag/ ) ;
} ) ;
2026-10-09 01:21:12 +02:00
test ( 'history is read-only even when approved entries can otherwise settle' , ( ) => {
const { ctx , state } = fixture ( [ { id : 1 , can _settle : true , can _approve : false } ] ) ; assert . equal ( ctx . actionable ( state . items [ 0 ] ) , true ) ; state . scope = 'history' ; assert . equal ( ctx . actionable ( state . items [ 0 ] ) , false ) ;
} ) ;
test ( 'approval submits reviewed snapshots without settlement' , async ( ) => {
const row = { id : 1 , can _approve : true , updated _at : '2026-10-09' , entry _fingerprint : 'canonical-reviewed-row' , billing _method : 'draft' , workflow _revision : 4 } ; const { ctx , state , controls } = fixture ( [ row ] ) ; let calls = [ ] ; ctx . api = async ( url , options ) => { calls . push ( { url , body : JSON . parse ( options . body ) } ) ; return { } } ; ctx . message = ( ) => { } ; ctx . load = async ( ) => { } ;
vm . runInContext ( html . slice ( html . indexOf ( ' async function approveSelected(){' ) , html . indexOf ( ' async function changeBilling(' ) ) , ctx ) ; await ctx . approveSelected ( ) ; assert . equal ( calls . length , 1 ) ; assert . match ( calls [ 0 ] . url , /approve-reviewed$/ ) ; assert . equal ( calls [ 0 ] . body . snapshots [ 0 ] . workflow _revision , 4 ) ; assert . equal ( calls [ 0 ] . body . snapshots [ 0 ] . billing _method , 'draft' ) ; assert . equal ( calls [ 0 ] . body . snapshots [ 0 ] . entry _fingerprint , 'canonical-reviewed-row' ) ; assert . equal ( state . selected . size , 0 ) ;
} ) ;
test ( 'new send flow previews saved choices and ignores obsolete bulk inputs' , ( ) => {
const { ctx , controls } = fixture ( [ { id : 1 } ] ) ; controls . settlementMethod . value = 'invoice' ; controls . prepaidCard . value = '9' ; controls . agreementId . value = '3' ; assert . deepEqual ( JSON . parse ( JSON . stringify ( ctx . payload ( ) ) ) , { ids : [ 1 ] , line _mode : 'separate' } ) ;
} ) ;
test ( 'permission booleans disable send and approved rows cannot change' , ( ) => {
const { ctx , controls } = fixture ( [ { id : 1 , can _approve : false , can _settle : false , can _change _billing : false , lifecycle _status : 'approved' } ] ) ; ctx . syncWorkflowControls ( ) ; assert . equal ( controls . previewBtn . disabled , true ) ; assert . equal ( controls . stickyPreviewBtn . disabled , true ) ; assert . equal ( controls . approveBtn . disabled , true ) ; assert . equal ( controls . setMethodBtn . disabled , true ) ;
} ) ;
test ( 'review snapshots bind decision identity and terminal history shows saved method' , ( ) => {
const { ctx } = fixture ( ) ; assert . equal ( ctx . approvalSnapshot ( { id : 1 , workflow _decision : { id : 15 , revision : 2 } } ) . workflow _decision _id , 15 ) ; assert . equal ( ctx . effectiveMethod ( { lifecycle _status : 'settled' , billing _method : 'fixed_price' , workflow _active : true , workflow _decision : { method : 'invoice' , status : 'overridden' } } ) , 'subscription' ) ;
} ) ;