Vai al contenuto
Contenuto principale
plugin-billing

PayPal Integration (Phase C)

PayPal Integration (Phase C) # Architettura PayPal — differenze chiave da Stripe # PayPal usa una struttura a 3 livelli: Product (catalogo): descrive cosa vendi (id PROD-xxx)Billing Plan: pricing structure ricorrente (id…

Aggiornato 18 Mag 2026 min di lettura

PayPal Integration (Phase C)

Architettura PayPal — differenze chiave da Stripe

PayPal usa una struttura a 3 livelli:

  1. Product (catalogo): descrive cosa vendi (id PROD-xxx)
  2. Billing Plan: pricing structure ricorrente (id P-xxx)
  3. Subscription: impegno cliente su un Plan (creato al checkout)

Stripe usa 2 livelli (Product + Price), PayPal aggiunge Plan come livello dedicato per le subscription. Per pagamenti one-time si saltano i livelli 2-3 e si usa direttamente Orders API.

Componenti

HWBillingPayPalPayPalClient

Wrapper REST + OAuth con token caching.

Token management:

  • access_token() → recupera token OAuth client_credentials cached in transient
  • Refresh automatico 5 min prima della scadenza (default PayPal ~9h TTL)
  • Cache key: hwbi_paypal_token_{md5(mode + client_id)}

Endpoint coperti:

  • /v1/oauth2/token — token generation
  • /v1/catalogs/products — products CRUD
  • /v1/billing/plans — plans CRUD + activate/deactivate
  • /v1/billing/subscriptions — create, get, cancel, suspend, activate
  • /v2/checkout/orders — create, get, capture
  • /v1/notifications/verify-webhook-signature — webhook verification (remote)

Mode: option hwbi_paypal_mode switches api-m.sandbox.paypal.comapi-m.paypal.com.

Idempotency POST: header PayPal-Request-Id: {uuid4} su ogni create.

HWBillingPayPalPayPalProductSync

Push one-way piano locale → PayPal Product + Billing Plan.

  • Product: idempotent. Se metadata.paypal_product_id esiste → PATCH (name/description). Altrimenti POST.
  • Plan (solo recurring): se paypal_plan_id esiste E pricing/period invariati → no-op. Altrimenti plan_deactivate vecchio + plan_create nuovo (PayPal plans immutabili su pricing, come Stripe).
  • One-time products non hanno Plan — usano Orders API direttamente al checkout.

Salva refs in wp_hwbi_plans:

  • paypal_plan_id colonna (per Billing Plan)
  • metadata_json.paypal_product_id (per Catalog Product)

REST Endpoints

EndpointMethodAuthScopo
/checkout/paypal/create-orderPOSTlogged-in + V2 modeCrea Subscription (recurring) o Order (one-time), ritorna approval_url
/checkout/paypal/capturePOSTlogged-inConfirms post-return; UX faster (webhook è canonical)
/webhooks/paypalPOSTHMAC remoteRiceve eventi PayPal
Flow recurring (hosting plan):
1. Frontend → POST /checkout/paypal/create-order { plan_slug }
2. Backend: PayPalClient::subscription_create({ plan_id, subscriber, application_context })
3. Backend → response: { approval_url, paypal_id, order_id }
4. Frontend → redirect window to approval_url
5. User approves su PayPal → PayPal redirect a return_url con ?subscription_id=I-xxx&token=xxx
6. Frontend → POST /checkout/paypal/capture { paypal_subscription_id, hwbi_order_id }
7. PayPal → POST /webhooks/paypal con BILLING.SUBSCRIPTION.ACTIVATED
8. Webhook handler: marca order paid + crea wp_hwbi_subscriptions row + enqueue Plesk provisioning

Flow one-time (credit pack):

1. POST /checkout/paypal/create-order → PayPalClient::order_create({ intent: CAPTURE, purchase_units })
2. Frontend redirect ad approval_url
3. User approves → return con ?token=xxx (Order ID)
4. POST /checkout/paypal/capture { paypal_order_id } → order_capture (server-side)
5. PayPal → POST /webhooks/paypal con PAYMENT.CAPTURE.COMPLETED
6. Webhook handler: marca order paid + record payment + enqueue provisioning

Webhook Events gestiti

PayPal EventAction
PAYMENT.CAPTURE.COMPLETEDOne-time order paid → enqueue provisioning
CHECKOUT.ORDER.APPROVED/COMPLETEDSame as above (alternate paths)
BILLING.SUBSCRIPTION.CREATEDCreate local subscription row, status=active
BILLING.SUBSCRIPTION.ACTIVATEDSame + mark order paid + enqueue provisioning
BILLING.SUBSCRIPTION.UPDATEDSync status changes
BILLING.SUBSCRIPTION.RE-ACTIVATEDStatus → active
PAYMENT.SALE.COMPLETEDRecurring tick succeeded → record payment, status → active
BILLING.SUBSCRIPTION.SUSPENDEDStatus → suspended
BILLING.SUBSCRIPTION.CANCELLED/EXPIREDStatus → cancelled, enqueue Plesk cancel
BILLING.SUBSCRIPTION.PAYMENT.FAILEDStatus → past_due
PAYMENT.SALE.DENIEDStatus → past_due

