Imported from Agwanyanjaba/peacepayout-backend (
AGENTS.md). Install upstream withnpx skills add Agwanyanjaba/peacepayout-backend. Copyright stays with the author.
AGENT.md - Peace Payout Development Guide
Project Overview
Peace Payout is a USSD-based dispute resolution system for conflict-affected regions in Africa (Sahel, DRC, Sudan, Mozambique). It allows community members to resolve disputes over land, water, grazing, debt, and marriage through a transparent, jury-based system that works on any feature phone (2G networks, no internet required).
Core Promise: Transform conflict into community wealth through economic incentives and anonymous jury voting.
Development Environment Setup
Prerequisites
Node.js 20+
npm
PostgreSQL 15+ (local or cloud)
Redis (for session management)
Africa's Talking account (sandbox)
Initial Setup
# Clone and install
git clone <repo-url>
cd peace-payout
npm install
# Environment configuration
cp .env.example .env
# Edit .env with your database and credentials
# Database setup with Prisma
npx prisma migrate dev --name init
npx prisma generate
# Start development server
npm run dev
Environment Variables
# Required - Database (PostgreSQL)
DATABASE_URL="postgresql://postgres:password@localhost:5432/peace_payout?schema=public"
# Required - Redis
REDIS_URL=redis://localhost:6379
# Required - Africa's Talking
AT_API_KEY=your_api_key
AT_USERNAME=sandbox
AT_USSD_CODE=*485#
# Required - JWT
JWT_SECRET=your_36_char_secret
# Optional - OpenAI (for voice transcription)
OPENAI_API_KEY=your_key
# Optional - Blockchain (Hedera)
HEDERA_ACCOUNT_ID=your_account_id
HEDERA_PRIVATE_KEY=your_private_key
HEDERA_NETWORK=testnet
# Optional - Mobile Money (mock for demo)
MTN_API_KEY=mock_key
MTN_API_SECRET=mock_secret
Database with Prisma
Schema Location
prisma/
├── schema.prisma # Main database schema
└── migrations/ # Generated migration files
Prisma Commands
# Generate Prisma Client (after schema changes)
npm run db:generate
# Run pending migrations (production)
npm run db:migrate
# Create new migration (development)
npx prisma migrate dev --name migration_name
# Reset database (development only)
npx prisma migrate reset
# Open Prisma Studio (GUI)
npx prisma studio
# Pull schema from existing database
npx prisma db pull
# Push schema without migrations (prototyping)
npx prisma db push
Sample Prisma Schema (prisma/schema.prisma)
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Dispute {
id String @id @default(uuid())
dispute_number String @unique
claimant_name String
claimant_phone String
respondent_name String?
respondent_phone String?
dispute_type DisputeType
description String
location String?
stake_amount Int @default(10000)
total_pot Int @default(0)
status DisputeStatus @default(awaiting_respondent)
claimant_paid Boolean @default(false)
respondent_paid Boolean @default(false)
votes_for_claimant Int @default(0)
votes_for_respondent Int @default(0)
selected_jurors String[]
winner String?
winner_amount Int?
community_amount Int?
created_at DateTime @default(now())
updated_at DateTime @updatedAt
voting_ends_at DateTime?
resolved_at DateTime?
evidence Evidence[]
votes JuryVote[]
logs SystemLog[]
}
model Elder {
id String @id @default(uuid())
name String
phone String @unique
village String
region String
reputation_score Int @default(50)
cases_judged Int @default(0)
fair_votes Int @default(0)
unfair_votes Int @default(0)
is_active Boolean @default(true)
registered_at DateTime @default(now())
votes JuryVote[]
}
model Evidence {
id String @id @default(uuid())
dispute_id String
submitted_by_phone String
evidence_type EvidenceType
content String // URL or description
content_hash String?
transcription String?
created_at DateTime @default(now())
dispute Dispute @relation(fields: [dispute_id], references: [id], onDelete: Cascade)
}
model JuryVote {
id String @id @default(uuid())
dispute_id String
elder_id String
vote Int // 1 = Claimant, 2 = Respondent
vote_hash String @unique
voted_at DateTime @default(now())
dispute Dispute @relation(fields: [dispute_id], references: [id], onDelete: Cascade)
elder Elder @relation(fields: [elder_id], references: [id])
}
model CommunityFund {
id String @id @default(uuid())
region String @unique
total_contributed Int @default(0)
total_spent Int @default(0)
current_balance Int @default(0)
disputes_resolved Int @default(0)
updated_at DateTime @updatedAt
}
model SystemLog {
id String @id @default(uuid())
action String
details String?
user_id String?
type String @default("info")
created_at DateTime @default(now())
}
enum DisputeType {
land
water
grazing
debt
marriage
other
}
enum DisputeStatus {
awaiting_respondent
active
voting
resolved
expired
cancelled
}
enum EvidenceType {
voice
witness
photo
document
}
Project Architecture
Directory Structure
src/
├── config/ # Configuration (database, redis, blockchain)
├── controllers/ # Request handlers (USSD, API endpoints)
├── services/ # Business logic (jury selection, voting, payments)
├── middleware/ # Auth, rate limiting, error handling
├── utils/ # Helpers (crypto, phone formatting, language)
├── types/ # TypeScript interfaces
├── workers/ # Background jobs (timeouts, reminders)
└── index.ts # Entry point
prisma/
├── schema.prisma # Database schema
└── migrations/ # Migration files
Key Design Patterns
| Pattern | Where Used | Why |
|---|---|---|
| Service Layer | services/jury/selectionService.ts |
Business logic isolated from controllers |
| Repository Pattern | Prisma Client | Database operations abstracted |
| Factory Pattern | services/notificationService.ts |
Creates notifications for different channels |
| Strategy Pattern | services/payment/mobileMoneyService.ts |
Multiple payment providers (MTN/Orange) |
| Middleware Pipeline | Express middleware | Auth, logging, rate limiting |
Core Workflows
1. Dispute Filing Flow (Claimant)
User dials *485# → Main menu → Select "File dispute"
→ Collect respondent info → Description & location
→ Set stake amount → Confirm → Create dispute record
→ Send SMS to respondent → Show dispute ID
Key Files:
controllers/ussdController.ts- USSD menu flowservices/notificationService.ts- SMS sending- Prisma Client - Database operations
2. Dispute Acceptance Flow (Respondent)
Respondent receives SMS → Dials *485# → Sees pending dispute
→ Reviews details → Accepts with PIN (1234 demo)
→ System matches stake → Dispute status becomes 'active'
→ Auto-select jury → Notifies selected elders
Key Files:
controllers/ussdController.ts- Acceptance menuservices/jury/selectionService.ts- Random jury selectionservices/payment/mobileMoneyService.ts- Stake processing
3. Jury Voting Flow
Elder receives notification → Dials *485# or uses web UI
→ Views evidence (voice, witness statements)
→ Casts anonymous vote (Claimant=1, Respondent=2)
→ System generates SHA256 hash → Updates vote counts
→ After all votes or 48hrs → Auto-resolution
Key Files:
services/jury/voteAggregator.ts- Vote recording and tallyingservices/blockchain/contractService.ts- Hash generationworkers/voteReminderWorker.ts- Reminder notifications
4. Resolution Flow
Voting completes → Tally votes → Determine winner
→ Calculate distribution (75% winner, 25% community)
→ Update dispute status → Send SMS notifications
→ Update community fund balance → Log resolution
Key Files:
services/jury/reputationEngine.ts- Elder reputation updatesservices/notificationService.ts- Results SMSworkers/disputeTimeoutWorker.ts- Expiry handling
Database Operations with Prisma
Query Examples
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
// Create dispute
const dispute = await prisma.dispute.create({
data: {
dispute_number: `D-BF-${Date.now()}`,
claimant_name: 'Fatima Moussa',
claimant_phone: '+22670123456',
dispute_type: 'land',
description: 'Boundary dispute',
stake_amount: 10000,
status: 'awaiting_respondent'
}
});
// Find dispute with votes
const disputeWithVotes = await prisma.dispute.findUnique({
where: { id: disputeId },
include: { votes: true, evidence: true }
});
// Update vote counts
await prisma.dispute.update({
where: { id: disputeId },
data: {
votes_for_claimant: { increment: 1 }
}
});
// Get statistics
const stats = await prisma.dispute.aggregate({
_count: { id: true },
where: { status: 'resolved' }
});
API Endpoints
USSD Webhook
| Endpoint | Method | Purpose |
|---|---|---|
/ussd |
POST | Main USSD entry point (Africa's Talking calls this) |
REST API
| Endpoint | Method | Purpose |
|---|---|---|
/api/disputes |
GET | List all disputes |
/api/disputes |
POST | Create new dispute |
/api/disputes/:id |
GET | Get dispute details |
/api/disputes/:id/accept |
POST | Accept dispute (respondent) |
/api/disputes/:id/vote |
POST | Cast vote (juror) |
/api/elders |
GET | List all elders |
/api/elders |
POST | Register new elder |
/api/admin/stats |
GET | System statistics |
/api/admin/disputes/:id/force-resolve |
POST | Admin override |
/api/logs |
GET | System activity logs |
Testing Strategy
Unit Tests (Jest)
npm test # Run all tests
npm test -- --watch # Watch mode
npm test -- --coverage # Coverage report
Database Testing
// Use transaction for test isolation
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
beforeEach(async () => {
await prisma.$transaction([
prisma.juryVote.deleteMany(),
prisma.evidence.deleteMany(),
prisma.dispute.deleteMany(),
prisma.elder.deleteMany(),
])
})
Manual Testing (USSD Simulator)
- Open Africa's Talking Sandbox
- Go to USSD → Simulator
- Enter phone number
+22670123456 - Dial
*485#and follow prompts
Common Development Tasks
Running Database Migrations
# Create new migration after schema change
npx prisma migrate dev --name add_new_field
# Apply migrations in production
npm run db:migrate
# Regenerate Prisma Client
npm run db:generate
# Reset database (development only)
npx prisma migrate reset
Adding a New Dispute Type
// 1. Update Prisma schema
// prisma/schema.prisma
enum DisputeType {
land
water
grazing
debt
marriage
other
new_type // Add this
}
// 2. Run migration
npx prisma migrate dev --name add_new_dispute_type
// 3. Update USSD menu (controllers/ussdController.ts)
// Add to dispute type selection options
// 4. Regenerate Prisma Client
npm run db:generate
Adding a New Field to Dispute
// prisma/schema.prisma
model Dispute {
// ... existing fields
new_field String? // Add optional field
}
// Create migration
npx prisma migrate dev --name add_new_field_to_dispute
Debugging Database Issues
// Enable query logging
const prisma = new PrismaClient({
log: ['query', 'info', 'warn', 'error']
});
// Check migration status
npx prisma migrate status
// View failed migrations
npm run db:failed
Deployment
Docker (Local)
docker-compose up -d
# App runs on port 3000, Postgres on 5432, Redis on 6379
Production Database Setup
# 1. Set DATABASE_URL in production environment
DATABASE_URL="postgresql://user:password@host:5432/peace_payout"
# 2. Run migrations
npm run db:migrate
# 3. Generate Prisma Client
npm run db:generate
# 4. Start application
npm start
Environment Checklist
- PostgreSQL database created and accessible
-
DATABASE_URLenvironment variable set - Prisma migrations run (
npm run db:migrate) - Prisma Client generated (
npm run db:generate) - Africa's Talking API key configured
- JWT secret (36+ characters)
- Redis URL configured
Performance Considerations
| Concern | Mitigation |
|---|---|
| USSD session loss | Redis with TTL + database backup |
| Database connection pooling | Prisma connection pool (default 10) |
| Query performance | Indexes on frequently queried fields |
| Vote counting race conditions | Database row-level locks via Prisma |
| Large log tables | Periodic archiving (30-day retention) |
Troubleshooting Prisma
Common Prisma Issues
| Error | Solution |
|---|---|
PrismaClient is not configured |
Run npm run db:generate |
Migration not applied |
Run npx prisma migrate deploy |
Relation does not exist |
Check schema and run migrations |
Connection refused |
Verify PostgreSQL is running and DATABASE_URL is correct |
Reset Development Database
# WARNING: Deletes all data
npx prisma migrate reset
npx prisma generate
npm run db:migrate
Useful Commands
# Development
npm run dev # Start with hot reload
npm run build # TypeScript build
npm start # Production start
# Database (Prisma)
npm run db:generate # Generate Prisma Client
npm run db:migrate # Run pending migrations
npx prisma studio # Open Prisma Studio GUI
npx prisma validate # Validate schema
# Workers
npm run worker:dispute # Timeout checker
npm run worker:vote # Reminder sender
# Testing
npm test # Unit tests
npm run test:coverage # Coverage report
Resources
Contact & Support
For issues during development:
- Check the
#peace-payoutchannel on Slack - Review open GitHub issues
- Contact the project maintainer