Skip to main content

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 provider column 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 sandbox profile and a production profile 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​

ColumnTypeDescription
idINT UNSIGNEDAuto-increment primary key.
user_idINTJoomla user ID (#__users.id).
providerVARCHAR(50)Lowercase gateway identifier — see Provider Naming Convention.
customer_profile_idVARCHAR(50)The gateway's customer or profile ID (e.g., Authorize.Net customerProfileId, Stripe cus_xxx).
environmentVARCHAR(10)sandbox or production. Never mix environments for the same user.
created_atDATETIMERow creation timestamp, set automatically.
updated_atDATETIMELast 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:

Gatewayprovider value
Authorize.Netauthorizenet
Stripestripe
Squaresquare
Braintreebraintree
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.