Skip to content

Core · Service · Active

platform-payments-service

Provider-agnostic CORE payment boundary for pricing catalog, checkout sessions, Stripe webhooks, purchase and subscription records, and normalized payment events to product backends.

  • TypeScript
  • NestJS 11
  • Prisma
  • PostgreSQL
  • Stripe SDK
  • @platform/contracts-payments

Spec sheet

Boundary

Core / Payments

Runtime

NestJS 11 HTTP service

Default port

3700

Proxy host

http://payments.cs.lvh.me:8080

Persistence

Dedicated payments-postgres via PAYMENTS_DATABASE_URL

Security

Internal routes guarded by x-platform-internal-token; provider webhook is public and signature-validated

Primary provider

Stripe

Responsibilities

  • Keep provider-specific payment SDK logic out of product BFFs.
  • Own the shared pricing item catalog and provider price references.
  • Create provider checkout sessions for product backends.
  • Ingest provider webhooks and persist purchase, subscription and event state.
  • Deliver normalized payment events back to product consumers such as FunnelAI.
  • Expose plan, checkout and payment record lookup APIs for backend-to-backend consumers.

Interfaces and contract surface

  • GET /health
  • GET /internal/payments/providers
  • GET /internal/payments/pricing-items
  • GET /internal/payments/public/pricing-items
  • POST /internal/payments/pricing-items
  • PATCH /internal/payments/pricing-items/:id
  • DELETE /internal/payments/pricing-items/:id
  • POST /internal/payments/checkout-sessions
  • GET /internal/payments/checkout-sessions/:providerCheckoutSessionId
  • GET /internal/payments/tenants/:consumerTenantId/plan-status
  • GET /internal/payments/purchases
  • GET /internal/payments/subscriptions
  • POST /internal/payments/event-deliveries/:id/retry
  • POST /webhooks/stripe

Consumers

Dependencies and external touchpoints

Notes

  • Frontend applications must not call this service directly; products expose their own BFF/facade.
  • CS Transaction Manage reads tenant plan status here to compute subscription-aware product capabilities.
  • The FunnelAI rollout includes a one-time import:funnel-billing backfill before legacy billing tables are removed.
  • PAYMENTS_CONSUMER_FUNNEL_EVENTS_URL points normalized payment events to the FunnelAI backend in the local stack.

Source references

  • platform-payments-service/README.md
  • docs/core-services-integration.md
  • platform-payments-service/prisma/schema.prisma
  • platform-payments-service/package.json

Come integrarsi davvero

platform-payments-service e il boundary CORE per pagamenti provider-agnostic. I frontend non lo chiamano direttamente: i prodotti espongono una BFF/facade e usano questo servizio solo backend-to-backend.

Variabili utili

bash
export PAYMENTS_URL=http://127.0.0.1:3700
export INTERNAL_TOKEN=platform-local-stack-internal-token
bash
curl -sS "$PAYMENTS_URL/internal/payments/providers" \
  -H "x-platform-internal-token: $INTERNAL_TOKEN"

La configurazione runtime dei provider e persistita in PaymentProviderConfig. STRIPE_SECRET_KEY e STRIPE_WEBHOOK_SECRET servono solo come bootstrap iniziale: la console status puo poi aggiornare secret, default route e stato enabled via API admin.

bash
curl -sS "$PAYMENTS_URL/internal/payments/public/pricing-items?active=true" \
  -H "x-platform-internal-token: $INTERNAL_TOKEN"

Checkout

Il backend prodotto crea o risolve il proprio tenant locale, poi chiede al payment service una checkout session.

bash
curl -sS -X POST "$PAYMENTS_URL/internal/payments/checkout-sessions" \
  -H "Content-Type: application/json" \
  -H "x-platform-internal-token: $INTERNAL_TOKEN" \
  -d '{
    "providerKey": "stripe",
    "pricingItemCode": "starter_month",
    "consumerKey": "funnel",
    "consumerTenantId": "tenant_123",
    "tenantName": "Demo",
    "legalName": "Demo SRL",
    "billingEmail": "billing@example.test",
    "successUrl": "http://funnel.cs.lvh.me:8080/#/pricing/complete?session_id={CHECKOUT_SESSION_ID}",
    "cancelUrl": "http://funnel.cs.lvh.me:8080/#/pricing?checkout=cancel"
  }'

Webhook provider

Stripe deve puntare a:

text
POST /webhooks/stripe

Il servizio salva l'evento provider, aggiorna purchase/subscription nel proprio database e invia eventi normalizzati al consumer configurato, ad esempio POST /api/internal/payments/events di FunnelAI.

Endpoint reference

Header comuni per le route interne:

  • x-platform-internal-token: $INTERNAL_TOKEN
  • Content-Type: application/json sulle route con body
