ahgCartPlugin - Technical Documentation¶
Overview¶
The ahgCartPlugin provides shopping cart and e-commerce functionality for AtoM, enabling both free reproduction requests and paid digital sales with PayFast payment integration.
Version: 2.0.0
Author: The Archive and Heritage Group
Dependencies: atom-framework (Laravel Query Builder)
Architecture¶
┌─────────────────────────────────────────────────────────────────────────┐
│ ahgCartPlugin Architecture │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ PRESENTATION LAYER │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ browseAction│ │checkoutAction│ │ paymentAction│ │ │
│ │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ │
│ │ │ │ │ │ │
│ │ ┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐ │ │
│ │ │ browseSuccess│ │checkoutSuccess│ │paymentSuccess│ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ SERVICE LAYER │ │
│ │ ┌──────────────────────┐ ┌──────────────────────┐ │ │
│ │ │ CartService │ │ EcommerceService │ │ │
│ │ │ - addToCart() │ │ - getCartWithPricing│ │ │
│ │ │ - getCart() │ │ - calculateTotals() │ │ │
│ │ │ - removeFromCart() │ │ - createOrder() │ │ │
│ │ │ - clearAll() │ │ - initiatePayment() │ │ │
│ │ │ - mergeGuestCart() │ │ - processNotify() │ │ │
│ │ └──────────────────────┘ └──────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ REPOSITORY LAYER │ │
│ │ ┌──────────────────────┐ ┌──────────────────────┐ │ │
│ │ │ EcommerceRepository │ │ (Laravel Query │ │ │
│ │ │ - getSettings() │ │ Builder Direct) │ │ │
│ │ │ - saveSettings() │ │ │ │ │
│ │ │ - getProductTypes() │ │ │ │ │
│ │ │ - getPricing() │ │ │ │ │
│ │ │ - createOrder() │ │ │ │ │
│ │ └──────────────────────┘ └──────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ DATABASE LAYER │ │
│ │ ┌────────────┐ ┌────────────────────┐ ┌─────────────────┐ │ │
│ │ │ cart │ │ahg_ecommerce_settings│ │ ahg_order │ │ │
│ │ ├────────────┤ ├────────────────────┤ ├─────────────────┤ │ │
│ │ │id │ │id │ │id │ │ │
│ │ │user_id │ │repository_id │ │order_number │ │ │
│ │ │session_id │ │is_enabled │ │user_id │ │ │
│ │ │archival_ │ │currency │ │session_id │ │ │
│ │ │ description│ │vat_rate │ │status │ │ │
│ │ │product_type│ │payfast_* │ │total │ │ │
│ │ │quantity │ │stripe_* │ │customer_* │ │ │
│ │ └────────────┘ └────────────────────┘ └─────────────────┘ │ │
│ │ │ │
│ │ ┌────────────────┐ ┌────────────────┐ ┌─────────────────┐ │ │
│ │ │ahg_product_type│ │ahg_product_ │ │ ahg_order_item │ │ │
│ │ │ │ │ pricing │ │ │ │ │
│ │ ├────────────────┤ ├────────────────┤ ├─────────────────┤ │ │
│ │ │id │ │id │ │id │ │ │
│ │ │name │ │product_type_id │ │order_id │ │ │
│ │ │is_digital │ │repository_id │ │archival_desc_id │ │ │
│ │ │description │ │price │ │product_type_id │ │ │
│ │ └────────────────┘ └────────────────┘ └─────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Database Schema (ERD)¶
┌─────────────────────────────────────────────────────────────────────────┐
│ E-COMMERCE DATABASE SCHEMA │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────┐ ┌──────────────────────────┐ │
│ │ cart │ │ ahg_ecommerce_settings │ │
│ ├──────────────────┤ ├──────────────────────────┤ │
│ │ id PK │ │ id PK │ │
│ │ user_id FK ─┼────┐ │ repository_id FK │──┐ │
│ │ session_id │ │ │ is_enabled │ │ │
│ │ archival_desc_id│ │ │ currency │ │ │
│ │ archival_desc │ │ │ vat_rate │ │ │
│ │ slug │ │ │ vat_number │ │ │
│ │ product_type_id │─┐ │ │ payment_gateway │ │ │
│ │ unit_price │ │ │ │ payfast_merchant_id │ │ │
│ │ quantity │ │ │ │ payfast_merchant_key │ │ │
│ │ completed_at │ │ │ │ payfast_passphrase │ │ │
│ │ created_at │ │ │ │ payfast_sandbox │ │ │
│ │ updated_at │ │ │ │ admin_notification_email │ │ │
│ └──────────────────┘ │ │ └──────────────────────────┘ │ │
│ │ │ │ │
│ ┌────────────────────┼──┼───────────────────────────────────┼───────┐ │
│ │ │ │ │ │ │
│ │ ▼ │ ▼ │ │
│ │ ┌──────────────────┐ │ ┌──────────────────┐ ┌────────────────┐│ │
│ │ │ahg_product_type │ │ │ user │ │ repository ││ │
│ │ ├──────────────────┤ │ ├──────────────────┤ ├────────────────┤│ │
│ │ │ id PK │ │ │ id PK │ │ id PK ││ │
│ │ │ name │ │ │ username │ │ ... ││ │
│ │ │ description │ │ │ email │ └────────────────┘│ │
│ │ │ is_digital │ │ └──────────────────┘ │ │
│ │ │ is_active │ │ ▲ │ │
│ │ │ sort_order │ │ │ │ │
│ │ └────────┬─────────┘ │ │ │ │
│ │ │ │ │ │ │
│ │ ▼ │ │ │ │
│ │ ┌──────────────────┐ │ │ │ │
│ │ │ahg_product_pricing│ │ │ │ │
│ │ ├──────────────────┤ │ │ │ │
│ │ │ id PK │ │ │ │ │
│ │ │ product_type_id FK│◀┘ │ │ │
│ │ │ repository_id FK│─────────────┼──────────────────────────────┘ │
│ │ │ name │ │ │
│ │ │ price │ │ │
│ │ │ is_active │ │ │
│ │ └──────────────────┘ │ │
│ │ │ │
│ │ ┌──────────────────┐ │ │
│ │ │ ahg_order │ │ │
│ │ ├──────────────────┤ │ │
│ │ │ id PK │ │ │
│ │ │ order_number UK │ │ │
│ │ │ user_id FK │─────────────┘ │
│ │ │ session_id │ (for guest orders) │
│ │ │ repository_id FK │ │
│ │ │ status │ (pending/paid/processing/completed/...) │
│ │ │ subtotal │ │
│ │ │ vat_amount │ │
│ │ │ total │ │
│ │ │ currency │ │
│ │ │ customer_name │ │
│ │ │ customer_email │ │
│ │ │ customer_phone │ │
│ │ │ billing_address │ │
│ │ │ shipping_address │ │
│ │ │ paid_at │ │
│ │ │ completed_at │ │
│ │ │ created_at │ │
│ │ └────────┬─────────┘ │
│ │ │ │
│ │ │ 1:N │
│ │ ▼ │
│ │ ┌──────────────────┐ │
│ │ │ ahg_order_item │ │
│ │ ├──────────────────┤ │
│ │ │ id PK │ │
│ │ │ order_id FK │ │
│ │ │ archival_desc_id │ │
│ │ │ description │ │
│ │ │ product_type_id │ │
│ │ │ product_name │ │
│ │ │ quantity │ │
│ │ │ unit_price │ │
│ │ │ total │ │
│ │ └──────────────────┘ │
│ │ │
│ │ ┌──────────────────┐ │
│ │ │ ahg_payment │ │
│ │ ├──────────────────┤ │
│ │ │ id PK │ │
│ │ │ order_id FK │ │
│ │ │ payment_gateway │ (payfast/stripe) │
│ │ │ transaction_id │ │
│ │ │ amount │ │
│ │ │ currency │ │
│ │ │ status │ (pending/completed/failed/refunded) │
│ │ │ gateway_response │ (JSON) │
│ │ │ paid_at │ │
│ │ └──────────────────┘ │
│ │ │
│ └─────────────────────────────────────────────────────────────────────┘
│ │
└─────────────────────────────────────────────────────────────────────────┘
File Structure¶
ahgCartPlugin/
├── config/
│ └── ahgCartPluginConfiguration.class.php # Routes and initialization
├── lib/
│ ├── Repositories/
│ │ └── EcommerceRepository.php # Database operations
│ └── Services/
│ ├── CartService.php # Cart operations
│ └── EcommerceService.php # E-commerce & payment logic
├── modules/
│ └── ahgCart/
│ ├── actions/
│ │ ├── addAction.class.php # Add item to cart
│ │ ├── browseAction.class.php # View cart
│ │ ├── checkoutAction.class.php # Checkout process
│ │ ├── paymentAction.class.php # Payment initiation
│ │ ├── paymentReturnAction.class.php # Return from payment
│ │ ├── paymentCancelAction.class.php # Payment cancelled
│ │ ├── paymentNotifyAction.class.php # ITN webhook
│ │ ├── orderConfirmationAction.class.php
│ │ ├── adminSettingsAction.class.php # E-commerce admin
│ │ ├── adminOrdersAction.class.php # Order management
│ │ └── thankYouAction.class.php # Guest thank you page
│ ├── config/
│ │ └── security.yml # Allow guest access
│ └── templates/
│ ├── browseSuccess.php
│ ├── checkoutSuccess.php
│ ├── paymentSuccess.php
│ ├── orderConfirmationSuccess.php
│ ├── adminSettingsSuccess.php
│ ├── adminOrdersSuccess.php
│ └── thankYouSuccess.php
└── data/
└── install.sql # Database schema
Routes¶
| Route | Action | Description |
|---|---|---|
/cart |
browse | View cart contents |
/cart/add/:slug |
add | Add item to cart |
/cart/remove/:id |
remove | Remove item from cart |
/cart/clear |
clear | Clear entire cart |
/cart/checkout |
checkout | Checkout page |
/cart/payment/:order |
payment | Payment page |
/cart/payment-return/:order |
paymentReturn | Return from gateway |
/cart/payment-cancel/:order |
paymentCancel | Payment cancelled |
/cart/payment/notify |
paymentNotify | ITN webhook (POST) |
/cart/order/:order |
orderConfirmation | Order details |
/cart/thank-you |
thankYou | Guest confirmation |
/admin/ecommerce |
adminSettings | E-commerce settings |
/admin/orders |
adminOrders | Order management |
Key Services¶
CartService¶
class CartService
{
// Add item to cart (supports user_id OR session_id)
public function addToCart($userId, $objectId, $title, $slug, $sessionId = null): array
// Get cart items for user or session
public function getCart($userId = null, $sessionId = null): array
// Remove item from cart
public function removeFromCart($cartId, $userId = null, $sessionId = null): bool
// Clear all cart items
public function clearAll($userId): int
public function clearAllBySession($sessionId): int
// Merge guest cart into user cart on login
public function mergeGuestCart($sessionId, $userId): int
}
EcommerceService¶
class EcommerceService
{
// Check if e-commerce is enabled
public function isEcommerceEnabled(): bool
// Get cart with pricing information
public function getCartWithPricing($userId, $repositoryId = null, $sessionId = null): array
// Calculate cart totals (subtotal, VAT, total)
public function calculateCartTotals($items, $repositoryId = null): array
// Create order from cart
public function createOrderFromCart(?int $userId, array $customerData, ?string $sessionId = null): array
// Initiate PayFast payment
public function initiatePayFastPayment(int $orderId): array
// Process PayFast ITN notification
public function processPayFastNotification(array $data): array
// Generate download tokens after payment
public function generateDownloadTokens(int $orderId): void
}
Guest Checkout Flow¶
The cart supports guest checkout (no account required):
- Session Tracking: Guest carts use PHP session ID
- Add to Cart: Items stored with
session_idinstead ofuser_id - Browse Cart: Items retrieved by session ID
- Checkout: Guest provides email for order confirmation
- Order Creation: Order created with
session_id,user_id = NULL - Payment: PayFast processes payment, returns to confirmation
- Merge on Login: If guest later logs in, cart items can merge
PayFast Integration¶
Configuration¶
Settings stored in ahg_ecommerce_settings:
- payfast_merchant_id - Merchant ID from PayFast
- payfast_merchant_key - Merchant Key from PayFast
- payfast_passphrase - Security passphrase (optional but recommended)
- payfast_sandbox - 1 for testing, 0 for live
Signature Generation¶
PayFast requires MD5 signature of all parameters:
$pfData = [
'merchant_id' => $settings->payfast_merchant_id,
'merchant_key' => $settings->payfast_merchant_key,
'return_url' => $siteUrl . '/cart/order/' . $order->order_number,
'cancel_url' => $siteUrl . '/cart',
'notify_url' => $siteUrl . '/cart/payment/notify',
'name_first' => $firstName,
'name_last' => $lastName,
'email_address' => $order->customer_email,
'm_payment_id' => $order->order_number,
'amount' => number_format($order->total, 2, '.', ''),
'item_name' => 'Order-' . $order->order_number,
];
// Generate signature
$signatureString = '';
foreach ($pfData as $key => $val) {
if ($val !== null && $val !== '') {
$signatureString .= $key . '=' . urlencode(trim($val)) . '&';
}
}
$signatureString = rtrim($signatureString, '&');
if (!empty($passphrase)) {
$signatureString .= '&passphrase=' . urlencode($passphrase);
}
$pfData['signature'] = md5($signatureString);
ITN (Instant Transaction Notification)¶
PayFast sends POST to /cart/payment/notify:
public function processPayFastNotification(array $data): array
{
$orderNumber = $data['m_payment_id'];
$paymentStatus = $data['payment_status'];
if ($paymentStatus === 'COMPLETE') {
// Update order status to 'paid'
// Generate download tokens
// Send confirmation email
}
}
Template Integration¶
Add cart button to item templates:
<?php
$userId = $sf_user->getAttribute('user_id');
$sessionId = session_id();
if (empty($sessionId) && !$userId) { @session_start(); $sessionId = session_id(); }
$cartId = null;
if ($userId) {
$cartId = DB::table('cart')
->where('user_id', $userId)
->where('archival_description_id', $resource->id)
->whereNull('completed_at')
->value('id');
} elseif ($sessionId) {
$cartId = DB::table('cart')
->where('session_id', $sessionId)
->where('archival_description_id', $resource->id)
->whereNull('completed_at')
->value('id');
}
$hasDigitalObject = DB::table('digital_object')
->where('object_id', $resource->id)
->exists();
?>
<?php if (class_exists('ahgCartPluginConfiguration') && $hasDigitalObject): ?>
<?php if ($cartId): ?>
<a href="<?php echo url_for(['module' => 'ahgCart', 'action' => 'browse']); ?>"
class="btn btn-xs btn-outline-success" title="Go to Cart">
<i class="fas fa-shopping-cart"></i>
</a>
<?php else: ?>
<a href="<?php echo url_for(['module' => 'ahgCart', 'action' => 'add', 'slug' => $resource->slug]); ?>"
class="btn btn-xs btn-outline-success" title="Add to Cart">
<i class="fas fa-cart-plus"></i>
</a>
<?php endif; ?>
<?php endif; ?>
Testing¶
Sandbox Credentials¶
Merchant ID: 10000100
Merchant Key: 46f0cd694581a
Passphrase: (leave empty for sandbox)
Test Buyer Email: sbtu01@payfast.co.za
Test Card: 5200000000000015
Expiry: Any future date
CVV: 123
Database Verification¶
-- Check cart items
SELECT * FROM cart WHERE completed_at IS NULL ORDER BY id DESC LIMIT 10;
-- Check orders
SELECT order_number, status, total, customer_email, created_at
FROM ahg_order ORDER BY id DESC LIMIT 10;
-- Check order items
SELECT oi.*, o.order_number
FROM ahg_order_item oi
JOIN ahg_order o ON oi.order_id = o.id
ORDER BY oi.id DESC LIMIT 10;
-- Check payments
SELECT * FROM ahg_payment ORDER BY id DESC LIMIT 10;
Version History¶
| Version | Date | Changes |
|---|---|---|
| 2.0.0 | 2026-01-13 | Guest checkout support, PayFast integration |
| 1.0.0 | 2026-01-13 | Initial release with e-commerce features |