Skip to content
🔒

Login Required

You need to be logged in to view this content. This page requires Developer access.

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

rust
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

  1. 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
  2. Authorization:

    • Users can only query their own payment history
    • Controller/admin can perform cleanup operations
  3. Filtering:

    • By payment type (Initial/Renewal)
    • By date range (start_date/end_date)
    • Combined filters supported
  4. 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

  1. Webhook Handling: Processes Stripe checkout.session.completed events
  2. Receipt URL Retrieval: Fetches Stripe receipt URLs from payment intents
  3. Treasury Recording: Calls treasury canister to record payment details

Flow

typescript
Stripe Webhook → handleCheckoutSessionCompleted()

                getStripeReceiptUrl(payment_intent_id)

                recordPaymentToTreasury(user_id, payment_data)

                Treasury Canister.record_payment()

Key Features

  1. Idempotency: Prevents duplicate records via payment_intent_id
  2. Fire-and-Forget: Non-blocking treasury recording (doesn't fail webhook)
  3. Receipt URLs: Automatically retrieves and stores Stripe receipt links
  4. 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

  1. Responsive Design:

    • Desktop: Table view with all columns
    • Mobile: Card view with stacked information
  2. Filtering:

    • Payment type filter (All/Initial/Renewal)
    • Date range filter (All/Last 30 days/Last year/Custom)
    • URL persistence for shareable filtered views
  3. Pagination:

    • 10 items per page
    • Previous/Next navigation
    • Page number display
  4. CSV Export:

    • Client-side CSV generation
    • Applies current filters
    • Date-stamped filename
  5. Failed Payment Handling:

    • Retry button for failed renewal payments
    • Redirects to renewal page
  6. 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.

rust
fn record_payment(
    user_id: Principal,
    amount: u128,
    payment_type: PaymentType,
    stripe_payment_intent_id: String,
    payment_method_last4: String,
    receipt_number: Option<String>,
) -> u64

Parameters:

  • user_id: Principal ID of the user
  • amount: Amount in cents
  • payment_type: Initial or Renewal
  • stripe_payment_intent_id: Stripe payment intent ID (for idempotency)
  • payment_method_last4: Last 4 digits of payment method
  • receipt_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.

rust
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 query
  • limit: Maximum number of records to return
  • offset: Number of records to skip
  • payment_type_filter: Optional filter by payment type
  • start_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.

rust
fn get_payment_count(
    user: Principal,
    payment_type_filter: Option<PaymentType>,
    start_date: Option<u64>,
    end_date: Option<u64>,
) -> u64

Parameters: 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.

rust
fn cleanup_old_payments() -> u64

Returns: Number of records deleted

Authorization: Controller only

GDPR Compliance: Implements 7-year retention policy for financial records

Data Flow

Payment Recording Flow

  1. User completes Stripe checkout
  2. Stripe sends webhook to oracle-bridge
  3. Oracle-bridge retrieves receipt URL from Stripe
  4. Oracle-bridge calls treasury.record_payment()
  5. Treasury canister:
    • Checks idempotency via payment_intent_index
    • Creates new PaymentRecord if not duplicate
    • Updates user_payments index
    • Returns payment ID

Payment Query Flow

  1. User opens PaymentHistory page in frontend
  2. Frontend checks authentication (sessionStorage)
  3. Frontend calls useTreasuryService.getPaymentHistory()
  4. Treasury canister:
    • Verifies caller is the user
    • Retrieves user's payment IDs from user_payments index
    • Applies filters and pagination
    • Returns sorted payment records
  5. Frontend displays records in table/cards

CSV Export Flow

  1. User clicks "Export CSV" button
  2. Frontend calls getPaymentHistory() with large limit (10,000)
  3. Frontend generates CSV from returned records
  4. 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

  1. Admin Dashboard: Allow admins to query any user's payment history
  2. Advanced Filtering: Add amount range, currency, status filters
  3. Search: Full-text search by payment_intent_id or receipt_number
  4. Notifications: Alert users when payments fail or succeed
  5. Refunds: Track refund records and display in history
  6. Multi-Currency: Support non-USD payments
  7. Batch Operations: Bulk export or delete for admins
  8. Analytics: Payment trends, revenue charts, etc.

Maintenance

Regular Tasks

  1. Cleanup Old Payments: Run cleanup_old_payments() annually or on-demand
  2. Monitor Storage: Track canister memory usage as payment records grow
  3. Upgrade Canister: Deploy new WASM when features/fixes are added
  4. 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

Hello World Co-Op DAO