EndpointScopoRequestEsempioRisposta rappresentativaErrori e consumer
GET /healthVerifica liveness del servizio pagamenti.Nessun body.curl -sS "$PAYMENTS_URL/health"{"status":"ok","service":"platform-payments-service"}5xx se runtime non disponibile. Consumer: status dashboard.
GET /internal/payments/providersElenca provider e capability pagamento.Header interno.curl -sS "$PAYMENTS_URL/internal/payments/providers" -H "x-platform-internal-token: $INTERNAL_TOKEN"{"defaultProviderKey":"stripe","items":[{"key":"stripe","label":"Stripe","enabled":true,"configured":true,"default":true,"status":"active","capabilities":{"checkoutSessions":true,"webhooks":true,"subscriptions":true,"oneTimePayments":true}}]}401/403 token. Consumer: BFF e operations.
GET /internal/payments/providers/:providerKeyDettaglio admin provider, senza secret in chiaro.Path providerKey.curl -sS "$PAYMENTS_URL/internal/payments/providers/stripe" -H "x-platform-internal-token: $INTERNAL_TOKEN"{"provider":{"key":"stripe","secretKeyConfigured":true,"webhookSecretConfigured":true,"baseUrl":null},"defaultProviderKey":"stripe"}404 provider sconosciuto, 401/403 token. Consumer: status console.
PUT /internal/payments/providers/:providerKeyAggiorna configurazione provider persistita.Opzionali enabled, makeDefault, secretKey, webhookSecret, baseUrl.curl -sS -X PUT "$PAYMENTS_URL/internal/payments/providers/stripe" -H "Content-Type: application/json" -H "x-platform-internal-token: $INTERNAL_TOKEN" -d '{"enabled":true,"makeDefault":true,"secretKey":"sk_test_x","webhookSecret":"whsec_x"}'{"provider":{"key":"stripe","status":"active"},"defaultProviderKey":"stripe"}400 payload non valido, 404 provider sconosciuto, 401/403 token. Consumer: status console.
GET /internal/payments/pricing-items?active=trueLista catalogo prezzi interno, inclusi riferimenti provider.Query opzionale active=true/false.curl -sS "$PAYMENTS_URL/internal/payments/pricing-items?active=true" -H "x-platform-internal-token: $INTERNAL_TOKEN"{"items":[{"id":"price_1","code":"starter_month","name":"Starter","type":"SUBSCRIPTION","billingInterval":"MONTH","amountCents":2900,"currency":"EUR","active":true,"providerRefs":[{"providerKey":"stripe","providerPriceId":"price_x"}]}]}400 query non valida, 401/403 token. Consumer: admin BFF e import/backfill.
GET /internal/payments/public/pricing-items?active=trueLista prezzi esponibili ai prodotti senza riferimenti provider.Query opzionale active, default true.curl -sS "$PAYMENTS_URL/internal/payments/public/pricing-items?active=true" -H "x-platform-internal-token: $INTERNAL_TOKEN"{"items":[{"id":"price_1","code":"starter_month","name":"Starter","description":null,"type":"SUBSCRIPTION","billingInterval":"MONTH","amountCents":2900,"currency":"EUR","active":true}]}401/403 token. Consumer: BFF pricing dei prodotti.
POST /internal/payments/pricing-itemsCrea un item di catalogo prezzi.code, name, type, billingInterval, amountCents; opzionali currency, active, description, providerRefs.curl -sS -X POST "$PAYMENTS_URL/internal/payments/pricing-items" -H "Content-Type: application/json" -H "x-platform-internal-token: $INTERNAL_TOKEN" -d '{"code":"starter_month","name":"Starter","type":"SUBSCRIPTION","billingInterval":"MONTH","amountCents":2900,"currency":"EUR","active":true,"providerRefs":[{"providerKey":"stripe","providerPriceId":"price_123"}]}'{"id":"price_1","code":"starter_month","name":"Starter","active":true,"providerRefs":[{"providerKey":"stripe","providerPriceId":"price_123"}]}400 DTO non valido, 409 codice duplicato, 401/403 token. Consumer: operations.
PATCH /internal/payments/pricing-items/:idAggiorna parzialmente un item prezzi.Path id, body parziale come create.curl -sS -X PATCH "$PAYMENTS_URL/internal/payments/pricing-items/price_1" -H "Content-Type: application/json" -H "x-platform-internal-token: $INTERNAL_TOKEN" -d '{"active":false}'{"id":"price_1","code":"starter_month","active":false}400 DTO non valido, 404 item mancante. Consumer: operations.
DELETE /internal/payments/pricing-items/:idDisattiva o rimuove item secondo policy del service.Path id.curl -sS -X DELETE "$PAYMENTS_URL/internal/payments/pricing-items/price_1" -H "x-platform-internal-token: $INTERNAL_TOKEN"{"id":"price_1","deleted":true}404 item mancante, 409 vincoli record collegati. Consumer: operations.
POST /internal/payments/checkout-sessionsCrea checkout provider per tenant prodotto.pricingItemCode, consumerKey, consumerTenantId, dati billing, successUrl, cancelUrl; opzionale providerKey.curl -sS -X POST "$PAYMENTS_URL/internal/payments/checkout-sessions" -H "Content-Type: application/json" -H "x-platform-internal-token: $INTERNAL_TOKEN" -d '{"providerKey":"stripe","pricingItemCode":"starter_month","consumerKey":"funnel","consumerTenantId":"tenant_123","tenantName":"Demo","legalName":"Demo SRL","billingEmail":"billing@example.test","successUrl":"http://funnel.cs.lvh.me:8080/#/pricing/complete?session_id={CHECKOUT_SESSION_ID}","cancelUrl":"http://funnel.cs.lvh.me:8080/#/pricing"}'{"checkoutSessionId":"cs_test_123","checkoutUrl":"https://checkout.stripe.com/c/pay/cs_test_123","providerKey":"stripe","mode":"subscription","consumerTenantId":"tenant_123"}400 payload non valido, 404 prezzo non trovato, 503 provider non configurato. Consumer: BFF pricing.
GET /internal/payments/checkout-sessions/:providerCheckoutSessionIdLegge stato normalizzato di una checkout session provider.Path providerCheckoutSessionId.curl -sS "$PAYMENTS_URL/internal/payments/checkout-sessions/cs_test_123" -H "x-platform-internal-token: $INTERNAL_TOKEN"{"checkoutSessionId":"cs_test_123","providerKey":"stripe","consumerKey":"funnel","consumerTenantId":"tenant_123","status":"SUCCEEDED","mode":"subscription","pricingItem":{"code":"starter_month","name":"Starter","amountCents":2900,"currency":"EUR"}}404 sessione sconosciuta. Consumer: completion page BFF e operations.
GET /internal/payments/tenants/:consumerTenantId/plan-statusRestituisce piano corrente e ultimo acquisto one-shot del tenant.Path consumerTenantId.curl -sS "$PAYMENTS_URL/internal/payments/tenants/tenant_123/plan-status" -H "x-platform-internal-token: $INTERNAL_TOKEN"{"currentSubscription":{"id":"sub_1","status":"ACTIVE","pricingItem":{"code":"starter_month"}},"latestOneTimePurchase":null}401/403 token. Consumer: BFF subscription-aware.
GET /internal/payments/purchasesLista acquisti normalizzati persistiti.Header interno.curl -sS "$PAYMENTS_URL/internal/payments/purchases" -H "x-platform-internal-token: $INTERNAL_TOKEN"{"items":[{"id":"pur_1","consumerKey":"funnel","consumerTenantId":"tenant_123","status":"SUCCEEDED","amountCents":2900,"currency":"EUR"}]}401/403 token, 5xx database. Consumer: operations e backoffice.
GET /internal/payments/subscriptionsLista sottoscrizioni normalizzate persistite.Header interno.curl -sS "$PAYMENTS_URL/internal/payments/subscriptions" -H "x-platform-internal-token: $INTERNAL_TOKEN"{"items":[{"id":"sub_1","consumerKey":"funnel","consumerTenantId":"tenant_123","providerSubscriptionId":"sub_123","status":"ACTIVE"}]}401/403 token, 5xx database. Consumer: operations e backoffice.
POST /internal/payments/event-deliveries/:id/retryRiprova una delivery evento consumer fallita.Path delivery id.curl -sS -X POST "$PAYMENTS_URL/internal/payments/event-deliveries/delivery_123/retry" -H "x-platform-internal-token: $INTERNAL_TOKEN"{"id":"delivery_123","status":"PENDING","attempts":2,"lastError":null}404 delivery mancante, 409 delivery non retryable. Consumer: operations.
POST /webhooks/stripeIngest pubblico webhook Stripe con firma provider.Raw body Stripe e header stripe-signature; non usa token interno.curl -sS -X POST "$PAYMENTS_URL/webhooks/stripe" -H "stripe-signature: <STRIPE_SIGNATURE>" --data-binary @stripe-event.json{"received":true,"eventId":"evt_123"}400 raw body/firma mancante, 401 firma non valida, 409 evento gia gestito. Consumer: Stripe.

Evento consumer normalizzato

Dopo webhook valido, il service consegna al consumer configurato un payload PaymentConsumerEventPayload:

json
{
  "eventId": "evt_123",
  "providerKey": "stripe",
  "consumerKey": "funnel",
  "consumerTenantId": "tenant_123",
  "eventType": "subscription.status_changed",
  "pricingItem": {
    "id": "price_1",
    "code": "starter_month",
    "name": "Starter",
    "type": "SUBSCRIPTION",
    "billingInterval": "MONTH",
    "amountCents": 2900,
    "currency": "EUR",
    "active": true
  },
  "purchase": null,
  "subscription": {
    "id": "sub_1",
    "providerKey": "stripe",
    "providerSubscriptionId": "sub_123",
    "status": "ACTIVE"
  }
}

Workspace reference: /Users/jeanpaul/projects/cs-repository