Skip to content

Conversation

@lissavxo
Copy link
Collaborator

@lissavxo lissavxo commented Aug 15, 2025

Related to #1021

Depends on

Description

Update Client Payment register when transaction is made

Test plan

Create a payment Id with the endpoint api/payments/paymentId?address=your_address, make a payment with the payment Id created in the op_return, make sure the payment Id register status updates according to transaction status

Summary by CodeRabbit

  • New Features

    • Add payment-ID creation API with CORS, input validation, and standardized success/error responses; backend now records client payments with statuses and exposes generate/get/update payment APIs; blockchain processing updates client-payment statuses on mempool/confirmation events.
  • Bug Fixes

    • Standardize "Method Not Allowed" responses to HTTP 405 and improve mapping of address-related errors to proper responses.
  • Tests

    • Add extensive unit tests covering payment-status update logic and edge cases.

@Klakurka Klakurka requested a review from chedieck August 15, 2025 21:26
@lissavxo lissavxo force-pushed the feat/update-payment-id branch from 0edfea9 to fa5727b Compare August 18, 2025 15:13
@lissavxo lissavxo force-pushed the feat/update-payment-id branch from ab9676e to 0714f9e Compare August 21, 2025 20:13
@lissavxo lissavxo requested a review from chedieck August 21, 2025 20:46
@lissavxo lissavxo force-pushed the feat/update-payment-id branch 2 times, most recently from a940c12 to bc6a25a Compare August 27, 2025 16:34
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 4, 2025

Walkthrough

Adds client payment support: new POST API to create paymentId; Prisma model, enum, and migration for ClientPayment; transactionService functions to generate/get/update client payments; chronikService updates to set client payment statuses on mempool/confirmed events; validator helpers; 405 constant rename and unit tests.

Changes

Cohort / File(s) Summary
API: Create Payment ID
pages/api/payments/paymentId/index.ts
New API route with CORS and runMiddleware. Accepts POST, validates payload via parseCreatePaymentIdPOSTRequest, calls generatePaymentId, returns { paymentId }, maps address-related errors to configured responses, and enforces allowed methods (GET, HEAD, POST).
API: Align 405 handling
pages/api/paybutton/download/transactions/[paybuttonId].ts, pages/api/paybutton/transactions/[id].ts, pages/api/payments/download/index.ts
Replace METHOD_NOT_ALLOWED usage with METHOD_NOT_ALLOWED_405 in method checks and error responses; no other logic changes.
Services: Client payment lifecycle
services/transactionService.ts, services/chronikService.ts
Add generatePaymentId, getClientPayment, updateClientPaymentStatus. Chronik client adds private handleUpdateClientPaymentStatus and invokes it for TX_ADDED_TO_MEMPOOL and TX_CONFIRMED to update ClientPayment status when OP_RETURN contains a paymentId and amount conditions match.
Validation
utils/validators.ts
Add parseCreatePaymentIdPOSTRequest and interfaces CreatePaymentIdPOSTParameters, CreatePaymentIdInput enforcing non-empty address and normalizing optional amount.
Data model & migration
prisma-local/schema.prisma, prisma-local/migrations/.../20250912183436_client_payment/migration.sql
Add ClientPayment model, ClientPaymentStatus enum (PENDING, ADDED_TO_MEMPOOL, CONFIRMED), clientPayments relation on Address, and SQL migration creating ClientPayment table with FK to Address.
Constants
constants/index.ts
Rename METHOD_NOT_ALLOWEDMETHOD_NOT_ALLOWED_405 in RESPONSE_MESSAGES and set statusCode to 405.
Tests
tests/unittests/handleUpdateClientPaymentStatus.test.ts
New unit tests covering OP_RETURN parsing, amount matching/precision, status transitions, and negative/edge cases for handleUpdateClientPaymentStatus.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Client
  participant API as API /api/payments/paymentId
  participant Validators as Validators
  participant TxSvc as transactionService
  participant DB as Prisma DB

  Client->>API: POST { address, amount? }
  API->>Validators: parseCreatePaymentIdPOSTRequest()
  Validators-->>API: { address, amount? }
  API->>TxSvc: generatePaymentId(address, amount?)
  TxSvc->>DB: upsert Address, create ClientPayment(status=PENDING)
  DB-->>TxSvc: client payment created
  TxSvc-->>API: paymentId
  API-->>Client: 200 { paymentId }
  alt invalid address
    API-->>Client: configured 4xx response
  else server error
    API-->>Client: 500 { statusCode:500, message }
  end
Loading
sequenceDiagram
  autonumber
  participant WS as Chronik WS
  participant Chronik as ChronikBlockchainClient
  participant TxSvc as transactionService
  participant DB as Prisma DB

  WS-->>Chronik: TX_ADDED_TO_MEMPOOL / TX_CONFIRMED (tx, outputs, OP_RETURN)
  Chronik->>Chronik: parse OP_RETURN -> paymentId?
  alt paymentId found
    Chronik->>TxSvc: getClientPayment(paymentId)
    TxSvc->>DB: find ClientPayment + Address
    DB-->>TxSvc: ClientPayment or null
    alt Found and (amount matches or amount is null)
      Chronik->>TxSvc: updateClientPaymentStatus(paymentId, status)
      TxSvc->>DB: update status (ADDED_TO_MEMPOOL or CONFIRMED)
    else Not found or amount mismatch
      Chronik-->>Chronik: no-op
    end
  else No paymentId
    Chronik-->>Chronik: no-op
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

enhancement (UI/UX/feature)

Suggested reviewers

  • Klakurka

Poem

I minted a tiny ID today,
a byte-sized carrot to guide the pay.
From mempool hops to blocks that chime,
OP_RETURN danced and kept the time.
PENDING to CONFIRMED — a carroted rhyme 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title succinctly describes the primary change—adding functionality to update the clientPayment status—and it is clear, concise, and directly related to the main change set without extraneous details.
Description Check ✅ Passed The description follows the repository template by including the Related to and optional Depends on sections, providing a clear Description of the change, and outlining a concrete Test plan; all required headings are present and appropriately filled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/update-payment-id

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e8aadce and 21f4c8f.

