Trackily Docs

Payment Accounts

TL;DR : un payment_account représente UN compte Stripe ou PayPal connecté à Trackily. Multi-compte natif : tu peux avoir 3 comptes Stripe et 2 PayPal en parallèle, et choisir lesquels une landing utilise. Les clés sont chiffrées at-rest. Connexion possible manuelle (paste de sk_live_…) ou OAuth (un clic, Phase 18 — voir stripe-connect.md).

Settings → Payments

Concept

payment_accounts est la table qui stocke les credentials de tes processeurs de paiement. Une ligne = un compte chez un provider (Stripe LLC, PayPal, demain Mollie ou Square). Le système est délibérément multi-compte parce qu'il n'est pas rare qu'un opérateur Trackily :

  • ait un compte Stripe par marque ou par pays (pour des raisons fiscales) ;
  • teste un compte Stripe en mode test à côté de son compte live ;
  • route certaines landings sur un sous-compte Stripe Connect d'un partenaire ;
  • garde PayPal en backup pour les visiteurs sans CB.

Chaque compte est indépendant : ses clés vivent dans sa propre ligne, chiffrées at-rest via le mécanisme CIPHER_TABLES (clé maître = SECRETS_MASTER_KEY).

Anatomie d'un compte (payment_accounts)

Champ Type Rôle
id SERIAL identifiant interne
name TEXT nom libre ("Stripe Pro EU", "PayPal backup")
provider TEXT stripe / paypal (string ouvert pour futurs ajouts)
mode TEXT live / test
api_key TEXT (chiffré) publishable / client_id
api_secret TEXT (chiffré) secret_key / client_secret
webhook_secret TEXT (chiffré) whsec_… pour Stripe (vide pour PayPal)
is_active BOOLEAN flag — désactive le compte sans le supprimer
is_default BOOLEAN flag — compte par défaut pour son provider
metadata JSONB extension
oauth_user_id TEXT acct_xxx Stripe (Phase 18 OAuth)
oauth_access_token TEXT (chiffré) sk_live_… scopé via OAuth
oauth_refresh_token TEXT (chiffré) rarement utilisé pour Stripe Standard, conservé pour Plus tard
oauth_scope TEXT read_write typiquement
oauth_livemode BOOLEAN mirror de mode pour les comptes OAuth
oauth_connected_at TIMESTAMPTZ date de connexion OAuth
created_at / updated_at TIMESTAMPTZ audit

Mapping par provider

Provider api_key api_secret webhook_secret
Stripe publishable key (pk_live_… ou pk_test_…) secret key (sk_live_… ou sk_test_…) webhook signing secret (whsec_…)
PayPal REST client_id REST client_secret (non utilisé)

Chiffrement at-rest

payment_accounts est dans le set CIPHER_TABLES. Le wrapper findById / findAll / createPaymentAccount / updatePaymentAccount chiffre / déchiffre automatiquement les colonnes sensibles (api_secret, webhook_secret, oauth_access_token, oauth_refresh_token) en lecture / écriture.

Concrètement :

  • Tu colles sk_live_xxx dans le formulaire admin → le helper encryptRowSecrets chiffre avant l'INSERT → la base contient le ciphertext.
  • À chaque création de Stripe Checkout Session, le helper appelle findById('payment_accounts', id) qui décrypte transparent → la requête vers api.stripe.com reçoit le secret en clair.
  • Si tu perds SECRETS_MASTER_KEY, tu perds l'accès aux clés. Backup-la ailleurs que dans le .env.

Comment Trackily choisit le compte au checkout

Le visiteur cliquant sur "Pay with Stripe" sur ta landing :

  1. La landing a une colonne payment_account_ids (JSONB array). Si elle contient [3, 7], le visiteur peut payer via le compte 3 OU le compte 7.
  2. Si l'array a un seul élément → ce compte est utilisé.
  3. Si l'array a plusieurs comptes → l'UI affiche un sélecteur (rarement utile au visiteur final, plus pour le multi-brand).
  4. Si l'array est vide → fallback sur is_default=true du provider.
  5. Si aucun n'a is_default → erreur "No payment account configured".

