Skip to Content
Living documentation — last reviewed 2026-05-28
FeaturesPaymentsPayments — Code Map

Payments — Code Map

API — apps/api/src/payments/

Module + DI

  • payments.module.ts — wires all services, adapters, controllers. onModuleInit registers each provider into PaymentProviderRegistry.

Providers (providers/)

  • payment-provider.registry.tsMap<PaymentProviderType, PaymentProviderAdapter> with register/get/getOrThrow/list.
  • cardcom.provider.ts — LowProfile/Create + GetLpResult + ChargeWithToken + v11 RefundByTransactionId. tokenOnly pages run CreateTokenOnly, or SuspendedDeal + J5 when the request carries cardValidation: 'issuer_hold'; classifyDeal reads Cardcom’s 700/701 J-validation success codes as settled on those two operations only, and the token mapper falls back from TokenInfo to TranzactionInfo. FailedRedirectUrl takes request.failedUrl when present (falling back to cancelUrl), so a declined card no longer lands on the “you cancelled” page; CancelRedirectUrl is unchanged. No webhook signature. getRefundCapability() → 'manual' = refund API implemented but unverified against a live terminal, overridable per-org via payment-automatic-refunds.
  • icredit.provider.ts — Rivhit hosted page. GroupPrivateToken body signature.
  • meshulam.provider.ts — Light-API iframe + JSON IPN. webhookKey === credentials.apiKey signature.
  • morning.provider.ts — Single-URL Morning webhook (?org=…). Signature TBD (:941). Issues חשבונית/קבלה automatically.
  • tranzila.provider.ts — Stub-grade adapter; signature returns true.

Services (services/)

  • payment.service.ts — orchestrator + the exported withSubParam (stamps sub=<id> on every return leg, idempotently). createHostedPayment, createCourseHostedPayment, charge, createRecurring, refund (capability switch), completeManualRefundTask, verifyAndActivateReturn, sendPaymentReceipt.
  • card-validation-policy.service.ts — answers card-issuer-validation per org for every tokenise-only page (presale checkout via PaymentService.createHostedPayment, card registration via CardRegistrationController). Only a definite true asks for an issuer hold; off/unset/unevaluable keeps the provider default.
  • refund-capability.service.ts — third gate in refund’s automatic-vs-manual routing: the per-org payment-automatic-refunds PostHog flag for bring-your-own terminals whose adapter reports manual. Default OFF, fails toward the manual task, refuses when the charge predates the active provider config.
  • payment-transaction.service.ts — CRUD on payment_transactions. upsertPending, completePendingBySubscriptionId, findByProviderTransactionId.
  • payment-provider-config.service.ts — encrypted credential CRUD on payment_provider_configs; redacted reads.
  • payment-provider-clients.service.tspayment_provider_clients upserts (Morning client linkage).
  • credential-encryption.service.ts — AES-256-GCM round trip. Key from PAYMENT_CREDENTIALS_ENCRYPTION_KEY (32-byte hex).
  • webhook-processing.service.ts — dispatches payment.completed | payment.failed | subscription.renewed | subscription.cancelled | refund.completed | client.created. Cross-org guard on course entitlements (:215, :362). handlePaymentFailed’s first-purchase arm marks the transaction failed and leaves the subscription pending (emitting payment.activation_failed with reason: 'first_charge_failed') — the charge failed, not the checkout.
  • recurring-charge.service.ts — daily 02:00 UTC cron. FOR UPDATE SKIP LOCKED sweep, charge-then-update, debt promotion at 3 fails.
  • payment-method.service.tsreplacePaymentMethod (deactivate old, insert new), getActivePaymentMethod.
  • debt.service.tsclearDebt(subscriptionId) charges the accumulated debtAmountInCents and zeroes it.
  • payment-analytics.service.ts — KPIs (collected this month, MRR snapshot).
  • payment-monitoring.service.ts — the stale/abandoned-checkout guard. detectStuckPending (15-min alert), sweepAbandonedCheckouts (24h release), voidPendingTransaction (owner, by txn id) and voidPendingSubscription (member rollback + staff member-page dismiss, by subscription id — takes requestedByUserId, reason, intent, source, and stamps the decision onto the abandoned transaction’s metadata.checkoutCancel). Exports ABANDONED_CHECKOUT_REASON, the marker that keeps a released row revivable.
  • checkout-urls.ts (+ .unit.spec.ts) — deriveFailedUrl(cancelUrl): flips status=cancelledstatus=failed so a REFUSED charge can land somewhere other than the “you cancelled” page. Returns undefined rather than guessing when there is nothing to flip, and adapters fall back to cancelUrl.
  • payment-observability.service.ts — typed emit(eventName, payload) interface; the central spine for all payment telemetry.
  • manual-charge.service.ts (FIT-254 §3.3/§3.4) — ManualChargeService.charge (desk charge) + .clearDebt (debt collection wrapper over DebtService). Exports CHARGE_VERIFIED_PROVIDERS (['cardcom', 'meshulam', 'test']), the synchronous-token-charge provider allowlist also imported by plan-change.service.ts. Flag key admin-card-on-file.
  • manual-charge.service.unit.spec.ts — unit tests.
  • terminal-provisioning.service.ts (FIT-286) — opens a Cardcom terminal under Taikan’s dealer agreement (CompanyOperations/NewCompany). Derives the KYC volume estimates from the org’s plans + member count, refuses a second application while one is pending_kyc, and stores the result via createPendingManagedConfig. Deliberately NOT a PaymentProviderAdapter — it uses dealer-level credentials, not the gym’s.
  • terminal-provisioning.service.unit.spec.ts — derivation, duplicate guard, pending-not-active storage, CompanyNumber-not-CompanyInternalID capture, GetCompanyStatus GET-only behavior.
  • managed-terminal-status.service.ts (FIT-286) — CompanyOperations/GetCompanyStatus checks for pending_kyc terminals. Two entry points on one code path: the hourly @Cron('30 * * * *') sweep, gated per-org on managed-terminal-kyc-poll (and skipping any terminal with no stored companyNumber), and the owner’s ungated on-demand “check status”. Activates on IsDone, re-encrypting the credentials from the approving response; otherwise stores the outstanding requirements in status_reason. The sweep swallows per-org failures so one bad terminal cannot stall it; the on-demand path deliberately propagates them instead.
  • managed-terminal-status.service.unit.spec.ts — approval flip, settle-before-email ordering, on-demand provider-failure propagation, failed-query safety, and the sweep: flag on/off/unevaluable, per-org rollout, missing-company-number skip, sweep isolation.

