db/heromodels/docs/payment_usage.md
2025-06-27 12:11:04 +03:00

9.5 KiB

Payment Model Usage Guide

This document provides comprehensive instructions for AI assistants on how to use the Payment model in the heromodels repository.

Overview

The Payment model represents a payment transaction in the system, typically associated with company registration or subscription payments. It integrates with Stripe for payment processing and maintains comprehensive status tracking.

Model Structure

pub struct Payment {
    pub base_data: BaseModelData,           // Auto-managed ID, timestamps, comments
    pub payment_intent_id: String,         // Stripe payment intent ID
    pub company_id: u32,                   // Foreign key to Company
    pub payment_plan: String,              // "monthly", "yearly", "two_year"
    pub setup_fee: f64,                    // One-time setup fee
    pub monthly_fee: f64,                  // Recurring monthly fee
    pub total_amount: f64,                 // Total amount paid
    pub currency: String,                  // Currency code (defaults to "usd")
    pub status: PaymentStatus,             // Current payment status
    pub stripe_customer_id: Option<String>, // Stripe customer ID (set on completion)
    pub created_at: i64,                   // Payment creation timestamp
    pub completed_at: Option<i64>,         // Payment completion timestamp
}

pub enum PaymentStatus {
    Pending,      // Initial state - payment created but not processed
    Processing,   // Payment is being processed by Stripe
    Completed,    // Payment successfully completed
    Failed,       // Payment processing failed
    Refunded,     // Payment was refunded
}

Basic Usage

1. Creating a New Payment

use heromodels::models::biz::{Payment, PaymentStatus};

// Create a new payment with required fields
let payment = Payment::new(
    "pi_1234567890".to_string(),    // Stripe payment intent ID
    company_id,                      // Company ID from database
    "monthly".to_string(),           // Payment plan
    100.0,                          // Setup fee
    49.99,                          // Monthly fee
    149.99,                         // Total amount
);

// Payment defaults:
// - status: PaymentStatus::Pending
// - currency: "usd"
// - stripe_customer_id: None
// - created_at: current timestamp
// - completed_at: None

2. Using Builder Pattern

let payment = Payment::new(
    "pi_1234567890".to_string(),
    company_id,
    "yearly".to_string(),
    500.0,
    99.99,
    1699.88,
)
.currency("eur".to_string())
.stripe_customer_id(Some("cus_existing_customer".to_string()));

3. Database Operations

use heromodels::db::Collection;

// Save payment to database
let db = get_db()?;
let (payment_id, saved_payment) = db.set(&payment)?;

// Retrieve payment by ID
let retrieved_payment: Payment = db.get_by_id(payment_id)?.unwrap();

// Update payment
let updated_payment = saved_payment.complete_payment(Some("cus_new_customer".to_string()));
let (_, final_payment) = db.set(&updated_payment)?;

Payment Status Management

Status Transitions

// 1. Start with Pending status (default)
let payment = Payment::new(/* ... */);
assert!(payment.is_pending());

// 2. Mark as processing when Stripe starts processing
let processing_payment = payment.process_payment();
assert!(processing_payment.is_processing());

// 3. Complete payment when Stripe confirms success
let completed_payment = processing_payment.complete_payment(Some("cus_123".to_string()));
assert!(completed_payment.is_completed());
assert!(completed_payment.completed_at.is_some());

// 4. Handle failure if payment fails
let failed_payment = processing_payment.fail_payment();
assert!(failed_payment.has_failed());

// 5. Refund if needed
let refunded_payment = completed_payment.refund_payment();
assert!(refunded_payment.is_refunded());

Status Check Methods

// Check current status
if payment.is_pending() {
    // Show "Payment Pending" UI
} else if payment.is_processing() {
    // Show "Processing Payment" UI
} else if payment.is_completed() {
    // Show "Payment Successful" UI
    // Enable company features
} else if payment.has_failed() {
    // Show "Payment Failed" UI
    // Offer retry option
} else if payment.is_refunded() {
    // Show "Payment Refunded" UI
}

Integration with Company Model

Complete Payment Flow

use heromodels::models::biz::{Company, CompanyStatus, Payment, PaymentStatus};

// 1. Create company with pending payment status
let company = Company::new(
    "TechStart Inc.".to_string(),
    "REG-TS-2024-001".to_string(),
    chrono::Utc::now().timestamp(),
)
.email("contact@techstart.com".to_string())
.status(CompanyStatus::PendingPayment);

let (company_id, company) = db.set(&company)?;

// 2. Create payment for the company
let payment = Payment::new(
    stripe_payment_intent_id,
    company_id,
    "yearly".to_string(),
    500.0,  // Setup fee
    99.0,   // Monthly fee
    1688.0, // Total (setup + 12 months)
);

let (payment_id, payment) = db.set(&payment)?;

// 3. Process payment through Stripe
let processing_payment = payment.process_payment();
let (_, processing_payment) = db.set(&processing_payment)?;

// 4. On successful Stripe webhook
let completed_payment = processing_payment.complete_payment(Some(stripe_customer_id));
let (_, completed_payment) = db.set(&completed_payment)?;

// 5. Activate company
let active_company = company.status(CompanyStatus::Active);
let (_, active_company) = db.set(&active_company)?;

Database Indexing

The Payment model provides custom indexes for efficient querying:

// Indexed fields for fast lookups:
// - payment_intent_id: Find payment by Stripe intent ID
// - company_id: Find all payments for a company
// - status: Find payments by status

// Example queries (conceptual - actual implementation depends on your query layer)
// let pending_payments = db.find_by_index("status", "Pending")?;
// let company_payments = db.find_by_index("company_id", company_id.to_string())?;
// let stripe_payment = db.find_by_index("payment_intent_id", "pi_1234567890")?;

Error Handling Best Practices

use heromodels::db::DbError;

fn process_payment_flow(payment_intent_id: String, company_id: u32) -> Result<Payment, DbError> {
    let db = get_db()?;
    
    // Create payment
    let payment = Payment::new(
        payment_intent_id,
        company_id,
        "monthly".to_string(),
        100.0,
        49.99,
        149.99,
    );
    
    // Save to database
    let (payment_id, payment) = db.set(&payment)?;
    
    // Process through Stripe (external API call)
    match process_stripe_payment(&payment.payment_intent_id) {
        Ok(stripe_customer_id) => {
            // Success: complete payment
            let completed_payment = payment.complete_payment(Some(stripe_customer_id));
            let (_, final_payment) = db.set(&completed_payment)?;
            Ok(final_payment)
        }
        Err(_) => {
            // Failure: mark as failed
            let failed_payment = payment.fail_payment();
            let (_, final_payment) = db.set(&failed_payment)?;
            Ok(final_payment)
        }
    }
}

Testing

The Payment model includes comprehensive tests in tests/payment.rs. When working with payments:

  1. Always test status transitions
  2. Verify timestamp handling
  3. Test database persistence
  4. Test integration with Company model
  5. Test builder pattern methods
# Run payment tests
cargo test payment

# Run specific test
cargo test test_payment_completion

Common Patterns

1. Payment Retry Logic

fn retry_failed_payment(payment: Payment) -> Payment {
    if payment.has_failed() {
        // Reset to pending for retry
        Payment::new(
            payment.payment_intent_id,
            payment.company_id,
            payment.payment_plan,
            payment.setup_fee,
            payment.monthly_fee,
            payment.total_amount,
        )
        .currency(payment.currency)
    } else {
        payment
    }
}

2. Payment Summary

fn get_payment_summary(payment: &Payment) -> String {
    format!(
        "Payment {} for company {}: {} {} ({})",
        payment.payment_intent_id,
        payment.company_id,
        payment.total_amount,
        payment.currency.to_uppercase(),
        payment.status
    )
}

3. Payment Validation

fn validate_payment(payment: &Payment) -> Result<(), String> {
    if payment.total_amount <= 0.0 {
        return Err("Total amount must be positive".to_string());
    }
    if payment.payment_intent_id.is_empty() {
        return Err("Payment intent ID is required".to_string());
    }
    if payment.company_id == 0 {
        return Err("Valid company ID is required".to_string());
    }
    Ok(())
}

Key Points for AI Assistants

  1. Always use auto-generated IDs - Don't manually set IDs, let OurDB handle them
  2. Follow status flow - Pending → Processing → Completed/Failed → (optionally) Refunded
  3. Update timestamps - completed_at is automatically set when calling complete_payment()
  4. Use builder pattern - For optional fields and cleaner code
  5. Test thoroughly - Payment logic is critical, always verify with tests
  6. Handle errors gracefully - Payment failures should be tracked, not ignored
  7. Integrate with Company - Payments typically affect company status
  8. Use proper indexing - Leverage indexed fields for efficient queries

This model follows the heromodels patterns and integrates seamlessly with the existing codebase architecture.