Skip to content

feat: add Paystack payment integration#29296

Open
MarvelNwachukwu wants to merge 29 commits into
calcom:mainfrom
MarvelNwachukwu:feat/paystack-upstream
Open

feat: add Paystack payment integration#29296
MarvelNwachukwu wants to merge 29 commits into
calcom:mainfrom
MarvelNwachukwu:feat/paystack-upstream

Conversation

@MarvelNwachukwu
Copy link
Copy Markdown

Summary

Adds Paystack as a payment provider for Cal.com — Africa's leading payment gateway, supporting NGN/GHS/ZAR/KES/USD, bank transfers, mobile money, and USSD across Nigeria, Ghana, South Africa, Kenya, and more.

This PR replaces the closed #28737. The original could not be reopened (UNPROCESSABLE from the GitHub API) after addressing the review feedback locally and rebasing onto main.

What changed since #28737 was closed

The original PR was closed by @romitg2 on 2026-05-06 with the request: "please reopen with addressing coderabbit comments". Every actionable review comment from coderabbitai and cubic-dev-ai has been addressed in the commits below; commit messages cite which reviewer raised each concern.

Critical / Major correctness fixes

  • webhook.ts — atomic updateMany idempotency lock (cubic P1); success sentinel no longer logs as error or rolls back; unknown references ack with 200 instead of 204 with a body (RFC 7230 §3.3.3) (coderabbitai)
  • verify.ts — same atomic idempotency lock so the verify endpoint and webhook can't both fire handlePaymentSuccess and duplicate calendar events / BOOKING_PAID webhooks / emails; explicit typeof reference === "string" validation (coderabbitai)
  • add.ts — duplicate-credential check + create wrapped in prisma.$transaction; brittle Error("Already installed") string-sentinel replaced with a direct 409 (cubic + coderabbitai)
  • Setup.tsxcredentialId = -1 fallback was truthy under !!, so the form rendered without an install and submission threw BAD_REQUEST: Could not find credential -1. Now renders AppNotInstalledMessage and short-circuits the mutation (coderabbitai)
  • _getServerSideProps.ts — drops key: true from the credential select (project rule violation); guards teamId against string[] | NaN (coderabbitai)

API client hardening

  • PaystackClient.ts — extracted shared request helper; bounded every call with AbortSignal.timeout(15s) so a slow Paystack response can't stall request threads; checks response.ok before parsing JSON so HTML/empty 502/504 bodies don't throw SyntaxError (coderabbitai)
  • PaymentService.tsdeletePayment now logs the underlying error before returning false (coderabbitai)
  • PaystackPaymentComponent.tsx — guards Intl.NumberFormat against unknown ISO codes (RangeError); drops the redundant as unknown as PaystackPaymentData cast (coderabbitai)

Generator hygiene

  • app-store/*.generated.{ts,tsx} — restored to canonical generator output; the original PR had cosmetic drift (single quotes, missing trailing newlines, inlined ternary) mixed with the legitimate Paystack additions (coderabbitai)
  • Added an ambient inline-js.d.ts for @paystack/inline-js (ships JS only) and registered it in apps/web/tsconfig.json — same precedent routing-forms already uses for react-awesome-query-builder

Other

  • chore: skip biome on .d.ts files in lint-staged — biome.json ignores **/*.d.ts but the lint-staged glob included them, breaking the pre-commit hook for any commit that touched a .d.ts
  • Merged upstream/main to resolve conflicts and pick up routing-forms in the regenerated app-store maps

Architecture

Follows the established Cal.com payment app patterns used by HitPay and Stripe:

packages/app-store/paystack/
├── _metadata.ts                          # App metadata
├── config.json                           # App store config
├── zod.ts                                # Zod schemas (appDataSchema, appKeysSchema)
├── inline-js.d.ts                        # Ambient declaration for @paystack/inline-js
├── api/
│   ├── add.ts                            # Install handler → redirects to setup
│   ├── verify.ts                         # Payment callback verification
│   └── webhook.ts                        # Webhook with signature verification
├── components/
│   ├── EventTypeAppCardInterface.tsx
│   ├── EventTypeAppSettingsInterface.tsx
│   └── PaystackPaymentComponent.tsx
├── lib/
│   ├── PaymentService.ts                 # IAbstractPaymentService implementation
│   ├── PaystackClient.ts                 # Paystack REST API client
│   ├── verifyWebhookSignature.ts         # HMAC-SHA512 verification
│   ├── currencyOptions.ts
│   └── __tests__/
└── pages/setup/
    └── _getServerSideProps.ts

Local verification

  • vitest run packages/app-store/paystack — 8/8 tests pass
  • tsc --noEmit -p packages/app-store/tsconfig.json — exit 0
  • tsc --noEmit -p apps/web/tsconfig.json — no Paystack-introduced errors (the remaining trpc.viewer warnings are repo-wide and pre-existing on this branch even with my changes stashed; caused by local @calcom/trpc build state, not new code)

Test plan

  • Install Paystack from /apps/paystack → setup page appears
  • Enter Paystack test API keys → save correctly
  • Configure event type with Paystack payment (price, currency) → app card shows
  • Book event as attendee → "Pay to book" button and price display
  • Complete payment via Paystack popup → booking confirmed
  • Test refund flow from bookings page
  • Test webhook endpoint with Paystack test events
  • CI: type-check, lint, and unit tests against full monorepo

Add Paystack as a payment provider for Cal.com, enabling hosts to collect
payments via Paystack when attendees book events. Paystack is widely used
across Africa and supports NGN, GHS, ZAR, KES, and USD currencies.

Key components:
- PaymentService implementing IAbstractPaymentService (create, refund, update)
- PaystackClient REST wrapper for Paystack API (initialize, verify, refund)
- Webhook endpoint with HMAC-SHA512 signature verification
- Verify endpoint for callback-based payment confirmation
- Event type app card UI for configuring price, currency, and refund policy
- Setup page for entering Paystack API keys (public + secret)
- PaystackPaymentComponent using Paystack inline popup checkout
- App install flow via add.ts with isOAuth: true for proper setup routing