Signature Verification — perché REMOTA?

PayPal usa firme asimmetriche basate su certificati X.509 + algoritmi multipli. Verificarle localmente richiederebbe:

  1. Scaricare paypal-cert-url (cert pubblico)
  2. Validare cert contro CA chain
  3. RSA verify del paypal-transmission-sig su HMAC del payload

Complesso e fragile. PayPal raccomanda chiamare POST /v1/notifications/verify-webhook-signature con i 5 headers + il payload + il webhook_id → loro fanno il check e ritornano verification_status: "SUCCESS" | "FAILURE".

Costo: 1 chiamata API extra per webhook (vs 0 per Stripe), ma robusto e zero codice criptografico nostro.

Setup (admin)

1. Crea App PayPal

  1. https://developer.paypal.com/dashboard/applications/live (o /sandbox per test)
  2. “Create App” → tipo: Business → assegna un nome (es. “Hostwebo Billing V2”)
  3. Copia Client ID + Secret

2. Configura nel plugin

  1. wp-admin → Hostwebo Billing → Impostazioni
  2. Sezione PayPal:
  • Modalità: sandbox (per test) o live (produzione)
  • Client ID + Secret → incolla
  1. Salva

3. Crea Webhook su PayPal

  1. https://developer.paypal.com/dashboard/applications → seleziona la tua app
  2. Scroll a “Webhooks” → “Add Webhook”
  3. URL: https://hostwebo.it/wp-json/hostwebo-billing/v1/webhooks/paypal
  4. Eventi:
  • Checkout order completed
  • Payment capture completed
  • Payment sale completed
  • Payment sale denied
  • Billing subscription activated
  • Billing subscription cancelled
  • Billing subscription expired
  • Billing subscription suspended
  • Billing subscription updated
  • Billing subscription payment failed
  1. Copia Webhook ID (formato WH-xxx)
  2. Plugin Impostazioni → PayPal → Webhook ID → incolla → Salva

4. Sync piani

  1. wp-admin → Hostwebo Billing → Piani
  2. Click “🅿️ Sync to PayPal (all)”
  3. Verifica che ogni piano abbia paypal_plan_id valorizzato

Test end-to-end (sandbox)

  1. Mode = sandbox + Sandbox API keys
  2. Sync piani → controlla su https://developer.paypal.com/dashboard/notifications/webhooks-events che i Products + Plans esistano
  3. Frontend: utente beta tester clicca “Acquista” → /v2-checkout/ → seleziona PayPal
  4. Approval PayPal con account sandbox personal (creato da PayPal Developer Dashboard)
  5. Approve → return su /v2-checkout/?paypal_subscription_id=I-xxx → frontend chiama /checkout/paypal/capture
  6. Webhook PayPal arriva entro 1-30 secondi → mark order paid + subscription created
  7. Verifica DB:
  • wp_hwbi_orders row con status=’paid’, payment_gateway=’paypal’
  • wp_hwbi_payments row con webhook_event_id univoco
  • wp_hwbi_subscriptions row (se recurring)
  • wp_hwbi_provisioning_jobs job pending → processed entro 1min

Filter / Action hooks

Tutti già documentati in stripe-integration.md — gli stessi action hooks emessi da Stripe sono emessi anche da PayPal:

  • hwbi_order_paid($order_id, $reference)
  • hwbi_subscription_renewed($subscription_id) (su PAYMENT.SALE.COMPLETED)
  • hwbi_subscription_cancelled($paypal_sub_id) (su BILLING.SUBSCRIPTION.CANCELLED)
  • hwbi_subscription_payment_failed($paypal_sub_id) (su BILLING.SUBSCRIPTION.PAYMENT.FAILED)

Plus filter PayPal-specific:

  • hwbi_paypal_return_url — URL frontend dopo approval (default /v2-checkout/)

Frontend integration (Fase G — page template /v2-checkout/)

In Phase G costruiremo il page-template /v2-checkout/ con UX dual-gateway:

  • 2 bottoni: “💳 Paga con carta (Stripe)” + “🅿️ Paga con PayPal”
  • Su click PayPal → fetch /checkout/paypal/create-orderwindow.location.href = approval_url
  • Su return da PayPal con ?paypal_subscription_id=... o ?token=... → fetch /checkout/paypal/capture → redirect a /grazie/

Cose da NON dimenticare

  • Sandbox prima: testare sempre prima in mode=sandbox prima di passare a live
  • Webhook timing: PayPal può ritardare webhook fino a 30s (vs Stripe <2s). UI deve mostrare "in attesa di conferma" finché order non è paid.
  • Currency: hardcoded EUR. Per multi-currency in futuro, aggiungere campo currency per piano.
  • Tax inclusive: prezzi nel nostro catalog sono IVA-incl, settiamo taxes.inclusive: true su PayPal Plan.
  • Trial: PayPal supporta trial via cycle tenure_type: TRIAL (1 cycle prima del REGULAR). Per ora non implementato — i nostri piani non hanno trial. Se serve, estendere PayPalProductSync::sync_plan.

Questa guida ti è stata utile?

Il tuo feedback ci aiuta a tenere la documentazione utile e aggiornata.

Continua a leggere