Payments
Provider integrations, transaction lifecycle, refunds, and webhook dispatch for member-facing money flow inside a gym.
What & why
Payments is the B2B2C money rail: a member pays the gym, the gym keeps the cash, FitKit only collects platform fees on the side (see platform-billing/). Every charge runs through a per-org payment_provider_configs row holding encrypted credentials for the org’s chosen gateway. Multiple Israeli acquirers are supported in parallel; the org picks one.
Persona impact:
| Persona | Surface |
|---|---|
| Member | Buy a plan, register a card, see receipts (/dashboard/plans, /buy/courses/[id]). Cancellation + refund request UI. |
| Owner / Admin | Configure provider, issue refunds, close manual-refund tasks, review cancellation requests, see analytics (/dashboard/payments). |
| Platform (FitKit) | Observability only — money never lands in a FitKit account on this rail. |
Capabilities
- Hosted-page checkout (Cardcom LowProfile, iCredit, Meshulam, Tranzila, Morning) — see Providers.
- Tokenised recurring charges driven by FitKit (no native recurring deals on Cardcom).
- Refunds split by capability:
automatic(single API call) vsmanual(opens amanual_refundtask the owner closes with the credit-doc number). - Verify-on-return fallback when a provider webhook never arrives (
POST /organizations/:orgId/payments/verify-return). - Tax-document linkage via
payment_provider_clients(Morning) +invoicing_configs(GreenInvoice plug-in scaffold). - Card-on-file registration by an admin on behalf of a member (
POST /members/:id/register-card).
Providers
Discovered from apps/api/src/payments/providers/. Registered in PaymentsModule.onModuleInit.
| Provider | Adapter | Webhook URL form | Signature validation | Refund capability | Notes |
|---|---|---|---|---|---|
cardcom | cardcom.provider.ts | path /webhooks/payments/cardcom/:orgId | none — Cardcom does not sign; trust comes from re-fetching GetLpResult server-side | manual | Drives platform-billing too. |
icredit | icredit.provider.ts | path /webhooks/payments/icredit/:orgId | GroupPrivateToken body field equals stored credential | manual | Rivhit-backed credit document. |
meshulam | meshulam.provider.ts | path /webhooks/payments/meshulam/:orgId | webhookKey body field equals stored apiKey | manual | Light-API iframe tokenisation. |
morning | morning.provider.ts | query /webhooks/payments/morning?org=… (single statically-registered URL per business) | TBD — temporary accept; controller dumps headers for debugging | manual | Auto-issues חשבונית מס/קבלה. |
tranzila | tranzila.provider.ts | path /webhooks/payments/tranzila/:orgId | stub — always valid (HMAC SHA256 TODO) | manual | Adapter is mostly a stub; not production-validated. |
test | (enum value only, no adapter) | n/a | n/a | n/a | Reserved for stubbed e2e seeding. |
All providers are manual for refunds today. The framework distinguishes automatic | manual (apps/api/src/payments/services/payment.service.ts:411); flipping a provider once we’ve verified its refund API end-to-end is a single return-value change.
Related features
subscriptions-plans/— owns the subscription state machine and cancellation requests. Payments fires the lifecycle transitions via webhook handlers.platform-billing/— separate money rail for FitKit’s own monthly fee. Uses the sameCardcomProvideradapter but a different DB schema and a single shared terminal (PLATFORM_BILLING_*env).courses/— course checkouts share the hosted-payment plumbing; the webhook activates acourse_entitlementsrow instead of a subscription.platform-tiers/— theautomated_billingfeature is gated to tierpro; the@RequiresFeatureguard fronts plan CRUD.webhooks/— the Clerk webhook lives here; payment provider webhooks live inapps/api/src/payments/controllers/payment-webhook.controller.ts(registered underPaymentsModule, notWebhooksModule).
Status
Production: Cardcom (live + platform billing terminal), iCredit, Meshulam.
Beta / debug: Morning (signature TBD — see morning.provider.ts:941), Tranzila (signature stub).
Refund automation: nothing in automatic yet — all providers open a manual task.
Gaps
- FIT-133 — webhook idempotency hardening: current logic short-circuits on
status === 'completed'per row, but no row-level advisory lock; concurrent webhook + verify-return can race on the period-advance update (webhook-processing.service.ts:257). - FIT-134 — capability flip: validate Cardcom
RefundDealso its capability can move from'manual'→'automatic'. - FIT-136 — failed renewal retry tuning:
RECURRING_INTERVAL_DAYS = [3, 7, 14]is hardcoded inrecurring-charge.service.ts:21; needs per-org override and a max-retries CTA path. - Morning webhook signature —
morning.provider.ts:941returnstrueregardless. Header capture is in place (payment-webhook.controller.ts:101); finalise the scheme before scaling Morning usage. - Tranzila adapter — signature + invoicing methods are stubs (
tranzila.provider.ts:135,tranzila.provider.ts:155). Treat as alpha.
See also docs/_archive/product/payments-prd.md (historical context, stale on the manual-refund task design) and docs/_archive/plans/cardcom-production-terminal.md (Cardcom rollout plan).