Payments — Code Map
API — apps/api/src/payments/
Module + DI
payments.module.ts— wires all services, adapters, controllers.onModuleInitregisters each provider intoPaymentProviderRegistry.
Providers (providers/)
payment-provider.registry.ts—Map<PaymentProviderType, PaymentProviderAdapter>withregister/get/getOrThrow/list.cardcom.provider.ts— LowProfile/Create + GetLpResult + ChargeWithToken + v11RefundByTransactionId.tokenOnlypages runCreateTokenOnly, orSuspendedDeal+ J5 when the request carriescardValidation: 'issuer_hold';classifyDealreads Cardcom’s 700/701 J-validation success codes as settled on those two operations only, and the token mapper falls back fromTokenInfotoTranzactionInfo.FailedRedirectUrltakesrequest.failedUrlwhen present (falling back tocancelUrl), so a declined card no longer lands on the “you cancelled” page;CancelRedirectUrlis unchanged. No webhook signature.getRefundCapability() → 'manual'= refund API implemented but unverified against a live terminal, overridable per-org viapayment-automatic-refunds.icredit.provider.ts— Rivhit hosted page.GroupPrivateTokenbody signature.meshulam.provider.ts— Light-API iframe + JSON IPN.webhookKey === credentials.apiKeysignature.morning.provider.ts— Single-URL Morning webhook (?org=…). Signature TBD (:941). Issues חשבונית/קבלה automatically.tranzila.provider.ts— Stub-grade adapter; signature returnstrue.
Services (services/)
payment.service.ts— orchestrator + the exportedwithSubParam(stampssub=<id>on every return leg, idempotently).createHostedPayment,createCourseHostedPayment,charge,createRecurring,refund(capability switch),completeManualRefundTask,verifyAndActivateReturn,sendPaymentReceipt.card-validation-policy.service.ts— answerscard-issuer-validationper org for every tokenise-only page (presale checkout viaPaymentService.createHostedPayment, card registration viaCardRegistrationController). Only a definitetrueasks for an issuer hold; off/unset/unevaluable keeps the provider default.refund-capability.service.ts— third gate inrefund’s automatic-vs-manual routing: the per-orgpayment-automatic-refundsPostHog flag for bring-your-own terminals whose adapter reportsmanual. Default OFF, fails toward the manual task, refuses when the charge predates the active provider config.payment-transaction.service.ts— CRUD onpayment_transactions.upsertPending,completePendingBySubscriptionId,findByProviderTransactionId.payment-provider-config.service.ts— encrypted credential CRUD onpayment_provider_configs; redacted reads.payment-provider-clients.service.ts—payment_provider_clientsupserts (Morning client linkage).credential-encryption.service.ts— AES-256-GCM round trip. Key fromPAYMENT_CREDENTIALS_ENCRYPTION_KEY(32-byte hex).webhook-processing.service.ts— dispatchespayment.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 transactionfailedand leaves the subscriptionpending(emittingpayment.activation_failedwithreason: 'first_charge_failed') — the charge failed, not the checkout.recurring-charge.service.ts— daily 02:00 UTC cron.FOR UPDATE SKIP LOCKEDsweep, charge-then-update, debt promotion at 3 fails.payment-method.service.ts—replacePaymentMethod(deactivate old, insert new),getActivePaymentMethod.debt.service.ts—clearDebt(subscriptionId)charges the accumulateddebtAmountInCentsand 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) andvoidPendingSubscription(member rollback + staff member-page dismiss, by subscription id — takesrequestedByUserId,reason,intent,source, and stamps the decision onto the abandoned transaction’smetadata.checkoutCancel). ExportsABANDONED_CHECKOUT_REASON, the marker that keeps a released row revivable.checkout-urls.ts(+.unit.spec.ts) —deriveFailedUrl(cancelUrl): flipsstatus=cancelled→status=failedso a REFUSED charge can land somewhere other than the “you cancelled” page. Returnsundefinedrather than guessing when there is nothing to flip, and adapters fall back tocancelUrl.payment-observability.service.ts— typedemit(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 overDebtService). ExportsCHARGE_VERIFIED_PROVIDERS(['cardcom', 'meshulam', 'test']), the synchronous-token-charge provider allowlist also imported byplan-change.service.ts. Flag keyadmin-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 ispending_kyc, and stores the result viacreatePendingManagedConfig. Deliberately NOT aPaymentProviderAdapter— it uses dealer-level credentials, not the gym’s.terminal-provisioning.service.unit.spec.ts— derivation, duplicate guard, pending-not-active storage,CompanyNumber-not-CompanyInternalIDcapture,GetCompanyStatusGET-only behavior.managed-terminal-status.service.ts(FIT-286) —CompanyOperations/GetCompanyStatuschecks forpending_kycterminals. Two entry points on one code path: the hourly@Cron('30 * * * *')sweep, gated per-org onmanaged-terminal-kyc-poll(and skipping any terminal with no storedcompanyNumber), and the owner’s ungated on-demand “check status”. Activates onIsDone, re-encrypting the credentials from the approving response; otherwise stores the outstanding requirements instatus_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.ts—GET/POST /organizations/:orgId/payments[…]. Member-facing list, refund, refund-task complete, verify-return, transaction detail.payment-webhook.controller.ts—@Publicpath-style and query-style webhook entry points.payment-provider-config.controller.ts—GET/POST/PATCH/DELETE /organizations/:orgId/payment-config, plusPOST …/provision(owner/admin, opens a managed terminal),POST …/managed-status/refresh(owner/admin, re-checks KYC now) andPOST …/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—@PublicGET /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.ts—GET /organizations/:orgId/payment-analytics/*.invoicing.controller.ts—/organizations/:orgId/invoicing/*proxy onto the provider’slistDocuments / getDocumentadapter 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. PlusSettleTerminalStatusDto.
Shared — libs/shared/src/lib/
schemas/payment.schema.ts— Zod schemas forPaymentTransactionResponse, refund DTOs, transaction filters.interfaces/payment-provider.ts(canonicalPaymentProviderAdaptercontract,ChargeRequest,RefundRequest,WebhookEvent,RefundCapability,ClientInfo, etc.).constants/platform-tiers.ts—tierHasFeatureconsumed byRequiresFeature.types/cardcom-provisioning.types.ts(FIT-286) —NewCompanywire contracts mirrored from Cardcom (including itsAvarageTransactionAmountInCreditCardmisspelling),TerminalProvisioningInput/Draft/Result.
DB — libs/db/src/lib/schema/
payments.ts—plans,subscriptions,member_payment_methods,payment_provider_configs,payment_transactions,invoicing_configs,payment_provider_clients,cancellation_requests.enums.ts—paymentProvider,planType,planInterval,subscriptionStatus,transactionStatus,transactionType,cancellationRequestStatus,taskType(includesmanual_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; callsverify-checkoutfor platform-billing.app/[lang]/buy/courses/[id]/page.tsx— public course buy page (handles Clerk email-code flow). Seecourses/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 thepending_kyc/rejectedstates, 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.tsapps/api/src/payments/services/credential-encryption.service.unit.spec.tsapps/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.ts—israel_cities(1,310 rows),israel_streets(63,571 rows, PK(city_code, code)). Street code9000is 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(seeplatform-billing/). CARDCOM_PROVISIONING_DEBUG(FIT-286) —truelogs theNewCompanybody 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;resolveSupplierthrowsServiceUnavailablenaming what is missing rather than half-opening a terminal.CARDCOM_AGREEMENT_IDis optional (Cardcom does not require it): unset means theAgreementInfoblock is omitted from the request rather than sent as0.