Follows existing payment app patterns (HitPay, Stripe) for:
- App store registration (config.json, _metadata.ts, zod schemas)
- Server-side props for setup page authentication
- Generated file registration (payment.services, apps.metadata, etc.)
- Seed script entry for local development
- Webhook: atomic idempotency via updateMany WHERE success=false
- Webhook: rollback success flag if Paystack API verification fails
- EventTypeSettings: guard parseInt("") returning NaN on cleared input
- PaymentComponent: use convertFromSmallestToPresentableCurrencyUnit for zero-decimal currencies
- Add handler: validate teamId is numeric before access check
- Add handler: consolidate owner filter, return 409 for duplicate installs
Wraps verification + handlePaymentSuccess in try/catch that resets
success=false on failure, so webhook retries can re-process the payment
instead of it being stuck as "already processed".
- webhook: skip payment.success rollback when handlePaymentSuccess signals
  success via HttpCode(200) sentinel, preventing idempotency state from
  being reset after successful processing
- add: return 201 Created for credential install
- payment component: use location.replace() so the payment step is not
  left in browser history
- PaystackClient: export PAYSTACK_BASE_URL so tests can reference the
  constant instead of duplicating the literal
- tests: use PAYSTACK_BASE_URL and a generic example.com callback_url
  instead of hard-coded api.paystack.co and cal.com strings
- Remove `key: true` from credential select; only id is used and the
  project rule forbids exposing credential.key.
- Guard against `string[]`/`NaN` teamId in the setup _getServerSideProps,
  mirroring the validation already done in api/add.ts so an invalid value
  cannot bypass the team-admin check.

Addresses [privy/coderabbitai] review on
packages/app-store/paystack/pages/setup/_getServerSideProps.ts.
Wrap the duplicate-credential check and create in a single
prisma.$transaction so a concurrent install cannot slip a duplicate
credential through the read/write gap. Replace the brittle
Error("Already installed")/error.message catch with a direct 409
response — the catch now only handles unexpected errors via
getServerErrorFromUnknown.

Addresses [privy/coderabbitai] (string-sentinel) and cubic-dev-ai
(non-atomic findFirst→create) review feedback on
packages/app-store/paystack/api/add.ts.
The previous `[-1]` fallback evaluated truthy under `!!credentialId`,
so the form rendered for un-installed users and submitting it triggered
a `BAD_REQUEST: Could not find credential -1` toast from
updateAppCredentials. Treat a missing/invalid credentialId as
"not installed" and short-circuit the form submit defensively.

Addresses [privy/coderabbitai] review on apps/web/components/apps/paystack/Setup.tsx.
- Extract a private `request` helper in PaystackClient so the three
  endpoints share identical request/error handling.
- Bound every call to Paystack with `AbortSignal.timeout(15s)` so a
  slow/hung Paystack response cannot stall webhook/verify/refund threads
  on whatever the undici default is.
- Wrap `response.json()` in a try/catch so non-JSON error bodies (HTML
  502/504s, empty gateway-timeout bodies) surface the HTTP status
  instead of throwing an opaque `SyntaxError`.

Tests are decoupled from the new `signal` argument via
`expect.objectContaining` and `expect.any(AbortSignal)`.

Addresses [privy/coderabbitai] review on
packages/app-store/paystack/lib/PaystackClient.ts.
Two related fixes on the verify callback endpoint:

- Race condition: Paystack posts the webhook the moment the charge
  succeeds, so a client redirect into /verify and the webhook regularly
  observe `success: false` simultaneously and both invoke
  handlePaymentSuccess — duplicating calendar events, BOOKING_PAID
  webhooks, workflow runs, and confirmation emails. Replace the
  read-then-call pattern with the same atomic
  `updateMany({ where: { success: false } })` lock the webhook uses,
  with rollback on real failure and pass-through of the
  HttpCode(200) success sentinel.

- Type safety: `req.query.reference` is `string | string[] | undefined`.
  The previous `as string` cast silently produced an array if the URL
  contained `?reference=a&reference=b`, which then crashed the Prisma
  query with an opaque error instead of returning 400. Validate
  explicitly with `typeof === "string"`.

Addresses [privy/coderabbitai] (race + reference type) review on
packages/app-store/paystack/api/verify.ts.
Two webhook fixes that come up together because they share the
outer-catch path:

- Unknown reference: previously threw HttpCode(204) which the outer
  catch then sent with a body, violating RFC 7230 §3.3.3 (a 204 cannot
  carry a message body). Worse, Paystack treats non-2xx as a delivery
  failure and retries indefinitely. Respond 200 directly with a warn
  log instead — Paystack stops retrying and we don't pollute the
  error feed with test-dashboard events.
- Success sentinel re-throw: handlePaymentSuccess signals success by
  throwing HttpCode(200). Re-throwing it from the inner catch made it
  bubble into the outer catch on every happy path, logging
  "Webhook Error: Booking … was paid and confirmed." and rendering
  the success `res.status(200).json({ received: true })` unreachable.
  Treat the sentinel as success — don't roll back, don't re-throw,
  fall through to the 200 response.
- Defensive guard so a real 204 thrown from elsewhere never sends a
  body either.

Addresses [privy/coderabbitai] review on
packages/app-store/paystack/api/webhook.ts.
- PaymentService.deletePayment was swallowing every Prisma error into
  `return false`, so DB outages and FK violations were invisible. Log
  via the standard logger before returning false.
- PaystackPaymentComponent: Intl.NumberFormat throws RangeError on
  unknown ISO codes — a stale event-type currency would crash the
  page. Wrap in try/catch and fall back to a plain numeric format.
- Drop the redundant `payment.data as unknown as PaystackPaymentData`
  cast; the prop type already narrows it.

Addresses [privy/coderabbitai] review on PaymentService.ts and
PaystackPaymentComponent.tsx.
Restore every app-store *.generated.ts(x) file to the format produced
by `yarn app-store:build`, then re-insert the paystack entries
alphabetically. The previous diff was a mix of legitimate paystack
additions and locally-divergent formatting (single quotes, missing
trailing newlines, inlined ternary, quoted keys) that would have been
flipped back the next time another contributor ran the generator.

After this commit the diff against upstream/main is exactly the
paystack additions in apps.browser, apps.keys-schemas, apps.schemas,
apps.metadata, apps.server, and payment.services — and nothing else.

Addresses [privy/coderabbitai] review on
packages/app-store/video.adapters.generated.ts (and the related
generated files).
Running `yarn install` triggered the app-store-cli postinstall and
added the routing-forms entry that the previous regeneration missed.
No paystack-specific code changed.
The (apps|packages|companion)/**/*.{js,ts,jsx,tsx} glob in
lint-staged.config.mjs catches *.d.ts files, but biome.json ignores
them via "!!**/*.d.ts". biome then errors with "No files were
processed in the specified paths", failing the pre-commit hook for
any commit that touches a .d.ts file.