Controllers (controllers/)

  • payment.controller.tsGET/POST /organizations/:orgId/payments[…]. Member-facing list, refund, refund-task complete, verify-return, transaction detail.
  • payment-webhook.controller.ts@Public path-style and query-style webhook entry points.
  • payment-provider-config.controller.tsGET/POST/PATCH/DELETE /organizations/:orgId/payment-config, plus POST …/provision (owner/admin, opens a managed terminal), POST …/managed-status/refresh (owner/admin, re-checks KYC now) and POST …/managed-status (@PlatformAdmin, manual override / the only way to close a rejection).
  • card-registration.controller.ts — admin-initiates-card-on-file for a member, plus member-side equivalents.
  • manual-charge.controller.ts (FIT-254 §3.3/§3.4) — POST members/:membershipId/charge (desk charge), POST subscriptions/:subscriptionId/clear-debt.
  • app-return.controller.ts@Public GET /payments/app-return. HTTPS→taikan:// bridge for hosted-page returns from the native app (providers reject custom-scheme return URLs; Morning errors 1103).
  • payment-analytics.controller.tsGET /organizations/:orgId/payment-analytics/*.
  • invoicing.controller.ts/organizations/:orgId/invoicing/* proxy onto the provider’s listDocuments / getDocument adapter methods.

DTOs (dto/)

  • refund.dto.ts{ amountInCents?, reason? }.
  • configure-provider.dto.ts{ provider, credentials, config? } + update variant.
  • manual-charge.dto.ts (FIT-254 §3.3) — { amountInCents: int ≥ 100, description: string }.
  • provision-terminal.dto.ts (FIT-286) — nested { business, owner, bankAccount, expectedActivity? }; the estimates are optional because the API derives them. Plus SettleTerminalStatusDto.

Shared — libs/shared/src/lib/

  • schemas/payment.schema.ts — Zod schemas for PaymentTransactionResponse, refund DTOs, transaction filters.
  • interfaces/payment-provider.ts (canonical PaymentProviderAdapter contract, ChargeRequest, RefundRequest, WebhookEvent, RefundCapability, ClientInfo, etc.).
  • constants/platform-tiers.tstierHasFeature consumed by RequiresFeature.
  • types/cardcom-provisioning.types.ts (FIT-286) — NewCompany wire contracts mirrored from Cardcom (including its AvarageTransactionAmountInCreditCard misspelling), TerminalProvisioningInput/Draft/Result.

DB — libs/db/src/lib/schema/

  • payments.tsplans, subscriptions, member_payment_methods, payment_provider_configs, payment_transactions, invoicing_configs, payment_provider_clients, cancellation_requests.
  • enums.tspaymentProvider, planType, planInterval, subscriptionStatus, transactionStatus, transactionType, cancellationRequestStatus, taskType (includes manual_refund, cancellation_review).

Web — apps/web/src/

  • components/payments/ — owner dashboard widgets: transactions-table.tsx, kpi-cards.tsx, payment-tabs.tsx, refund-task-complete-dialog.tsx, cancellation-requests-list.tsx, cancellations-widget.tsx, recurring-charges-table.tsx, documents-table.tsx, payments-data-table.tsx, summary-detail-toggle.tsx, date-range-filter.tsx.
  • components/member/plan-card.tsx — member-facing plan/subscription summary.
  • app/[lang]/(protected)/dashboard/payments/ — owner payment console.
  • app/[lang]/(protected)/dashboard/settings/checkout-return/page.tsx — Cardcom return page; calls verify-checkout for platform-billing.
  • app/[lang]/buy/courses/[id]/page.tsx — public course buy page (handles Clerk email-code flow). See courses/code-map.md.
  • components/settings/provider-config-dialog.tsx (+ .driver.tsx, .int.spec.tsx) — mode choice (Taikan-managed vs bring-your-own) → explanation → application → one-time provider login.
  • components/settings/managed-terminal-form.tsx (FIT-286) — the ~15-field terminal application.
  • components/address/israeli-address-picker.tsx (FIT-286) — reusable locality + street combobox over the national registry. Emits סמל ישוב / סמל רחוב codes; the street list is city-scoped and resets when the city changes, because street codes are unique per city only.
  • components/settings/payment-provider-config.tsx (+ .driver.tsx, .int.spec.tsx) — settings card; renders the pending_kyc / rejected states, lists what Cardcom is still waiting on, swaps Configure for Check status while a review is outstanding.

Tests

  • apps/api/src/payments/services/refund-flow.int.spec.ts — end-to-end refund integration test.
  • apps/api/src/payments/services/payment-transaction.service.unit.spec.ts
  • apps/api/src/payments/services/credential-encryption.service.unit.spec.ts
  • apps/api/src/payments/services/manual-charge.service.unit.spec.ts (FIT-254 §3.3/§3.4) — guard-stack ordering, pending-row-first, allowlist.
  • apps/api/src/payments/providers/*.unit.spec.ts — per-provider adapter contract tests.
  • apps/api/src/subscriptions/cancellation-flow.int.spec.ts — cancellation-request + refund handshake.

Web — card-on-file & manual charge (FIT-254 §3)

  • apps/web/src/components/overview/members/payment-methods-card.tsx (+ .driver.tsx, .int.spec.tsx) — member-detail Memberships-tab card: active card, expiry warning, collapsed inactive history, “Register card” CTA.
  • apps/web/src/components/members/manual-charge-dialog.tsx (+ .driver.tsx, .int.spec.tsx) — staff desk-charge confirm dialog (amount, description, last-4, confirm-with-amount button).
  • apps/web/src/components/members/collect-debt-dialog.tsx — staff debt-collection confirm dialog.
  • apps/web/src/components/payments/debt-report-table.tsx — payments-dashboard debt report rows, links into collect-debt.

Address registry (FIT-286)

  • libs/db/src/lib/schema/israel-addresses.tsisrael_cities (1,310 rows), israel_streets (63,571 rows, PK (city_code, code)). Street code 9000 is the registry’s “the locality itself” entry, present once per city and the only option where a locality has no named streets.
  • libs/db/data/israel-streets.csv.gz — committed UTF-8 snapshot of data.gov.il’s “רשימת רחובות בישראל” (source file is cp1255).
  • scripts/seed-israel-streets.ts (pnpm db:seed:streets) — idempotent upsert; needs running once per environment, including production.
  • apps/api/src/addresses/AddressesModule, IsraelAddressService, GET /addresses/israel/cities|streets. Authenticated but not org-scoped: public government data, identical for every gym.

Env

  • PAYMENT_CREDENTIALS_ENCRYPTION_KEY — 64-char hex (32 bytes) AES-256-GCM key.
  • API_BASE_URL — used to construct webhook URLs (payment.service.ts:128).
  • FRONTEND_URL — used for cancellation-request review links.
  • Cardcom platform terminal: PLATFORM_BILLING_TERMINAL_NUMBER, PLATFORM_BILLING_API_NAME, PLATFORM_BILLING_API_PASSWORD (see platform-billing/).
  • CARDCOM_PROVISIONING_DEBUG (FIT-286) — true logs the NewCompany body verbatim for a Cardcom support ticket. Default (unset/false) redacts the dealer secret, ת.ז, bank account, mother’s maiden name and the generated dashboard password, keeping field names, structure and every other value. Local dev only.
  • Cardcom dealer provisioning (FIT-286): CARDCOM_SUPPLIER_USERNAME, CARDCOM_SUPPLIER_PASSWORD, CARDCOM_SUPPLIER_SECRET, CARDCOM_MODULE_IDS — required together; resolveSupplier throws ServiceUnavailable naming what is missing rather than half-opening a terminal. CARDCOM_AGREEMENT_ID is optional (Cardcom does not require it): unset means the AgreementInfo block is omitted from the request rather than sent as 0.