diff --git a/mobile-oa-h5/src/market/action-policy.js b/mobile-oa-h5/src/market/action-policy.js index ec061972..3869d7ab 100644 --- a/mobile-oa-h5/src/market/action-policy.js +++ b/mobile-oa-h5/src/market/action-policy.js @@ -26,6 +26,29 @@ const ACTION_ALIASES = Object.freeze({ 'withdraw-transfer': 'withdraw' }) +const PRESIGN_TOP_ACTIONS = new Set([ + 'view', + 'create', + 'edit', + 'convert', + 'delete', + 'create-transfer' +]) + +const PRESIGN_ORDER_ACTIONS = new Set([ + 'edit', + 'convert', + 'delete', + 'create-transfer' +]) + +const PRESIGN_TRANSFER_ACTION_MAP = Object.freeze({ + edit: 'edit-presign-transfer', + delete: 'delete-transfer', + submit: 'submit-transfer', + withdraw: 'withdraw-transfer' +}) + export function moduleActions(module, role) { return [...(ACTIONS[module] || [])] } @@ -45,7 +68,9 @@ function normalizeAction(action) { } export function intersectAllowedActions(module, role, backendActions) { - const whitelist = new Set(moduleActions(module, role)) + const whitelist = module === 'presign' + ? PRESIGN_TOP_ACTIONS + : new Set(moduleActions(module, role)) return (Array.isArray(backendActions) ? backendActions : []) .map((action) => { const original = typeof action === 'string' @@ -62,36 +87,45 @@ export function intersectAllowedActions(module, role, backendActions) { export function collectAllowedActions(module, role, detail) { if (module === 'presign') { - const whitelist = new Set(moduleActions(module, role)) - const transferActionMap = { - edit: 'edit-presign-transfer', - delete: 'delete-transfer', - submit: 'submit-transfer', - withdraw: 'withdraw-transfer' - } + const frontendWhitelist = new Set(moduleActions(module, role)) const nested = [] for (const transfer of detail?.transfers || []) { for (const action of transfer?.allowedActions || []) { - const normalized = normalizeAction(action) - if (!normalized) continue - const backendCode = typeof normalized === 'string' ? normalized : normalized.code - const code = transferActionMap[backendCode] || backendCode - if (!whitelist.has(code)) continue + const backendCode = typeof action === 'string' + ? action + : String(action?.code || action?.action || action?.key || '').trim() + const code = PRESIGN_TRANSFER_ACTION_MAP[backendCode] + if (!code || !frontendWhitelist.has(code)) continue + const metadata = typeof action === 'string' + ? {} + : (() => { + const { key: _key, action: _action, ...rest } = action + return rest + })() nested.push({ - ...(typeof normalized === 'string' ? {} : normalized), + ...metadata, code, transferId: transfer.id, actionKey: `${code}:transfer:${transfer.id}` }) } } + const top = (Array.isArray(detail?.allowedActions || detail?.actions) + ? detail.allowedActions || detail.actions + : []) + .map((action) => { + const code = typeof action === 'string' + ? action + : String(action?.code || action?.action || action?.key || '').trim() + if (!PRESIGN_ORDER_ACTIONS.has(code)) return null + if (typeof action === 'string') return action + const { key: _key, action: _action, ...metadata } = action + return { ...metadata, code } + }) + .filter(Boolean) return [ ...nested, - ...intersectAllowedActions( - module, - role, - detail?.allowedActions || detail?.actions - ) + ...top ] } if (module !== 'payment') { diff --git a/mobile-oa-h5/src/market/module-actions.test.js b/mobile-oa-h5/src/market/module-actions.test.js index ca4a1de5..5c02dda2 100644 --- a/mobile-oa-h5/src/market/module-actions.test.js +++ b/mobile-oa-h5/src/market/module-actions.test.js @@ -128,7 +128,7 @@ test('presign actions never exceed the backend authorization set', () => { 'finance-confirm', 'admin-only' ]), - ['delete', 'create-transfer', 'withdraw-transfer'] + ['delete', 'create-transfer'] ) assert.equal( intersectAllowedActions('presign', 'specialist', ['edit']).includes('delete'), @@ -136,9 +136,55 @@ test('presign actions never exceed the backend authorization set', () => { ) }) +test('presign order actions reject transfer-only backend action codes', () => { + assert.deepEqual( + intersectAllowedActions('presign', 'specialist', [ + 'create', + 'edit', + 'convert', + 'delete', + 'create-transfer', + 'edit-presign-transfer', + 'delete-transfer', + 'submit-transfer', + 'withdraw-transfer' + ]), + ['create', 'edit', 'convert', 'delete', 'create-transfer'] + ) +}) + +test('presign transfer actions accept only raw transfer authorization codes without mutation', () => { + const detail = { + allowedActions: [], + transfers: [{ + id: 'transfer-1', + allowedActions: [ + 'edit', + 'delete', + 'submit', + 'withdraw', + 'convert', + 'create-transfer', + 'edit-presign-transfer' + ] + }] + } + const before = structuredClone(detail) + + const actions = collectAllowedActions('presign', 'manager', detail) + + assert.deepEqual(actions.map((item) => item.code), [ + 'edit-presign-transfer', + 'delete-transfer', + 'submit-transfer', + 'withdraw-transfer' + ]) + assert.deepEqual(detail, before) +}) + test('collects only backend-authorized presign order and nested transfer actions', () => { const actions = collectAllowedActions('presign', 'manager', { - allowedActions: ['edit', 'convert', 'delete', 'admin-only'], + allowedActions: ['create', 'edit', 'convert', 'delete', 'withdraw-transfer', 'admin-only'], transfers: [ { id: 'draft', allowedActions: ['edit', 'delete', 'submit', 'finance-confirm'] }, { id: 'pending', allowedActions: ['withdraw'] }, diff --git a/mobile-oa-h5/src/market/presign/presign-form-state.js b/mobile-oa-h5/src/market/presign/presign-form-state.js index 1053c8e1..dde4aef4 100644 --- a/mobile-oa-h5/src/market/presign/presign-form-state.js +++ b/mobile-oa-h5/src/market/presign/presign-form-state.js @@ -81,10 +81,14 @@ export function isPresignLocked(form) { return String(form?.orderStatus || '').trim() === 'converted' } -export function isPresignTransferLocked(transfer, order) { - if (isPresignLocked(order || transfer)) return true - const status = String(transfer?.payStatus || '').trim() - return Boolean(status && status !== 'dfj') +export function isPresignTransferLocked(transfer, orderOrStatus) { + const orderStatus = String( + typeof orderOrStatus === 'string' + ? orderOrStatus + : orderOrStatus?.orderStatus ?? transfer?.orderStatus ?? '' + ).trim() + if (!['', 'draft', 'active'].includes(orderStatus)) return true + return String(transfer?.payStatus || '').trim() !== 'dfj' } function attachmentUrls(transfer) { diff --git a/mobile-oa-h5/src/market/presign/presign-form-state.test.js b/mobile-oa-h5/src/market/presign/presign-form-state.test.js index dd4b097a..343f9092 100644 --- a/mobile-oa-h5/src/market/presign/presign-form-state.test.js +++ b/mobile-oa-h5/src/market/presign/presign-form-state.test.js @@ -66,17 +66,24 @@ test('changing region clears stale hospital and manager selections', () => { ) }) -test('locks converted orders and every transfer status except draft payment', () => { +test('keeps void orders editable while converted orders are locked', () => { assert.equal(isPresignLocked({ orderStatus: 'converted' }), true) assert.equal(isPresignLocked({ orderStatus: 'active' }), false) + assert.equal(isPresignLocked({ orderStatus: 'void' }), false) +}) + +test('allows draft-payment transfers only under writable parent order states', () => { assert.equal(isPresignTransferLocked({ payStatus: 'dfj' }), false) - assert.equal(isPresignTransferLocked({ payStatus: '' }), false) - assert.equal(isPresignTransferLocked({ payStatus: 'jfqrz' }), true) - assert.equal(isPresignTransferLocked({ payStatus: 'yjf' }), true) - assert.equal( - isPresignTransferLocked({ payStatus: 'dfj' }, { orderStatus: 'converted' }), - true - ) + assert.equal(isPresignTransferLocked({ payStatus: 'dfj' }, { orderStatus: 'draft' }), false) + assert.equal(isPresignTransferLocked({ payStatus: 'dfj' }, { orderStatus: 'active' }), false) + assert.equal(isPresignTransferLocked({ payStatus: 'dfj' }, 'active'), false) + + assert.equal(isPresignTransferLocked({ payStatus: 'dfj' }, { orderStatus: 'void' }), true) + assert.equal(isPresignTransferLocked({ payStatus: 'dfj' }, { orderStatus: 'converted' }), true) + assert.equal(isPresignTransferLocked({ payStatus: 'dfj' }, { orderStatus: 'unknown' }), true) + assert.equal(isPresignTransferLocked({ payStatus: '' }, { orderStatus: 'active' }), true) + assert.equal(isPresignTransferLocked({ payStatus: 'jfqrz' }, { orderStatus: 'active' }), true) + assert.equal(isPresignTransferLocked({ payStatus: 'yjf' }, { orderStatus: 'active' }), true) }) test('builds an order payload from only backend-writable camelCase fields', () => {