Pour les pages produit standalone /p/<slug>, c'est native_products.default_payment_account_ids qui prend le relais (puisqu'il n'y a pas de landing).

Effective payment account (OAuth vs manual)

Le code applicatif utilise un helper :

function effectivePaymentAccount(row) {
  if (!row) return null;
  // Si OAuth → on remplace api_secret par oauth_access_token côté API.
  if (row.oauth_user_id && row.oauth_access_token) {
    return {
      ...row,
      api_secret: row.oauth_access_token,
      api_key: row.api_key || row.oauth_publishable_key,
    };
  }
  return row;
}

Pratique : du point de vue du reste du code (createCheckoutSession, refund…), un compte OAuth est indiscernable d'un compte manuel. Le oauth_access_token est un sk_live_… scopé à l'account_id du merchant.

Comment faire (UI + MCP)

Connexion manuelle (paste de clés)

C'est la voie historique. Toujours dispo si tu n'as pas configuré Stripe Connect côté plateforme.

  1. Settings → Payments (ou /admin#commerce/payment-methods).
  2. Clique + Add manually.
  3. Modal :
    • Account name — "Stripe Pro EU".
    • Provider — Stripe ou PayPal.
    • Mode — Live ou Test.
    • Publishable key (Stripe) — pk_live_xxx ou pk_test_xxx. Sans c'est pas grave si tu n'utilises pas le SDK front (les clients sont redirigés vers Stripe Checkout qui n'en a pas besoin).
    • Secret keysk_live_xxx ou sk_test_xxx.
    • Webhook secret (Stripe) — whsec_xxx, à récupérer après avoir créé le webhook côté Stripe.
  4. Save → la ligne payment_accounts est créée, secrets chiffrés.

Étape webhook (manuelle) : dans le dashboard Stripe → Developers → Webhooks → Add endpoint :

  • URL : https://ton-instance.com/webhook/stripe/native-pending
  • Events : checkout.session.completed, payment_intent.succeeded, payment_intent.payment_failed, customer.subscription.updated, customer.subscription.deleted
  • Récupère le whsec_… → colle-le dans Trackily.

Connexion OAuth (Phase 18, recommandé)

Si la plateforme (trackily.online ou ton instance self-hosted) a configuré son commerce_stripe_oauth_client_id + client_secret, tu vois un bouton Connect Stripe sur la page Settings → Payments. Un clic :

  1. Tu es redirigé vers Stripe.
  2. Tu te connectes avec ton compte Stripe.
  3. Tu autorises Trackily à utiliser ton compte (scope read_write).
  4. Tu reviens sur Trackily, la ligne payment_accounts est créée automatiquement (avec un webhook auto-créé côté Stripe).
  5. Tu n'as rien copié-collé.

Voir stripe-connect.md pour le walkthrough complet.

Via MCP / admin REST

Pas d'outil MCP dédié à la création de payment_accounts (volontairement — c'est trop sensible pour qu'un agent puisse créer un compte de paiement sans confirmation visuelle humaine sur les credentials). Tu passes par l'admin REST :

POST /admin/api/payment-accounts
Body: {
  "name": "Stripe Pro EU",
  "provider": "stripe",
  "mode": "live",
  "api_key": "pk_live_xxx",
  "api_secret": "sk_live_xxx",
  "webhook_secret": "whsec_xxx",
  "is_active": true,
  "is_default": true
}
PUT /admin/api/payment-accounts/:id
Body: { "is_active": false }
DELETE /admin/api/payment-accounts/:id

Les endpoints sont protégés par authMiddleware + requirePermission('settings', 'write').

Mode live vs test

live test
Endpoints Stripe api.stripe.com (production) api.stripe.com (mêmes endpoints, scopés via le sk_test_)
Argent réel oui non — carte de test 4242 4242 4242 4242
Visible dans le dashboard Stripe oui onglet "Test data"
Pour quoi faire production tests de bout-en-bout, démos

Bonne pratique : avoir un compte test permanent et passer toutes tes nouvelles landings dessus avant de switcher sur le live. Tu peux flip un compte de live à test (et inversement) en éditant le champ mode — mais tu dois aussi remplacer la api_secret correspondante (les clés sk_live_ et sk_test_ sont différentes).

Exemples concrets

1. Setup minimal : 1 Stripe live + 1 PayPal live

Stripe Pro EU      (provider=stripe,  mode=live, is_default=true)
PayPal Sauvegarde  (provider=paypal,  mode=live, is_default=true)

Sur chaque landing, payment_account_ids = [] → fallback sur les defaults. Le visiteur voit deux boutons : "Pay with Stripe" + "Pay with PayPal".

2. Multi-brand : 3 comptes Stripe

Stripe Brand A  (provider=stripe, mode=live, is_default=false)
Stripe Brand B  (provider=stripe, mode=live, is_default=false)
Stripe Brand C  (provider=stripe, mode=live, is_default=false)

Sur chaque landing de la brand A : payment_account_ids=[1]. Brand B : [2]. Etc. Aucun is_default (pour éviter qu'une landing oublieuse pioche dans le mauvais compte).

3. Setup test + live côte à côte

Stripe Test  (provider=stripe, mode=test, is_default=false)
Stripe Live  (provider=stripe, mode=live, is_default=true)

Tu duppliques une landing live → tu lui assignes payment_account_ids=[<id du test>] → tu testes le checkout end-to-end avec une carte 4242, sans risquer de polluer la prod.

Sécurité

  • Ne commit jamais ton .env qui contient SECRETS_MASTER_KEY.
  • Ne paste jamais un sk_live_ dans Slack / Notion / un repo. Si tu l'as fait, rotate immédiatement côté Stripe Dashboard → API keys → Roll key.
  • Restreins les scopes côté Stripe — le scope read_write est nécessaire pour OAuth, mais tu peux créer un compte API restreint manuellement avec uniquement les permissions Charges/Refunds/PaymentIntents/CheckoutSessions/Webhooks si tu veux du locked-down.
  • Audit ton historique des refunds via order_transactions (kind='refund'). Une explosion de refunds depuis un seul compte est un signal de fraude.
  • Tourne les clés tous les 12 mois — édite la ligne avec la nouvelle clé, ne supprime pas (les commandes historiques gardent leur payment_account_id).

Erreurs courantes

  • "No active key on Stripe account" — la api_secret n'a pas été setté, ou la décryption a échoué (mauvaise SECRETS_MASTER_KEY après restore).
  • "Webhook signature verification failed" — le webhook_secret configuré dans Trackily ne match pas celui de l'endpoint Stripe. Le whsec_… est différent pour chaque endpoint Stripe — vérifie le bon.
  • "Account is inactive"is_active=false. Toggle dans l'UI.
  • "No payment account configured for this landing" — la landing a payment_account_ids=[] ET aucun compte n'a is_default=true pour ce provider. Définis un default ou attache au moins un compte à la landing.
  • "Stripe in test mode but I'm getting real charges" — vérifie que la api_secret est bien sk_test_xxx et pas sk_live_xxx. Le champ mode='test' n'a aucun effet runtime — c'est juste un label. C'est la api_secret qui décide pour de vrai.
  • "OAuth connect a échoué, le webhook n'a pas été créé" — le webhook auto-create est best-effort. Si ça a foiré, configure-le à la main dans le dashboard Stripe (URL /webhook/stripe/native-pending, events checkout.session.completed au minimum) et colle le whsec_… dans le compte via Edit.
  • "Disconnect d'un compte OAuth, mais le compte reste en base" — c'est attendu. Trackily clear les colonnes oauth_* mais garde la ligne pour que les commandes historiques conservent leur payment_account_id. Le compte passe en is_active=false automatiquement.

Voir aussi