Filter .d.ts out of the file list before invoking biome, and skip the
command entirely if nothing remains.
@paystack/inline-js ships JavaScript without types, so importing it
from PaystackPaymentComponent triggered TS7016 ("Could not find a
declaration file for module") under apps/web's strict
type-check:ci. An inline `declare module` inside the .tsx couldn't
augment it (TS2665: "untyped module ... cannot be augmented") — only
an ambient .d.ts works.

Add a focused declaration covering the small surface area we use
(`resumeTransaction(accessCode, { onSuccess, onCancel, onError })`)
and register it in apps/web/tsconfig.json — same pattern routing-forms
already uses for `react-awesome-query-builder`.

Verified locally: vitest 8/8 green, packages/app-store tsc exits 0,
apps/web tsc no longer reports paystack errors (the remaining
`trpc.viewer` warnings are pre-existing and unrelated to paystack).
@github-actions
Copy link
Copy Markdown
Contributor

Welcome to Cal.diy, @MarvelNwachukwu! Thanks for opening this pull request.

A few things to keep in mind:

  • This is Cal.diy, not Cal.com. Cal.diy is a community-driven, fully open-source fork of Cal.com licensed under MIT. Your changes here will be part of Cal.diy — they will not be deployed to the Cal.com production app.
  • Please review our Contributing Guidelines if you haven't already.
  • Make sure your PR title follows the Conventional Commits format.

A maintainer will review your PR soon. Thanks for contributing!

@MarvelNwachukwu MarvelNwachukwu marked this pull request as ready for review May 10, 2026 01:50
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 10, 2026

Review Change Stack
No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fcc4e734-df95-401a-b4b4-3df3ba200960

📥 Commits

Reviewing files that changed from the base of the PR and between 4f3ed11 and bd25254.

📒 Files selected for processing (3)
  • packages/app-store/paystack/api/verify.ts
  • packages/app-store/paystack/api/webhook.ts
  • packages/app-store/paystack/components/PaystackPaymentComponent.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/app-store/paystack/components/PaystackPaymentComponent.tsx
  • packages/app-store/paystack/api/verify.ts
  • packages/app-store/paystack/api/webhook.ts

📝 Walkthrough

Walkthrough

This pull request adds complete Paystack payment provider support to Cal.com. The integration includes a Paystack API client (initialize/verify/refund), a PaystackPaymentService, Next.js API routes for credential install, webhook and manual verification with signature checks and idempotency, client checkout and PaymentPage integration, event-type settings and setup UI with credential management, app-store metadata/registration, and unit tests.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding Paystack payment integration to Cal.com. It is specific and directly relates to the changeset.
Description check ✅ Passed The description comprehensively documents the Paystack payment integration, addressing review comments from the previous PR, architectural decisions, and local verification results.
Linked Issues check ✅ Passed The PR comprehensively implements all coding objectives from issue #28737: PaymentService (create, refund, update, deletePayment), PaystackClient REST wrapper, webhook and verify endpoints with signature verification, UI components, setup page, event-type configuration, and complete payment flow with tests.
Out of Scope Changes check ✅ Passed All changes are either directly part of the Paystack integration or necessary supporting changes: app-store files, UI components, API handlers, tests, configuration, and the lint-staged fix for .d.ts exclusion which resolves a pre-commit hook issue affecting the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (9)
packages/app-store/paystack/api/verify.ts (1)

103-121: Success sentinel pattern is intentional—verified.

The code correctly implements the established handlePaymentSuccess API contract. The function explicitly throws HttpCode(200) on success (handlePaymentSuccess.ts:217), and the sentinel check correctly avoids rolling back the idempotency lock for statusCode < 400. This pattern is documented and consistently used across payment handlers (e.g., paystack webhook.ts has detailed comment explaining the behavior).

The implementation is correct. If desired as a future improvement, consider whether handlePaymentSuccess could return a status tuple instead of throwing exceptions for success signaling, though the current pattern is functional and intentional.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/app-store/paystack/api/verify.ts` around lines 103 - 121, This
try/catch is intentional and correct: leave the logic in verify.ts unchanged
(the try block calling handlePaymentSuccess and the catch checking for HttpCode
sentinel), but add a concise clarifying comment above the catch referencing
handlePaymentSuccess and HttpCode to document that handlePaymentSuccess throws
HttpCode(200) as a success sentinel so maintainers understand why success does
not trigger a rollback of payment.success.
packages/app-store/paystack/lib/PaymentService.ts (4)

129-164: ⚡ Quick win

Simplify refund() to avoid redundant findUnique round trips.

When the payment is already refunded or never succeeded, the current code re-fetches the full row with another findUnique. Since you already loaded the row, just expand the initial select (or drop it) and return that record directly. Also note: returning a non-refunded payment when payment.success === false masks an attempted refund of a never-paid record — consider throwing ErrorWithCode for that case so the caller can surface it rather than silently noop.

♻️ Proposed change
   async refund(paymentId: Payment["id"]): Promise<Payment | null> {
-    const payment = await prisma.payment.findUnique({
-      where: { id: paymentId },
-      select: {
-        id: true,
-        success: true,
-        refunded: true,
-        externalId: true,
-      },
-    });
+    const payment = await prisma.payment.findUnique({ where: { id: paymentId } });

     if (!payment) {
       return null;
     }
-    if (payment.refunded) {
-      return await prisma.payment.findUnique({ where: { id: paymentId } });
-    }
-    if (!payment.success) {
-      return await prisma.payment.findUnique({ where: { id: paymentId } });
-    }
+    if (payment.refunded) return payment;
+    if (!payment.success) {
+      throw new ErrorWithCode("PAYMENT_NOT_SUCCESSFUL", "Cannot refund a payment that did not succeed");
+    }
     ...
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/app-store/paystack/lib/PaymentService.ts` around lines 129 - 164,
The refund() method does redundant DB round-trips and silently no-ops on
non-successful payments; change the initial prisma.payment.findUnique to select
the full record (or remove select) so you can return the loaded payment directly
when payment.refunded is true, and when payment.success is false throw an
ErrorWithCode (instead of returning the record) so callers can handle the
invalid refund attempt; keep the existing this.client.createRefund call and
prisma.payment.update(refunded: true) path unchanged, just eliminate the extra
findUnique calls.

86-91: 💤 Low value

Avoid the as unknown as Prisma.InputJsonValue double cast.

Double-casting via unknown defeats the type system in the same spirit the guideline forbids as any. The object you're storing is already a plain JSON-serializable record; the cast is only needed because data is typed as the broader Prisma.JsonValue. Either type the literal explicitly or use Prisma.JsonObject directly so the value is structurally assignable without a cast.

♻️ Proposed change
-        data: {
-          access_code: paystackResponse.access_code,
-          authorization_url: paystackResponse.authorization_url,
-          publicKey: this.credentials.public_key,
-          reference,
-        } as unknown as Prisma.InputJsonValue,
+        data: {
+          access_code: paystackResponse.access_code,
+          authorization_url: paystackResponse.authorization_url,
+          publicKey: this.credentials.public_key,
+          reference,
+        } satisfies Prisma.InputJsonObject,

As per coding guidelines: "Never use as any - use proper type-safe solutions instead" — the same principle applies to as unknown as T escape hatches.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/app-store/paystack/lib/PaymentService.ts` around lines 86 - 91, The
object assigned to the data property in PaymentService.ts is being double-cast
with "as unknown as Prisma.InputJsonValue"; remove the unsafe double cast and
make the value structurally typed: either annotate the literal as
Prisma.JsonObject (e.g. set data: <Prisma.JsonObject>{ access_code: ...,
authorization_url: ..., publicKey: ..., reference }) or change the target type
to Prisma.JsonValue/JsonObject so the plain record is assignable without
casting; update the import/use of Prisma types if needed and ensure the data
property uses Prisma.JsonObject instead of forcing the unknown cast.

55-56: 💤 Low value

Reference suffix has only ~32 bits of entropy; consider widening it.

uid.slice(0, 8) keeps just 8 hex chars (~32 bits). The bookingId prefix scopes uniqueness to a single booking, but if the same booking has many payment retries you can hit collisions — and Paystack rejects duplicate references. Using the full uuid (or slice(0, 16)) costs nothing and removes the risk.

♻️ Proposed change
-    const reference = `cal_${bookingId}_${uid.slice(0, 8)}`;
+    const reference = `cal_${bookingId}_${uid.replace(/-/g, "")}`;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/app-store/paystack/lib/PaymentService.ts` around lines 55 - 56, The
generated reference `const reference = \`cal_${bookingId}_${uid.slice(0, 8)}\``
uses only 8 hex chars (~32 bits) which risks collisions; update the logic that
creates `uid`/`reference` (where `uuidv4()` is called) to use a longer suffix
(e.g., `uid.slice(0,16)` or the full `uid`) so `reference` carries sufficient
entropy and avoids Paystack duplicate-reference rejections; keep the `bookingId`
prefix but replace `uid.slice(0, 8)` with the chosen longer slice or `uid` in
the `reference` assignment.

58-68: ⚡ Quick win

Use the WEBAPP_URL constant instead of reading process.env.NEXT_PUBLIC_WEBAPP_URL directly.

If the env var is unset at runtime, the callback URL becomes "undefined/api/integrations/paystack/verify", which Paystack will reject or silently store with an invalid URL. @calcom/lib/constants exports WEBAPP_URL with proper fallback handling—the rest of the codebase, including other payment services like PayPal and HitPay, already follow this pattern.

♻️ Proposed change
+import { WEBAPP_URL } from "@calcom/lib/constants";
 import logger from "@calcom/lib/logger";
 ...
-      callback_url: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/api/integrations/paystack/verify`,
+      callback_url: `${WEBAPP_URL}/api/integrations/paystack/verify`,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/app-store/paystack/lib/PaymentService.ts` around lines 58 - 68, In
PaymentService where you call this.client.initializeTransaction (the block that
builds callback_url), replace direct use of process.env.NEXT_PUBLIC_WEBAPP_URL
with the exported WEBAPP_URL constant from `@calcom/lib/constants` and import it
at the top of the file; ensure you join WEBAPP_URL and
"/api/integrations/paystack/verify" robustly (handle missing/trailing slash) so
the callback_url never becomes "undefined/..." or a malformed URL.
apps/web/app/(use-page-wrapper)/payment/[uid]/PaymentPage.tsx (2)

63-68: 💤 Low value

Import path inconsistent with sibling payment components.

Other payment components are imported from @calcom/web/components/apps/.../PaymentComponent while this one uses @calcom/app-store/paystack/components/.... Functionally fine, but mixing the two conventions makes maintenance harder. Consider either re-exporting the component under apps/web/components/apps/paystack/ (parity with paypal, hitpay, btcpayserver) or migrating the others over time.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/app/`(use-page-wrapper)/payment/[uid]/PaymentPage.tsx around lines
63 - 68, The dynamic import of PaystackPaymentComponent uses an inconsistent
package path; update the import for PaystackPaymentComponent (the dynamic(...)
call) to match the sibling payment components pattern (i.e., import from your
web-facing re-export location used by paypal/hitpay/btcpayserver, e.g., the
apps/web components namespace), or add a re-export from the existing
`@calcom/app-store/paystack/components/PaystackPaymentComponent` into the web
components folder and then change the dynamic import to that re-exporting module
so the symbol PaystackPaymentComponent follows the same import convention as the
other payment components.

181-192: ⚡ Quick win

Avoid the double cast and the silent empty-string fallback for publicKey.

(props.payment.data as unknown as { publicKey: string }).publicKey ?? "" defeats the type system and, if publicKey is missing or malformed, hands an empty key to @paystack/inline-js, surfacing a confusing client-side failure deep inside the popup. Validate the shape with a small type guard (or zod) and either bail to an error state or assert presence at server-side render time.

♻️ Proposed change
-                  {props.payment.appId === "paystack" && !props.payment.success && (
-                    <PaystackPaymentComponent
-                      payment={props.payment}
-                      clientId={
-                        (props.payment.data as unknown as { publicKey: string }).publicKey ?? ""
-                      }
-                      bookingUid={props.booking.uid}
-                      bookingTitle={eventName}
-                      amount={props.payment.amount}
-                      currency={props.payment.currency}
-                    />
-                  )}
+                  {props.payment.appId === "paystack" &&
+                    !props.payment.success &&
+                    typeof props.payment.data?.publicKey === "string" &&
+                    props.payment.data.publicKey.length > 0 && (
+                      <PaystackPaymentComponent
+                        payment={props.payment}
+                        clientId={props.payment.data.publicKey}
+                        bookingUid={props.booking.uid}
+                        bookingTitle={eventName}
+                        amount={props.payment.amount}
+                        currency={props.payment.currency}
+                      />
+                    )}

As per coding guidelines: "Never use as any - use proper type-safe solutions instead" — the same principle applies to as unknown as { ... } escape hatches.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/app/`(use-page-wrapper)/payment/[uid]/PaymentPage.tsx around lines
181 - 192, Replace the double-cast extraction of the Paystack publicKey from
props.payment.data with a proper runtime type check and explicit error handling:
add a small type guard (or zod schema) that validates props.payment.data has a
string publicKey, use it in PaymentPage.tsx before rendering
PaystackPaymentComponent, and if the guard fails either render an error state /
fallback UI or prevent rendering (so you don't pass an empty string into
Paystack). Target symbols: props.payment.data, payment.publicKey, and the
PaystackPaymentComponent invocation; remove the "(as unknown as { publicKey:
string }).publicKey ?? ''" pattern and replace it with the validated value or an
early error path.
packages/app-store/paystack/package.json (2)

