@ -462,18 +462,20 @@ export async function convertDocument(db: DB, userId: string, docId: string, to:
}
}
/** Only issued, unpaid documents; the consumed number is never reused. */
/** Only issued, unpaid documents; the consumed number is never reused. */
export async function cancelDocument ( db : DB , userId : string , docId : string ): Promise < Doc > {
export async function cancelDocument ( db : DB , userId : string , docId : string , reason = '' ): Promise < Doc > {
return db . transaction ( async ( ) = > {
return db . transaction ( async ( ) = > {
const before = await getDocument ( db , docId )
const before = await getDocument ( db , docId )
if ( before === null ) throw new Error ( 'Document not found' )
if ( before === null ) throw new Error ( 'Document not found' )
if ( before . docNo === null ) throw new Error ( 'Only issued documents can be cancelled' )
if ( before . docNo === null ) throw new Error ( 'Only issued documents can be cancelled' )
if ( before . status === 'cancelled' ) throw new Error ( 'Document is already cancelled' )
if ( before . status === 'cancelled' ) throw new Error ( 'Document is already cancelled' )
const why = reason . trim ( )
if ( why === '' ) throw new Error ( 'A reason is required to cancel a document' )
const alloc = ( await db . get < { n : number } > (
const alloc = ( await db . get < { n : number } > (
` SELECT COUNT(*) AS n FROM payment_allocation WHERE document_id=? ` , docId ) ) !
` SELECT COUNT(*) AS n FROM payment_allocation WHERE document_id=? ` , docId ) ) !
if ( alloc . n > 0 ) throw new Error ( 'Cannot cancel a document with payments allocated to it' )
if ( alloc . n > 0 ) throw new Error ( 'Cannot cancel a document with payments allocated to it' )
await db . run ( ` UPDATE document SET status='cancelled' WHERE id=? ` , docId )
await db . run ( ` UPDATE document SET status='cancelled' WHERE id=? ` , docId )
await addEvent ( db , docId , 'cancelled' )
await addEvent ( db , docId , 'cancelled' , { reason : why } )
await writeAudit ( db , userId , 'update' , 'document' , docId , { status : before.status } , { status : 'cancelled' } )
await writeAudit ( db , userId , 'update' , 'document' , docId , { status : before.status } , { status : 'cancelled' , reason : why } )
if ( before . docType === 'QUOTATION' ) {
if ( before . docType === 'QUOTATION' ) {
// STOP cleanup (spec §7): cancelled paper is dead — its open follow-up nudges
// STOP cleanup (spec §7): cancelled paper is dead — its open follow-up nudges
// must not chase the client. Same transaction, one audited row each.
// must not chase the client. Same transaction, one audited row each.
@ -507,7 +509,7 @@ export async function supersedeProforma(db: DB, userId: string, docId: string):
throw new Error ( ` Already converted: a live ${ child . doc_type } exists for this document ` )
throw new Error ( ` Already converted: a live ${ child . doc_type } exists for this document ` )
}
}
if ( src . docNo !== null ) {
if ( src . docNo !== null ) {
await cancelDocument ( db , userId , docId ) // issued: number stays consumed (rule 4)
await cancelDocument ( db , userId , docId , 'Superseded & recreated' ) // issued: number stays consumed (rule 4)
} else {
} else {
// Unissued draft: no number to preserve; retire it the same audited way.
// Unissued draft: no number to preserve; retire it the same audited way.
await db . run ( ` UPDATE document SET status='cancelled' WHERE id=? ` , docId )
await db . run ( ` UPDATE document SET status='cancelled' WHERE id=? ` , docId )