@@ -242,6 +242,205 @@ describe('PoliciesService', () => {
242242 } ) ;
243243 } ) ;
244244
245+ describe ( 'acceptChanges' , ( ) => {
246+ const buildPendingPolicy = ( overrides : Record < string , unknown > = { } ) => ( {
247+ id : 'pol_1' ,
248+ organizationId : 'org_abc' ,
249+ pendingVersionId : 'ver_1' ,
250+ approverId : 'mem_approver' ,
251+ frequency : 'yearly' ,
252+ ...overrides ,
253+ } ) ;
254+
255+ const mockTransactionTx = ( ) => {
256+ db . $transaction . mockImplementation (
257+ async ( callback : ( tx : unknown ) => Promise < unknown > ) => {
258+ const tx = {
259+ policyVersion : { update : db . policyVersion . update } ,
260+ policy : { update : db . policy . update } ,
261+ } ;
262+ return callback ( tx ) ;
263+ } ,
264+ ) ;
265+ } ;
266+
267+ it ( 'publishes the pending version on a successful approve' , async ( ) => {
268+ const pendingVersion = {
269+ id : 'ver_1' ,
270+ version : 2 ,
271+ content : [ { type : 'paragraph' } ] ,
272+ } ;
273+ db . policy . findUnique . mockResolvedValueOnce ( buildPendingPolicy ( ) ) ;
274+ db . policyVersion . findUnique . mockResolvedValueOnce ( pendingVersion ) ;
275+ db . member . findFirst . mockResolvedValueOnce ( { id : 'mem_caller' } ) ;
276+ mockTransactionTx ( ) ;
277+
278+ const result = await service . acceptChanges (
279+ 'pol_1' ,
280+ 'org_abc' ,
281+ { approverId : 'mem_approver' } ,
282+ 'usr_caller' ,
283+ ) ;
284+
285+ expect ( result ) . toEqual ( { versionId : 'ver_1' , version : 2 } ) ;
286+ expect ( db . policyVersion . update ) . toHaveBeenCalledWith ( {
287+ where : { id : 'ver_1' } ,
288+ data : { publishedById : 'mem_caller' } ,
289+ } ) ;
290+ const policyUpdateArg = db . policy . update . mock . calls [ 0 ] [ 0 ] ;
291+ expect ( policyUpdateArg . data . status ) . toBe ( 'published' ) ;
292+ expect ( policyUpdateArg . data . currentVersionId ) . toBe ( 'ver_1' ) ;
293+ expect ( policyUpdateArg . data . pendingVersionId ) . toBeNull ( ) ;
294+ expect ( policyUpdateArg . data . approverId ) . toBeNull ( ) ;
295+ expect ( policyUpdateArg . data . signedBy ) . toEqual ( [ ] ) ;
296+ } ) ;
297+
298+ it ( 'succeeds when called via session impersonation — caller userId differs from approverId' , async ( ) => {
299+ // Simulates an admin impersonating the assigned approver:
300+ // the impersonated session's userId belongs to the approver, but
301+ // the authorization check only requires the body-supplied approverId
302+ // to match policy.approverId — which it does.
303+ const pendingVersion = {
304+ id : 'ver_1' ,
305+ version : 2 ,
306+ content : [ ] ,
307+ } ;
308+ db . policy . findUnique . mockResolvedValueOnce ( buildPendingPolicy ( ) ) ;
309+ db . policyVersion . findUnique . mockResolvedValueOnce ( pendingVersion ) ;
310+ db . member . findFirst . mockResolvedValueOnce ( { id : 'mem_impersonated' } ) ;
311+ mockTransactionTx ( ) ;
312+
313+ const result = await service . acceptChanges (
314+ 'pol_1' ,
315+ 'org_abc' ,
316+ { approverId : 'mem_approver' } ,
317+ 'usr_impersonated' ,
318+ ) ;
319+
320+ expect ( result ) . toEqual ( { versionId : 'ver_1' , version : 2 } ) ;
321+ expect ( db . policyVersion . update ) . toHaveBeenCalledWith ( {
322+ where : { id : 'ver_1' } ,
323+ data : { publishedById : 'mem_impersonated' } ,
324+ } ) ;
325+ } ) ;
326+
327+ it ( 'rejects when the body approverId does not match the assigned approver' , async ( ) => {
328+ db . policy . findUnique . mockResolvedValueOnce ( buildPendingPolicy ( ) ) ;
329+
330+ await expect (
331+ service . acceptChanges ( 'pol_1' , 'org_abc' , { approverId : 'mem_wrong' } ) ,
332+ ) . rejects . toThrow ( / o n l y t h e a s s i g n e d a p p r o v e r / i) ;
333+
334+ expect ( db . $transaction ) . not . toHaveBeenCalled ( ) ;
335+ } ) ;
336+
337+ it ( 'self-heals stale approverId when no pending version exists' , async ( ) => {
338+ const orgId = 'org_abc' ;
339+ const approverId = 'mem_approver' ;
340+ const stalePolicy = {
341+ id : 'pol_1' ,
342+ organizationId : orgId ,
343+ pendingVersionId : null ,
344+ approverId,
345+ } ;
346+ db . policy . findUnique . mockResolvedValueOnce ( stalePolicy ) ;
347+ db . policy . update . mockResolvedValueOnce ( { ...stalePolicy , approverId : null } ) ;
348+
349+ await expect (
350+ service . acceptChanges ( 'pol_1' , orgId , { approverId } ) ,
351+ ) . rejects . toThrow ( / n o p e n d i n g c h a n g e s / i) ;
352+
353+ expect ( db . policy . update ) . toHaveBeenCalledWith ( {
354+ where : { id : 'pol_1' } ,
355+ data : { approverId : null } ,
356+ } ) ;
357+ expect ( db . $transaction ) . not . toHaveBeenCalled ( ) ;
358+ } ) ;
359+
360+ it ( 'throws without mutating when the policy has no approval state at all' , async ( ) => {
361+ const orgId = 'org_abc' ;
362+ const cleanPolicy = {
363+ id : 'pol_1' ,
364+ organizationId : orgId ,
365+ pendingVersionId : null ,
366+ approverId : null ,
367+ } ;
368+ db . policy . findUnique . mockResolvedValueOnce ( cleanPolicy ) ;
369+
370+ await expect (
371+ service . acceptChanges ( 'pol_1' , orgId , { approverId : 'mem_x' } ) ,
372+ ) . rejects . toThrow ( / n o p e n d i n g v e r s i o n / i) ;
373+
374+ expect ( db . policy . update ) . not . toHaveBeenCalled ( ) ;
375+ } ) ;
376+ } ) ;
377+
378+ describe ( 'denyChanges' , ( ) => {
379+ it ( 'reverts to draft on a successful deny when never published' , async ( ) => {
380+ db . policy . findUnique . mockResolvedValueOnce ( {
381+ id : 'pol_1' ,
382+ organizationId : 'org_abc' ,
383+ pendingVersionId : 'ver_1' ,
384+ approverId : 'mem_approver' ,
385+ lastPublishedAt : null ,
386+ } ) ;
387+ db . policy . update . mockResolvedValueOnce ( { } ) ;
388+
389+ const result = await service . denyChanges ( 'pol_1' , 'org_abc' , {
390+ approverId : 'mem_approver' ,
391+ } ) ;
392+
393+ expect ( result ) . toEqual ( { status : 'draft' } ) ;
394+ expect ( db . policy . update ) . toHaveBeenCalledWith ( {
395+ where : { id : 'pol_1' } ,
396+ data : {
397+ status : 'draft' ,
398+ pendingVersionId : null ,
399+ approverId : null ,
400+ } ,
401+ } ) ;
402+ } ) ;
403+
404+ it ( 'reverts to published on a successful deny when previously published' , async ( ) => {
405+ db . policy . findUnique . mockResolvedValueOnce ( {
406+ id : 'pol_1' ,
407+ organizationId : 'org_abc' ,
408+ pendingVersionId : 'ver_2' ,
409+ approverId : 'mem_approver' ,
410+ lastPublishedAt : new Date ( '2026-01-01' ) ,
411+ } ) ;
412+ db . policy . update . mockResolvedValueOnce ( { } ) ;
413+
414+ const result = await service . denyChanges ( 'pol_1' , 'org_abc' , {
415+ approverId : 'mem_approver' ,
416+ } ) ;
417+
418+ expect ( result ) . toEqual ( { status : 'published' } ) ;
419+ } ) ;
420+
421+ it ( 'self-heals stale approverId when no pending version exists' , async ( ) => {
422+ const orgId = 'org_abc' ;
423+ const approverId = 'mem_approver' ;
424+ const stalePolicy = {
425+ id : 'pol_1' ,
426+ organizationId : orgId ,
427+ pendingVersionId : null ,
428+ approverId,
429+ } ;
430+ db . policy . findUnique . mockResolvedValueOnce ( stalePolicy ) ;
431+ db . policy . update . mockResolvedValueOnce ( { ...stalePolicy , approverId : null } ) ;
432+
433+ await expect (
434+ service . denyChanges ( 'pol_1' , orgId , { approverId } ) ,
435+ ) . rejects . toThrow ( / n o p e n d i n g c h a n g e s / i) ;
436+
437+ expect ( db . policy . update ) . toHaveBeenCalledWith ( {
438+ where : { id : 'pol_1' } ,
439+ data : { approverId : null } ,
440+ } ) ;
441+ } ) ;
442+ } ) ;
443+
245444 describe ( 'createVersion' , ( ) => {
246445 const organizationId = 'org_123' ;
247446 const policyId = 'pol_1' ;
0 commit comments