10-18: 💤 Low value

Consider declaring @calcom/prisma as an explicit dependency.

packages/app-store/paystack/lib/PaymentService.ts imports from @calcom/prisma and @calcom/prisma/client, but the workspace package is not listed under dependencies. While workspace hoisting may resolve this in the monorepo, declaring it explicitly (as other app-store packages do) avoids brittleness if hoisting behavior changes or the package is consumed in isolation.

📦 Proposed addition
   "dependencies": {
     "@calcom/lib": "workspace:*",
+    "@calcom/prisma": "workspace:*",
     "@paystack/inline-js": "^2.0.0",
     "uuid": "^9.0.0"
   },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/app-store/paystack/package.json` around lines 10 - 18, The
package.json is missing an explicit dependency on `@calcom/prisma` even though
lib/PaymentService.ts imports from '@calcom/prisma' and '@calcom/prisma/client';
add "@calcom/prisma": "workspace:*" (or the same version strategy used by other
app-store packages) to the "dependencies" section so the package declares the
runtime dependency explicitly and won't rely on workspace hoisting.

12-12: 💤 Low value

Replace the custom inline-js.d.ts with the official @types/paystack__inline-js package. The DefinitelyTyped package covers the PaystackPop class and resumeTransaction method used in your code, letting you remove both the custom inline-js.d.ts file and the special tsconfig include in apps/web/tsconfig.json, while gaining upstream type maintenance.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/app-store/paystack/package.json` at line 12, Replace the custom
declaration file inline-js.d.ts with the official DefinitelyTyped package by
adding `@types/paystack__inline-js` as a devDependency (replace or add in
packages/app-store/paystack/package.json where "@paystack/inline-js" is
declared), remove the custom inline-js.d.ts file, and remove the special
tsconfig include entry in apps/web/tsconfig.json that was only there to pick up
the custom declarations; ensure code references to PaystackPop and
resumeTransaction remain and compile against the new `@types` package, then run
install to update lockfiles.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/components/apps/paystack/Setup.tsx`:
- Around line 67-77: The TextField for the Paystack public key includes
role="presentation", which hides its interactive semantics from assistive tech;
remove the role attribute from the TextField that renders the public key input
(the element with name="public_key", id="public_key", value={newPublicKey} and
onChange={setNewPublicKey}). Also remove the same role="presentation" from the
secret key input TextField (the corresponding name="secret_key"/id="secret_key"
input that uses newSecretKey and setNewSecretKey) so both inputs retain their
native interactive accessibility.

In `@packages/app-store/paystack/components/PaystackPaymentComponent.tsx`:
- Line 67: In PaystackPaymentComponent replace the direct interpolation of
paymentData.reference in the fetch call with an encoded query parameter: build
the query using URLSearchParams (or encodeURIComponent) so the verify request
uses an encoded reference (paymentData.reference) when calling
`/api/integrations/paystack/verify`, ensuring special characters won’t corrupt
the query string.

In `@packages/app-store/paystack/lib/PaymentService.ts`:
- Around line 47-54: Replace all bare Error throws in PaymentService.ts with
ErrorWithCode: import ErrorWithCode from your shared errors module and change
throws at the locations shown (the "Booking not found" at the top of the file
and the other throws around lines 109, 116, 153, 171) to use ErrorWithCode with
the original message and a stable error code (e.g., "NOT_FOUND" for missing
booking, "PAYSTACK_NOT_CONFIGURED" for missing credentials, or other
site-standard codes); update each throw site in the PaymentService class/methods
accordingly so callers can branch on the provided code.

In `@packages/app-store/paystack/pages/setup/_getServerSideProps.ts`:
- Around line 20-27: The current ternary uses truthiness for teamId which treats
0 as falsy and incorrectly falls back to userId; change the branching to a
strict null check so a parsed 0 is treated as a valid teamId. Update the
creation of installForObject to use "teamId !== null ? { teamId } : { userId:
session.user.id }" and ensure the earlier parsing (rawTeamId -> teamId) and the
call to throwIfNotHaveAdminAccessToTeam({ teamId, userId: session.user.id })
continue to accept 0 as a legitimate team id.

In `@packages/i18n/locales/en/common.json`:
- Line 3745: The string value for the i18n key
"paystack_getting_started_description" contains a hardcoded brand "Cal.com";
update the translation to use the templated brand variable by replacing
"Cal.com" with "{{appName}}" so the key paystack_getting_started_description
reads about obtaining API keys from your {{appName}} account, preserving
surrounding punctuation and spacing.

---

Nitpick comments:
In `@apps/web/app/`(use-page-wrapper)/payment/[uid]/PaymentPage.tsx:
- Around line 63-68: The dynamic import of PaystackPaymentComponent uses an
inconsistent package path; update the import for PaystackPaymentComponent (the
dynamic(...) call) to match the sibling payment components pattern (i.e., import
from your web-facing re-export location used by paypal/hitpay/btcpayserver,
e.g., the apps/web components namespace), or add a re-export from the existing
`@calcom/app-store/paystack/components/PaystackPaymentComponent` into the web
components folder and then change the dynamic import to that re-exporting module
so the symbol PaystackPaymentComponent follows the same import convention as the
other payment components.
- Around line 181-192: Replace the double-cast extraction of the Paystack
publicKey from props.payment.data with a proper runtime type check and explicit
error handling: add a small type guard (or zod schema) that validates
props.payment.data has a string publicKey, use it in PaymentPage.tsx before
rendering PaystackPaymentComponent, and if the guard fails either render an
error state / fallback UI or prevent rendering (so you don't pass an empty
string into Paystack). Target symbols: props.payment.data, payment.publicKey,
and the PaystackPaymentComponent invocation; remove the "(as unknown as {
publicKey: string }).publicKey ?? ''" pattern and replace it with the validated
value or an early error path.

In `@packages/app-store/paystack/api/verify.ts`:
- Around line 103-121: This try/catch is intentional and correct: leave the
logic in verify.ts unchanged (the try block calling handlePaymentSuccess and the
catch checking for HttpCode sentinel), but add a concise clarifying comment
above the catch referencing handlePaymentSuccess and HttpCode to document that
handlePaymentSuccess throws HttpCode(200) as a success sentinel so maintainers
understand why success does not trigger a rollback of payment.success.

In `@packages/app-store/paystack/lib/PaymentService.ts`:
- Around line 129-164: The refund() method does redundant DB round-trips and
silently no-ops on non-successful payments; change the initial
prisma.payment.findUnique to select the full record (or remove select) so you
can return the loaded payment directly when payment.refunded is true, and when
payment.success is false throw an ErrorWithCode (instead of returning the
record) so callers can handle the invalid refund attempt; keep the existing
this.client.createRefund call and prisma.payment.update(refunded: true) path
unchanged, just eliminate the extra findUnique calls.
- Around line 86-91: The object assigned to the data property in
PaymentService.ts is being double-cast with "as unknown as
Prisma.InputJsonValue"; remove the unsafe double cast and make the value
structurally typed: either annotate the literal as Prisma.JsonObject (e.g. set
data: <Prisma.JsonObject>{ access_code: ..., authorization_url: ..., publicKey:
..., reference }) or change the target type to Prisma.JsonValue/JsonObject so
the plain record is assignable without casting; update the import/use of Prisma
types if needed and ensure the data property uses Prisma.JsonObject instead of
forcing the unknown cast.
- Around line 55-56: The generated reference `const reference =
\`cal_${bookingId}_${uid.slice(0, 8)}\`` uses only 8 hex chars (~32 bits) which
risks collisions; update the logic that creates `uid`/`reference` (where
`uuidv4()` is called) to use a longer suffix (e.g., `uid.slice(0,16)` or the
full `uid`) so `reference` carries sufficient entropy and avoids Paystack
duplicate-reference rejections; keep the `bookingId` prefix but replace
`uid.slice(0, 8)` with the chosen longer slice or `uid` in the `reference`
assignment.
- Around line 58-68: In PaymentService where you call
this.client.initializeTransaction (the block that builds callback_url), replace
direct use of process.env.NEXT_PUBLIC_WEBAPP_URL with the exported WEBAPP_URL
constant from `@calcom/lib/constants` and import it at the top of the file; ensure
you join WEBAPP_URL and "/api/integrations/paystack/verify" robustly (handle
missing/trailing slash) so the callback_url never becomes "undefined/..." or a
malformed URL.