📒 Files selected for processing (2)
  • services/chronikService.ts (5 hunks)
  • services/transactionService.ts (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
services/chronikService.ts (2)
utils/validators.ts (1)
  • parseOpReturnData (436-464)
services/transactionService.ts (2)
  • getClientPayment (1023-1028)
  • updateClientPaymentStatus (1016-1021)
services/transactionService.ts (4)
utils/validators.ts (1)
  • parseAddress (31-48)
constants/index.ts (1)
  • NETWORK_IDS_FROM_SLUGS (132-135)
services/addressService.ts (1)
  • addressExists (127-141)
services/chronikService.ts (1)
  • multiBlockchainClient (1108-1108)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run Tests

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
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: 8

♻️ Duplicate comments (1)
services/chronikService.ts (1)

437-440: Good move: pre-fetching tx and related addresses once.

This addresses the prior request to centralize context and reduces duplication across branches. The helper usage on confirmation is clean.

Also applies to: 454-454

🧹 Nitpick comments (7)
prisma-local/schema.prisma (2)

25-25: Rename relation field for clarity.

clientPaymentIds holds ClientPayment[], not just IDs. Suggest renaming to clientPayments to match semantics and existing naming patterns.

Apply:

-  clientPaymentIds                     ClientPayment[]
+  clientPayments                       ClientPayment[]

279-281: Optional: tighten paymentId column type.

If paymentId is always a 32-char hex (uuid without dashes), you can use @db.Char(32) to reduce storage and improve index density. Requires updating the migration too.

pages/api/payments/paymentId/index.ts (2)

17-23: Return 400 for invalid address as well.

Map INVALID_ADDRESS_400 to its status code for clearer client errors. If you adopt the amount validation above, map INVALID_INPUT_400 too.

Apply:

       switch (error.message) {
         case RESPONSE_MESSAGES.ADDRESS_NOT_PROVIDED_400.message:
           res.status(RESPONSE_MESSAGES.ADDRESS_NOT_PROVIDED_400.statusCode).json(RESPONSE_MESSAGES.ADDRESS_NOT_PROVIDED_400)
           break
+        case RESPONSE_MESSAGES.INVALID_ADDRESS_400.message:
+          res.status(RESPONSE_MESSAGES.INVALID_ADDRESS_400.statusCode).json(RESPONSE_MESSAGES.INVALID_ADDRESS_400)
+          break
+        case RESPONSE_MESSAGES.INVALID_INPUT_400.message:
+          res.status(RESPONSE_MESSAGES.INVALID_INPUT_400.statusCode).json(RESPONSE_MESSAGES.INVALID_INPUT_400)
+          break
         default:
           res.status(500).json({ statusCode: 500, message: error.message })
       }

26-28: Use 405 for unsupported methods.

METHOD_NOT_ALLOWED in constants uses 500; respond with 405 here to be accurate (and consider fixing the constant in a follow-up).

Apply:

-    res.status(RESPONSE_MESSAGES.METHOD_NOT_ALLOWED.statusCode)
-      .json(RESPONSE_MESSAGES.METHOD_NOT_ALLOWED)
+    res.status(405).json({ statusCode: 405, message: 'Method not allowed.' })
prisma-local/migrations/20250904155444_client_payment/migration.sql (1)

12-14: Optional: add index for addressString.

MySQL may auto-create an index for FK columns, but adding it explicitly is clearer and portable.

Apply:

 ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
 
+-- Index to support FK lookups
+CREATE INDEX `ClientPayment_addressString_idx` ON `ClientPayment`(`addressString`);
 
 -- AddForeignKey
 ALTER TABLE `ClientPayment` ADD CONSTRAINT `ClientPayment_addressString_fkey` FOREIGN KEY (`addressString`) REFERENCES `Address`(`address`) ON DELETE RESTRICT ON UPDATE CASCADE;
services/chronikService.ts (1)

14-19: Optional: prefer enum member over string casting.

If Prisma generated enums are available at runtime, use ClientPaymentStatus.CONFIRMED/ADDED_TO_MEMPOOL to avoid typos.

services/transactionService.ts (1)

12-13: Avoid circular dependency between services.

transactionService imports multiBlockchainClient from chronikService, while chronikService imports functions from transactionService. Extract multiBlockchainClient to a small module (e.g., services/multiBlockchainClient.ts) and import it from both sides to break the cycle.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 11af13c and 5c40621.

📒 Files selected for processing (7)
  • pages/api/payments/paymentId/index.ts (1 hunks)
  • prisma-local/migrations/20250904155444_client_payment/migration.sql (1 hunks)
  • prisma-local/migrations/migration_lock.toml (1 hunks)
  • prisma-local/schema.prisma (2 hunks)
  • services/chronikService.ts (4 hunks)
  • services/transactionService.ts (3 hunks)
  • utils/validators.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
pages/api/payments/paymentId/index.ts (3)
utils/validators.ts (2)
  • parseCreatePaymentIdPOSTRequest (575-587)
  • parseAddress (31-48)
services/transactionService.ts (1)
  • generatePaymentId (969-1002)
constants/index.ts (1)
  • RESPONSE_MESSAGES (8-107)
utils/validators.ts (1)
constants/index.ts (1)
  • RESPONSE_MESSAGES (8-107)
services/transactionService.ts (3)
constants/index.ts (1)
  • NETWORK_IDS_FROM_SLUGS (131-134)
services/addressService.ts (1)
  • addressExists (127-141)
services/chronikService.ts (1)
  • multiBlockchainClient (886-886)
services/chronikService.ts (3)
types/chronikTypes.ts (1)
  • AddressWithTransaction (11-14)
utils/validators.ts (1)
  • parseOpReturnData (433-461)
services/transactionService.ts (2)
  • updatePaymentStatus (1004-1009)
  • getClientPayment (1011-1016)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run Tests
🔇 Additional comments (5)
prisma-local/migrations/migration_lock.toml (1)

3-3: Correct Prisma provider entry.

Good catch fixing the stray dash; Prisma will read the provider correctly now.

prisma-local/schema.prisma (1)

273-277: ClientPaymentStatus enum looks good.

pages/api/payments/paymentId/index.ts (1)

7-8: Test plan mismatch (GET vs POST).

The PR test plan mentions calling api/payments/paymentId?address=... (suggesting GET), but this handler only accepts POST. Either update the test plan or add GET support.

services/transactionService.ts (2)

1004-1009: LGTM: simple, idempotent status update.

Works as expected; callers should already guard existence.


1011-1016: LGTM: explicit include of address for downstream checks.

Returning the address relation enables safe validations at call sites.

Comment on lines 5 to 6
`addressString` VARCHAR(191) NOT NULL,
`amount` DECIMAL(65, 30) NULL,
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix FK column length to match referenced column.

ClientPayment.addressString must match Address.address length (255). As written (191), the FK add will fail on MySQL.

Apply:

-    `addressString` VARCHAR(191) NOT NULL,
+    `addressString` VARCHAR(255) NOT NULL,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
`addressString` VARCHAR(191) NOT NULL,
`amount` DECIMAL(65, 30) NULL,
`addressString` VARCHAR(255) NOT NULL,
`amount` DECIMAL(65, 30) NULL,
🤖 Prompt for AI Agents
In prisma-local/migrations/20250904155444_client_payment/migration.sql around
lines 5 to 6, the foreign-key column `addressString` is defined as VARCHAR(191)
which does not match the referenced `Address.address` length (255) and will
break the FK creation; update the column definition to VARCHAR(255) NOT NULL so
its length and nullability match the referenced column (and re-run/apply the
migration or regenerate migration SQL accordingly).

Comment on lines 279 to 287
model ClientPayment {
paymentId String @id
status ClientPaymentStatus
addressString String
amount Decimal?
address Address @relation(fields: [addressString], references: [address])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Foreign key length mismatch will break migration.

ClientPayment.addressString references Address.address (@db.VarChar(255)), but here it has no column length (migration creates it as VARCHAR(191)). MySQL requires identical type/length/collation for FK columns; this will fail when adding the FK.

Apply:

 model ClientPayment {
   paymentId            String                 @id
   status               ClientPaymentStatus
-  addressString        String
+  addressString        String @db.VarChar(255)
   amount               Decimal?
   address              Address   @relation(fields: [addressString], references: [address])
   createdAt            DateTime               @default(now())
   updatedAt            DateTime               @updatedAt
+  @@index([addressString], map: "ClientPayment_addressString_fkey")
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
model ClientPayment {
paymentId String @id
status ClientPaymentStatus
addressString String
amount Decimal?
address Address @relation(fields: [addressString], references: [address])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model ClientPayment {
paymentId String @id
status ClientPaymentStatus
addressString String @db.VarChar(255)
amount Decimal?
address Address @relation(fields: [addressString], references: [address])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([addressString], map: "ClientPayment_addressString_fkey")
}
🤖 Prompt for AI Agents
In prisma-local/schema.prisma around lines 279 to 287, the
ClientPayment.addressString FK column lacks the explicit VARCHAR length and will
be generated as VARCHAR(191), which mismatches Address.address
(@db.VarChar(255)) and will break the FK migration; fix by annotating
ClientPayment.addressString with the same database type/length (e.g., add
@db.VarChar(255)) and ensure any custom collation matches Address.address so
both columns have identical type/length/collation before running the migration.

Copy link
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: 3

♻️ Duplicate comments (3)
prisma-local/schema.prisma (1)

279-286: Fix FK length mismatch (+ align referential action and decimal precision).

  • ClientPayment.addressString must match Address.address (@db.VarChar(255)), or MySQL will reject the FK at migration time.
  • Explicitly set onUpdate: Cascade to match the migration.
  • Align amount precision with Transaction.amount (@db.Decimal(24, 8)) to avoid inconsistent scales.

Apply:

 model ClientPayment {
   paymentId            String                 @id
   status               ClientPaymentStatus
-  addressString        String
-  amount               Decimal?
-  address              Address   @relation(fields: [addressString], references: [address])
+  addressString        String                  @db.VarChar(255)
+  amount               Decimal?                @db.Decimal(24, 8)
+  address              Address   @relation(fields: [addressString], references: [address], onUpdate: Cascade)
   createdAt            DateTime               @default(now())
   updatedAt            DateTime               @updatedAt
+  @@index([addressString], map: "ClientPayment_addressString_fkey")
 }
prisma-local/migrations/20250912183436_client_payment/migration.sql (1)

3-6: Make FK column length consistent and index it; align amount scale.

  • Use VARCHAR(255) for addressString to match Address.address and prevent FK creation failure.
  • Add an index on addressString (good practice; some engines auto-create, but being explicit avoids surprises).
  • Use DECIMAL(24, 8) for amount to match application semantics and Transaction.amount.

Apply:

 CREATE TABLE `ClientPayment` (
     `paymentId` VARCHAR(191) NOT NULL,
     `status` ENUM('PENDING', 'ADDED_TO_MEMPOOL', 'CONFIRMED') NOT NULL,
-    `addressString` VARCHAR(191) NOT NULL,
-    `amount` DECIMAL(65, 30) NULL,
+    `addressString` VARCHAR(255) NOT NULL,
+    `amount` DECIMAL(24, 8) NULL,
     `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
     `updatedAt` DATETIME(3) NOT NULL,
 
     PRIMARY KEY (`paymentId`)
 ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
 
+-- Ensure supporting index exists for FK (explicit)
+CREATE INDEX `ClientPayment_addressString_fkey` ON `ClientPayment`(`addressString`);
+
 -- AddForeignKey
 ALTER TABLE `ClientPayment` ADD CONSTRAINT `ClientPayment_addressString_fkey` FOREIGN KEY (`addressString`) REFERENCES `Address`(`address`) ON DELETE RESTRICT ON UPDATE CASCADE;

Also applies to: 13-14

services/chronikService.ts (1)

424-436: Harden status updates: validate paymentId, compare Decimals correctly, guard downgrades, and verify address.

Current code can throw on missing/unknown IDs, mis-compare decimals via Number(), and update mismatched addresses; it may also downgrade CONFIRMED back to ADDED_TO_MEMPOOL.

Apply:

-  private async updateClientPaymentStatus (txAmount: string | number | Prisma.Decimal | DecimalJsLike, opReturn: string | undefined, status: ClientPaymentStatus): Promise<void> {
-    const parsedOpReturn = parseOpReturnData(opReturn ?? '')
-    const paymentId = parsedOpReturn.paymentId
-
-    const clientPayment = await getClientPayment(paymentId)
-    if (clientPayment.amount !== null) {
-      if (Number(clientPayment.amount) === Number(txAmount)) {
-        await updatePaymentStatus(paymentId, status)
-      }
-    } else {
-      await updatePaymentStatus(paymentId, status)
-    }
-  }
+  private async updateClientPaymentStatus (txAmount: Prisma.Decimal, opReturn: string | undefined, status: ClientPaymentStatus, addressString: string): Promise<void> {
+    const parsed = parseOpReturnData(opReturn ?? '') as any
+    const paymentId: string = typeof parsed?.paymentId === 'string' ? parsed.paymentId : ''
+    // TODO: confirm required length; adjust regex if generatePaymentId uses a different size
+    if (!/^[0-9a-fA-F]{32}$/.test(paymentId)) return
+    try {
+      const clientPayment = await getClientPayment(paymentId)
+      // Prevent mismatched updates and status downgrades/no‑ops
+      if (clientPayment.address.address !== addressString) return
+      if (status === 'ADDED_TO_MEMPOOL' && clientPayment.status === 'CONFIRMED') return
+      if (clientPayment.status === status) return
+      if (clientPayment.amount !== null) {
+        if (clientPayment.amount.equals(new Prisma.Decimal(txAmount))) {
+          await updatePaymentStatus(paymentId, status)
+        }
+      } else {
+        await updatePaymentStatus(paymentId, status)
+      }
+    } catch {
+      // Unknown paymentId — ignore
+    }
+  }
🧹 Nitpick comments (2)
services/chronikService.ts (2)

33-33: Narrow the type if possible.

If callers always pass Prisma.Decimal, you can drop DecimalJsLike from the signature to simplify types.


459-462: Optional: de‑dupe repeated updates per tx.

Multiple related addresses can trigger duplicate status changes for the same paymentId. Consider tracking a Set per processed tx.

Example:

-        for (const addressWithTransaction of addressesWithTransactions) {
+        const seen = new Set<string>()
+        for (const addressWithTransaction of addressesWithTransactions) {
+          const parsed = parseOpReturnData(addressWithTransaction.transaction.opReturn ?? '') as any
+          const pid = typeof parsed?.paymentId === 'string' ? parsed.paymentId : ''
+          if (pid === '' || seen.has(pid)) continue
+          seen.add(pid)
           // ...
         }

Also applies to: 479-481

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5c40621 and 0015825.

📒 Files selected for processing (3)
  • prisma-local/migrations/20250912183436_client_payment/migration.sql (1 hunks)
  • prisma-local/schema.prisma (2 hunks)
  • services/chronikService.ts (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
services/chronikService.ts (2)
utils/validators.ts (1)
  • parseOpReturnData (433-461)
services/transactionService.ts (2)
  • getClientPayment (1011-1016)
  • updatePaymentStatus (1004-1009)
🔇 Additional comments (3)
prisma-local/schema.prisma (2)

25-25: LGTM: added back‑relation on Address.

clientPayments ClientPayment[] correctly models the inverse relation.


273-277: LGTM: enum covers current lifecycle.

PENDING | ADDED_TO_MEMPOOL | CONFIRMED matches the flow described in the PR.

services/chronikService.ts (1)

14-17: Imports look good.

Using updatePaymentStatus/getClientPayment here is appropriate for the new lifecycle.

Comment on lines 424 to 461
private async updateClientPaymentStatus (txAmount: string | number | Prisma.Decimal | DecimalJsLike, opReturn: string | undefined, status: ClientPaymentStatus): Promise<void> {
const parsedOpReturn = parseOpReturnData(opReturn ?? '')
const paymentId = parsedOpReturn.paymentId

const clientPayment = await getClientPayment(paymentId)
if (clientPayment.amount !== null) {
if (Number(clientPayment.amount) === Number(txAmount)) {
await updatePaymentStatus(paymentId, status)
}
} else {
await updatePaymentStatus(paymentId, status)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Confirm expected paymentId format.

If generatePaymentId uses a different length/format, adjust the regex accordingly.


🏁 Script executed:

#!/bin/bash
# Inspect paymentId generation and OP_RETURN parsing to confirm format/length.
rg -nP "generatePaymentId|CreatePaymentId|paymentId" -C3
rg -nP "paymentId[^\n]*@id" prisma-local/schema.prisma

Length of output: 27906


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== show generatePaymentId in services/transactionService.ts ==="
nl -ba services/transactionService.ts | sed -n '960,1005p' || true

echo
echo "=== show OP_RETURN parsing in services/chronikService.ts (full parse function) ==="
nl -ba services/chronikService.ts | sed -n '1,140p' || true

echo
echo "=== show updateClientPaymentStatus in services/chronikService.ts ==="
nl -ba services/chronikService.ts | sed -n '416,440p' || true

echo
echo "=== look for explicit regex/validation for paymentId in repo ==="
rg -n --hidden -S "\\b[0-9a-fA-F]{32}\\b|validate.*paymentId|is.*paymentId|paymentId.*match|paymentId.*test" || true

Length of output: 521


Confirm paymentId format and validate before DB lookup.

generatePaymentId() (services/transactionService.ts) produces a UUIDv4 with dashes removed (32 hex characters). parseOpReturn() (services/chronikService.ts) builds the paymentId from raw OP_RETURN bytes and can be variable length or ''. updateClientPaymentStatus (services/chronikService.ts — lines ~424–436) calls getClientPayment(paymentId) without validating/normalizing the parsed value, which can throw or mismatch.

  • Enforce/normalize paymentId to /^[0-9a-f]{32}$/ before calling getClientPayment; if invalid or empty, skip/update no-op and log. Apply the check either in parseOpReturn (where ret.paymentId is set) or at the start of updateClientPaymentStatus.
  • If OP_RETURN is intended to carry a shorter binary ID, decide and document the canonical format and update generatePaymentId / DB lookup accordingly.
🤖 Prompt for AI Agents
In services/chronikService.ts around lines 424 to 436, the parsed paymentId from
OP_RETURN can be empty or variable-length and is used directly in
getClientPayment; validate and normalize it before DB lookup by: coerce the
parsed paymentId to a lowercase hex string, strip any dashes, then verify it
matches /^[0-9a-f]{32}$/; if it fails validation (empty or wrong
length/characters) log a warning and return/do nothing; only call
getClientPayment/updatePaymentStatus when the normalized ID passes the regex.
Ensure the validation is added at the start of updateClientPaymentStatus (or
alternatively enforce/normalize in parseOpReturn) so DB lookups never receive
malformed IDs.

Copy link
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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
services/triggerService.ts (2)

445-449: Type mismatch and potential JSON.stringify failure in logs.

PostDataTriggerLog declares postedData/responseData as string, but objects are assigned here, which will fail TS checks and may throw when stringified (e.g., BigInt). Stringify explicitly and defensively.

-    const responseData = await response.data
+    const responseData = response.data
     logData = {
-      postedData: parsedPostDataParameters,
+      postedData: JSON.stringify(parsedPostDataParameters),
       postedURL: trigger.postURL,
-      responseData
+      responseData: typeof responseData === 'string' ? responseData : JSON.stringify(responseData)
     }

436-443: Harden outbound POST against SSRF and oversized responses.

User-controlled URLs are posted to directly. Add URL validation (https only, block private/link‑local IPs, limit redirects) and cap response sizes.

-    const response = await axios.post(
-      trigger.postURL,
+    // Basic SSRF guard
+    const url = new URL(trigger.postURL)
+    if (url.protocol !== 'https:') {
+      throw new Error('POST URL must use https')
+    }
+    const response = await axios.post(
+      url.toString(),
       parsedPostDataParameters,
       {
-        timeout: config.triggerPOSTTimeout
+        timeout: config.triggerPOSTTimeout,
+        maxRedirects: 3,
+        maxContentLength: 1_000_000, // 1 MB
+        maxBodyLength: 1_000_000
       }
     )

Add a helper (outside this hunk) to DNS-resolve and block private ranges if you need stronger guarantees:

// helpers/net.ts
export async function isPrivateAddress(hostname: string): Promise<boolean> { /* resolve + CIDR check */ }
🧹 Nitpick comments (12)
.github/coding-instructions.md (1)

12-12: Fix markdown list indentation (MD007).

Adjust indentation to satisfy markdownlint (expected 0).

- - Make sure new tests added into the "integration-tests" directory are actually integration tests.
+- Make sure new tests added into the "integration-tests" directory are actually integration tests.
services/triggerService.ts (2)

406-435: Good pre-validation + early return; minor nits.

  • Type the parsed payload as unknown/Record to avoid leaking any.
  • Error log message “should never happen” is misleading; recommend actionable text with trigger id and error.message.
-  let parsedPostDataParameters: any
+  let parsedPostDataParameters: Record<string, unknown>
@@
-    console.error(`[ERROR] Invalid trigger data in DB for trigger ${trigger.id} (should never happen)`)
+    console.error(`[TRIGGER] Invalid trigger postData for trigger ${trigger.id}: ${jsonErr?.message}`)

444-444: Remove unnecessary await on response.data.

response.data is not a promise.

-    const responseData = await response.data
+    const responseData = response.data
tests/unittests/triggerService.test.ts (4)

56-110: “Execution scenarios” use console logs and never hit service paths.

Drive the real code path and assert side effects; drop console logs.

- console.log('=== Test Case 1: Valid JSON ===')
- ...
- console.log('\n=== Test Case 2: Invalid JSON ===')
+ // Valid path -> one POST, non-error log
+ mockedParseTriggerPostData.mockReturnValue({ amount: 100, currency: 'XEC' })
+ mockedAxios.post.mockResolvedValue({ data: { ok: true } })
+ await __testOnly__.postDataForTrigger(trigger, {} as any)
+ expect(mockedAxios.post).toHaveBeenCalledTimes(1)
+ expect(prismaMock.triggerLog.create).toHaveBeenLastCalledWith(expect.objectContaining({ data: expect.objectContaining({ isError: false }) }))
+
+ // Invalid path -> zero POSTs, error log
+ jest.clearAllMocks()
+ mockedParseTriggerPostData.mockImplementation(() => { throw new Error('boom') })
+ await __testOnly__.postDataForTrigger(trigger, {} as any)
+ expect(mockedAxios.post).not.toHaveBeenCalled()
+ expect(prismaMock.triggerLog.create).toHaveBeenCalledWith(expect.objectContaining({ data: expect.objectContaining({ isError: true }) }))

111-153: Error-shape assertions don’t reflect real parser behavior.

The real parseTriggerPostData wraps errors with RESPONSE_MESSAGES, so expecting native SyntaxError is brittle. Prefer message substring/HTTP code mapping produced by the helper, or exercise service logging side-effects instead.


155-192: Edge-case tests still only exercise the mock.

Same issue: call the service and assert no network call + error log persisted.


195-236: Avoid testing Axios directly; assert service behavior and counts.

Count mockedAxios.post calls made by the service, not by the test. Remove console noise.

- await mockedAxios.post('https://example.com', { amount: 100 })
- console.log('✅ Valid JSON: Network request made')
+ await __testOnly__.postDataForTrigger(trigger, {} as any)
+ expect(mockedAxios.post).toHaveBeenCalledTimes(1)
@@
- console.log(`Network requests made: ${networkRequestCount}`)
- expect(networkRequestCount).toBe(1)
+ expect(mockedAxios.post).toHaveBeenCalledTimes(1)
utils/files.ts (5)

153-154: Avoid accumulating empty arrays in group map.

Delete processed groups to prevent unbounded growth of tempTxGroups.

-    this.tempTxGroups[groupKey] = []
+    delete this.tempTxGroups[groupKey]

269-283: Stabilize group-key construction: sort names to ensure deterministic keys.

Different iteration orders can produce different group keys for the same set of paybuttons.

-  const uniqueButtonNames = new Set(
-    tx.address.paybuttons
-      .filter(pb => pb.paybutton.providerUserId === userId)
-      .map(pb => pb.paybutton.name)
-  )
-
-  if (uniqueButtonNames.size > 1) {
+  const uniqueButtonNames = new Set(
+    tx.address.paybuttons
+      .filter(pb => pb.paybutton.providerUserId === userId)
+      .map(pb => pb.paybutton.name)
+  )
+  const names = Array.from(uniqueButtonNames).sort()
+
+  if (names.length > 1) {
     if (paybuttonId !== undefined) {
       return tx.address.paybuttons.find(pb => pb.paybutton.id === paybuttonId)?.paybutton.name ?? ''
     } else {
-      return [...uniqueButtonNames].join('_')
+      return names.join('_')
     }
   } else {
-    return uniqueButtonNames.values().next().value ?? ''
+    return names[0] ?? ''
   }

Note: Using _ as a delimiter remains fragile if names include _. Consider encoding names (e.g., encodeURIComponent) or using a structured/base64 key in a follow-up.


147-152: Deduplicate single-tx push logic to one helper.

TransactionGroupManager.addSingleTransaction duplicates addSingleTransactionToResults, risking drift.

-    if (tempTxGroup.length === 1) {
-      this.addSingleTransaction(tempTxGroup[0], groupKey, treatedPayments)
-    } else {
+    if (tempTxGroup.length === 1) {
+      addSingleTransactionToResults(tempTxGroup[0], groupKey, this.currency, treatedPayments, this.timezone)
+    } else {
       this.addCollapsedTransactionGroup(tempTxGroup, groupKey, treatedPayments)
     }
-  private addSingleTransaction(
-    tx: TransactionsWithPaybuttonsAndPrices,
-    groupKey: string,
-    treatedPayments: TransactionFileData[]
-  ): void {
-    const { timestamp, hash, address, amount } = tx
-    const values = getTransactionValue(tx)
-    const value = Number(values[this.currency])
-    const rate = tx.prices.find(p => p.price.quoteId === QUOTE_IDS[this.currency.toUpperCase()])!.price.value
-    const buttonNames = this.extractButtonNamesFromGroupKey(groupKey)
-
-    treatedPayments.push({
-      amount,
-      value,
-      date: moment.tz(timestamp * 1000, this.timezone),
-      transactionId: hash,
-      rate,
-      currency: this.currency,
-      address: address.address,
-      notes: buttonNames,
-      newtworkId: address.networkId
-    } as TransactionFileData)
-  }
+  // Use top-level addSingleTransactionToResults to keep a single source of truth.

Also applies to: 156-178


324-324: Remove unused variable.

timestamp is never used.

-    const { timestamp } = tx

205-208: Group-key parsing via split('_') is brittle.

If button names contain _, parsing yields incorrect notes. Prefer structured encoding (e.g., base64(JSON)) or at least encode names.

If you opt for minimal change now, switch to encodeURIComponent for names in generateGroupKey and decodeURIComponent in the extractors, or adopt a structured key in a follow-up.

Also applies to: 297-298

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0015825 and de45d6b.

📒 Files selected for processing (5)
  • .github/coding-instructions.md (1 hunks)
  • package.json (1 hunks)
  • services/triggerService.ts (1 hunks)
  • tests/unittests/triggerService.test.ts (1 hunks)
  • utils/files.ts (2 hunks)
✅ Files skipped from review due to trivial changes (1)
  • package.json
🧰 Additional context used
🧬 Code graph analysis (3)
services/triggerService.ts (1)
utils/validators.ts (1)
  • parseTriggerPostData (261-287)
tests/unittests/triggerService.test.ts (2)
utils/validators.ts (1)
  • parseTriggerPostData (261-287)
prisma-local/mockedClient.ts (1)
  • prismaMock (19-19)
utils/files.ts (2)
services/transactionService.ts (2)
  • TransactionsWithPaybuttonsAndPrices (134-134)
  • getTransactionValue (15-32)
constants/index.ts (4)
  • SupportedQuotesType (163-163)
  • QUOTE_IDS (205-205)
  • DEFAULT_MULTI_VALUES_LINE_LABEL (266-266)
  • RESPONSE_MESSAGES (8-107)
🪛 ESLint
utils/files.ts

[error] 125-125: Missing space before function parentheses.

(@typescript-eslint/space-before-function-paren)


[error] 130-130: Missing space before function parentheses.

(@typescript-eslint/space-before-function-paren)


[error] 137-137: Missing space before function parentheses.

(@typescript-eslint/space-before-function-paren)


[error] 143-143: Missing space before function parentheses.

(@typescript-eslint/space-before-function-paren)


[error] 156-156: Missing space before function parentheses.

(@typescript-eslint/space-before-function-paren)


[error] 180-180: Missing space before function parentheses.

(@typescript-eslint/space-before-function-paren)


[error] 205-205: Missing space before function parentheses.

(@typescript-eslint/space-before-function-paren)


[error] 209-209: Missing space before function parentheses.

(@typescript-eslint/space-before-function-paren)


[error] 212-212: Trailing spaces not allowed.

(no-trailing-spaces)


[error] 225-225: Missing space before function parentheses.

(@typescript-eslint/space-before-function-paren)


[error] 324-324: 'timestamp' is assigned a value but never used.

(@typescript-eslint/no-unused-vars)

🪛 markdownlint-cli2 (0.17.2)
.github/coding-instructions.md

12-12: Unordered list indentation
Expected: 0; Actual: 1

(MD007, ul-indent)

🔇 Additional comments (2)
utils/files.ts (2)

336-338: Flushing all groups on a big tx may split unrelated groups; flush only the current group.

This can prematurely flush other active groups and fragment later collapses.

-      groupManager.processAllGroups(treatedPayments)
+      groupManager.processGroup(groupKey, treatedPayments)
       addSingleTransactionToResults(tx, groupKey, currency, treatedPayments, timezone)

Please confirm expected ordering vs prior behavior; if ordering across groups must be preserved, we may need an ordered queue rather than global flushes.


125-125: Lint: fix spacing before parens and trailing whitespace.

ESLint flags space-before-function-paren and trailing-space issues in utils/files.ts — fix at lines: 125, 130, 137, 143, 156, 180, 205, 209, 212, 225. Auto-fix couldn't be run here (pnpm/npm/yarn reported no "lint" script); run your linter locally (e.g. npx eslint --fix utils/files.ts or add a lint script) and re-run checks.

Comment on lines 19 to 54
describe('Trigger JSON Validation Unit Tests', () => {
describe('Trigger Creation with JSON Validation', () => {
it('should reject trigger creation with invalid JSON during validation', async () => {
const invalidPostData = '{"amount": <amount>, "currency": <currency>'

mockedParseTriggerPostData.mockImplementation(() => {
throw new SyntaxError('Unexpected end of JSON input')
})

expect(() => {
parseTriggerPostData({
userId: 'test-user',
postData: invalidPostData,
postDataParameters: {} as any
})
}).toThrow('Unexpected end of JSON input')

expect(mockedParseTriggerPostData).toHaveBeenCalled()
})

it('should allow trigger creation with valid JSON', async () => {
const validPostData = '{"amount": <amount>, "currency": <currency>}'
const expectedParsedData = { amount: 100, currency: 'XEC' }

mockedParseTriggerPostData.mockReturnValue(expectedParsedData)

const result = parseTriggerPostData({
userId: 'test-user',
postData: validPostData,
postDataParameters: {} as any
})

expect(result).toEqual(expectedParsedData)
expect(mockedParseTriggerPostData).toHaveBeenCalled()
})
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

These tests mock and then “test” the mock, not the service.

You’re asserting behavior of your own mocked parseTriggerPostData; no service code is exercised. Rework to call the function that uses parsing (e.g., postDataForTrigger) and assert DB log + network side effects.

Recommended approach:

  • Re-export postDataForTrigger for tests (e.g., export const __testOnly__ = { postDataForTrigger }), or inject it.
  • Assert prisma.triggerLog.create and axios.post calls per path.

Example (after re-export):

- expect(() => { parseTriggerPostData(...)}).toThrow(...)
+ const trigger = { id:'t1', isEmailTrigger:false, postURL:'https://example.com', postData:'{}', paybutton:{ name:'btn', providerUserId:'u1' } } as any
+ mockedParseTriggerPostData.mockImplementation(() => { throw new Error('Invalid JSON') })
+ await expect(__testOnly__.postDataForTrigger(trigger, {} as any)).resolves.toBeUndefined()
+ expect(prismaMock.triggerLog.create).toHaveBeenCalledWith(expect.objectContaining({ data: expect.objectContaining({ isError: true }) }))
+ expect(mockedAxios.post).not.toHaveBeenCalled()

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In tests/unittests/triggerService.test.ts around lines 19 to 54, the current
tests only exercise the mocked parseTriggerPostData and assert the mock rather
than the service behavior; change them to call the actual service function that
uses parsing (postDataForTrigger) so side effects are tested. Re-export or
otherwise expose postDataForTrigger (e.g., export __testOnly__ = {
postDataForTrigger }) or inject the parser into the service, then in the tests
mock parseTriggerPostData only as an input dependency, call postDataForTrigger
with the same payloads, and assert the expected side effects: that
prisma.triggerLog.create was invoked with the correct args and that axios.post
was called for the success path (or not called on parse error). Remove
assertions that directly test mockedParseTriggerPostData and instead assert
service-level outcomes (DB log + network calls and thrown errors).

Comment on lines 176 to 177
newtworkId: address.networkId
} as TransactionFileData)
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

🧩 Analysis chain

Fix typo: newtworkId → networkId (propagate consistently).

The misspelling leaks into output and helper usage; standardize to networkId.

Apply in these spots:

-      newtworkId: address.networkId
+      networkId: address.networkId
-      newtworkId: tempTxGroup[0].address.networkId,
+      networkId: tempTxGroup[0].address.networkId,
-    newtworkId: address.networkId
+    networkId: address.networkId

Then update:

  • TransactionFileData interface (newtworkIdnetworkId)
  • formatPaybuttonTransactionsFileData (read networkId)
  • Any other references.

Run to locate remaining occurrences:

Also applies to: 200-201, 309-309


🏁 Script executed:

#!/bin/bash
rg -n --color=never '\bnewtworkId\b' -S -C2

Length of output: 1304


Fix typo: rename newtworkId → networkId and propagate across utils/files.ts

Rename the interface property and all usages (TransactionFileData, formatPaybuttonTransactionsFileData, callers). Replace occurrences like:

-      newtworkId: address.networkId
+      networkId: address.networkId
-      newtworkId: tempTxGroup[0].address.networkId,
+      networkId: tempTxGroup[0].address.networkId,
-    newtworkId: address.networkId
+    networkId: address.networkId

Occurrences to update (from grep): utils/files.ts lines 30, 62, 64, 176, 200, 308, 399. Also update any exported types/serializers that read/write this field and adjust callers/tests that rely on the old name.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
newtworkId: address.networkId
} as TransactionFileData)
networkId: address.networkId
} as TransactionFileData)
🤖 Prompt for AI Agents
In utils/files.ts around lines 176-177 (and also update lines 30, 62, 64, 200,
308, 399), the TransactionFileData property is misspelled as "newtworkId" —
rename it to "networkId" across the file: update the interface/type declaration,
all references in formatPaybuttonTransactionsFileData and callers, plus any
serializers/deserializers or exported types that read/write this field; run a
project-wide replace for "newtworkId"→"networkId", update affected tests/call
sites to use the new name, and ensure any JSON (de)serialization keys are
adjusted accordingly so runtime reads/writes and typings stay consistent.

@lissavxo lissavxo force-pushed the feat/update-payment-id branch from de45d6b to cd36580 Compare September 16, 2025 18:40
Copy link
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: 3

♻️ Duplicate comments (3)
services/chronikService.ts (3)

484-487: Call the correct method and pass the address for cross-address safety.

this.updateClientPaymentStatus was not defined (method was named differently) and lacked the address guard. Update the call to use the class method with address.

-          const { amount, opReturn } = addressWithTransaction.transaction
-          await this.updateClientPaymentStatus(amount, opReturn, 'CONFIRMED' as ClientPaymentStatus)
+          const { amount, opReturn } = addressWithTransaction.transaction
+          await this.updateClientPaymentStatus(
+            amount as Prisma.Decimal,
+            opReturn,
+            'CONFIRMED' as ClientPaymentStatus,
+            addressWithTransaction.address.address
+          )

504-506: Same fix for mempool path + rely on callee to prevent downgrades.

Pass the address and let the updater guard against downgrades.

-            const { amount, opReturn } = addressWithTransaction.transaction
-            const newClientPaymentStatus = 'ADDED_TO_MEMPOOL' as ClientPaymentStatus
-            await this.updateClientPaymentStatus(amount, opReturn, newClientPaymentStatus)
+            const { amount, opReturn } = addressWithTransaction.transaction
+            await this.updateClientPaymentStatus(
+              amount as Prisma.Decimal,
+              opReturn,
+              'ADDED_TO_MEMPOOL' as ClientPaymentStatus,
+              addressWithTransaction.address.address
+            )

449-461: Harden payment-status updater: fix decimals, validation, downgrades, and address check.

  • Use Prisma.Decimal.equals, not Number coercion.
  • Validate paymentId before DB calls; ignore unknown IDs.
  • Prevent downgrades (CONFIRMED → ADDED_TO_MEMPOOL).
  • Ensure tx address matches the ClientPayment’s address.
  • Rename method to match call sites and use aliased DB updater.

Apply:

-  private async handleUpdateClientPaymentStatus (txAmount: string | number | Prisma.Decimal | DecimalJsLike, opReturn: string | undefined, status: ClientPaymentStatus): Promise<void> {
-    const parsedOpReturn = parseOpReturnData(opReturn ?? '')
-    const paymentId = parsedOpReturn.paymentId
-
-    const clientPayment = await getClientPayment(paymentId)
-    if (clientPayment.amount !== null) {
-      if (Number(clientPayment.amount) === Number(txAmount)) {
-        await updateClientPaymentStatus(paymentId, status)
-      }
-    } else {
-      await updateClientPaymentStatus(paymentId, status)
-    }
-  }
+  private async updateClientPaymentStatus(
+    txAmount: string | number | Prisma.Decimal | DecimalJsLike,
+    opReturn: string | undefined,
+    status: ClientPaymentStatus,
+    address?: string
+  ): Promise<void> {
+    // opReturn is stored as JSON from getNullDataScriptData()
+    let parsed: any
+    try {
+      parsed = JSON.parse(opReturn ?? '')
+    } catch {
+      return
+    }
+    const paymentId: string = typeof parsed?.paymentId === 'string' ? parsed.paymentId : ''
+    if (!/^[0-9a-fA-F]{32}$/.test(paymentId)) return
+
+    let clientPayment
+    try {
+      clientPayment = await getClientPayment(paymentId)
+    } catch {
+      return // unknown paymentId
+    }
+    if (address && clientPayment.address.address !== address) return
+    if (clientPayment.status === 'CONFIRMED' && status !== 'CONFIRMED') return
+    if (clientPayment.status === status) return
+
+    if (clientPayment.amount !== null) {
+      const txDec = new Prisma.Decimal(txAmount as any)
+      if (!clientPayment.amount.equals(txDec)) return
+    }
+    await updateClientPaymentStatusDb(paymentId, status)
+  }
🧹 Nitpick comments (2)
services/chronikService.ts (2)

482-487: Optional: de-dupe repeated paymentId updates per tx.

If the same paymentId appears on multiple related addresses for one tx, you can avoid redundant DB calls with a Set.

-        for (const addressWithTransaction of addressesWithTransactions) {
+        const seen = new Set<string>()
+        for (const addressWithTransaction of addressesWithTransactions) {
           const { amount, opReturn } = addressWithTransaction.transaction
-          await this.updateClientPaymentStatus(
+          const parsed = (() => { try { return JSON.parse(opReturn ?? '') } catch { return {} } })()
+          const pid = typeof parsed?.paymentId === 'string' ? parsed.paymentId : ''
+          if (pid && !seen.has(pid)) seen.add(pid); else continue
+          await this.updateClientPaymentStatus(
             amount as Prisma.Decimal,
             opReturn,
             'CONFIRMED' as ClientPaymentStatus,
             addressWithTransaction.address.address
           )
         }

449-461: Minor: prefer direct JSON.parse over parseOpReturnData for opReturn.

opReturn here is the JSON emitted by getNullDataScriptData; using JSON.parse avoids the lenient KV/array fallbacks.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between de45d6b and cd36580.

📒 Files selected for processing (7)
  • pages/api/payments/paymentId/index.ts (1 hunks)
  • prisma-local/migrations/20250912183436_client_payment/migration.sql (1 hunks)
  • prisma-local/migrations/migration_lock.toml (1 hunks)
  • prisma-local/schema.prisma (2 hunks)
  • services/chronikService.ts (5 hunks)
  • services/transactionService.ts (3 hunks)
  • utils/validators.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • prisma-local/migrations/20250912183436_client_payment/migration.sql
  • utils/validators.ts
  • prisma-local/schema.prisma
  • prisma-local/migrations/migration_lock.toml
  • services/transactionService.ts
  • pages/api/payments/paymentId/index.ts
🧰 Additional context used
🧬 Code graph analysis (1)
services/chronikService.ts (2)
utils/validators.ts (1)
  • parseOpReturnData (436-464)
services/transactionService.ts (1)
  • getClientPayment (1012-1017)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run Tests
🔇 Additional comments (2)
services/chronikService.ts (2)

18-18: LGTM — needed types are imported.


33-33: LGTM — DecimalJsLike import is appropriate.

@lissavxo lissavxo force-pushed the feat/update-payment-id branch from cd36580 to d0d25cd Compare September 22, 2025 16:24
Copy link
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: 0

🧹 Nitpick comments (1)
services/chronikService.ts (1)

14-17: Alias DB updater to avoid confusion.

Alias the imported DB function to make call sites explicit and avoid confusion with class helpers.

   connectAllTransactionsToPrices,
-  updateClientPaymentStatus,
+  updateClientPaymentStatus as updateClientPaymentStatusDb,
   getClientPayment
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d0d25cd and 5f8fe19.

📒 Files selected for processing (1)
  • services/chronikService.ts (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
services/chronikService.ts (2)
utils/validators.ts (1)
  • parseOpReturnData (436-464)
services/transactionService.ts (1)
  • getClientPayment (1012-1017)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run Tests
🔇 Additional comments (6)
services/chronikService.ts (6)

18-18: LGTM on Prisma/Status imports.


33-34: LGTM on DecimalJsLike import (used in helper signature).


467-471: Good: precomputes input/output addresses once per tx (fixes prior undefined).

This addresses the previous “undefined outputAddresses” issue and reduces repeated work.


449-461: Harden payment status updates: validate paymentId, compare Decimals correctly, guard downgrades, verify address, and handle unknown IDs.

  • Validate paymentId (32 hex) before DB access.
  • Wrap getClientPayment in try/catch; ignore unknown IDs instead of throwing.
  • Use Prisma.Decimal.equals() for amount comparison (avoid Number() coercion).
  • Prevent downgrades (skip if current is CONFIRMED and new is mempool).
  • Skip redundant updates (same status).
  • Ensure tx address matches ClientPayment.address to prevent cross‑address updates.
  • Call the aliased DB updater.
-  private async handleUpdateClientPaymentStatus (txAmount: string | number | Prisma.Decimal | DecimalJsLike, opReturn: string | undefined, status: ClientPaymentStatus): Promise<void> {
-    const parsedOpReturn = parseOpReturnData(opReturn ?? '')
-    const paymentId = parsedOpReturn.paymentId
-
-    const clientPayment = await getClientPayment(paymentId)
-    if (clientPayment.amount !== null) {
-      if (Number(clientPayment.amount) === Number(txAmount)) {
-        await updateClientPaymentStatus(paymentId, status)
-      }
-    } else {
-      await updateClientPaymentStatus(paymentId, status)
-    }
-  }
+  private async handleUpdateClientPaymentStatus (
+    txAmount: string | number | Prisma.Decimal | DecimalJsLike,
+    opReturn: string | undefined,
+    status: ClientPaymentStatus,
+    txAddress: string
+  ): Promise<void> {
+    const parsed = parseOpReturnData(opReturn ?? '') as any
+    const rawPaymentId = typeof parsed?.paymentId === 'string' ? parsed.paymentId : ''
+    const paymentId = rawPaymentId.replace(/-/g, '').toLowerCase()
+    if (!/^[0-9a-f]{32}$/.test(paymentId)) return
+    try {
+      const clientPayment = await getClientPayment(paymentId)
+      // Guard wrong address, downgrades, and redundant updates
+      if (clientPayment.address.address !== txAddress) return
+      if (clientPayment.status === 'CONFIRMED' && status !== 'CONFIRMED') return
+      if (clientPayment.status === status) return
+      if (clientPayment.amount !== null) {
+        const txDec = txAmount instanceof Prisma.Decimal ? txAmount : new Prisma.Decimal(txAmount as any)
+        if (clientPayment.amount.equals(txDec)) {
+          await updateClientPaymentStatusDb(paymentId, status)
+        }
+      } else {
+        await updateClientPaymentStatusDb(paymentId, status)
+      }
+    } catch {
+      // Unknown paymentId — ignore
+      return
+    }
+  }

486-489: Pass tx address into helper; optional: dedupe by paymentId per tx.

  • Provide the tx address so the helper can verify it matches the ClientPayment address.
  • (Optional) Consider deduping by paymentId within this block to avoid duplicate updates across multiple related addresses.
-        for (const addressWithTransaction of addressesWithTransactions) {
-          const { amount, opReturn } = addressWithTransaction.transaction
-          await this.handleUpdateClientPaymentStatus(amount, opReturn, 'CONFIRMED' as ClientPaymentStatus)
-        }
+        for (const addressWithTransaction of addressesWithTransactions) {
+          const { amount, opReturn } = addressWithTransaction.transaction
+          await this.handleUpdateClientPaymentStatus(
+            amount,
+            opReturn,
+            'CONFIRMED' as ClientPaymentStatus,
+            addressWithTransaction.address.address
+          )
+        }

506-509: Same fix for mempool: pass tx address; downgrades are guarded in helper.

-            const { amount, opReturn } = addressWithTransaction.transaction
-            const newClientPaymentStatus = 'ADDED_TO_MEMPOOL' as ClientPaymentStatus
-            await this.handleUpdateClientPaymentStatus(amount, opReturn, newClientPaymentStatus)
+            const { amount, opReturn } = addressWithTransaction.transaction
+            const newClientPaymentStatus = 'ADDED_TO_MEMPOOL' as ClientPaymentStatus
+            await this.handleUpdateClientPaymentStatus(
+              amount,
+              opReturn,
+              newClientPaymentStatus,
+              addressWithTransaction.address.address
+            )

@lissavxo lissavxo force-pushed the feat/update-payment-id branch from 5f8fe19 to 23bf29f Compare September 23, 2025 16:04
Copy link
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: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5f8fe19 and 23bf29f.

📒 Files selected for processing (2)
  • services/chronikService.ts (5 hunks)
  • services/transactionService.ts (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
services/chronikService.ts (2)
utils/validators.ts (1)
  • parseOpReturnData (436-464)
services/transactionService.ts (2)
  • getClientPayment (1012-1017)
  • updateClientPaymentStatus (1005-1010)
services/transactionService.ts (3)
constants/index.ts (1)
  • NETWORK_IDS_FROM_SLUGS (131-134)
services/addressService.ts (1)
  • addressExists (127-141)
services/chronikService.ts (1)
  • multiBlockchainClient (914-914)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run Tests
🔇 Additional comments (10)
services/transactionService.ts (4)

2-3: Imports look fine.

Types/constants import aligns with new usage.


1005-1010: Updater is OK as a thin DAO.

Idempotency and downgrade prevention are handled by callers; this is fine.


1012-1017: Getter looks good.

Appropriate include of address for downstream checks.


970-1003: Normalize/validate address and guard unknown network in generatePaymentId; use canonical address; avoid cycle via lazy import.

Prevents subtle bugs and bad DB writes; also avoids cross‑module init hazards.

Apply:

 export const generatePaymentId = async (address: string, amount?: Prisma.Decimal): Promise<string> => {
   const rawUUID = uuidv4()
   const cleanUUID = rawUUID.replace(/-/g, '')
   const status = 'PENDING' as ClientPaymentStatus
-  const prefix = address.split(':')[0].toLowerCase()
-  const networkId = NETWORK_IDS_FROM_SLUGS[prefix]
-  const isAddressRegistered = await addressExists(address)
+  const normalizedAddress = parseAddress(address)
+  const prefix = normalizedAddress.split(':')[0].toLowerCase()
+  const networkId = NETWORK_IDS_FROM_SLUGS[prefix]
+  if (networkId === undefined) {
+    throw new Error(RESPONSE_MESSAGES.INVALID_NETWORK_SLUG_400.message)
+  }
+  const isAddressRegistered = await addressExists(normalizedAddress)
 
   const clientPayment = await prisma.clientPayment.create({
     data: {
       address: {
         connectOrCreate: {
-          where: { address },
+          where: { address: normalizedAddress },
           create: {
-            address,
+            address: normalizedAddress,
             networkId
           }
         }
       },
       paymentId: cleanUUID,
       status,
       amount
     },
     include: {
       address: true
     }
   })
 
-  if (!isAddressRegistered) {
-    void multiBlockchainClient.syncAndSubscribeAddresses([clientPayment.address])
-  }
+  if (!isAddressRegistered) {
+    // Lazy import to avoid circular dependency with chronikService
+    void import('services/chronikService')
+      .then(({ multiBlockchainClient }) => multiBlockchainClient.syncAndSubscribeAddresses([clientPayment.address]))
+      .catch(() => {})
+  }
 
   return clientPayment.paymentId
 }
services/chronikService.ts (6)

14-17: Import usage is coherent now.

Using service-level update/get avoids earlier name collision issues.


33-34: OK to import DecimalJsLike.

Type matches the helper signature.


467-471: Good: precomputes inputs/outputs once per tx.

Fixes previous undefined outputAddresses issue and reduces duplicate work.


506-509: Same fix for mempool path; pass tx address.

Also relies on handle() to avoid downgrades/duplicates.

Apply:

-            const { amount, opReturn } = addressWithTransaction.transaction
-            const newClientPaymentStatus = 'ADDED_TO_MEMPOOL' as ClientPaymentStatus
-            await this.handleUpdateClientPaymentStatus(amount, opReturn, newClientPaymentStatus)
+            const { amount, opReturn } = addressWithTransaction.transaction
+            const newClientPaymentStatus = 'ADDED_TO_MEMPOOL' as ClientPaymentStatus
+            await this.handleUpdateClientPaymentStatus(
+              amount,
+              opReturn,
+              newClientPaymentStatus,
+              addressWithTransaction.address.address
+            )

486-489: Pass tx address into validator.

Needed to prevent cross‑address updates per the new handle signature.

Apply:

-          const { amount, opReturn } = addressWithTransaction.transaction
-          await this.handleUpdateClientPaymentStatus(amount, opReturn, 'CONFIRMED' as ClientPaymentStatus)
+          const { amount, opReturn } = addressWithTransaction.transaction
+          await this.handleUpdateClientPaymentStatus(
+            amount,
+            opReturn,
+            'CONFIRMED' as ClientPaymentStatus,
+            addressWithTransaction.address.address
+          )

449-461: Harden client payment updates: validate paymentId, compare Decimals correctly, ensure address matches, and prevent downgrades.

Current code can throw on missing/invalid paymentId, mis-update across addresses, and compares Decimals via Number(). Add guards and proper Decimal equality; also avoid downgrading CONFIRMED to ADDED_TO_MEMPOOL and skip no-ops.

Apply:

-  private async handleUpdateClientPaymentStatus (txAmount: string | number | Prisma.Decimal | DecimalJsLike, opReturn: string | undefined, status: ClientPaymentStatus): Promise<void> {
-    const parsedOpReturn = parseOpReturnData(opReturn ?? '')
-    const paymentId = parsedOpReturn.paymentId
-
-    const clientPayment = await getClientPayment(paymentId)
-    if (clientPayment.amount !== null) {
-      if (Number(clientPayment.amount) === Number(txAmount)) {
-        await updateClientPaymentStatus(paymentId, status)
-      }
-    } else {
-      await updateClientPaymentStatus(paymentId, status)
-    }
-  }
+  private async handleUpdateClientPaymentStatus (
+    txAmount: string | number | Prisma.Decimal | DecimalJsLike,
+    opReturn: string | undefined,
+    status: ClientPaymentStatus,
+    txAddress: string
+  ): Promise<void> {
+    const parsed = parseOpReturnData(opReturn ?? '')
+    const paymentIdRaw = typeof parsed?.paymentId === 'string' ? parsed.paymentId : ''
+    const paymentId = paymentIdRaw.replace(/-/g, '').toLowerCase()
+    if (!/^[0-9a-f]{32}$/.test(paymentId)) return
+    try {
+      const clientPayment = await getClientPayment(paymentId)
+      // Avoid cross-address updates and downgrades/no-ops
+      if (clientPayment.address.address !== txAddress) return
+      if (clientPayment.status === status) return
+      if (clientPayment.status === 'CONFIRMED' && status !== 'CONFIRMED') return
+      if (clientPayment.amount !== null) {
+        const txDec = new Prisma.Decimal(txAmount as any)
+        if (!clientPayment.amount.equals(txDec)) return
+      }
+      await updateClientPaymentStatus(paymentId, status)
+    } catch {
+      // Unknown/malformed paymentId — ignore
+    }
+  }

Comment on lines +12 to 14
import { v4 as uuidv4 } from 'uuid'
import { multiBlockchainClient } from 'services/chronikService'

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Break circular dependency with chronikService.

Top-level import of multiBlockchainClient creates a cycle (chronikService → transactionService → chronikService) that can yield undefined bindings at runtime. Lazy import inside the function avoids it.

Apply:

-import { v4 as uuidv4 } from 'uuid'
-import { multiBlockchainClient } from 'services/chronikService'
+import { v4 as uuidv4 } from 'uuid'

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In services/transactionService.ts around lines 12 to 14, the top-level import of
multiBlockchainClient creates a circular dependency with chronikService; remove
the top-level import and perform a lazy/dynamic import of multiBlockchainClient
inside the function(s) that use it (e.g., call const { multiBlockchainClient } =
await import('services/chronikService') or use require(...) if not in an async
context) so the module is resolved at call-time and avoids undefined bindings at
runtime; update all call sites in this file to use the locally-imported
multiBlockchainClient instance.

Copy link
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: 3

🧹 Nitpick comments (4)
services/transactionService.ts (1)

976-976: Consider using a database transaction for atomicity

The function checks addressExists outside the Prisma create operation, which could lead to race conditions if the address is created between the check and the create operation.

Consider wrapping the entire operation in a transaction to ensure atomicity:

export const generatePaymentId = async (address: string, amount?: Prisma.Decimal): Promise<string> => {
  const rawUUID = uuidv4()
  const cleanUUID = rawUUID.replace(/-/g, '')
  const normalizedAddress = parseAddress(address)
  const prefix = normalizedAddress.split(':')[0].toLowerCase()
  const networkId = NETWORK_IDS_FROM_SLUGS[prefix]
  
  if (networkId === undefined) {
    throw new Error(RESPONSE_MESSAGES.INVALID_NETWORK_SLUG_400.message)
  }

  return await prisma.$transaction(async (tx) => {
    const isAddressRegistered = await tx.address.findUnique({
      where: { address: normalizedAddress }
    }) !== null

    const clientPayment = await tx.clientPayment.create({
      data: {
        address: {
          connectOrCreate: {
            where: { address: normalizedAddress },
            create: {
              address: normalizedAddress,
              networkId
            }
          }
        },
        paymentId: cleanUUID,
        status: 'PENDING',
        amount
      },
      include: {
        address: true
      }
    })

    if (!isAddressRegistered) {
      const { multiBlockchainClient } = await import('services/chronikService')
      void multiBlockchainClient.syncAndSubscribeAddresses([clientPayment.address])
    }

    return clientPayment.paymentId
  })
}

Also applies to: 991-991

services/chronikService.ts (3)

18-18: Consider importing Prisma module more precisely

Instead of importing the entire Prisma namespace, consider importing only what you need.

-import { Address, Prisma, ClientPaymentStatus } from '@prisma/client'
+import { Address, ClientPaymentStatus } from '@prisma/client'
+import type { Prisma } from '@prisma/client'

33-33: Remove unused import

The DecimalJsLike type appears to be imported but not actually used in a meaningful way in the code.

-import { DecimalJsLike } from '@prisma/client/runtime/library'

449-464: Add logging for better observability

Consider adding structured logging to track client payment status updates for debugging and monitoring.

private async handleUpdateClientPaymentStatus (...) {
  // ... existing code ...
  
  console.log(`[CLIENT_PAYMENT] Updating payment ${paymentId} status from ${clientPayment.status} to ${status}`)
  await updateClientPaymentStatus(paymentId, status)
  console.log(`[CLIENT_PAYMENT] Successfully updated payment ${paymentId} to ${status}`)
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 23bf29f and ecf0a65.

📒 Files selected for processing (2)
  • services/chronikService.ts (5 hunks)
  • services/transactionService.ts (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
services/transactionService.ts (3)
constants/index.ts (1)
  • NETWORK_IDS_FROM_SLUGS (131-134)
services/addressService.ts (1)
  • addressExists (127-141)
services/chronikService.ts (1)
  • multiBlockchainClient (917-917)
services/chronikService.ts (2)
utils/validators.ts (1)
  • parseOpReturnData (436-464)
services/transactionService.ts (2)
  • getClientPayment (1012-1017)
  • updateClientPaymentStatus (1005-1010)
🔇 Additional comments (4)
services/chronikService.ts (2)

470-474: Good improvement: Computing addresses early

Computing addressesWithTransactions and helper addresses before the conditional branches ensures they're available for all message types.


510-512: Apply the same address validation for mempool transactions

The mempool path should also validate addresses before updating payment status.

Update the mempool call to include address validation:

 const { amount, opReturn } = addressWithTransaction.transaction
 const newClientPaymentStatus = 'ADDED_TO_MEMPOOL' as ClientPaymentStatus
-await this.handleUpdateClientPaymentStatus(amount, opReturn, newClientPaymentStatus)
+await this.handleUpdateClientPaymentStatus(
+  amount, 
+  opReturn, 
+  newClientPaymentStatus,
+  addressWithTransaction.address.address
+)
services/transactionService.ts (2)

970-1003: Validate and normalize address; guard unknown network; avoid subtle bugs in generatePaymentId

The function should validate and normalize the input address before DB operations to prevent issues with different address formats and invalid networks.

Apply:

 export const generatePaymentId = async (address: string, amount?: Prisma.Decimal): Promise<string> => {
   const rawUUID = uuidv4()
   const cleanUUID = rawUUID.replace(/-/g, '')
   const status = 'PENDING' as ClientPaymentStatus
-  const prefix = address.split(':')[0].toLowerCase()
-  const networkId = NETWORK_IDS_FROM_SLUGS[prefix]
-  const isAddressRegistered = await addressExists(address)
+  const normalizedAddress = parseAddress(address)
+  const prefix = normalizedAddress.split(':')[0].toLowerCase()
+  const networkId = NETWORK_IDS_FROM_SLUGS[prefix]
+  if (networkId === undefined) {
+    throw new Error(RESPONSE_MESSAGES.INVALID_NETWORK_SLUG_400.message)
+  }
+  const isAddressRegistered = await addressExists(normalizedAddress)
 
   const clientPayment = await prisma.clientPayment.create({
     data: {
       address: {
         connectOrCreate: {
-          where: { address },
+          where: { address: normalizedAddress },
           create: {
-            address,
+            address: normalizedAddress,
             networkId
           }
         }
       },
       paymentId: cleanUUID,
       status,
       amount
     },
     include: {
       address: true
     }
   })
 
   if (!isAddressRegistered) {
     void multiBlockchainClient.syncAndSubscribeAddresses([clientPayment.address])
   }
 
   return clientPayment.paymentId
 }

12-13: Break circular dependency with chronikService

The top-level import of multiBlockchainClient creates a circular dependency (chronikService → transactionService → chronikService) that can yield undefined bindings at runtime.

Consider lazy importing multiBlockchainClient inside the function that uses it:

 import { v4 as uuidv4 } from 'uuid'
-import { multiBlockchainClient } from 'services/chronikService'

Then in generatePaymentId:

   if (!isAddressRegistered) {
+    const { multiBlockchainClient } = await import('services/chronikService')
     void multiBlockchainClient.syncAndSubscribeAddresses([clientPayment.address])
   }

Comment on lines +1004 to +1020

export const updateClientPaymentStatus = async (paymentId: string, status: ClientPaymentStatus): Promise<void> => {
await prisma.clientPayment.update({
where: { paymentId },
data: { status }
})
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add error handling for non-existent payment IDs

The updateClientPaymentStatus function should handle cases where the payment ID doesn't exist.

 export const updateClientPaymentStatus = async (paymentId: string, status: ClientPaymentStatus): Promise<void> => {
-  await prisma.clientPayment.update({
-    where: { paymentId },
-    data: { status }
-  })
+  try {
+    await prisma.clientPayment.update({
+      where: { paymentId },
+      data: { status }
+    })
+  } catch (error: any) {
+    if (error.code === 'P2025') {
+      console.warn(`Client payment not found: ${paymentId}`)
+    } else {
+      throw error
+    }
+  }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const updateClientPaymentStatus = async (paymentId: string, status: ClientPaymentStatus): Promise<void> => {
await prisma.clientPayment.update({
where: { paymentId },
data: { status }
})
}
export const updateClientPaymentStatus = async (paymentId: string, status: ClientPaymentStatus): Promise<void> => {
try {
await prisma.clientPayment.update({
where: { paymentId },
data: { status }
})
} catch (error: any) {
if (error.code === 'P2025') {
console.warn(`Client payment not found: ${paymentId}`)
} else {
throw error
}
}
}
🤖 Prompt for AI Agents
In services/transactionService.ts around lines 1004–1010, the
updateClientPaymentStatus currently calls prisma.clientPayment.update without
handling the case where paymentId doesn't exist; wrap the update in a try/catch
(or use prisma.clientPayment.updateMany and check the returned count) and handle
the "not found" case explicitly — on Prisma's P2025 error (or when count === 0)
either throw a clear NotFoundError or return a controlled result, and ensure the
function signature and callers are adjusted to expect/propagate that error.

Copy link
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: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ecf0a65 and 960ce82.

📒 Files selected for processing (1)
  • pages/api/payments/paymentId/index.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
pages/api/payments/paymentId/index.ts (4)
utils/index.ts (1)
  • runMiddleware (137-151)
utils/validators.ts (2)
  • parseCreatePaymentIdPOSTRequest (578-590)
  • parseAddress (31-48)
services/transactionService.ts (1)
  • generatePaymentId (970-1003)
constants/index.ts (1)
  • RESPONSE_MESSAGES (8-107)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run Tests
🔇 Additional comments (1)
pages/api/payments/paymentId/index.ts (1)

1-21: Construct a Prisma.Decimal before invoking generatePaymentId.

Casting values.amount to Decimal leaves it as a string at runtime, so we're still passing a string into generatePaymentId, which expects a Prisma.Decimal. That breaks the function contract and can surface subtle precision/validation issues once the service uses decimal-specific operations. This was already called out in the prior review and still needs to be addressed. Import Prisma from @prisma/client and build a real Prisma.Decimal instead of type-casting.

-import { Decimal } from '@prisma/client/runtime/library'
+import { Prisma } from '@prisma/client'
 ...
-      const amount = values.amount as Decimal | undefined
+      const amount = values.amount !== undefined ? new Prisma.Decimal(values.amount) : undefined

Copy link
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: 1

🧹 Nitpick comments (3)
pages/api/payments/paymentId/index.ts (3)

13-13: Type the API handler with NextApiRequest/NextApiResponse.

Improves type safety and DX.

-export default async (req: any, res: any): Promise<void> => {
+export default async function handler(req: NextApiRequest, res: NextApiResponse): Promise<void> {

Add this import near the top of the file:

import type { NextApiRequest, NextApiResponse } from 'next'

14-15: Optionally short‑circuit OPTIONS preflight.

In some Next setups, this avoids falling through when CORS doesn’t end the response.

   await runMiddleware(req, res, cors)
+  if (req.method === 'OPTIONS') {
+    res.status(200).end()
+    return
+  }

9-11: Optional CORS tweak: optionsSuccessStatus for legacy agents.

Preflight 204 can trip some clients; 200 is safer.

 const cors = Cors({
-  methods: ['POST']
+  methods: ['POST'],
+  optionsSuccessStatus: 200
 })
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 960ce82 and a2ca940.

📒 Files selected for processing (1)
  • pages/api/payments/paymentId/index.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
pages/api/payments/paymentId/index.ts (4)
utils/index.ts (1)
  • runMiddleware (137-151)
utils/validators.ts (2)
  • parseCreatePaymentIdPOSTRequest (578-590)
  • parseAddress (31-48)
services/transactionService.ts (1)
  • generatePaymentId (970-1003)
constants/index.ts (1)
  • RESPONSE_MESSAGES (8-107)
🔇 Additional comments (2)
pages/api/payments/paymentId/index.ts (2)

1-1: Construct Prisma.Decimal instead of casting; import from '@prisma/client'.

Casting keeps a string at runtime. Build a Prisma.Decimal and import Prisma from the public entrypoint.

-import { Decimal } from '@prisma/client/runtime/library'
+import { Prisma } from '@prisma/client'
@@
-      const amount = values.amount as Decimal | undefined
+      const amount = values.amount !== undefined ? new Prisma.Decimal(values.amount) : undefined

Also applies to: 19-19


25-31: Return 400 for invalid addresses instead of 500.

Map INVALID_ADDRESS_400 explicitly to avoid misreporting as 500.

       switch (error.message) {
         case RESPONSE_MESSAGES.ADDRESS_NOT_PROVIDED_400.message:
           res.status(RESPONSE_MESSAGES.ADDRESS_NOT_PROVIDED_400.statusCode).json(RESPONSE_MESSAGES.ADDRESS_NOT_PROVIDED_400)
           break
+        case RESPONSE_MESSAGES.INVALID_ADDRESS_400.message:
+          res.status(RESPONSE_MESSAGES.INVALID_ADDRESS_400.statusCode).json(RESPONSE_MESSAGES.INVALID_ADDRESS_400)
+          break
         default:
           res.status(500).json({ statusCode: 500, message: error.message })

Comment on lines 34 to 36
res.status(RESPONSE_MESSAGES.METHOD_NOT_ALLOWED.statusCode)
.json(RESPONSE_MESSAGES.METHOD_NOT_ALLOWED)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Respond 405 (not 500) and include the Allow header for unsupported methods.

Current constant yields 500. Use a proper 405.

-    res.status(RESPONSE_MESSAGES.METHOD_NOT_ALLOWED.statusCode)
-      .json(RESPONSE_MESSAGES.METHOD_NOT_ALLOWED)
+    res.setHeader('Allow', ['POST'])
+    res.status(405).json({ statusCode: 405, message: 'Method not allowed.' })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
res.status(RESPONSE_MESSAGES.METHOD_NOT_ALLOWED.statusCode)
.json(RESPONSE_MESSAGES.METHOD_NOT_ALLOWED)
}
res.setHeader('Allow', ['POST'])
res.status(405).json({ statusCode: 405, message: 'Method not allowed.' })
}
🤖 Prompt for AI Agents
In pages/api/payments/paymentId/index.ts around lines 34 to 36, the handler
currently returns the wrong constant that yields a 500 for unsupported HTTP
methods; change it to respond with HTTP 405 (Method Not Allowed) and set the
Allow header to the list of supported methods (e.g., "GET, POST" or whichever
methods this endpoint accepts). Replace the current
RESPONSE_MESSAGES.METHOD_NOT_ALLOWED constant with a proper 405 response (or
construct res.status(405).json(...)) and add res.setHeader('Allow',
'<allowed-methods>') before sending the response so clients receive both the 405
status and the Allow header.

@PayButton PayButton deleted a comment from coderabbitai bot Oct 2, 2025
@PayButton PayButton deleted a comment from coderabbitai bot Oct 2, 2025
Copy link
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: 1

♻️ Duplicate comments (2)
services/chronikService.ts (2)

595-598: Update call site to pass transaction address.

The handleUpdateClientPaymentStatus call is missing the transaction address parameter needed for address verification (see method signature fix above).

Apply this diff:

 for (const addressWithTransaction of addressesWithTransactions) {
   const { amount, opReturn } = addressWithTransaction.transaction
-  await this.handleUpdateClientPaymentStatus(amount, opReturn, 'CONFIRMED' as ClientPaymentStatus)
+  await this.handleUpdateClientPaymentStatus(
+    amount,
+    opReturn,
+    'CONFIRMED' as ClientPaymentStatus,
+    addressWithTransaction.address.address
+  )
 }

557-572: Critical: Multiple safety and correctness issues in payment status update logic.

The method has several problems that can cause incorrect behavior:

  1. Missing paymentId validation: No format check for the 32-character hex UUID (line 559-560).
  2. Incorrect Decimal comparison: Using Number() comparison (line 564) will fail for Prisma Decimal objects and may lose precision.
  3. No address verification: The method doesn't verify that the transaction address matches the ClientPayment's registered address.
  4. Missing status downgrade prevention: No check to prevent downgrading from CONFIRMED to ADDED_TO_MEMPOOL.
  5. No error handling: Database operations can throw but aren't wrapped in try/catch.

Apply this diff to fix all issues:

-private async handleUpdateClientPaymentStatus (txAmount: string | number | Prisma.Decimal | DecimalJsLike, opReturn: string | undefined, status: ClientPaymentStatus): Promise<void> {
+private async handleUpdateClientPaymentStatus (
+  txAmount: string | number | Prisma.Decimal | DecimalJsLike,
+  opReturn: string | undefined,
+  status: ClientPaymentStatus,
+  txAddress: string
+): Promise<void> {
   const parsedOpReturn = parseOpReturnData(opReturn ?? '')
   const paymentId = parsedOpReturn.paymentId
-  if (paymentId !== undefined && paymentId !== '') {
-    const clientPayment = await getClientPayment(paymentId)
-    if (clientPayment !== null) {
-      if (clientPayment?.amount !== null) {
-        if (Number(clientPayment?.amount) === Number(txAmount)) {
-          await updateClientPaymentStatus(paymentId, status)
-        }
-      } else {
-        await updateClientPaymentStatus(paymentId, status)
-      }
-    }
+  
+  // Validate paymentId format (32 hex characters)
+  if (typeof paymentId !== 'string' || !/^[0-9a-fA-F]{32}$/.test(paymentId)) {
+    return
+  }
+  
+  try {
+    const clientPayment = await getClientPayment(paymentId)
+    if (clientPayment === null) {
+      return
+    }
+    
+    // Verify transaction address matches payment's registered address
+    if (clientPayment.address.address !== txAddress) {
+      console.warn(`Address mismatch for payment ${paymentId}: expected ${clientPayment.address.address}, got ${txAddress}`)
+      return
+    }
+    
+    // Prevent status downgrades
+    if (clientPayment.status === 'CONFIRMED' && status === 'ADDED_TO_MEMPOOL') {
+      return
+    }
+    
+    // Compare amounts using Decimal equality
+    if (clientPayment.amount !== null) {
+      const clientAmount = new Prisma.Decimal(clientPayment.amount)
+      const txAmountDecimal = new Prisma.Decimal(txAmount.toString())
+      if (!clientAmount.equals(txAmountDecimal)) {
+        console.warn(`Payment amount mismatch for ${paymentId}: expected ${clientAmount}, got ${txAmountDecimal}`)
+        return
+      }
+    }
+    
+    await updateClientPaymentStatus(paymentId, status)
+  } catch (error) {
+    console.error(`Failed to update client payment status for ${paymentId}: ${String(error)}`)
   }
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 723cdb4 and ab4b944.

📒 Files selected for processing (1)
  • services/chronikService.ts (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
services/chronikService.ts (2)
utils/validators.ts (1)
  • parseOpReturnData (436-464)
services/transactionService.ts (2)
  • getClientPayment (1022-1027)
  • updateClientPaymentStatus (1015-1020)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run Tests
🔇 Additional comments (2)
services/chronikService.ts (2)

14-17: LGTM! Imports are clear and no name collision.

The imported functions updateClientPaymentStatus and getClientPayment are used correctly in the private method handleUpdateClientPaymentStatus without naming conflicts.


578-580: LGTM! Transaction fetching correctly positioned.

Moving the transaction and address fetching logic earlier (before the message type conditionals) is a good refactor that eliminates duplication and makes both the TX_CONFIRMED and TX_ADDED_TO_MEMPOOL paths cleaner.

Copy link
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

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ab4b944 and af461e7.

📒 Files selected for processing (2)
  • tests/unittests/chronikService.test.ts (21 hunks)
  • tests/unittests/handleUpdateClientPaymentStatus.test.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
tests/unittests/chronikService.test.ts (1)
services/chronikService.ts (6)
  • toHash160 (903-911)
  • ChronikBlockchainClient (118-884)
  • multiBlockchainClient (1100-1100)
  • fromHash160 (886-901)
  • outputScriptToAddress (914-942)
  • getNullDataScriptData (37-110)
🪛 GitHub Actions: Pull Request Tests
tests/unittests/chronikService.test.ts

[error] 1322-1565: Unit tests in chronikService.test.ts failing: multiple expectations not met (e.g., mockGetClientPayment calls, payment status updates) and chronik fetch-related errors leading to test failures.

Comment on lines 1296 to 1324

it('Should update payment status when paymentId exists and amounts match', async () => {
// Mock parseOpReturnData to return a paymentId
const mockOpReturn = JSON.stringify({
paymentId: 'test-payment-id-123',
message: 'test message'
})

const mockClientPayment = {
id: '1',
paymentId: 'test-payment-id-123',
amount: new Prisma.Decimal('100.00'),
status: 'PENDING',
address: { id: '1', address: 'test-address' }
}

mockGetClientPayment.mockResolvedValue(mockClientPayment)
mockUpdateClientPaymentStatus.mockResolvedValue(undefined)

// Access the private method using bracket notation
await (client as any).handleUpdateClientPaymentStatus(
new Prisma.Decimal('100.00'),
mockOpReturn,
'CONFIRMED'
)

expect(mockGetClientPayment).toHaveBeenCalledWith('test-payment-id-123')
expect(mockUpdateClientPaymentStatus).toHaveBeenCalledWith('test-payment-id-123', 'CONFIRMED')
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Payment-ID never extracted from mocked opReturn

All of these new handleUpdateClientPaymentStatus tests build the opReturn payload with JSON.stringify(...). However handleUpdateClientPaymentStatus calls the real parseOpReturnData, which only understands the PAY protocol’s key/value (foo=bar baz=qux) format, not JSON. Consequently parsedOpReturn.paymentId is always undefined, so getClientPayment/updateClientPaymentStatus are never invoked and the expectations fail (as seen in CI). Please switch the mocked opReturn strings to the format that parseOpReturnData actually parses, or stub parseOpReturnData itself. Until then the suite will keep failing.

🤖 Prompt for AI Agents
In tests/unittests/chronikService.test.ts around lines 1296 to 1324, the mocked
opReturn is JSON but the real parseOpReturnData expects PAY protocol key/value
strings (e.g. "paymentId=test-payment-id-123 message=test message"), so
parsedOpReturn.paymentId is undefined and the expectations never run; fix by
either (A) change the mocked opReturn strings to the PAY key=value format that
parseOpReturnData parses (e.g. "paymentId=test-payment-id-123 message=test
message") for these tests, or (B) stub/mock parseOpReturnData to return an
object with paymentId and message for the given opReturn — implement one of
these approaches so getClientPayment and updateClientPaymentStatus are called
with the expected paymentId.

Comment on lines 86 to 124
it('Should update payment status when paymentId exists and amounts match', async () => {
// Mock parseOpReturnData to return a paymentId
const mockOpReturn = JSON.stringify({
paymentId: 'test-payment-id-123',
message: 'test message'
})

const mockClientPayment = {
paymentId: 'test-payment-id-123',
amount: new Prisma.Decimal('100.00'),
status: 'PENDING' as ClientPaymentStatus,
addressString: 'test-address',
createdAt: new Date(),
updatedAt: new Date(),
address: {
id: '1',
address: 'test-address',
networkId: 1,
syncing: false,
createdAt: new Date(),
updatedAt: new Date(),
lastSynced: null
}
}

mockGetClientPayment.mockResolvedValue(mockClientPayment)
mockUpdateClientPaymentStatus.mockResolvedValue(undefined)

// Access the private method using bracket notation
await (client as any).handleUpdateClientPaymentStatus(
new Prisma.Decimal('100.00'),
mockOpReturn,
'CONFIRMED' as ClientPaymentStatus
)

expect(mockGetClientPayment).toHaveBeenCalledWith('test-payment-id-123')
expect(mockUpdateClientPaymentStatus).toHaveBeenCalledWith('test-payment-id-123', 'CONFIRMED')
})

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Tests build impossible opReturn payloads

Here too the mocked opReturn is a JSON string, but handleUpdateClientPaymentStatus runs the real parseOpReturnData, which never returns a paymentId for JSON input. Because parsedOpReturn.paymentId is always undefined, none of the mocked transactionService methods are called and every expectation fails (matching the CI report). Please provide the PAY protocol key/value payload that parseOpReturnData can parse, or mock parseOpReturnData to return the desired structure.

🤖 Prompt for AI Agents
In tests/unittests/handleUpdateClientPaymentStatus.test.ts around lines 86 to
124, the test supplies a JSON string for opReturn but the real parseOpReturnData
used by handleUpdateClientPaymentStatus does not extract paymentId from plain
JSON; update the test to either (a) supply a valid PAY protocol key/value
opReturn string that parseOpReturnData recognizes (including the paymentId key
with the test-payment-id-123 value), or (b) mock parseOpReturnData to return an
object with paymentId: 'test-payment-id-123' (and any other needed fields)
before calling handleUpdateClientPaymentStatus, and keep the existing
transactionService mocks and assertions unchanged.

@lissavxo lissavxo force-pushed the feat/update-payment-id branch from af461e7 to 22b7370 Compare October 2, 2025 16:33
Copy link
Collaborator

@chedieck chedieck left a comment

Choose a reason for hiding this comment

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

Making a payment to the wrong address with some paymentId will validate the payment in the DB — need to check output destination

@lissavxo lissavxo force-pushed the feat/update-payment-id branch from 9f832f2 to 962c953 Compare October 7, 2025 17:12
Copy link
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: 0

♻️ Duplicate comments (4)
services/transactionService.ts (3)

12-13: Break circular dependency with chronikService.

Top-level import of multiBlockchainClient creates a cycle (chronikService → transactionService → chronikService) that can yield undefined bindings at runtime. Lazy import inside the function avoids it.

Based on previous review.


980-1013: Validate and normalize address; guard unknown network; avoid subtle bugs in generatePaymentId.

  • Use parseAddress to normalize/validate input before DB ops.
  • Derive networkId from the normalized prefix and fail fast if unknown.
  • Use the normalized address in addressExists and connectOrCreate.

Based on previous review.


1015-1020: Add error handling for non-existent payment IDs.

The updateClientPaymentStatus function should handle cases where the payment ID doesn't exist.

Based on previous review.

services/chronikService.ts (1)

557-578: Fix critical property access error and Prisma Decimal comparison.

  • Lines 568, 572: clientPayment?.addressString does not exist. The correct property path is clientPayment?.address.address (based on the return type from getClientPayment).
  • Line 567: Prisma.Decimal objects must be compared via .equals(), not by converting to Number() which can lose precision.
  • Line 563: No validation that paymentId is a valid format (32 hex characters expected).
  • Missing status downgrade prevention (e.g., CONFIRMED → ADDED_TO_MEMPOOL).
  • Missing error handling for getClientPayment and updateClientPaymentStatus.

Apply this diff to fix the critical bugs:

 private async handleUpdateClientPaymentStatus (
   txAmount: string | number | Prisma.Decimal | DecimalJsLike,
   opReturn: string | undefined, status: ClientPaymentStatus,
   txAdress: string): Promise<void> {
   const parsedOpReturn = parseOpReturnData(opReturn ?? '')
   const paymentId = parsedOpReturn.paymentId
-  if (paymentId !== undefined && paymentId !== '') {
+  // Validate paymentId format (32 hex characters for UUID without dashes)
+  if (typeof paymentId !== 'string' || !/^[0-9a-fA-F]{32}$/.test(paymentId)) {
+    return
+  }
+  
+  try {
     const clientPayment = await getClientPayment(paymentId)
-    if (clientPayment !== null) {
-      if (clientPayment?.amount !== null) {
-        if (Number(clientPayment?.amount) === Number(txAmount) &&
-          (clientPayment?.addressString === txAdress)) {
-          await updateClientPaymentStatus(paymentId, status)
-        }
-      } else {
-        if (clientPayment?.addressString === txAdress) {
-          await updateClientPaymentStatus(paymentId, status)
-        }
-      }
+    if (clientPayment === null) {
+      return
+    }
+    
+    // Prevent status downgrades
+    if (clientPayment.status === 'CONFIRMED' && status === 'ADDED_TO_MEMPOOL') {
+      return
     }
+    
+    // Verify address matches
+    if (clientPayment.address.address !== txAdress) {
+      console.warn(`Address mismatch for payment ${paymentId}: expected ${clientPayment.address.address}, got ${txAdress}`)
+      return
+    }
+    
+    // Check amount if specified
+    if (clientPayment.amount !== null) {
+      const clientAmount = new Prisma.Decimal(clientPayment.amount)
+      const txAmountDecimal = new Prisma.Decimal(txAmount.toString())
+      if (!clientAmount.equals(txAmountDecimal)) {
+        console.warn(`Payment amount mismatch for ${paymentId}: expected ${clientAmount}, got ${txAmountDecimal}`)
+        return
+      }
+    }
+    
+    await updateClientPaymentStatus(paymentId, status)
+  } catch (error) {
+    console.error(`Failed to update client payment status: ${error}`)
   }
 }
🧹 Nitpick comments (2)
services/chronikService.ts (2)

601-604: Consider deduplicating paymentId updates across addresses.

While the call to handleUpdateClientPaymentStatus now passes the correct parameters, if the same transaction involves multiple addresses with the same paymentId in the OP_RETURN, the status update will be attempted multiple times. Consider tracking processed paymentId values in a Set within the loop to avoid redundant DB calls.


624-625: Consider deduplicating paymentId updates across addresses.

Same as the TX_CONFIRMED path above: if the same transaction involves multiple addresses with the same paymentId in the OP_RETURN, consider deduplicating to avoid redundant status updates.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9f832f2 and 962c953.

📒 Files selected for processing (3)
  • services/chronikService.ts (5 hunks)
  • services/transactionService.ts (3 hunks)
  • tests/unittests/handleUpdateClientPaymentStatus.test.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/unittests/handleUpdateClientPaymentStatus.test.ts
🧰 Additional context used
🧬 Code graph analysis (2)
services/chronikService.ts (2)
utils/validators.ts (1)
  • parseOpReturnData (436-464)
services/transactionService.ts (2)
  • getClientPayment (1022-1027)
  • updateClientPaymentStatus (1015-1020)
services/transactionService.ts (3)
constants/index.ts (1)
  • NETWORK_IDS_FROM_SLUGS (132-135)
services/addressService.ts (1)
  • addressExists (127-141)
services/chronikService.ts (1)
  • multiBlockchainClient (1106-1106)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run Tests

Copy link
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: 1

🧹 Nitpick comments (2)
services/chronikService.ts (2)

604-607: Update call to match fixed method signature.

Once you fix the handleUpdateClientPaymentStatus method signature (parameter txAdresstxAddress), update this call site to maintain consistency.

Apply this diff after fixing the method signature:

         for (const addressWithTransaction of addressesWithTransactions) {
           const { amount, opReturn } = addressWithTransaction.transaction
-          await this.handleUpdateClientPaymentStatus(amount, opReturn, 'CONFIRMED' as ClientPaymentStatus, addressWithTransaction.address.address)
+          await this.handleUpdateClientPaymentStatus(
+            amount,
+            opReturn,
+            'CONFIRMED' as ClientPaymentStatus,
+            addressWithTransaction.address.address
+          )
         }

627-628: Consistent with TX_CONFIRMED path.

This call correctly mirrors the TX_CONFIRMED logic. Consider formatting for readability once the method signature is fixed.

Apply this diff for improved readability:

             const { amount, opReturn } = addressWithTransaction.transaction
-            await this.handleUpdateClientPaymentStatus(amount, opReturn, 'ADDED_TO_MEMPOOL' as ClientPaymentStatus, addressWithTransaction.address.address)
+            await this.handleUpdateClientPaymentStatus(
+              amount,
+              opReturn,
+              'ADDED_TO_MEMPOOL' as ClientPaymentStatus,
+              addressWithTransaction.address.address
+            )
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 962c953 and 6ff0d94.

📒 Files selected for processing (1)
  • services/chronikService.ts (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
services/chronikService.ts (2)
utils/validators.ts (1)
  • parseOpReturnData (436-464)
services/transactionService.ts (2)
  • getClientPayment (1022-1027)
  • updateClientPaymentStatus (1015-1020)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run Tests
🔇 Additional comments (1)
services/chronikService.ts (1)

14-17: LGTM on imports.

The new imports for client payment functionality are correctly added and match the usage in the implementation.

Also applies to: 18-18, 33-33

Comment on lines 557 to 581
private async handleUpdateClientPaymentStatus (
txAmount: string | number | Prisma.Decimal | DecimalJsLike,
opReturn: string | undefined, status: ClientPaymentStatus,
txAdress: string): Promise<void> {
const parsedOpReturn = parseOpReturnData(opReturn ?? '')
const paymentId = parsedOpReturn.paymentId
if (paymentId !== undefined && paymentId !== '') {
const clientPayment = await getClientPayment(paymentId)
if (clientPayment !== null) {
if (clientPayment.status === 'CONFIRMED') {
return
}
if (clientPayment?.amount !== null) {
if (Number(clientPayment?.amount) === Number(txAmount) &&
(clientPayment?.addressString === txAdress)) {
await updateClientPaymentStatus(paymentId, status)
}
} else {
if (clientPayment?.addressString === txAdress) {
await updateClientPaymentStatus(paymentId, status)
}
}
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix typo, property access, Decimal comparison, validation, and error handling.

Several critical issues:

  1. Line 560: Typo in parameter name txAdress (missing 'd') — should be txAddress
  2. Lines 571, 575: clientPayment?.addressString doesn't exist. Based on getClientPayment returning { include: { address: true } }, use clientPayment.address.address instead
  3. Line 570: Incorrect Decimal comparison using Number() equality. Prisma Decimal must be compared via .equals() method
  4. Line 563: Missing paymentId format validation. Should validate against /^[0-9a-fA-F]{32}$/ before DB lookup
  5. Missing error handling: Wrap DB calls in try-catch to handle unknown paymentIds gracefully

Apply this diff:

-  private async handleUpdateClientPaymentStatus (
-    txAmount: string | number | Prisma.Decimal | DecimalJsLike,
-    opReturn: string | undefined, status: ClientPaymentStatus,
-    txAdress: string): Promise<void> {
+  private async handleUpdateClientPaymentStatus (
+    txAmount: string | number | Prisma.Decimal | DecimalJsLike,
+    opReturn: string | undefined,
+    status: ClientPaymentStatus,
+    txAddress: string
+  ): Promise<void> {
     const parsedOpReturn = parseOpReturnData(opReturn ?? '')
     const paymentId = parsedOpReturn.paymentId
-    if (paymentId !== undefined && paymentId !== '') {
+    
+    // Validate paymentId format (32 hex characters)
+    if (typeof paymentId !== 'string' || !/^[0-9a-fA-F]{32}$/.test(paymentId)) {
+      return
+    }
+    
+    try {
       const clientPayment = await getClientPayment(paymentId)
-      if (clientPayment !== null) {
-        if (clientPayment.status === 'CONFIRMED') {
-          return
-        }
-        if (clientPayment?.amount !== null) {
-          if (Number(clientPayment?.amount) === Number(txAmount) &&
-            (clientPayment?.addressString === txAdress)) {
-            await updateClientPaymentStatus(paymentId, status)
-          }
-        } else {
-          if (clientPayment?.addressString === txAdress) {
-            await updateClientPaymentStatus(paymentId, status)
-          }
-        }
+      if (clientPayment === null) {
+        return
+      }
+      
+      // Prevent status downgrades
+      if (clientPayment.status === 'CONFIRMED' && status === 'ADDED_TO_MEMPOOL') {
+        return
+      }
+      
+      // Verify transaction address matches payment address
+      if (clientPayment.address.address !== txAddress) {
+        return
+      }
+      
+      // Check amount if specified
+      if (clientPayment.amount !== null) {
+        const clientAmount = new Prisma.Decimal(clientPayment.amount)
+        const txAmountDecimal = new Prisma.Decimal(txAmount.toString())
+        if (!clientAmount.equals(txAmountDecimal)) {
+          return
+        }
       }
+      
+      await updateClientPaymentStatus(paymentId, status)
+    } catch (error) {
+      console.error(`Failed to update client payment status: ${String(error)}`)
     }
   }
🤖 Prompt for AI Agents
In services/chronikService.ts around lines 557 to 581, fix several issues:
rename parameter txAdress to txAddress (and update all uses inside the
function); validate parsed paymentId against /^[0-9a-fA-F]{32}$/ before calling
getClientPayment and return early if invalid; wrap the DB calls
(getClientPayment and updateClientPaymentStatus) in a try-catch and handle/log
errors gracefully to avoid unhandled rejections; when checking stored address
use clientPayment.address.address (per include: { address: true }) instead of
clientPayment?.addressString; and compare Prisma Decimal values using the
Decimal equals method (e.g., clientPayment.amount.equals(txAmount) or convert
txAmount to Decimal) rather than Number(...) equality. Ensure the function
returns/short-circuits cleanly on invalid/missing values.

Copy link
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: 1

♻️ Duplicate comments (1)
services/chronikService.ts (1)

557-581: Critical: Fix multiple issues in handleUpdateClientPaymentStatus.

Several critical defects:

  1. Line 562-563: Missing paymentId format validation. Per generatePaymentId() (UUIDv4 without dashes = 32 hex chars), validate /^[0-9a-fA-F]{32}$/ before DB lookup.
  2. Line 564: No try-catch around getClientPayment — throws on unknown/invalid IDs.
  3. Line 570: Incorrect Prisma Decimal comparison using Number(). Must use .equals() method or convert both to Decimal and compare.
  4. Lines 571, 575: Property error: clientPayment.addressString doesn't exist. Per getClientPayment return type (include: { address: true }), use clientPayment.address.address.
  5. Line 566-567: Incomplete status downgrade check — only prevents overwriting CONFIRMED, doesn't check if new status is a downgrade.
  6. Line 572: No error handling for updateClientPaymentStatus.

Apply this diff:

   private async handleUpdateClientPaymentStatus (
     txAmount: string | number | Prisma.Decimal | DecimalJsLike,
-    opReturn: string | undefined, status: ClientPaymentStatus,
-    txAddress: string): Promise<void> {
+    opReturn: string | undefined,
+    status: ClientPaymentStatus,
+    txAddress: string
+  ): Promise<void> {
     const parsedOpReturn = parseOpReturnData(opReturn ?? '')
     const paymentId = parsedOpReturn.paymentId
-    if (paymentId !== undefined && paymentId !== '') {
+    
+    // Validate paymentId format (32 hex characters for UUIDv4 without dashes)
+    if (typeof paymentId !== 'string' || !/^[0-9a-fA-F]{32}$/.test(paymentId)) {
+      return
+    }
+    
+    try {
       const clientPayment = await getClientPayment(paymentId)
-      if (clientPayment !== null) {
-        if (clientPayment.status === 'CONFIRMED') {
-          return
-        }
-        if (clientPayment.amount !== null) {
-          if (Number(clientPayment.amount) === Number(txAmount) &&
-            (clientPayment.addressString === txAddress)) {
-            await updateClientPaymentStatus(paymentId, status)
-          }
-        } else {
-          if (clientPayment.addressString === txAddress) {
-            await updateClientPaymentStatus(paymentId, status)
-          }
-        }
+      if (clientPayment === null) {
+        return
+      }
+      
+      // Prevent status downgrades: CONFIRMED > ADDED_TO_MEMPOOL > PENDING
+      if (clientPayment.status === 'CONFIRMED' && status !== 'CONFIRMED') {
+        return
+      }
+      if (clientPayment.status === 'ADDED_TO_MEMPOOL' && status === 'PENDING') {
+        return
+      }
+      
+      // Verify transaction address matches payment address
+      if (clientPayment.address.address !== txAddress) {
+        return
+      }
+      
+      // Check amount if specified
+      if (clientPayment.amount !== null) {
+        const clientAmount = new Prisma.Decimal(clientPayment.amount)
+        const txAmountDecimal = new Prisma.Decimal(txAmount.toString())
+        if (!clientAmount.equals(txAmountDecimal)) {
+          return
+        }
       }
+      
+      await updateClientPaymentStatus(paymentId, status)
+    } catch (error) {
+      console.error(`${this.CHRONIK_MSG_PREFIX}: Failed to update client payment status for ${paymentId}: ${String(error)}`)
     }
   }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6ff0d94 and 92b2ecc.

📒 Files selected for processing (1)
  • services/chronikService.ts (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
services/chronikService.ts (2)
utils/validators.ts (1)
  • parseOpReturnData (436-464)
services/transactionService.ts (2)
  • getClientPayment (1022-1027)
  • updateClientPaymentStatus (1015-1020)
🔇 Additional comments (1)
services/chronikService.ts (1)

627-628: Same verification needed for ADDED_TO_MEMPOOL path.

This call site also looks correct. The fixes to handleUpdateClientPaymentStatus will ensure proper status updates for mempool transactions.

Comment on lines +604 to +607
for (const addressWithTransaction of addressesWithTransactions) {
const { amount, opReturn } = addressWithTransaction.transaction
await this.handleUpdateClientPaymentStatus(amount, opReturn, 'CONFIRMED' as ClientPaymentStatus, addressWithTransaction.address.address)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Verify call sites after fixing handleUpdateClientPaymentStatus.

The call to handleUpdateClientPaymentStatus looks correct with proper parameters (amount, opReturn, CONFIRMED status, address). Once the method is fixed per the previous comment, this should work as expected.

After applying the fixes to handleUpdateClientPaymentStatus, verify that:

  1. CONFIRMED status updates work correctly
  2. Amount matching validation works with Prisma Decimal
  3. Address validation prevents mismatched updates
  4. Errors are logged without crashing the WebSocket handler

🏁 Script executed:

#!/bin/bash
# Verify the handleUpdateClientPaymentStatus usage in TX_CONFIRMED and TX_ADDED_TO_MEMPOOL handlers
rg -n "handleUpdateClientPaymentStatus" services/chronikService.ts -A2 -B2

Length of output: 878


🏁 Script executed:

#!/bin/bash
rg -n "private async handleUpdateClientPaymentStatus" -A200 services/chronikService.ts

Length of output: 10453


Handle errors & use precise Decimal comparison in handleUpdateClientPaymentStatus

  • Wrap both await updateClientPaymentStatus(...) calls in a try/catch inside handleUpdateClientPaymentStatus, logging errors rather than letting them bubble up and crash the WebSocket handler.
  • Replace Number(clientPayment.amount) === Number(txAmount) with a Decimal‐aware check, e.g.:
    if (clientPayment.amount.equals(new Prisma.Decimal(txAmount))) {  }
🤖 Prompt for AI Agents
In services/chronikService.ts around lines 604 to 607, the loop calls
handleUpdateClientPaymentStatus which currently lets internal update errors
bubble up and uses Number(...) for amount comparison; modify
handleUpdateClientPaymentStatus to wrap both await
updateClientPaymentStatus(...) calls in a try/catch that logs the error (so the
WebSocket handler doesn't crash) and continues, and change the numeric
comparison to use a Decimal-aware check (e.g., create a Prisma.Decimal from
txAmount and call .equals on clientPayment.amount) instead of
Number(clientPayment.amount) === Number(txAmount).

Copy link
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: 1

♻️ Duplicate comments (1)
pages/api/payments/paymentId/index.ts (1)

37-38: Add Allow header to 405 response.

The 405 Method Not Allowed response is missing the required Allow header that indicates which HTTP methods are supported by this endpoint.

Apply this diff to add the Allow header:

   } else {
+    res.setHeader('Allow', ['POST'])
     res.status(RESPONSE_MESSAGES.METHOD_NOT_ALLOWED_405.statusCode)
       .json(RESPONSE_MESSAGES.METHOD_NOT_ALLOWED_405)
   }
🧹 Nitpick comments (2)
pages/api/payments/paymentId/index.ts (2)

9-11: Consider restricting CORS origin for production security.

The CORS configuration currently allows all origins by default since no origin option is specified. If this API endpoint should only be accessed from specific domains, consider adding an explicit origin whitelist.

Example with origin restriction:

 const cors = Cors({
-  methods: ['POST']
+  methods: ['POST'],
+  origin: process.env.NODE_ENV === 'production' 
+    ? ['https://yourdomain.com']
+    : true
 })

13-13: Use proper Next.js types for better type safety.

The handler parameters use any types, which bypasses TypeScript's type checking. Consider using NextApiRequest and NextApiResponse from 'next' for improved type safety and IDE support.

Apply this diff:

+import type { NextApiRequest, NextApiResponse } from 'next'
+
-export default async (req: any, res: any): Promise<void> => {
+export default async (req: NextApiRequest, res: NextApiResponse): Promise<void> => {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 92b2ecc and d80801e.

📒 Files selected for processing (1)
  • pages/api/payments/paymentId/index.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
pages/api/payments/paymentId/index.ts (4)
utils/index.ts (1)
  • runMiddleware (137-151)
utils/validators.ts (2)
  • parseCreatePaymentIdPOSTRequest (578-590)
  • parseAddress (31-48)
services/transactionService.ts (1)
  • generatePaymentId (980-1013)
constants/index.ts (1)
  • RESPONSE_MESSAGES (8-108)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run Tests
🔇 Additional comments (1)
pages/api/payments/paymentId/index.ts (1)

7-7: CORS package version is valid in package.json
The cors dependency is set to ^2.8.5, matching the latest stable release.

Comment on lines +17 to +21
const values = parseCreatePaymentIdPOSTRequest(req.body)
const address = parseAddress(values.address)
const amount = values.amount as Decimal | undefined

const paymentId = await generatePaymentId(address, amount)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix unsafe type cast for amount parameter.

Line 19 performs an unsafe type cast of values.amount to Decimal | undefined. According to the validator code in utils/validators.ts, parseCreatePaymentIdPOSTRequest returns amount as a string or undefined, not a Decimal. This cast does not perform any conversion and will cause type mismatches when passed to generatePaymentId, which expects Prisma.Decimal.

Apply this diff to properly convert the amount:

+import { Prisma } from '@prisma/client'
+
       const values = parseCreatePaymentIdPOSTRequest(req.body)
       const address = parseAddress(values.address)
-      const amount = values.amount as Decimal | undefined
+      const amount = values.amount !== undefined 
+        ? new Prisma.Decimal(values.amount) 
+        : undefined
 
       const paymentId = await generatePaymentId(address, amount)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const values = parseCreatePaymentIdPOSTRequest(req.body)
const address = parseAddress(values.address)
const amount = values.amount as Decimal | undefined
const paymentId = await generatePaymentId(address, amount)
import { Prisma } from '@prisma/client'
const values = parseCreatePaymentIdPOSTRequest(req.body)
const address = parseAddress(values.address)
const amount = values.amount !== undefined
? new Prisma.Decimal(values.amount)
: undefined
const paymentId = await generatePaymentId(address, amount)

Copy link
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: 1

♻️ Duplicate comments (1)
services/chronikService.ts (1)

557-582: Critical issues in handleUpdateClientPaymentStatus: wrong property access, incorrect Decimal comparison, missing validation & error handling.

Multiple critical issues remain from previous reviews:

  1. Lines 572-573, 577: clientPayment.addressString doesn't exist. Based on getClientPayment return type (includes { address: true }), use clientPayment.address.address.
  2. Line 572: Number(clientPayment.amount) === Number(txAmount) is incorrect for Prisma.Decimal. Use .equals() method: clientPayment.amount.equals(new Prisma.Decimal(txAmount)).
  3. Lines 563-564: Missing paymentId format validation. Should validate against /^[0-9a-fA-F]{32}$/ before DB lookup.
  4. Missing try-catch: Wrap getClientPayment and updateClientPaymentStatus calls to handle unknown paymentIds gracefully without crashing the WebSocket handler.

Apply this diff:

   private async handleUpdateClientPaymentStatus (
     txAmount: string | number | Prisma.Decimal | DecimalJsLike,
-    opReturn: string | undefined, status: ClientPaymentStatus,
-    txAddress: string): Promise<void> {
+    opReturn: string | undefined,
+    status: ClientPaymentStatus,
+    txAddress: string
+  ): Promise<void> {
     const parsedOpReturn = parseOpReturnData(opReturn ?? '')
     const paymentId = parsedOpReturn.paymentId
-    if (paymentId === undefined || paymentId === '') {
+    
+    // Validate paymentId format (32 hex characters for UUID without dashes)
+    if (typeof paymentId !== 'string' || !/^[0-9a-fA-F]{32}$/.test(paymentId)) {
       return
     }
-    const clientPayment = await getClientPayment(paymentId)
-    if (clientPayment !== null) {
-      if (clientPayment.status === 'CONFIRMED') {
-        return
-      }
-      if (clientPayment.amount !== null) {
-        if (Number(clientPayment.amount) === Number(txAmount) &&
-          (clientPayment.addressString === txAddress)) {
-          await updateClientPaymentStatus(paymentId, status)
-        }
-      } else {
-        if (clientPayment.addressString === txAddress) {
-          await updateClientPaymentStatus(paymentId, status)
-        }
+    
+    try {
+      const clientPayment = await getClientPayment(paymentId)
+      if (clientPayment === null) {
+        return
+      }
+      
+      // Prevent status downgrades
+      if (clientPayment.status === 'CONFIRMED' && status === 'ADDED_TO_MEMPOOL') {
+        return
+      }
+      
+      // Verify transaction address matches payment address
+      if (clientPayment.address.address !== txAddress) {
+        return
+      }
+      
+      // Check amount if specified
+      if (clientPayment.amount !== null) {
+        const clientAmount = new Prisma.Decimal(clientPayment.amount)
+        const txAmountDecimal = new Prisma.Decimal(txAmount.toString())
+        if (!clientAmount.equals(txAmountDecimal)) {
+          return
+        }
       }
+      
+      await updateClientPaymentStatus(paymentId, status)
+    } catch (error) {
+      console.error(`Failed to update client payment status for ${paymentId}: ${String(error)}`)
     }
   }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d80801e and e8aadce.

📒 Files selected for processing (1)
  • services/chronikService.ts (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
services/chronikService.ts (2)
utils/validators.ts (1)
  • parseOpReturnData (436-464)
services/transactionService.ts (2)
  • getClientPayment (1022-1027)
  • updateClientPaymentStatus (1015-1020)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run Tests

@chedieck chedieck merged commit 55e10cb into master Oct 15, 2025
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants