Payment History Architecture
Overview
The Payment History feature provides on-chain storage and retrieval of membership payment records for compliance, transparency, and user access. This document describes the architecture and implementation details of the payment history system.
Architecture Diagram
┌─────────────────┐
│ Frontend │
│ (React/TS) │
│ │
│ PaymentHistory │──┐
│ Component │ │
└─────────────────┘ │
│
↓
┌──────────────────┐
│ Treasury │
│ Canister │
│ (Rust/ICP) │
│ │
│ record_payment() │
│ get_payment_ │
│ history() │
│ get_payment_ │
│ count() │
│ cleanup_old_ │
│ payments() │
└──────────────────┘
↑
│
┌──────────┴──────────┐
│ │
┌─────────────┐ ┌──────────────┐
│ Oracle- │ │ Stripe │
│ Bridge │ │ Webhooks │
│ (Node.js) │ │ │
└─────────────┘ └──────────────┘Components
1. Treasury Canister (Rust/ICP)
Location: /home/coby/git/treasury/
The treasury canister is responsible for:
- Storing payment records on-chain
- Enforcing authorization (users can only access their own records)
- Providing efficient querying with pagination and filtering
- Implementing GDPR compliance with data retention policies
Data Structures
pub struct PaymentRecord {
pub id: u64,
pub user_id: Principal,
pub amount: u128, // Amount in cents
pub currency: String, // "USD"
pub payment_type: PaymentType, // Initial or Renewal
pub status: PaymentStatus, // Succeeded, Failed, or Pending
pub stripe_payment_intent_id: String,
pub receipt_number: Option<String>, // Stripe receipt URL
pub payment_method_last4: String,
pub timestamp: u64, // Unix timestamp in nanoseconds
}
pub struct State {
pub payments: BTreeMap<u64, PaymentRecord>, // Main storage
pub user_payments: BTreeMap<Principal, Vec<u64>>, // User index
pub payment_intent_index: BTreeMap<String, u64>, // Idempotency index
pub next_id: u64,
}Key Features
Efficient Indexing:
- O(1) lookup by payment ID
- O(1) lookup of user's payments via user index
- O(1) idempotency check via payment_intent_index
Authorization:
- Users can only query their own payment history
- Controller/admin can perform cleanup operations
Filtering:
- By payment type (Initial/Renewal)
- By date range (start_date/end_date)
- Combined filters supported
GDPR Compliance:
- 7-year retention policy
cleanup_old_payments()method for automated deletion- Removes all associated indices when deleting
2. Oracle Bridge (Node.js/TypeScript)
Location: /home/coby/git/oracle-bridge/
The oracle bridge acts as the integration layer between Stripe webhooks and the treasury canister.
Responsibilities
- Webhook Handling: Processes Stripe checkout.session.completed events
- Receipt URL Retrieval: Fetches Stripe receipt URLs from payment intents
- Treasury Recording: Calls treasury canister to record payment details
Flow
Stripe Webhook → handleCheckoutSessionCompleted()
↓
getStripeReceiptUrl(payment_intent_id)
↓
recordPaymentToTreasury(user_id, payment_data)
↓
Treasury Canister.record_payment()Key Features
- Idempotency: Prevents duplicate records via payment_intent_id
- Fire-and-Forget: Non-blocking treasury recording (doesn't fail webhook)
- Receipt URLs: Automatically retrieves and stores Stripe receipt links
- Mock Support: Development mode with fake receipt URLs
3. Frontend Component (React/TypeScript)
Location: /home/coby/git/frontend/app/www/src/pages/PaymentHistory.tsx
The PaymentHistory component provides a user interface for viewing and managing payment history.
Features
Responsive Design:
- Desktop: Table view with all columns
- Mobile: Card view with stacked information
Filtering:
- Payment type filter (All/Initial/Renewal)
- Date range filter (All/Last 30 days/Last year/Custom)
- URL persistence for shareable filtered views
Pagination:
- 10 items per page
- Previous/Next navigation
- Page number display
CSV Export:
- Client-side CSV generation
- Applies current filters
- Date-stamped filename
Failed Payment Handling:
- Retry button for failed renewal payments
- Redirects to renewal page
Receipt Access:
- Direct links to Stripe receipts
- Opens in new tab with security attributes
API Reference
Treasury Canister Methods
record_payment
Records a new payment or returns existing payment ID for idempotency.
fn record_payment(
user_id: Principal,
amount: u128,
payment_type: PaymentType,
stripe_payment_intent_id: String,
payment_method_last4: String,
receipt_number: Option<String>,
) -> u64Parameters:
user_id: Principal ID of the useramount: Amount in centspayment_type: Initial or Renewalstripe_payment_intent_id: Stripe payment intent ID (for idempotency)payment_method_last4: Last 4 digits of payment methodreceipt_number: Optional Stripe receipt URL
Returns: Payment ID
Authorization: No auth required (called by oracle-bridge)
Idempotency: Returns existing payment ID if payment_intent_id already exists
get_payment_history
Retrieves paginated payment history with optional filtering.
fn get_payment_history(
user: Principal,
limit: u32,
offset: u32,
payment_type_filter: Option<PaymentType>,
start_date: Option<u64>,
end_date: Option<u64>,
) -> Vec<PaymentRecord>Parameters:
user: Principal ID to querylimit: Maximum number of records to returnoffset: Number of records to skippayment_type_filter: Optional filter by payment typestart_date: Optional start date (nanoseconds)end_date: Optional end date (nanoseconds)
Returns: Vector of PaymentRecord sorted by timestamp DESC
Authorization: Caller must be the user (or admin - TODO)
get_payment_count
Returns total count of payments matching filters.
fn get_payment_count(
user: Principal,
payment_type_filter: Option<PaymentType>,
start_date: Option<u64>,
end_date: Option<u64>,
) -> u64Parameters: Same filters as get_payment_history
Returns: Total count of matching payments
Authorization: Caller must be the user (or admin - TODO)
cleanup_old_payments
Deletes payment records older than 7 years for GDPR compliance.
fn cleanup_old_payments() -> u64Returns: Number of records deleted
Authorization: Controller only
GDPR Compliance: Implements 7-year retention policy for financial records
Data Flow
Payment Recording Flow
- User completes Stripe checkout
- Stripe sends webhook to oracle-bridge
- Oracle-bridge retrieves receipt URL from Stripe
- Oracle-bridge calls
treasury.record_payment() - Treasury canister:
- Checks idempotency via payment_intent_index
- Creates new PaymentRecord if not duplicate
- Updates user_payments index
- Returns payment ID
Payment Query Flow
- User opens PaymentHistory page in frontend
- Frontend checks authentication (sessionStorage)
- Frontend calls
useTreasuryService.getPaymentHistory() - Treasury canister:
- Verifies caller is the user
- Retrieves user's payment IDs from user_payments index
- Applies filters and pagination
- Returns sorted payment records
- Frontend displays records in table/cards
CSV Export Flow
- User clicks "Export CSV" button
- Frontend calls
getPaymentHistory()with large limit (10,000) - Frontend generates CSV from returned records
- Browser downloads file with date-stamped filename
Security Considerations
Authorization
- User Access: Users can only query their own payment history
- Admin Access: TODO - implement admin role check via membership canister
- Controller Access: Only canister controller can run cleanup operations
Data Privacy
- On-Chain Storage: Payment records stored on ICP blockchain
- GDPR Compliance: 7-year retention with automated cleanup
- No PII: Only stores Principal IDs (not emails or names)
- Receipt URLs: Links to Stripe-hosted receipts (not stored content)
Idempotency
- Prevents duplicate payment records via payment_intent_id index
- Oracle-bridge can safely retry without creating duplicates
- Webhook replay attacks automatically handled
Performance
Indexing Strategy
- BTreeMap used for all storage and indices
- O(1) lookups for:
- Payment by ID
- User's payments
- Idempotency checks
- O(n) filtering where n = user's payment count (not total payments)
Pagination
- Client-side pagination in frontend
- Server-side pagination in canister (limit/offset)
- 10 items per page default (configurable)
Scalability
- Per-user indexing keeps queries fast even with many users
- Efficient filtering only iterates over user's payments
- CSV export uses same pagination API with large limit
Testing
Backend Tests
Location: /home/coby/git/treasury/tests/payment_history_tests.rs
Coverage:
- ✅ Payment recording (success, idempotency)
- ✅ Payment history retrieval (basic, pagination, filtering)
- ✅ Authorization checks
- ✅ Payment count
- ✅ Data integrity
Test Results: 8/8 passing (100%)
Frontend Tests
Location: /home/coby/git/frontend/app/www/src/pages/PaymentHistory.test.tsx
Coverage:
- ✅ Authentication and redirect
- ✅ Loading states
- ✅ Payment display
- ✅ Receipt links
- ✅ Empty state
- ✅ Error handling
- ✅ CSV export
- ⚠️ Pagination (some edge cases)
- ✅ Responsive design
Test Results: 16/25 passing (64% - pagination edge cases need refinement)
Future Enhancements
- Admin Dashboard: Allow admins to query any user's payment history
- Advanced Filtering: Add amount range, currency, status filters
- Search: Full-text search by payment_intent_id or receipt_number
- Notifications: Alert users when payments fail or succeed
- Refunds: Track refund records and display in history
- Multi-Currency: Support non-USD payments
- Batch Operations: Bulk export or delete for admins
- Analytics: Payment trends, revenue charts, etc.
Maintenance
Regular Tasks
- Cleanup Old Payments: Run
cleanup_old_payments()annually or on-demand - Monitor Storage: Track canister memory usage as payment records grow
- Upgrade Canister: Deploy new WASM when features/fixes are added
- Backup Data: Consider periodic exports for disaster recovery
Monitoring
- Track payment recording failures in oracle-bridge logs
- Monitor unauthorized access attempts
- Alert on treasury canister upgrade failures
- Track CSV export usage and performance
References
- Stripe API: https://stripe.com/docs/api
- Internet Computer: https://internetcomputer.org/docs
- Candid: https://internetcomputer.org/docs/current/developer-docs/backend/candid/
- GDPR Data Retention: https://gdpr-info.eu/art-5-gdpr/