2026-11-02 23:27:07 +00:00
/ * *
* Real PocketBase hook smoke test . Never connects to the live instance .
* Usage : POCKETBASE _TEST _BINARY = / a b s o l u t e / p a t h / p o c k e t b a s e n o d e s c r i p t s / s m o k e / o r d e r - p a y m e n t s - h o o k s . m j s
* Downloads / installations are deliberately left to the caller . Uses the pinned
* deployment version ( 0.39 . 8 ) , an ephemeral port , and a temporary data folder .
* /
import assert from "node:assert/strict" ;
import fs from "node:fs/promises" ;
import os from "node:os" ;
import path from "node:path" ;
import net from "node:net" ;
import { spawn , spawnSync } from "node:child_process" ;
import PocketBase from "pocketbase" ;
import { createHash } from "node:crypto" ;
const binary = process . env . POCKETBASE _TEST _BINARY ;
if ( ! binary || ! path . isAbsolute ( binary ) ) throw new Error ( "Set POCKETBASE_TEST_BINARY to an absolute path to PocketBase 0.39.8." ) ;
const version = spawnSync ( binary , [ "--version" ] , { encoding : "utf8" } ) ;
assert . match ( version . stdout || "" , /0\.39\.8/ , "Use the deployed PocketBase version, 0.39.8." ) ;
const directory = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , "wfdash-payment-hooks-" ) ) ;
const port = await new Promise ( ( resolve , reject ) => {
const socket = net . createServer ( ) ;
socket . once ( "error" , reject ) ;
socket . listen ( 0 , "127.0.0.1" , ( ) => {
const address = socket . address ( ) ;
socket . close ( ( ) => resolve ( address . port ) ) ;
} ) ;
} ) ;
const origin = ` http://127.0.0.1: ${ port } ` ;
const data = path . join ( directory , "data" ) ;
const hooks = path . join ( directory , "hooks" ) ;
await fs . mkdir ( hooks ) ;
for ( const name of [ "order_payment_utils.js" , "order_payments.pb.js" , "audit_log_utils.js" ] ) {
await fs . copyFile ( path . resolve ( "pocketbase/pb_hooks" , name ) , path . join ( hooks , name ) ) ;
}
const email = "payment-test@example.test" ;
const password = "LocalPaymentTestPassword123" ;
const createAdmin = spawnSync ( binary , [ "superuser" , "create" , email , password , ` --dir= ${ data } ` ] , { encoding : "utf8" } ) ;
assert . equal ( createAdmin . status , 0 , createAdmin . stderr ) ;
const server = spawn ( binary , [ "serve" , ` --http=127.0.0.1: ${ port } ` , ` --dir= ${ data } ` , ` --hooksDir= ${ hooks } ` , "--hooksWatch=false" ] , { stdio : [ "ignore" , "pipe" , "pipe" ] } ) ;
let logs = "" ;
server . stdout . on ( "data" , ( chunk ) => { logs += chunk ; } ) ;
server . stderr . on ( "data" , ( chunk ) => { logs += chunk ; } ) ;
try {
let healthy = false ;
for ( let attempt = 0 ; attempt < 100 ; attempt ++ ) {
try { healthy = ( await fetch ( ` ${ origin } /api/health ` ) ) . ok ; } catch { /* server is starting */ }
if ( healthy ) break ;
await new Promise ( ( resolve ) => setTimeout ( resolve , 50 ) ) ;
}
assert . ok ( healthy , ` Local PocketBase failed to start: ${ logs } ` ) ;
const pb = new PocketBase ( origin ) ;
pb . autoCancellation ( false ) ;
await pb . collection ( "_superusers" ) . authWithPassword ( email , password ) ;
for ( const name of [ "orders" , "suppliers" , "purchase_orders" ] ) {
await pb . collections . create ( { name , type : "base" , fields : [
{ name : "label" , type : "text" } , { name : "documents" , type : "file" , maxSelect : 10 } , { name : "documents_history" , type : "file" , maxSelect : 10 } ,
] } ) ;
}
const order = await pb . collection ( "orders" ) . create ( { label : "Local order" } ) ;
const supplier = await pb . collection ( "suppliers" ) . create ( { label : "Local supplier" } ) ;
await pb . collections . create ( { name : "order_items" , type : "base" , fields : [
... [ "order" , "supplier" , "purchase_order" , "item_status" , "drv_status" , "drv_id" , "description" ] . map ( ( name ) => ( { name , type : "text" } ) ) ,
... [ "quantity" , "order_qty" , "cost_price" ] . map ( ( name ) => ( { name , type : "number" } ) ) ,
{ name : "order_item_details" , type : "json" , maxSize : 100000 } ,
{ name : "ready_for_collection" , type : "bool" } ,
{ name : "order_item_attachements" , type : "file" , maxSelect : 10 , maxSize : 10485760 } ,
] } ) ;
await pb . collections . create ( { name : "audit_log" , type : "base" , fields : [
... [ "collection_name" , "record_id" , "parent_type" , "parent_id" , "action" , "label" , "actor_id" , "actor_name" , "actor_email" , "source" , "refs" ] . map ( ( name ) => ( { name , type : "text" } ) ) ,
{ name : "changes" , type : "json" , maxSize : 5000000 } ,
] } ) ;
const migrate = spawnSync ( "npx" , [ "tsx" , "scripts/migrations/2026-11-02-order-payments.ts" ] , {
cwd : process . cwd ( ) , encoding : "utf8" ,
env : { ... process . env , NEXT _PUBLIC _POCKETBASE _URL : origin , PB _URL : origin , PB _ADMIN _EMAIL : email , PB _ADMIN _PASSWORD : password } ,
} ) ;
assert . equal ( migrate . status , 0 , migrate . stderr || migrate . stdout ) ;
const migrateAgain = spawnSync ( "npx" , [ "tsx" , "scripts/migrations/2026-11-02-order-payments.ts" ] , {
cwd : process . cwd ( ) , encoding : "utf8" ,
env : { ... process . env , NEXT _PUBLIC _POCKETBASE _URL : origin , PB _URL : origin , PB _ADMIN _EMAIL : email , PB _ADMIN _PASSWORD : password } ,
} ) ;
assert . equal ( migrateAgain . status , 0 , "Migration must be idempotent." ) ;
const actor = { id : "localtest" , name : "Local Test" } ;
const command = async ( body ) => ( await pb . send ( "/api/wfdash/order-payments" , { method : "POST" , body : { actor , ... body } } ) ) . request ;
const rejects = async ( fn , pattern ) => {
try { await fn ( ) ; assert . fail ( "Unexpected success" ) ; }
catch ( error ) { if ( error . code === "ERR_ASSERTION" ) throw error ; assert . match ( error . response ? . message || error . message , pattern ) ; }
} ;
2026-11-03 05:18:56 +00:00
assert . equal ( ( await pb . send ( "/api/wfdash/order-payments-capability" , { } ) ) . version , 3 ) ;
2026-11-02 23:27:07 +00:00
const base = { order : order . id , supplier : supplier . id , item _status : "processed" , quantity : 4 , order _qty : 10 , cost _price : 3 } ;
let item = await pb . collection ( "order_items" ) . create ( { ... base , item _status : "approval_needed" , description : "Local item" } ) ;
const request = await command ( { op : "require" , item _id : item . id } ) ;
assert . equal ( request . status , "draft" ) ;
assert . equal ( ( await command ( { op : "require" , item _id : item . id } ) ) . id , request . id , "Require must be idempotent." ) ;
item = await pb . collection ( "order_items" ) . getOne ( item . id ) ;
assert . equal ( item . payment _status , "required" ) ;
assert . equal ( item . item _status , "approval_needed" ) ;
await rejects ( ( ) => pb . collection ( "order_items" ) . update ( item . id , { ready _for _collection : true } ) , /payment/i ) ;
await rejects ( ( ) => pb . collection ( "order_items" ) . update ( item . id , { payment _status : "paid" } ) , /payment/i ) ;
await rejects ( ( ) => pb . collection ( "order_items" ) . update ( item . id , { item _status : "assigned" , drv _id : "driver1" } ) , /payment/i ) ;
await pb . collection ( "order_items" ) . update ( item . id , { item _status : "po_generated" , purchase _order : "localpo" } ) ;
await pb . collection ( "order_items" ) . update ( item . id , { item _status : "collection_pool" , ready _for _collection : false } ) ;
const files = new FormData ( ) ;
files . append ( "order_item_attachements+" , new Blob ( [ "invoice" ] ) , "invoice.pdf" ) ;
files . append ( "order_item_attachements+" , new Blob ( [ "proof" ] ) , "proof.pdf" ) ;
item = await pb . collection ( "order_items" ) . update ( item . id , files ) ;
const documents = item . order _item _attachements . map ( ( filename ) => ( { collection : "order_items" , recordId : item . id , filename , role : filename . startsWith ( "invoice" ) ? "support" : "proof" } ) ) ;
2026-11-03 05:18:56 +00:00
const pending = await command ( { op : "save" , request _id : request . id , revision : request . revision , documents : documents . filter ( ( doc ) => doc . role === "support" ) } ) ;
2026-11-02 23:27:07 +00:00
assert . equal ( pending . status , "pending" ) ;
2026-11-03 05:18:56 +00:00
await rejects ( ( ) => command ( { op : "paid" , request _id : pending . id , revision : pending . revision , documents : [ ] } ) , /proof/i ) ;
const paid = await command ( { op : "paid" , request _id : pending . id , revision : pending . revision , documents : documents . filter ( ( doc ) => doc . role === "proof" ) } ) ;
2026-11-02 23:27:07 +00:00
assert . equal ( paid . status , "paid" ) ;
item = await pb . collection ( "order_items" ) . getOne ( item . id ) ;
assert . equal ( item . payment _status , "paid" ) ;
assert . equal ( item . ready _for _collection , false , "Paying must not mark ready." ) ;
await rejects ( ( ) => pb . collection ( "order_items" ) . update ( item . id , { cost _price : 8 } ) , /changed|payment/i ) ;
await rejects ( ( ) => pb . collection ( "order_items" ) . update ( item . id , { supplier : "supplier2" } ) , /changed|payment/i ) ;
await pb . collection ( "order_items" ) . update ( item . id , { ready _for _collection : true } ) ;
await rejects ( ( ) => pb . collection ( "order_items" ) . update ( item . id , { "order_item_attachements-" : documents . find ( ( doc ) => doc . role === "proof" ) . filename } ) , /proof|document/i ) ;
const source = await pb . collection ( "order_items" ) . create ( base ) ;
const sourceRequest = await command ( { op : "require" , item _id : source . id } ) ;
let remainder = await pb . collection ( "order_items" ) . create ( { order : order . id , item _status : "pending" , quantity : 1 , order _qty : 1 , cost _price : 3 , order _item _details : { split : { sourceItemId : source . id } } } ) ;
remainder = await pb . collection ( "order_items" ) . getOne ( remainder . id ) ;
assert . equal ( remainder . payment _status , "required" , "JSON-field split marker must inherit a hold." ) ;
const changed = await pb . collection ( "supplier_payment_requests" ) . getOne ( sourceRequest . id ) ;
await command ( { op : "cancel" , request _id : changed . id , revision : changed . revision , reason : "Entered in error" } ) ;
await rejects ( ( ) => pb . collection ( "order_items" ) . delete ( source . id ) , /history|payment/i ) ;
// Regression: storage and deduplication are serialized with settlement by PB.
const uploadDocument = async ( r , role , content ) => {
const hash = createHash ( "sha256" ) . update ( content ) . digest ( "hex" ) . slice ( 0 , 16 ) ;
const form = new FormData ( ) ;
form . append ( "request_id" , r . id ) ;
form . append ( "revision" , String ( r . revision ) ) ;
form . append ( "role" , role ) ;
form . append ( "actor_id" , actor . id ) ;
form . append ( "actor_name" , actor . name ) ;
form . append ( "file" , new Blob ( [ content ] ) , ` pay_ ${ r . id } _ ${ role } _ ${ hash } .pdf ` ) ;
return ( await pb . send ( "/api/wfdash/order-payments-document" , { method : "POST" , body : form } ) ) . documents ;
} ;
const payItem = async ( row ) => {
2026-11-03 05:18:56 +00:00
const r = await command ( { op : "require" , item _id : row . id } ) ;
2026-11-02 23:27:07 +00:00
const support = await uploadDocument ( r , "support" , "Atomic supplier invoice" ) ;
const proof = await uploadDocument ( r , "proof" , "Atomic payment proof" ) ;
2026-11-03 05:18:56 +00:00
return command ( { op : "paid" , request _id : r . id , revision : r . revision , documents : [ ... support , ... proof ] } ) ;
2026-11-02 23:27:07 +00:00
} ;
2026-11-03 05:18:56 +00:00
// A supplier PO linked after the toggle is automatic supporting evidence.
const poForm = new FormData ( ) ;
poForm . append ( "label" , "Automatically linked supplier PO" ) ;
poForm . append ( "documents" , new Blob ( [ "Existing supplier purchase-order PDF" ] ) , "supplier_po.pdf" ) ;
const supplierPo = await pb . collection ( "purchase_orders" ) . create ( poForm ) ;
const poItem = await pb . collection ( "order_items" ) . create ( base ) ;
let poRequest = await command ( { op : "require" , item _id : poItem . id } ) ;
await pb . collection ( "order_items" ) . update ( poItem . id , { purchase _order : supplierPo . id , item _status : "po_generated" } ) ;
poRequest = await pb . collection ( "supplier_payment_requests" ) . getOne ( poRequest . id ) ;
assert . equal ( poRequest . status , "draft" , "Linked PO payment needs no operator save form." ) ;
await rejects ( ( ) => command ( { op : "paid" , request _id : poRequest . id , revision : poRequest . revision , documents : [ ] } ) , /proof/i ) ;
const poProof = await uploadDocument ( poRequest , "proof" , "Proof for automatically linked PO" ) ;
const poPaid = await command ( { op : "paid" , request _id : poRequest . id , revision : poRequest . revision , documents : poProof } ) ;
assert . equal ( poPaid . status , "paid" ) ;
assert . ok ( poPaid . documents . some ( ( doc ) => doc . collection === "purchase_orders" && doc . recordId === supplierPo . id && doc . role === "support" ) ) ;
assert . equal ( ( await pb . collection ( "order_items" ) . getOne ( poItem . id ) ) . item _status , "po_generated" ) ;
// Procurement edits invalidate stale payer tabs, without a reconfirmation form.
const changedItem = await pb . collection ( "order_items" ) . create ( { ... base , purchase _order : supplierPo . id } ) ;
const staleRequest = await command ( { op : "require" , item _id : changedItem . id } ) ;
await pb . collection ( "order_items" ) . update ( changedItem . id , { cost _price : 4 } ) ;
await rejects ( ( ) => command ( { op : "paid" , request _id : staleRequest . id , revision : staleRequest . revision , documents : [ ] } ) , /changed|refresh/i ) ;
const refreshedRequest = await pb . collection ( "supplier_payment_requests" ) . getOne ( staleRequest . id ) ;
const changedProof = await uploadDocument ( refreshedRequest , "proof" , "Proof after refreshed procurement details" ) ;
const refreshedPaid = await command ( { op : "paid" , request _id : refreshedRequest . id , revision : refreshedRequest . revision , documents : changedProof } ) ;
assert . equal ( refreshedPaid . item _snapshot [ changedItem . id ] . cost _price , 4 ) ;
// Supplier changes update request routing and retire the previous invoice.
const routedItem = await pb . collection ( "order_items" ) . create ( base ) ;
let routed = await command ( { op : "require" , item _id : routedItem . id } ) ;
const oldInvoice = await uploadDocument ( routed , "support" , "Invoice for the original supplier" ) ;
routed = await command ( { op : "save" , request _id : routed . id , revision : routed . revision , documents : oldInvoice } ) ;
const newSupplier = await pb . collection ( "suppliers" ) . create ( { label : "New supplier" } ) ;
await pb . collection ( "order_items" ) . update ( routedItem . id , { supplier : newSupplier . id } ) ;
routed = await pb . collection ( "supplier_payment_requests" ) . getOne ( routed . id ) ;
assert . equal ( routed . supplier , newSupplier . id , "Payment metadata must follow the current single-item supplier." ) ;
assert . equal ( routed . documents . some ( ( doc ) => doc . filename === oldInvoice [ 0 ] . filename ) , false ) ;
assert . ok ( JSON . stringify ( routed . history ) . includes ( oldInvoice [ 0 ] . filename ) , "Retired evidence must remain in history." ) ;
assert . ok ( ( await pb . collection ( "order_items" ) . getOne ( routedItem . id ) ) . order _item _attachements . includes ( oldInvoice [ 0 ] . filename ) , "Retiring evidence must not remove its file." ) ;
const newSupplierProof = await uploadDocument ( routed , "proof" , "Proof for the new supplier" ) ;
await rejects ( ( ) => command ( { op : "paid" , request _id : routed . id , revision : routed . revision , documents : newSupplierProof } ) , /supplier invoice|support/i ) ;
const newInvoice = await uploadDocument ( routed , "support" , "Invoice from the new supplier" ) ;
routed = await command ( { op : "save" , request _id : routed . id , revision : routed . revision , documents : newInvoice } ) ;
assert . equal ( ( await command ( { op : "paid" , request _id : routed . id , revision : routed . revision , documents : newSupplierProof } ) ) . status , "paid" ) ;
2026-11-02 23:27:07 +00:00
// Match splitOrderItemRemainder's create-then-shrink ordering exactly.
const splitSource = await pb . collection ( "order_items" ) . create ( { ... base , quantity : 20 , order _qty : 10 } ) ;
const splitPayment = await payItem ( splitSource ) ;
const paidRemainder = await pb . collection ( "order_items" ) . create ( { order : order . id , item _status : "pending" , quantity : 10 , order _qty : 10 , cost _price : 3 , order _item _details : { split : { sourceItemId : splitSource . id } } } ) ;
const shrunk = await pb . collection ( "order_items" ) . update ( splitSource . id , { quantity : 10 , order _item _details : { split : { remainderItemId : paidRemainder . id } } } ) ;
assert . equal ( shrunk . quantity + paidRemainder . quantity , 20 , "Paid split must conserve the original customer quantity." ) ;
assert . equal ( paidRemainder . payment _status , "" , "A paid source must not automatically require or settle its new remainder." ) ;
await rejects ( ( ) => pb . collection ( "order_items" ) . update ( splitSource . id , { order _qty : 20 } ) , /changed|payment/i ) ;
const oldProof = splitPayment . documents . find ( ( doc ) => doc . role === "proof" ) ;
const withoutOldProof = ( ) => pb . collection ( "order_items" ) . update ( splitSource . id , { "order_item_attachements-" : oldProof . filename } ) ;
let freshRequirement = await command ( { op : "require" , item _id : splitSource . id } ) ;
await rejects ( withoutOldProof , /proof/i ) ;
await command ( { op : "cancel" , request _id : freshRequirement . id , revision : freshRequirement . revision , reason : "Payment correction cancelled" } ) ;
await rejects ( withoutOldProof , /proof/i ) ;
const secondPayment = await payItem ( await pb . collection ( "order_items" ) . getOne ( splitSource . id ) ) ;
assert . notEqual ( secondPayment . id , splitPayment . id ) ;
await rejects ( withoutOldProof , /proof/i ) ;
assert . equal ( ( await fetch ( ` ${ origin } /api/files/order_items/ ${ splitSource . id } / ${ oldProof . filename } ` ) ) . status , 200 ) ;
const groupedA = await pb . collection ( "order_items" ) . create ( base ) ;
const groupedB = await pb . collection ( "order_items" ) . create ( base ) ;
const groupedReqA = await command ( { op : "require" , item _id : groupedA . id } ) ;
const groupedReqB = await command ( { op : "require" , item _id : groupedB . id } ) ;
let grouped = await command ( { op : "combine" , request _ids : [ groupedReqA . id , groupedReqB . id ] , revisions : { [ groupedReqA . id ] : groupedReqA . revision , [ groupedReqB . id ] : groupedReqB . revision } } ) ;
const groupedSupport = await uploadDocument ( grouped , "support" , "One invoice for both selected items" ) ;
2026-11-03 05:18:56 +00:00
grouped = await command ( { op : "save" , request _id : grouped . id , revision : grouped . revision , documents : groupedSupport } ) ;
2026-11-02 23:27:07 +00:00
const [ overlapA , overlapB ] = await Promise . all ( [
uploadDocument ( grouped , "proof" , "The same overlapping proof" ) ,
uploadDocument ( grouped , "proof" , "The same overlapping proof" ) ,
] ) ;
assert . deepEqual ( overlapA , overlapB , "Overlapping uploads must return exactly the same stored names." ) ;
for ( const member of [ groupedA , groupedB ] ) {
const files = ( await pb . collection ( "order_items" ) . getOne ( member . id ) ) . order _item _attachements ;
assert . equal ( files . length , 2 , "Grouped concurrent uploads must append one support and one proof, without duplicates." ) ;
assert . equal ( files . filter ( ( name ) => name . startsWith ( ` pay_ ${ grouped . id } _proof_ ` ) ) . length , 1 ) ;
}
2026-11-03 05:18:56 +00:00
await command ( { op : "paid" , request _id : grouped . id , revision : grouped . revision , documents : overlapA } ) ;
2026-11-02 23:27:07 +00:00
await rejects ( ( ) => uploadDocument ( grouped , "proof" , "The same overlapping proof" ) , /paid|changed|refresh/i ) ;
for ( const member of [ groupedA , groupedB ] ) {
assert . equal ( ( await pb . collection ( "order_items" ) . getOne ( member . id ) ) . order _item _attachements . length , 2 , "An upload attempted after settlement must append nothing." ) ;
}
const audits = await pb . collection ( "audit_log" ) . getFullList ( ) ;
assert . ok ( audits . some ( ( row ) => row . actor _name === "Local Test" && row . collection _name === "supplier_payment_requests" ) ) ;
console . log ( "PASS: real PB 0.39.8 payment commands, readiness/dispatch guards, proof, split inheritance, immutable historical proof, paid split conservation, overlapping upload deduplication and migration idempotence." ) ;
} catch ( error ) {
console . error ( logs . slice ( - 8000 ) ) ;
throw error ;
} finally {
server . kill ( "SIGTERM" ) ;
if ( server . exitCode === null && server . signalCode === null ) await new Promise ( ( resolve ) => server . once ( "exit" , resolve ) ) ;
await fs . rm ( directory , { recursive : true , force : true } ) ;
}