In `@packages/app-store/paystack/package.json`:
- Around line 10-18: The package.json is missing an explicit dependency on
`@calcom/prisma` even though lib/PaymentService.ts imports from '@calcom/prisma'
and '@calcom/prisma/client'; add "@calcom/prisma": "workspace:*" (or the same
version strategy used by other app-store packages) to the "dependencies" section
so the package declares the runtime dependency explicitly and won't rely on
workspace hoisting.
- Line 12: Replace the custom declaration file inline-js.d.ts with the official
DefinitelyTyped package by adding `@types/paystack__inline-js` as a devDependency
(replace or add in packages/app-store/paystack/package.json where
"@paystack/inline-js" is declared), remove the custom inline-js.d.ts file, and
remove the special tsconfig include entry in apps/web/tsconfig.json that was
only there to pick up the custom declarations; ensure code references to
PaystackPop and resumeTransaction remain and compile against the new `@types`
package, then run install to update lockfiles.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d9a57d50-5300-417d-813f-a94e10c70ea4

📥 Commits

Reviewing files that changed from the base of the PR and between a4a01a0 and cf7ce0b.

⛔ Files ignored due to path filters (2)
  • packages/app-store/paystack/static/icon.svg is excluded by !**/*.svg
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (34)
  • apps/web/app/(use-page-wrapper)/payment/[uid]/PaymentPage.tsx
  • apps/web/components/apps/AppSetupPage.tsx
  • apps/web/components/apps/paystack/Setup.tsx
  • apps/web/tsconfig.json
  • lint-staged.config.mjs
  • packages/app-store/_pages/setup/_getServerSideProps.tsx
  • packages/app-store/apps.browser.generated.tsx
  • packages/app-store/apps.keys-schemas.generated.ts
  • packages/app-store/apps.metadata.generated.ts
  • packages/app-store/apps.schemas.generated.ts
  • packages/app-store/apps.server.generated.ts
  • packages/app-store/payment.services.generated.ts
  • packages/app-store/paystack/_metadata.ts
  • packages/app-store/paystack/api/add.ts
  • packages/app-store/paystack/api/index.ts
  • packages/app-store/paystack/api/verify.ts
  • packages/app-store/paystack/api/webhook.ts
  • packages/app-store/paystack/components/EventTypeAppCardInterface.tsx
  • packages/app-store/paystack/components/EventTypeAppSettingsInterface.tsx
  • packages/app-store/paystack/components/PaystackPaymentComponent.tsx
  • packages/app-store/paystack/config.json
  • packages/app-store/paystack/index.ts
  • packages/app-store/paystack/inline-js.d.ts
  • packages/app-store/paystack/lib/PaymentService.ts
  • packages/app-store/paystack/lib/PaystackClient.ts
  • packages/app-store/paystack/lib/__tests__/PaystackClient.test.ts
  • packages/app-store/paystack/lib/__tests__/verifyWebhookSignature.test.ts
  • packages/app-store/paystack/lib/currencyOptions.ts
  • packages/app-store/paystack/lib/verifyWebhookSignature.ts
  • packages/app-store/paystack/package.json
  • packages/app-store/paystack/pages/setup/_getServerSideProps.ts
  • packages/app-store/paystack/zod.ts
  • packages/i18n/locales/en/common.json
  • scripts/seed-app-store.ts

Comment thread apps/web/components/apps/paystack/Setup.tsx
Comment thread packages/app-store/paystack/components/PaystackPaymentComponent.tsx Outdated
Comment thread packages/app-store/paystack/lib/PaymentService.ts
Comment thread packages/app-store/paystack/pages/setup/_getServerSideProps.ts Outdated
Comment thread packages/i18n/locales/en/common.json Outdated
role="presentation" on a focusable form input tells assistive tech to
ignore the element's implicit interactive role, which makes the public
and secret key fields invisible to screen readers. Per WAI-ARIA,
presentation is reserved for non-interactive elements; remove it from
both TextFields and let the native input semantics speak for
themselves.

Addresses [coderabbitai] review on apps/web/components/apps/paystack/Setup.tsx.
Per the project's error-handling convention (services/repositories
must throw ErrorWithCode, never bare Error), replace every Error in
PaystackPaymentService with the matching ErrorCode:

- "Booking not found"               → ErrorCode.BookingNotFound
- "Paystack credentials …"          → ErrorCode.MissingPaymentCredential
- "Paystack does not support hold"  → ErrorCode.BadRequest
- "Method not implemented."         → ErrorCode.InternalServerError

This lets callers branch on stable codes instead of message strings
and ensures the trpc errorConversionMiddleware maps them to the right
HTTP statuses.

Addresses [coderabbitai] review on
packages/app-store/paystack/lib/PaymentService.ts.
Four small cleanups from CodeRabbit's review of PaymentService:

- Use WEBAPP_URL from @calcom/lib/constants instead of reading
  process.env.NEXT_PUBLIC_WEBAPP_URL directly. If the env var were
  unset the callback URL would become "undefined/api/…" and Paystack
  would silently store a malformed URL; WEBAPP_URL ships proper
  fallback handling and matches the PayPal/HitPay convention.
- Widen the reference suffix from `uid.slice(0, 8)` (~32 bits of
  entropy) to the full uuid (`uid.replace(/-/g, "")`, 122 bits). The
  bookingId prefix already scopes uniqueness, but repeated retries on
  the same booking could collide and Paystack rejects duplicate
  references.
- Replace the `as unknown as Prisma.InputJsonValue` double-cast with
  `satisfies Prisma.InputJsonObject`. The object is structurally a
  JSON record; the cast was only there to bridge the broader
  JsonValue target type, which `satisfies` does without losing type
  information.
- Simplify refund(): the previous version re-ran findUnique twice on
  the already-refunded and never-succeeded branches. Now it returns
  the loaded record for the refunded case, and throws ErrorWithCode
  on the never-succeeded case so callers can surface the invalid
  refund attempt instead of getting a silent no-op back.

Addresses [coderabbitai] review on PaymentService.ts.
PaymentPage previously extracted the public key with
`(props.payment.data as unknown as { publicKey: string }).publicKey ?? ""`,
a double-cast that silently passed an empty string through to the
Paystack popup if event-type metadata was malformed. Follow the
existing paypal/hitpay/btcpayserver convention instead:

- Add `apps/web/components/apps/paystack/PaystackPaymentComponent.tsx`
  as a thin web-side wrapper that validates `payment.data` with a Zod
  schema (access_code, authorization_url, publicKey, reference) and
  renders `payment_failed_try_again` if invalid.
- Update PaymentPage to import the wrapper namespace and stop
  extracting publicKey at the call site.
- Tighten the inner component's prop type to only what it uses
  (`{ data: PaystackPaymentData }`, no full Payment row) and drop
  the unused `clientId` prop — the inner component already reads
  publicKey from payment.data.

Addresses [coderabbitai] review on apps/web/.../PaymentPage.tsx.
Interpolating `paymentData.reference` directly into the verify URL
trusts the reference to be URL-safe. Paystack-generated references
are normally cal_<bookingId>_<uuid>, but defending against
out-of-spec characters costs nothing — build the query string via
URLSearchParams so any odd characters round-trip cleanly.

Addresses [coderabbitai] review on PaystackPaymentComponent.tsx.
- _getServerSideProps: the previous `teamId ? { teamId } : { userId }`
  branching used truthiness, so a parsed `0` (a legitimate team id in
  the data model) would silently fall through to user-scoped install
  and the team admin check would be bypassed. Switch to
  `teamId !== null` branching and replace `Number.isNaN` with
  `Number.isInteger` so non-integer values are also rejected at the
  same gate.
- i18n: `paystack_getting_started_description` hardcoded "Cal.com",
  which breaks downstream deployments that ship with a different
  APP_NAME (Cal.diy, white-label installs, etc.). Templatize via
  `{{appName}}` and pass `APP_NAME` from the Setup component, matching
  the pattern AppPage and the Apple Calendar setup already use.

Addresses [coderabbitai] review on
_getServerSideProps.ts and common.json:3745.
…ne-js

The previous ambient declaration was a stopgap to silence TS7016 for
`@paystack/inline-js`, which ships JS only. DefinitelyTyped publishes
@types/paystack__inline-js (latest 1.0.2) covering PaystackPop and the
resumeTransaction API we use. Replace the in-repo declaration with the
upstream package:

- packages/app-store/paystack/package.json:
  - add @types/paystack__inline-js as a devDependency
  - add @calcom/prisma as an explicit dependency (PaymentService imports
    from @calcom/prisma and @calcom/prisma/client; relying on workspace
    hoisting is fragile)
- delete packages/app-store/paystack/inline-js.d.ts
- drop the bespoke include for inline-js.d.ts from apps/web/tsconfig.json
  (the routing-forms entry stays as the precedent for legitimate
  app-specific declarations)

Verified locally: app-store tsc exits 0; apps/web tsc reports no new
paystack errors (only pre-existing trpc.viewer issues unrelated to
this change).

Addresses [coderabbitai] review on
packages/app-store/paystack/package.json.
…rify

The pre-existing one-line comment on the inner catch was easy to skim
past, and the contract (handlePaymentSuccess throwing 200 as its
success signal) is non-obvious to anyone seeing the code for the first
time. Expand the block comment to spell out the contract, why a
2xx HttpCode is treated as success, and what happens on a real failure.
No logic change.

Addresses [coderabbitai] nitpick on packages/app-store/paystack/api/verify.ts.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/app-store/paystack/components/PaystackPaymentComponent.tsx (1)

38-47: 💤 Low value

Consider using the user's locale instead of hardcoded "en".

Intl.NumberFormat("en", …) ignores the viewer's locale for grouping and decimal separators (relevant for fr/de/etc., even though the currency code itself stays the same). useLocale() exposes the active locale and could be passed in here.

♻️ Example
-  const { t } = useLocale();
+  const { t, i18n } = useLocale();
   ...
-    formattedAmount = new Intl.NumberFormat("en", {
+    formattedAmount = new Intl.NumberFormat(i18n.language, {
       style: "currency",
       currency: currency.toUpperCase(),
     }).format(presentableAmount);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/app-store/paystack/components/PaystackPaymentComponent.tsx` around
lines 38 - 47, The currency formatting uses a hardcoded "en" locale; update
PaystackPaymentComponent to call useLocale() and pass the returned locale into
Intl.NumberFormat instead of "en" (e.g., replace the literal with the locale
variable), and keep the existing try/catch fallback for unknown currency codes;
also ensure you handle the case where useLocale() may return undefined by
defaulting to "en" or navigator.language.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/app-store/paystack/api/verify.ts`:
- Around line 55-72: The credential lookup can fall back to a { userId: null,
appId: "paystack" } query when payment.booking?.userId is missing; change the
credentialQuery construction so the fallback is only used when a concrete userId
exists—if paystackAppData?.credentialId is absent and payment.booking?.userId is
null/undefined, immediately throw the HttpCode error instead of calling
prisma.credential.findFirst. Update the logic around
metadata/paystackAppData/credentialQuery (the variables payment, metadata,
paystackAppData, credentialQuery and the prisma.credential.findFirst call) to
validate presence of userId before building/using the fallback query and retain
the existing error throw (HttpCode) when no valid identifier is available.

In `@packages/app-store/paystack/components/PaystackPaymentComponent.tsx`:
- Around line 57-73: In the popup.resumeTransaction onSuccess handler, don't
await the verification fetch since it can stall the redirect; make the verify
call fire-and-forget (e.g., call
fetch('/api/integrations/paystack/verify?...').catch(() => {}) without awaiting)
and move the setTimeout(window.location.replace(`/booking/${bookingUid}`), 2000)
to run unconditionally (outside the try/catch) so the redirect is scheduled
regardless of verify completion; update references in the onSuccess callback
around the existing setStatus, verify fetch, and setTimeout logic.

---

Nitpick comments:
In `@packages/app-store/paystack/components/PaystackPaymentComponent.tsx`:
- Around line 38-47: The currency formatting uses a hardcoded "en" locale;
update PaystackPaymentComponent to call useLocale() and pass the returned locale
into Intl.NumberFormat instead of "en" (e.g., replace the literal with the
locale variable), and keep the existing try/catch fallback for unknown currency
codes; also ensure you handle the case where useLocale() may return undefined by
defaulting to "en" or navigator.language.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 446916b0-b13d-4e3e-8236-d38193386264

📥 Commits

Reviewing files that changed from the base of the PR and between cf7ce0b and 4f3ed11.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (9)
  • apps/web/app/(use-page-wrapper)/payment/[uid]/PaymentPage.tsx
  • apps/web/components/apps/paystack/PaystackPaymentComponent.tsx
  • apps/web/components/apps/paystack/Setup.tsx
  • packages/app-store/paystack/api/verify.ts
  • packages/app-store/paystack/components/PaystackPaymentComponent.tsx
  • packages/app-store/paystack/lib/PaymentService.ts
  • packages/app-store/paystack/package.json
  • packages/app-store/paystack/pages/setup/_getServerSideProps.ts
  • packages/i18n/locales/en/common.json
✅ Files skipped from review due to trivial changes (1)
  • packages/i18n/locales/en/common.json
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/app-store/paystack/package.json
  • apps/web/app/(use-page-wrapper)/payment/[uid]/PaymentPage.tsx
  • packages/app-store/paystack/lib/PaymentService.ts
  • packages/app-store/paystack/pages/setup/_getServerSideProps.ts
  • apps/web/components/apps/paystack/Setup.tsx

Comment thread packages/app-store/paystack/api/verify.ts Outdated
Comment thread packages/app-store/paystack/components/PaystackPaymentComponent.tsx
Both verify and webhook fall back to
`{ userId: payment.booking?.userId, appId: "paystack" }` when the
event-type metadata doesn't carry a specific credentialId. If
`payment.booking?.userId` is null/undefined the query becomes
`{ userId: null, appId: "paystack" }` and Prisma will literally match
a credential where `userId IS NULL` — silently resolving to an
unintended row. In the webhook path that means verifying the signature
against the wrong secret.

Branch the fallback explicitly: use credentialId when present, fall
back to userId only when it's a real number, and otherwise throw
500 "Cannot resolve payment credentials" so the caller fails closed
instead of guessing.

Same change applied to verify.ts and webhook.ts since they share the
pattern; webhook also logs an error before throwing for observability.

Addresses [coderabbitai] review on packages/app-store/paystack/api/verify.ts.
…t block redirect

`await fetch('/api/integrations/paystack/verify?...')` inside the
inline-js onSuccess handler ran before the setTimeout that schedules
the redirect to `/booking/{uid}`. If the verify endpoint stalled (slow
upstream, transient outage), the user got stuck on the green "Payment
successful" screen indefinitely even though Paystack had confirmed the
charge and the webhook would reconcile the booking shortly.

The verify call is best-effort — the webhook is the source of truth —
so drop the await, attach a no-op .catch for the rejection, and let
the redirect schedule unconditionally. Also drop the now-unnecessary
`async` from onSuccess.

Addresses [coderabbitai] review on
packages/app-store/paystack/components/PaystackPaymentComponent.tsx.
Hardcoding "en" for Intl.NumberFormat formats the amount with English
grouping/decimal separators regardless of the user's actual locale,
which reads oddly in fr/de/etc. Pull `i18n.language` from useLocale()
and fall back to "en" if it's unset.

The try/catch around Intl.NumberFormat stays — RangeError is still
possible for unknown ISO currency codes.

Addresses [coderabbitai] nitpick on
packages/app-store/paystack/components/PaystackPaymentComponent.tsx.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant