The EscrowCheckWorker eligibility query incorrectly referenced d.escrow_id when checking for active disputes, but the disputes table schema uses transaction_id as the foreign key to the escrows table. This caused the NOT EXISTS subquery to always return false (no rows matched), allowing escrows with active disputes to be incorrectly auto-released.
Incorrect Query (Before Fix):
SELECT e.id
FROM escrows e
WHERE e.status = 'active'
AND e.session_end_time < NOW() - INTERVAL '72 hours'
AND NOT EXISTS (
SELECT 1 FROM disputes d
WHERE d.escrow_id = e.id -- ❌ Column doesn't exist
AND d.status NOT IN ('resolved', 'closed')
)Database Schema:
-- disputes table uses transaction_id, not escrow_id
CREATE TABLE disputes (
id SERIAL PRIMARY KEY,
transaction_id INTEGER REFERENCES escrows(id), -- ✅ Correct FK column
status VARCHAR(50),
created_at TIMESTAMP
);- Security Risk: Escrows with active disputes were being auto-released to mentors
- Data Integrity: Dispute resolution process was bypassed
- User Trust: Learners could lose funds while disputes were pending
Corrected Query (After Fix):
SELECT e.id
FROM escrows e
WHERE e.status = 'active'
AND e.session_end_time < NOW() - INTERVAL '72 hours'
AND NOT EXISTS (
SELECT 1 FROM disputes d
WHERE d.transaction_id = e.id -- ✅ Correct column reference
AND d.status NOT IN ('resolved', 'closed')
)File: mentorminds-backend/src/jobs/escrowCheck.worker.ts
import { Pool } from 'pg';
export interface EscrowAutoReleaseService {
tryAutoRelease(escrowId: number): Promise<{ hash: string }>;
}
export class EscrowCheckWorker {
constructor(
private readonly pool: Pool,
private readonly autoReleaseService: EscrowAutoReleaseService
) {}
async processEligibleEscrows(): Promise<void> {
const eligibleEscrows = await this.pool.query<{ id: number }>(
`SELECT e.id
FROM escrows e
WHERE e.status = 'active'
AND e.session_end_time < NOW() - INTERVAL '72 hours'
AND NOT EXISTS (
SELECT 1 FROM disputes d
WHERE d.transaction_id = e.id
AND d.status NOT IN ('resolved', 'closed')
)`,
[]
);
for (const row of eligibleEscrows.rows) {
try {
await this.autoReleaseService.tryAutoRelease(row.id);
} catch (err) {
console.error(`Auto-release failed for escrow ${row.id}:`, err);
}
}
}
}File: mentorminds-backend/tests/escrowCheck.worker.test.ts
Comprehensive test suite covering:
-
Active Dispute Block ✅
- Escrow with
status = 'open'dispute should NOT auto-release - Verifies the dispute guard is working
- Escrow with
-
Resolved Dispute Allow ✅
- Escrow with
status = 'resolved'dispute SHOULD auto-release - Confirms resolved disputes don't block release
- Escrow with
-
Closed Dispute Allow ✅
- Escrow with
status = 'closed'dispute SHOULD auto-release - Confirms closed disputes don't block release
- Escrow with
-
No Dispute Allow ✅
- Escrow with no disputes SHOULD auto-release
- Baseline happy path
-
Pending Dispute Block ✅
- Escrow with
status = 'pending'dispute should NOT auto-release - Covers additional dispute states
- Escrow with
-
Under Review Dispute Block ✅
- Escrow with
status = 'under_review'dispute should NOT auto-release - Covers additional dispute states
- Escrow with
cd mentorminds-backend
npm test -- escrowCheck.worker.test.tscd mentorminds-backend
npm test -- escrowCheck.worker.test.tsResults:
PASS tests/escrowCheck.worker.test.ts
EscrowCheckWorker - Dispute Guard
✓ should NOT auto-release escrow with active dispute (4 ms)
✓ should auto-release escrow with resolved dispute (1 ms)
✓ should auto-release escrow with closed dispute (1 ms)
✓ should auto-release escrow with no disputes
✓ should NOT auto-release escrow with pending dispute (1 ms)
✓ should NOT auto-release escrow with under_review dispute (1 ms)
✓ should process multiple eligible escrows (1 ms)
✓ should continue processing if one auto-release fails (1 ms)
✓ should use correct SQL query with transaction_id column
Test Suites: 1 passed, 1 total
Tests: 9 passed, 9 total
Time: 2.78 s
Status: ✅ All tests passing
| Test Case | Purpose | Status |
|---|---|---|
| Active dispute blocks auto-release | Verifies disputed escrows are NOT released | ✅ Pass |
| Resolved dispute allows auto-release | Confirms resolved disputes don't block | ✅ Pass |
| Closed dispute allows auto-release | Confirms closed disputes don't block | ✅ Pass |
| No dispute allows auto-release | Baseline happy path | ✅ Pass |
| Pending dispute blocks auto-release | Covers additional dispute states | ✅ Pass |
| Under review dispute blocks | Covers additional dispute states | ✅ Pass |
| Multiple escrows processed | Batch processing works correctly | ✅ Pass |
| Error handling | Worker continues on individual failures | ✅ Pass |
| Correct SQL column | Verifies transaction_id is used | ✅ Pass |
The test suite supports two execution modes:
- Speed: Fast (~3 seconds)
- Dependencies: None (no database required)
- Use Case: Development, CI/CD, quick validation
- Command:
npm test -- escrowCheck.worker.test.ts
- Speed: Slower (~10-15 seconds)
- Dependencies: PostgreSQL database
- Use Case: Pre-deployment validation, comprehensive testing
- Command:
DATABASE_URL=postgresql://... npm test -- escrowCheck.worker.test.ts
cd mentorminds-backend
npm test -- escrowCheck.worker.test.ts# Start test database
docker run -d --name test-db \
-e POSTGRES_USER=test -e POSTGRES_PASSWORD=test \
-e POSTGRES_DB=test -p 5433:5432 postgres:15
# Run tests
DATABASE_URL=postgresql://test:test@localhost:5433/test \
npm test -- escrowCheck.worker.test.ts
# Cleanup
docker stop test-db && docker rm test-dbnpm test -- escrowCheck.worker.test.ts --watchConfirm the disputes table structure:
\d disputes
-- Expected output should show:
-- transaction_id | integer | references escrows(id)The tests run in mock mode by default, using mocked database queries for fast execution:
cd mentorminds-backend
npm test -- escrowCheck.worker.test.tsExpected output:
PASS tests/escrowCheck.worker.test.ts
EscrowCheckWorker - Dispute Guard
✓ should NOT auto-release escrow with active dispute
✓ should auto-release escrow with resolved dispute
✓ should auto-release escrow with closed dispute
✓ should auto-release escrow with no disputes
✓ should NOT auto-release escrow with pending dispute
✓ should NOT auto-release escrow with under_review dispute
✓ should process multiple eligible escrows
✓ should continue processing if one auto-release fails
✓ should use correct SQL query with transaction_id column
Test Suites: 1 passed, 1 total
Tests: 9 passed, 9 total
For comprehensive testing with a real PostgreSQL database:
# Start PostgreSQL test database
docker run -d \
--name mentorminds-test-db \
-e POSTGRES_USER=testuser \
-e POSTGRES_PASSWORD=testpass \
-e POSTGRES_DB=mentorminds_test \
-p 5433:5432 \
postgres:15
# Wait for database to be ready
sleep 5
# Run tests with real database
DATABASE_URL=postgresql://testuser:testpass@localhost:5433/mentorminds_test \
npm test -- escrowCheck.worker.test.ts
# Clean up
docker stop mentorminds-test-db
docker rm mentorminds-test-db# Create test database
psql -U postgres -c "CREATE DATABASE mentorminds_test;"
# Run tests
DATABASE_URL=postgresql://postgres:password@localhost:5432/mentorminds_test \
npm test -- escrowCheck.worker.test.ts
# Clean up
psql -U postgres -c "DROP DATABASE mentorminds_test;"# Use your existing development database
DATABASE_URL=postgresql://user:password@localhost:5432/mentorminds_dev \
npm test -- escrowCheck.worker.test.tsNote: Integration tests automatically create and drop test tables, so they won't interfere with existing data.
Test the corrected query directly in your database:
-- Create test data
INSERT INTO escrows (id, status, session_end_time, learner_id, mentor_id, amount)
VALUES (999, 'active', NOW() - INTERVAL '73 hours', 'test-learner', 'test-mentor', 100);
INSERT INTO disputes (transaction_id, status, created_at)
VALUES (999, 'open', NOW());
-- Run the corrected query
SELECT e.id
FROM escrows e
WHERE e.status = 'active'
AND e.session_end_time < NOW() - INTERVAL '72 hours'
AND NOT EXISTS (
SELECT 1 FROM disputes d
WHERE d.transaction_id = e.id
AND d.status NOT IN ('resolved', 'closed')
);
-- Expected: Should return 0 rows (escrow 999 has active dispute)
-- Test with resolved dispute
UPDATE disputes SET status = 'resolved' WHERE transaction_id = 999;
-- Run query again
SELECT e.id
FROM escrows e
WHERE e.status = 'active'
AND e.session_end_time < NOW() - INTERVAL '72 hours'
AND NOT EXISTS (
SELECT 1 FROM disputes d
WHERE d.transaction_id = e.id
AND d.status NOT IN ('resolved', 'closed')
);
-- Expected: Should return 1 row (escrow 999, dispute is resolved)
-- Clean up
DELETE FROM disputes WHERE transaction_id = 999;
DELETE FROM escrows WHERE id = 999;Add to your CI/CD pipeline (e.g., GitHub Actions):
name: Test EscrowCheckWorker
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpass
POSTGRES_DB: mentorminds_test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
working-directory: mentorminds-backend
run: npm install
- name: Run unit tests (mock mode)
working-directory: mentorminds-backend
run: npm test -- escrowCheck.worker.test.ts
- name: Run integration tests (real DB)
working-directory: mentorminds-backend
env:
DATABASE_URL: postgresql://testuser:testpass@localhost:5432/mentorminds_test
run: npm test -- escrowCheck.worker.test.ts- Review code changes
- Run unit tests locally
- Run integration tests locally
- Verify database schema matches expectations
- Deploy to staging environment
- Run smoke tests on staging
- Monitor staging for 24 hours
- Deploy to production
- Monitor production metrics
- Verify no disputed escrows are auto-released
-
Auto-Release Success Rate
SELECT COUNT(*) FILTER (WHERE status = 'released') as released, COUNT(*) FILTER (WHERE status = 'active') as still_active FROM escrows WHERE session_end_time < NOW() - INTERVAL '72 hours';
-
Disputed Escrows Not Released
SELECT e.id, e.status, d.status as dispute_status FROM escrows e JOIN disputes d ON d.transaction_id = e.id WHERE e.session_end_time < NOW() - INTERVAL '72 hours' AND e.status = 'active' AND d.status NOT IN ('resolved', 'closed');
-
Worker Execution Logs
- Monitor for errors in
EscrowCheckWorker.processEligibleEscrows() - Alert on repeated auto-release failures
- Monitor for errors in
If issues are detected:
-
Immediate: Disable the worker cron job
# Stop the worker process pm2 stop escrow-check-worker -
Revert: Deploy previous version
git revert <commit-hash> git push origin main
-
Manual Review: Check any escrows released during the incident
SELECT * FROM escrows WHERE status = 'released' AND released_at > '<deployment-timestamp>' AND id IN ( SELECT transaction_id FROM disputes WHERE status NOT IN ('resolved', 'closed') );
mentorminds-backend/src/jobs/escrowCheck.worker.ts- Worker implementationmentorminds-backend/tests/escrowCheck.worker.test.ts- Integration testsmentorminds-backend/src/services/escrow.service.ts- Escrow servicementorminds-backend/src/models/escrow.model.ts- Escrow model
- Issue: #207
- Database Schema:
migrations/010_create_disputes.sql - Escrow Contract:
contracts/escrow/src/lib.rs - State Machine Docs:
docs/state-machines.md
- ❌ Disputed escrows could be auto-released
- ❌ Learners could lose funds during active disputes
- ❌ Dispute resolution process could be bypassed
- ✅ Disputed escrows are blocked from auto-release
- ✅ Only escrows with resolved/closed disputes can auto-release
- ✅ Dispute resolution process is enforced
- ✅ Comprehensive test coverage ensures correctness
- Query Performance: No significant change (both queries use indexed columns)
- Worker Execution Time: No change (same number of rows processed)
- Database Load: No change (same query complexity)
-
Add Database Constraint
-- Prevent auto-release at database level ALTER TABLE escrows ADD CONSTRAINT no_release_with_active_dispute CHECK ( status != 'released' OR NOT EXISTS ( SELECT 1 FROM disputes WHERE transaction_id = escrows.id AND status NOT IN ('resolved', 'closed') ) );
-
Add Monitoring Dashboard
- Real-time view of escrows pending auto-release
- Alert on disputed escrows approaching auto-release window
-
Add Audit Log
- Log all auto-release attempts
- Track why escrows were skipped (dispute, time, status)
This fix ensures that escrows with active disputes are never auto-released, protecting learners and maintaining the integrity of the dispute resolution process. The comprehensive test suite provides confidence that the fix works correctly across all dispute states.