Payment Profiles Table
The #__j2commerce_paymentprofiles table is a shared resource that any J2Commerce payment plugin can use to store gateway-level customer profile IDs — such as an Authorize.Net CIM customerProfileId or a Stripe cus_xxx ID. Plugins write to this table directly; no separate table per plugin is required.
Design Principles
- One table, all gateways. The
providercolumn namespaces each row. Plugins never create or drop this table. - Customer mapping only. Only the gateway's customer-level identifier is stored locally. Individual payment method IDs (cards, bank accounts, saved sources) live on the gateway and are fetched via API at runtime.
- Environment isolation. A
sandboxprofile and aproductionprofile are separate rows for the same user. They must never be mixed. - Table lifecycle. The table is created by the core J2Commerce
install.mysql.utf8.sql. When a payment plugin is uninstalled, its rows may optionally be cleaned up, but the table itself must remain.
Schema
CREATE TABLE IF NOT EXISTS `#__j2commerce_paymentprofiles` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`user_id` INT NOT NULL,
`provider` VARCHAR(50) NOT NULL DEFAULT 'authorizenet'
COMMENT 'Payment gateway identifier',
`customer_profile_id` VARCHAR(50) NOT NULL
COMMENT 'Gateway customer/profile ID',
`environment` VARCHAR(10) NOT NULL DEFAULT 'production'
COMMENT 'sandbox or production',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_user_provider_env` (`user_id`, `provider`, `environment`),
KEY `idx_customer_profile` (`customer_profile_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
Columns
| Column | Type | Description |
|---|---|---|
id | INT UNSIGNED | Auto-increment primary key. |
user_id | INT | Joomla user ID (#__users.id). |
provider | VARCHAR(50) | Lowercase gateway identifier — see Provider Naming Convention. |
customer_profile_id | VARCHAR(50) | The gateway's customer or profile ID (e.g., Authorize.Net customerProfileId, Stripe cus_xxx). |
environment | VARCHAR(10) | sandbox or production. Never mix environments for the same user. |
created_at | DATETIME | Row creation timestamp, set automatically. |
updated_at | DATETIME | Last modification timestamp, updated automatically on every write. |
The UNIQUE constraint on (user_id, provider, environment) enforces exactly one profile per user per gateway per environment. Use INSERT ... ON DUPLICATE KEY UPDATE to upsert safely.
Provider Naming Convention
Use lowercase identifiers with no spaces or special characters:
| Gateway | provider value |
|---|---|
| Authorize.Net | authorizenet |
| Stripe | stripe |
| Square | square |
| Braintree | braintree |
| PayPal (vault) | paypal |
Establish your identifier in the plugin's documentation and use it consistently. Changing it after rows exist will orphan existing profiles.
PHP Usage
Save a Profile
use Joomla\Database\ParameterType;
$db = $this->getDatabase();
$query = $db->getQuery(true);
$provider = 'stripe';
$query->insert($db->quoteName('#__j2commerce_paymentprofiles'))
->columns(['user_id', 'provider', 'customer_profile_id', 'environment'])
->values(':userId, :provider, :profileId, :env')
->bind(':userId', $userId, ParameterType::INTEGER)
->bind(':provider', $provider)
->bind(':profileId', $customerProfileId)
->bind(':env', $environment);
$db->setQuery($query)->execute();
Upsert (Insert or Update)
Because the UNIQUE constraint will reject a duplicate insert, use the MySQL ON DUPLICATE KEY UPDATE idiom when you want to overwrite an existing profile:
$db->setQuery(
'INSERT INTO ' . $db->quoteName('#__j2commerce_paymentprofiles') .
' (' . $db->quoteName('user_id') . ', ' .
$db->quoteName('provider') . ', ' .
$db->quoteName('customer_profile_id') . ', ' .
$db->quoteName('environment') . ')' .
' VALUES (:userId, :provider, :profileId, :env)' .
' ON DUPLICATE KEY UPDATE ' .
$db->quoteName('customer_profile_id') . ' = VALUES(' . $db->quoteName('customer_profile_id') . ')'
)->bind(':userId', $userId, ParameterType::INTEGER)
->bind(':provider', $provider)
->bind(':profileId', $customerProfileId)
->bind(':env', $environment);
$db->execute();
Look Up a Profile
$query = $db->getQuery(true)
->select($db->quoteName('customer_profile_id'))
->from($db->quoteName('#__j2commerce_paymentprofiles'))
->where($db->quoteName('user_id') . ' = :userId')
->where($db->quoteName('provider') . ' = :provider')
->where($db->quoteName('environment') . ' = :env')
->bind(':userId', $userId, ParameterType::INTEGER)
->bind(':provider', $provider)
->bind(':env', $environment);
$profileId = $db->setQuery($query)->loadResult();
loadResult() returns null when no row exists — treat that as "no saved profile, proceed as a new customer."
Delete on Plugin Uninstall (Optional)
$query = $db->getQuery(true)
->delete($db->quoteName('#__j2commerce_paymentprofiles'))
->where($db->quoteName('provider') . ' = :provider')
->bind(':provider', $provider);
$db->setQuery($query)->execute();
Run this in the plugin's onExtensionBeforeUninstall handler only if your privacy policy requires it. The table itself must not be dropped.
Displaying Saved Payment Methods
J2Commerce provides a unified "Payment Methods" tab in the customer's My Account profile. Instead of each payment plugin creating its own tab, all saved payment methods appear in a single consolidated view.
Unified Tab Integration
To display your saved payment methods in the unified tab, implement the onJ2CommerceGetSavedPaymentMethods event in your payment plugin. Return structured data about each payment method:
public function onGetSavedPaymentMethods(Event $event): void
{
$userId = $event->getArgument('user_id', 0);
if ($userId < 1 || !(int) $this->params->get('allow_saved_cards', 1)) {
return;
}
$customerProfileId = $this->getCustomerProfileId($userId);
if (!$customerProfileId) {
return;
}
$methods = $this->fetchPaymentMethodsFromGateway($customerProfileId);
$result = $event->getArgument('result', []);
foreach ($methods as $method) {
$result[] = [
'id' => $method->id,
'provider' => 'yourplugin',
'type' => 'card',
'display_name' => ucfirst($method->brand) . ' ending in ' . $method->last4,
'brand' => strtolower($method->brand),
'last4' => $method->last4,
'exp_month' => $method->expMonth,
'exp_year' => $method->expYear,
'is_default' => $method->isDefault,
'actions' => ['delete', 'set_default'],
];
}
$event->setArgument('result', $result);
}
See Saved Payment Methods Event for complete implementation details.
Migration from J2Store
If the previous J2Store installation stored Authorize.Net profiles in #__j2store_payment_profiles, migrate them with:
INSERT INTO `#__j2commerce_paymentprofiles`
(`user_id`, `provider`, `customer_profile_id`, `environment`)
SELECT
`user_id`,
'authorizenet',
`profile_id`,
CASE WHEN `provider` LIKE '%Sandbox%' THEN 'sandbox' ELSE 'production' END
FROM `#__j2store_payment_profiles`
WHERE `provider` LIKE 'Authorize.net%';
Adjust the WHERE clause and provider mapping for any other gateways stored in the legacy table.
Related
- Saved Payment Methods Event - Unified Payment Methods tab integration
- Payment Plugin Development
- Order Lifecycle