@ -1,6 +1,6 @@
import { Router , type Request , type RequestHandler , type Response } from 'express'
import { Router , type Request , type RequestHandler , type Response } from 'express'
import { fyOf , validateGstin } from '@sims/domain'
import { fyOf , validateGstin } from '@sims/domain'
import { isManagerial , login , requireAuth , require Owner } from './auth'
import { isManagerial , login , requireAuth , require Managerial, require Owner } from './auth'
import type { DB } from './db'
import type { DB } from './db'
import {
import {
createClient , getClient , listClients , revealClientDbPassword , setClientDbPassword ,
createClient , getClient , listClients , revealClientDbPassword , setClientDbPassword ,
@ -947,7 +947,8 @@ export function apiRouter(
}
}
return convertDocument ( db , staffId ( res ) , id , to )
return convertDocument ( db , staffId ( res ) , id , to )
} ) )
} ) )
r . post ( '/documents/:id/cancel' , requireAuth , docAction ( ( id , res ) = >
// Cancel is financially destructive (consumes a document number) — managerial only (D24).
r . post ( '/documents/:id/cancel' , requireAuth , requireManagerial , docAction ( ( id , res ) = >
cancelDocument ( db , staffId ( res ) , id ) ) )
cancelDocument ( db , staffId ( res ) , id ) ) )
// Supersede & recreate (spec §8): proforma-only cancel + fresh linked draft.
// Supersede & recreate (spec §8): proforma-only cancel + fresh linked draft.
r . post ( '/documents/:id/supersede' , requireAuth , docAction ( ( id , res ) = >
r . post ( '/documents/:id/supersede' , requireAuth , docAction ( ( id , res ) = >
@ -1008,15 +1009,18 @@ export function apiRouter(
}
}
} ) ( )
} ) ( )
} )
} )
r . post ( '/documents/:id/credit-note' , requireAuth , docAction ( ( id , res , req ) = > {
// A credit note reverses money on an issued invoice — managerial only (D24).
r . post ( '/documents/:id/credit-note' , requireAuth , requireManagerial , docAction ( ( id , res , req ) = > {
const { lines } = req . body as { lines? : DraftLineInput [ ] }
const { lines } = req . body as { lines? : DraftLineInput [ ] }
return createCreditNote ( db , staffId ( res ) , id , lines )
return createCreditNote ( db , staffId ( res ) , id , lines )
} ) )
} ) )
// ---------- shareable links ----------
// ---------- shareable links ----------
// Owner mints an opaque public token for one document (default +30d expiry). The
// Owner/manager mints an opaque public token for one document (default +30d expiry).
// token is returned to the authenticated owner (they build the link) but never logged.
// Minting an UNAUTHENTICATED public link to a client's document is privileged (D24) —
r . post ( '/documents/:id/share' , requireAuth , async ( req , res ) = > {
// gated managerial so any staff account cannot exfiltrate any invoice as a public PDF.
// The token is returned to the caller (they build the link) but never logged.
r . post ( '/documents/:id/share' , requireAuth , requireManagerial , async ( req , res ) = > {
const id = String ( req . params [ 'id' ] ? ? '' )
const id = String ( req . params [ 'id' ] ? ? '' )
if ( ( await getDocument ( db , id ) ) === null ) { res . status ( 404 ) . json ( { ok : false , error : 'Document not found' } ) ; return }
if ( ( await getDocument ( db , id ) ) === null ) { res . status ( 404 ) . json ( { ok : false , error : 'Document not found' } ) ; return }
try {
try {
@ -1232,7 +1236,14 @@ export function apiRouter(
} )
} )
r . patch ( '/interactions/:id' , requireAuth , async ( req , res ) = > {
r . patch ( '/interactions/:id' , requireAuth , async ( req , res ) = > {
const id = String ( req . params [ 'id' ] ? ? '' )
const id = String ( req . params [ 'id' ] ? ? '' )
if ( ( await getInteraction ( db , id ) ) === null ) { res . status ( 404 ) . json ( { ok : false , error : 'Interaction not found' } ) ; return }
const existing = await getInteraction ( db , id )
if ( existing === null ) { res . status ( 404 ) . json ( { ok : false , error : 'Interaction not found' } ) ; return }
// D24: only the author or a managerial role may edit a logged interaction —
// staff cannot overwrite another employee's call/visit record.
const viewer = res . locals [ 'staff' ] as { id : string ; role : string }
if ( ! isManagerial ( viewer . role ) && existing . staffId !== viewer . id ) {
res . status ( 403 ) . json ( { ok : false , error : 'You can only edit interactions you logged' } ) ; return
}
try {
try {
res . json ( { ok : true , interaction : await updateInteraction ( db , staffId ( res ) , id , req . body as InteractionPatch ) } )
res . json ( { ok : true , interaction : await updateInteraction ( db , staffId ( res ) , id , req . body as InteractionPatch ) } )
} catch ( err ) {
} catch ( err ) {