319 lines
9.5 KiB
Markdown
319 lines
9.5 KiB
Markdown
# 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
|
|
|
|
```rust
|
|
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
|
|
|
|
```rust
|
|
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
|
|
|
|
```rust
|
|
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
|
|
|
|
```rust
|
|
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
|
|
|
|
```rust
|
|
// 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
|
|
|
|
```rust
|
|
// 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
|
|
|
|
```rust
|
|
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:
|
|
|
|
```rust
|
|
// 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
|
|
|
|
```rust
|
|
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**
|
|
|
|
```bash
|
|
# Run payment tests
|
|
cargo test payment
|
|
|
|
# Run specific test
|
|
cargo test test_payment_completion
|
|
```
|
|
|
|
## Common Patterns
|
|
|
|
### 1. Payment Retry Logic
|
|
```rust
|
|
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
|
|
```rust
|
|
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
|
|
```rust
|
|
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.
|