- Add a new Rhai example showcasing complete payment flow for company registration. This includes company creation, payment record creation, successful payment processing, status updates, and handling failed and refunded payments. - Add new example demonstrating payment integration in Rhai scripting. This example showcases the usage of various payment status methods and verifies data from the database after processing payments. - Add new examples to Cargo.toml to facilitate building and running the examples. This makes it easier to integrate and test payment functionality using Rhai scripting.
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.
|