Skip to content

Feature/payment request ux#310

Merged
OlufunbiIK merged 3 commits intoOlufunbiIK:mainfrom
mijinummi:feature/payment-request-ux
Mar 28, 2026
Merged

Feature/payment request ux#310
OlufunbiIK merged 3 commits intoOlufunbiIK:mainfrom
mijinummi:feature/payment-request-ux

Conversation

@mijinummi
Copy link
Copy Markdown
Contributor

@mijinummi mijinummi commented Mar 27, 2026

📝 Description

Overview

This PR enhances the PaymentURIPage into a production-ready payment request and settlement UX.
It provides a complete flow for reviewing, submitting, and understanding payment requests, with robust error handling and recovery options.

Key Features

  • Payment Review: Asset, amount, memo, sender, recipient details
  • Transaction Progress: Clear submission state
  • Result Handling: Success receipt with transaction hash, failure with retry guidance
  • Navigation: Link back to related split
  • Error Handling: Invalid URIs, failed transactions

✅ Acceptance Criteria

  • User can confidently review a payment request
  • Submit transaction with visible progress
  • Understand success or failure results
  • Retry failed transactions
  • Navigate back to related split

📂 File Changes

  • frontend/src/pages/PaymentURIPage.tsx — orchestrates payment flow
  • frontend/src/components/Payment/PaymentReview.tsx — review screen
  • frontend/src/components/Payment/PaymentProgress.tsx — progress screen
  • frontend/src/components/Payment/PaymentResult.tsx — result screen
  • frontend/src/utils/stellar/paymentUri.ts — URI parsing
  • frontend/src/types/payment.ts — type definitions

Closes #230

Summary by CodeRabbit

  • New Features

    • Added a complete payment processing workflow including review, submission progress tracking, and result confirmation
    • Implemented settlement suggestion engine with ability to snooze/unsnooze transactions
    • Integrated blockchain verification to validate payment balances
    • Added support for payment URI parsing to streamline payment requests
    • Enabled payment tracking and navigation during transaction processing
  • Tests

    • Added unit tests for settlement service functionality

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 27, 2026

@mijinummi is attempting to deploy a commit to the olufunbiik's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 27, 2026

📝 Walkthrough

Walkthrough

This PR introduces a complete payment settlement system spanning backend and frontend. The backend adds a User entity with settlement snooze state, a service layer for handling settlement suggestions and verification, repository functions for database persistence, and HTTP endpoints for the settlement API. The frontend adds typed payment request/result interfaces, URI parsing utilities, and multi-step payment flow components (review, progress, result) orchestrated through a new payment page.

Changes

Cohort / File(s) Summary
Backend Settlement Entity & Repository
backend/src/settlement/entities/user.entity.ts, backend/src/settlement/settlement.repository.ts
Added User entity with UUID primary key, balances (JSONB), and snoozedUntil timestamp. Repository exposes findActiveParticipants() to filter non-snoozed users and updateSnooze()/clearSnooze() to persist snooze state.
Backend Settlement Service
backend/src/settlement/settlement.service.ts
Implements generateSuggestions() that aggregates active participant balances to compute settlement status, plus snoozeSettlement(), unsnoozeSettlement(), and verifySettlement() for managing user preferences and balance validation.
Backend Settlement API
backend/src/settlement/settlement.controller.ts
Exposes four HTTP endpoints: GET /suggestions, POST /snooze, POST /unsnooze, and POST /verify delegating to service layer functions.
Backend Testing
backend/src/settlement/tests/settlement.service.spec.ts
Test suite validating settlement suggestion generation, snooze persistence, and snooze clearing behavior.
Backend Stellar Integration
backend/src/utils/stellarVerification.ts
Added verifyBalance() function querying Stellar Horizon testnet to confirm account balances match expected amounts for a given asset.
Frontend Payment Types & Utilities
frontend/src/types/payment.ts, frontend/src/utils/stellar/paymentUri.ts
Defined PaymentRequest and PaymentResult interfaces. Added parsePaymentUri() to extract payment details from query parameters with sensible defaults.
Frontend Payment Components
frontend/src/components/Payment/PaymentProgress.tsx, frontend/src/components/Payment/PaymentResult.tsx, frontend/src/components/Payment/PaymentReview.tsx
Three presentational components: PaymentProgress shows submission status, PaymentResult displays success/error outcomes with retry and navigation callbacks, PaymentReview renders payment details for user confirmation.
Frontend Payment Page
frontend/src/pages/PaymentURIPage.tsx
Main orchestration page parsing payment URI, managing multi-step state (review → progress → result), handling submission via mocked async handler, and enabling retry and split navigation.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Frontend as PaymentURIPage
    participant Service as settlement.service
    participant Repo as settlement.repository
    participant DB as Database
    participant Stellar as Stellar API

    User->>Frontend: Opens payment URI
    Frontend->>Frontend: Parse URI → PaymentRequest
    Frontend->>Frontend: Show PaymentReview
    
    User->>Frontend: Confirms payment
    Frontend->>Frontend: Show PaymentProgress
    
    Frontend->>Service: verifySettlement(user, asset, amount)
    Service->>Stellar: verifyBalance(userId, asset, amount)
    Stellar-->>Service: balance ≥ amount?
    Service-->>Frontend: verified: boolean
    
    alt Verification Successful
        Frontend->>Frontend: Set PaymentResult (success)
        Frontend->>Frontend: Show PaymentResult with txHash
    else Verification Failed
        Frontend->>Frontend: Set PaymentResult (error)
        Frontend->>Frontend: Show PaymentResult with error
    end
    
    User->>Frontend: Retry or Back to Split
Loading
sequenceDiagram
    participant API as settlement.controller
    participant Service as settlement.service
    participant Repo as settlement.repository
    participant DB as Database

    API->>Service: generateSuggestions()
    Service->>Repo: findActiveParticipants()
    Repo->>DB: Query users WHERE snoozedUntil IS NULL OR snoozedUntil < now()
    DB-->>Repo: User[] with balances
    Repo-->>Service: Active participants
    
    Service->>Service: Aggregate balances across participants
    Service->>Service: Compute status (completed/partial/invalid)
    Service-->>API: SettlementSuggestion[]
    API-->>Client: suggestions JSON
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

Poem

🐰 A rabbit hops through payment gates,
With snooze and verify—no more waits!
From URI parse to settlement flow,
Progress and results now brightly glow. ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 4

❌ Failed checks (3 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR implements all primary frontend payment UX objectives from #230: payment review, progress display, result handling, and split navigation—however backend settlement features were also added but are unrelated to the linked issue. Clarify whether backend settlement changes (#settlement entity, repository, service, and Stellar verification) are scope for this PR or belong in a separate issue.
Out of Scope Changes check ⚠️ Warning Backend settlement implementation (User entity, repository, service, controller, tests, and Stellar verification) is outside the stated scope of issue #230, which focuses exclusively on frontend payment request UX. Either move backend settlement changes to a separate PR/issue or update PR objectives and linked issues to explicitly include backend settlement requirements.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive Title is vague and uses a generic branch-naming convention pattern that does not clearly convey the changeset content. Replace with a concise, descriptive title summarizing the main frontend payment UX feature, e.g., 'Add payment review, progress, and result components'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch feature/payment-request-ux

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

❤️ Share

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

Copy link
Copy Markdown

@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: 15

🧹 Nitpick comments (2)
frontend/src/pages/PaymentURIPage.tsx (1)

38-38: URL-encode dynamic split ids in navigation paths.

Encode splitId before interpolating into the route to avoid malformed URLs on special characters.

Suggested update
-      window.location.href = `/split/${splitId}`;
+      window.location.href = `/split/${encodeURIComponent(splitId)}`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/pages/PaymentURIPage.tsx` at line 38, The navigation uses an
unencoded splitId when setting window.location.href in the PaymentURIPage (the
line assigning window.location.href = `/split/${splitId}`), which can produce
malformed URLs for ids with special characters; fix it by URL-encoding the
dynamic segment (use encodeURIComponent on splitId) before interpolating into
the route so the link becomes safe for all splitId values. Ensure the change is
applied where splitId is read/used inside the PaymentURIPage component or its
redirect handler.
frontend/src/components/Payment/PaymentProgress.tsx (1)

4-7: Add live-region semantics for the progress state.

This screen is status-only; add role="status" and aria-live="polite" so assistive tech announces submission progress reliably.

Suggested update
-  <div>
+  <div role="status" aria-live="polite" aria-busy="true">
     <h2>Submitting Transaction...</h2>
     <p>Please wait while we process your payment.</p>
   </div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/Payment/PaymentProgress.tsx` around lines 4 - 7, Wrap
the status-only markup in PaymentProgress (the JSX returned in
PaymentProgress.tsx) with accessible live-region attributes: add role="status"
and aria-live="polite" to the container element (the top-level <div> that
contains the <h2> and <p>) so assistive technologies announce the "Submitting
Transaction..." progress reliably; ensure the element remains non-interactive
and only used for status updates.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/src/settlement/entities/user.entity.ts`:
- Around line 3-13: The settlement code is importing the wrong User entity
lacking balances and snoozedUntil; update the imports in
settlement.repository.ts and settlement.service.ts to import the
settlement-scoped User class (the User entity that declares balances:
Record<string, number> and snoozedUntil: Date | null) instead of the old
../entities/user.entity, or alternatively consolidate by moving balances and
snoozedUntil into the shared User entity; ensure the import statements reference
the file that contains the User class with the `@Column`("jsonb") balances and
`@Column`({ type: "timestamp", nullable: true }) snoozedUntil so all settlement
queries use the correct entity.

In `@backend/src/settlement/settlement.controller.ts`:
- Around line 11-26: Routes router.post("/snooze"), router.post("/unsnooze"),
and router.post("/verify") currently trust userId/user from req.body; instead
resolve the subject from the authenticated principal (e.g., req.user, req.auth,
or token-derived id) and pass that to snoozeSettlement, unsnoozeSettlement, and
verifySettlement; keep only user-editable fields (like until, asset, amount) in
req.body and validate them, reject or ignore any userId/user in the body, and
return a 4xx if the principal is missing or unauthorized before calling the
functions.

In `@backend/src/settlement/settlement.repository.ts`:
- Around line 1-4: The file exports a standalone userRepo bound to AppDataSource
which is never initialized; replace this pattern with Nest DI by removing the
singleton export and instead obtain the repository from the app-managed
DataSource/TypeORM module—e.g., stop using AppDataSource.getRepository(User) and
refactor so that callers receive either an injected DataSource or an injected
Repository<User> (use the Repository<User> token or inject DataSource in a
provider/service and call dataSource.getRepository(User) there); update any
consumers of userRepo to accept the injected Repository<User> or a service
wrapper that resolves the repository from the injected DataSource.
- Around line 6-13: The findActiveParticipants function uses a MongoDB-style
filter ({ snoozedUntil: { $lt: now } }) which is invalid for TypeORM; update the
userRepo.find call to use TypeORM FindOperator helpers—replace the array where
clause with conditions using IsNull() for snoozedUntil and LessThan(now) to
match expired snoozes (refer to the findActiveParticipants function and the
snoozedUntil property in the userRepo.find call) so the query becomes a proper
TypeORM query that returns users with snoozedUntil IS NULL or snoozedUntil <
now.

In `@backend/src/settlement/settlement.service.ts`:
- Around line 15-24: The current reduction using participants.reduce and summing
Object.values(u.balances) into a single numeric total (and then using total ===
0) is incorrect and brittle for money and multi-asset balances; update the logic
in the settlement calculation (the block that currently computes total and calls
suggestions.push) to: aggregate balances per asset key across all participants
using a decimal-safe approach (e.g., Big.js or integer smallest-unit arithmetic)
instead of a single Number, treat an empty participants array explicitly,
determine settlement status by checking each per-asset aggregate is exactly zero
(using the decimal-safe equality) and push suggestions with status "completed"
only if every asset total is zero, "partial" if some assets are non-zero, and
"invalid" if mismatched conditions apply; reference the existing symbols
participants, u.balances, and suggestions.push when making the change.
- Around line 37-39: The verifySettlement function is passing user.id (a UUID)
into verifyBalance which expects a Stellar public key; add a stellarPublicKey
string field to both User entity classes (main User and settlement User entity),
populate it where users are created/loaded, and change verifySettlement to call
verifyBalance(user.stellarPublicKey, asset, amount) (or validate and return
false/throw if user.stellarPublicKey is missing/invalid). Also add basic
validation (startsWith 'G' and ~56 chars) and clear error handling/logging so
verifyBalance receives a valid Stellar public key rather than the UUID.

In `@backend/src/settlement/tests/settlement.service.spec.ts`:
- Around line 5-8: The test "should generate completed suggestion when balances
net to zero" currently calls generateSuggestions() without arranging
participants; update the test to explicitly arrange zero-balance participants
before invoking generateSuggestions(): either seed the test repository or
stub/mock the participant repository call (e.g., mock the method used by
generateSuggestions such as participantRepo.findAll or
settlementService.getParticipants) to return participants whose balances sum to
zero, then call generateSuggestions() and assert suggestions[0].status ===
"completed"; also ensure any test setup/teardown (database cleanup or mock
restores) is present so the test is deterministic.
- Around line 11-23: The tests call snoozeSettlement and unsnoozeSettlement but
never create a user, so userRepo.findOneBy may return null; either seed a user
in beforeEach or mock userRepo. Fix by adding a beforeEach that creates/saves a
user with id "test-user" (use userRepo.save or insert) so
snoozeSettlement/unsnoozeSettlement operate on a real row, or replace the repo
with a mocked object and assert on userRepo.update calls instead of findOneBy;
ensure the created user's id matches "test-user" and initialize/clear
snoozedUntil as needed for the unsnooze test.

In `@backend/src/utils/stellarVerification.ts`:
- Around line 5-8: Wrap the server.loadAccount(accountId) call in try/catch and
return false (or propagate a controlled error) on failures to avoid unhandled
rejections; then identify balances correctly by checking account.balances
entries for native XLM (match entry.asset_type === "native" when asset is
"native" or "XLM") and for issued assets require both asset_code and
asset_issuer to match (e.g., parse asset input like "CODE:ISSUER" into code and
issuer and compare against balance.asset_code and balance.asset_issuer); finally
compare Number(balance.balance) >= expected as before. Target symbols:
verifyBalance, server.loadAccount, account.balances, asset_type, asset_code,
asset_issuer.
- Around line 1-3: The import and endpoint in stellarVerification.ts are
incorrect: replace the legacy import from "stellar-sdk" with the installed
package "@stellar/stellar-sdk" and stop hard-coding the testnet URL; instead
read the Horizon endpoint from configuration (e.g., a config module or
process.env like STELLAR_HORIZON_URL) and use that value when instantiating
Server (the server constant), falling back to a sensible default only if config
is empty.

In `@frontend/src/components/Payment/PaymentResult.tsx`:
- Around line 15-17: In PaymentResult, the Back to Split button is calling
onBackToSplit(result.txHash) which breaks routing; change it to pass the split
identifier (e.g., result.splitId or result.split_id depending on the payload)
instead of txHash and update the onBackToSplit prop type/signature if necessary
so PaymentResult and its parent expect a splitId; locate the PaymentResult
component and the onBackToSplit usage to replace result.txHash with the correct
split id field and ensure any caller/handler (router/navigation) uses that split
id for navigation.

In `@frontend/src/pages/PaymentURIPage.tsx`:
- Around line 22-24: In PaymentURIPage.tsx replace the stubbed txHash
("mock-tx-hash") and the immediate setResult call with a real Stellar SDK
submission: build or decode the transaction (where the mock is currently set),
call server or use the StellarSdk.Server().submitTransaction(...) to submit the
XDR, await the response, then call setResult({ success: true, txHash:
response.hash }) on success or setResult({ success: false, error: err.message ||
err }) on failure; ensure you catch and log/propagate submission errors and keep
existing UI state handling around the submit handler in this component.
- Around line 13-17: The component is calling parsePaymentUri and setRequest
during render which can throw (parsePaymentUri uses new URL) and cause crashes;
move the URI extraction and state update into a useEffect that runs once (or
when window.location.search changes) and only attempt parsing when the uri param
exists, wrapping parsePaymentUri in a try/catch to guard against invalid/empty
input and avoid calling setRequest on parse failure; refer to the
parsePaymentUri function and the setRequest state updater (and the local
variable uri / request) when making these changes so the effect reads new
URLSearchParams(window.location.search).get("uri"), tries parsing, and calls
setRequest(parsed) only on success.

In `@frontend/src/types/payment.ts`:
- Around line 10-14: The PaymentResult interface is missing splitId, causing
callers to reuse txHash for routing; update the PaymentResult type (export
interface PaymentResult) to include a splitId?: string property (optional
string) and then update any related consumers/return sites to populate and
consume splitId instead of overloading txHash; verify functions that construct
or return PaymentResult (e.g., payment handlers, createPaymentResult helpers)
are updated to set splitId where available.

In `@frontend/src/utils/stellar/paymentUri.ts`:
- Around line 5-13: The current parsing returns amount and to without validation
so NaN/empty strings can leak; update the parsing function (where URL is
constructed into `url` and the returned object with properties asset, amount,
memo, from, to, splitId) to validate that `amount` is present and a finite
positive number (use Number(...) and Number.isFinite / > 0) and that `to`
(destination) is a non-empty string; if validation fails, throw or return a
clearly typed error (or a Result/nullable) so downstream payment handlers won't
receive NaN or empty destination values.

---

Nitpick comments:
In `@frontend/src/components/Payment/PaymentProgress.tsx`:
- Around line 4-7: Wrap the status-only markup in PaymentProgress (the JSX
returned in PaymentProgress.tsx) with accessible live-region attributes: add
role="status" and aria-live="polite" to the container element (the top-level
<div> that contains the <h2> and <p>) so assistive technologies announce the
"Submitting Transaction..." progress reliably; ensure the element remains
non-interactive and only used for status updates.

In `@frontend/src/pages/PaymentURIPage.tsx`:
- Line 38: The navigation uses an unencoded splitId when setting
window.location.href in the PaymentURIPage (the line assigning
window.location.href = `/split/${splitId}`), which can produce malformed URLs
for ids with special characters; fix it by URL-encoding the dynamic segment (use
encodeURIComponent on splitId) before interpolating into the route so the link
becomes safe for all splitId values. Ensure the change is applied where splitId
is read/used inside the PaymentURIPage component or its redirect handler.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f7dcddc3-542b-44ce-9d8d-e9d6ae9225c7

📥 Commits

Reviewing files that changed from the base of the PR and between e82ee57 and d6d100f.

📒 Files selected for processing (12)
  • backend/src/settlement/entities/user.entity.ts
  • backend/src/settlement/settlement.controller.ts
  • backend/src/settlement/settlement.repository.ts
  • backend/src/settlement/settlement.service.ts
  • backend/src/settlement/tests/settlement.service.spec.ts
  • backend/src/utils/stellarVerification.ts
  • frontend/src/components/Payment/PaymentProgress.tsx
  • frontend/src/components/Payment/PaymentResult.tsx
  • frontend/src/components/Payment/PaymentReview.tsx
  • frontend/src/pages/PaymentURIPage.tsx
  • frontend/src/types/payment.ts
  • frontend/src/utils/stellar/paymentUri.ts

Comment on lines +3 to +13
@Entity("users")
export class User {
@PrimaryGeneratedColumn("uuid")
id: string;

@Column("jsonb", { default: {} })
balances: Record<string, number>; // asset → amount

@Column({ type: "timestamp", nullable: true })
snoozedUntil: Date | null;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd -i 'user.entity.ts' backend/src
rg -n --no-heading 'import\s+\{\s*User\s*\}\s+from\s+"../entities/user\.entity"' backend/src/settlement
rg -n --no-heading 'entities:\s*\[' backend/src

Repository: OlufunbiIK/StellarSplit

Length of output: 1026


🏁 Script executed:

cat -n backend/src/entities/user.entity.ts

Repository: OlufunbiIK/StellarSplit

Length of output: 1206


🏁 Script executed:

rg -n 'from.*settlement/entities/user' backend/src/settlement

Repository: OlufunbiIK/StellarSplit

Length of output: 49


🏁 Script executed:

grep -r 'balances\|snoozedUntil' backend/src --include='*.ts' | head -20

Repository: OlufunbiIK/StellarSplit

Length of output: 2307


🏁 Script executed:

rg -A2 'Object.values\(u\.balances\)' backend/src/settlement/settlement.service.ts

Repository: OlufunbiIK/StellarSplit

Length of output: 200


🏁 Script executed:

cat -n backend/src/settlement/settlement.service.ts | sed -n '1,60p'

Repository: OlufunbiIK/StellarSplit

Length of output: 2739


🏁 Script executed:

cat -n backend/src/settlement/settlement.repository.ts | head -50

Repository: OlufunbiIK/StellarSplit

Length of output: 835


🏁 Script executed:

rg -B5 'findActiveParticipants' backend/src/settlement/settlement.repository.ts | head -30

Repository: OlufunbiIK/StellarSplit

Length of output: 289


🏁 Script executed:

rg -n 'snoozedUntil' backend/src/entities/user.entity.ts

Repository: OlufunbiIK/StellarSplit

Length of output: 49


🏁 Script executed:

rg -n 'balances' backend/src/entities/user.entity.ts

Repository: OlufunbiIK/StellarSplit

Length of output: 49


Fix import path to use the new settlement-scoped User entity.

Settlement code in settlement.repository.ts and settlement.service.ts imports User from ../entities/user.entity, which lacks the balances and snoozedUntil columns required by settlement queries. The new User entity at backend/src/settlement/entities/user.entity.ts defines these properties but is never imported or used. Update all settlement module imports to reference ./entities/user.entity instead, or relocate these fields to the shared User entity if they should be global.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/settlement/entities/user.entity.ts` around lines 3 - 13, The
settlement code is importing the wrong User entity lacking balances and
snoozedUntil; update the imports in settlement.repository.ts and
settlement.service.ts to import the settlement-scoped User class (the User
entity that declares balances: Record<string, number> and snoozedUntil: Date |
null) instead of the old ../entities/user.entity, or alternatively consolidate
by moving balances and snoozedUntil into the shared User entity; ensure the
import statements reference the file that contains the User class with the
`@Column`("jsonb") balances and `@Column`({ type: "timestamp", nullable: true })
snoozedUntil so all settlement queries use the correct entity.

Comment on lines +11 to +26
router.post("/snooze", async (req, res) => {
const { userId, until } = req.body;
await snoozeSettlement(userId, new Date(until));
res.json({ success: true });
});

@Get("net-position")
async getNetPosition(@Req() req: any) {
return this.settlementService.calculateNetPosition(req.user.walletAddress);
}
router.post("/unsnooze", async (req, res) => {
const { userId } = req.body;
await unsnoozeSettlement(userId);
res.json({ success: true });
});

@Post("suggestions/snooze")
async snooze(@Req() req: any) {
return this.settlementService.snoozeSuggestions(req.user.id);
}
router.post("/verify", async (req, res) => {
const { user, asset, amount } = req.body;
const verified = await verifySettlement(user, asset, amount);
res.json({ verified });
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Don't authorize user-scoped actions from request bodies.

/snooze, /unsnooze, and /verify all trust userId/user from req.body. Once this route is reachable, any caller can snooze, unsnooze, or verify on behalf of another user by posting a different identifier. Resolve the subject from the authenticated principal and validate only the user-editable fields.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/settlement/settlement.controller.ts` around lines 11 - 26, Routes
router.post("/snooze"), router.post("/unsnooze"), and router.post("/verify")
currently trust userId/user from req.body; instead resolve the subject from the
authenticated principal (e.g., req.user, req.auth, or token-derived id) and pass
that to snoozeSettlement, unsnoozeSettlement, and verifySettlement; keep only
user-editable fields (like until, asset, amount) in req.body and validate them,
reject or ignore any userId/user in the body, and return a 4xx if the principal
is missing or unauthorized before calling the functions.

Comment on lines +1 to +4
import { AppDataSource } from "../data-source";
import { User } from "../entities/user.entity";

export const userRepo = AppDataSource.getRepository(User);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n --no-heading '\bAppDataSource\b' backend/src
rg -n --no-heading '\.initialize\s*\(' backend/src
rg -n --no-heading 'TypeOrmModule\.forRootAsync' backend/src/app.module.ts

Repository: OlufunbiIK/StellarSplit

Length of output: 828


🏁 Script executed:

cat -n backend/src/database/data-source.ts

Repository: OlufunbiIK/StellarSplit

Length of output: 1016


🏁 Script executed:

fd -type f -name "main.ts" -o -name "index.ts" -o -name "bootstrap.ts" | head -5 | xargs cat -n 2>/dev/null || echo "No bootstrap files found, checking package.json"

Repository: OlufunbiIK/StellarSplit

Length of output: 237


🏁 Script executed:

cat -n backend/package.json | grep -A 5 -B 5 "main\|start" || echo "Checking tsconfig or main entry"

Repository: OlufunbiIK/StellarSplit

Length of output: 684


🏁 Script executed:

cat -n backend/src/main.ts

Repository: OlufunbiIK/StellarSplit

Length of output: 6471


🏁 Script executed:

cat -n backend/src/app.module.ts

Repository: OlufunbiIK/StellarSplit

Length of output: 5914


userRepo uses an uninitialized standalone AppDataSource instead of Nest's managed DataSource.

settlement.repository.ts imports AppDataSource from database/data-source.ts and exports a repository bound to it. However, AppDataSource.initialize() is never called during app bootstrap. The app instead configures its own DataSource through TypeOrmModule.forRootAsync() in app.module.ts. Any attempt to use userRepo at runtime will fail because the underlying connection is uninitialized. Inject Nest's DataSource or a Repository<User> token instead of using this singleton export.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/settlement/settlement.repository.ts` around lines 1 - 4, The file
exports a standalone userRepo bound to AppDataSource which is never initialized;
replace this pattern with Nest DI by removing the singleton export and instead
obtain the repository from the app-managed DataSource/TypeORM module—e.g., stop
using AppDataSource.getRepository(User) and refactor so that callers receive
either an injected DataSource or an injected Repository<User> (use the
Repository<User> token or inject DataSource in a provider/service and call
dataSource.getRepository(User) there); update any consumers of userRepo to
accept the injected Repository<User> or a service wrapper that resolves the
repository from the injected DataSource.

Comment on lines +6 to +13
export async function findActiveParticipants(): Promise<User[]> {
const now = new Date();
return userRepo.find({
where: [
{ snoozedUntil: null },
{ snoozedUntil: { $lt: now } }
]
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

In TypeORM 0.3.28, is { snoozedUntil: { $lt: now } }supported infind({ where: ... })for Postgres, or shouldLessThan(now)/IsNull() be used?

💡 Result:

No, { snoozedUntil: { $lt: now } } is not supported in find({ where: ... }) for PostgreSQL in TypeORM 0.3.28. Use LessThan(now) instead. TypeORM's relational database drivers (including Postgres) use specific FindOperator functions like LessThan, IsNull, etc., in the where clause of find methods. MongoDB-style operators like $lt are not supported for Postgres and result in SQL errors, as shown in examples where { field: { $lt: value } } fails with "invalid input syntax". Official documentation explicitly shows LessThan(10) generating WHERE "likes" < 10, with no mention of $lt. Changelogs for 0.3.x confirm MongoDB-specific ObjectLiteral support (e.g., PR #9632) does not extend to relational DBs. IsNull is the standard for null checks across drivers.

Citations:


🏁 Script executed:

# First, find and check TypeORM version
fd -t f "package.json" | head -5 | xargs cat 2>/dev/null | grep -A 5 -B 5 "typeorm"

Repository: OlufunbiIK/StellarSplit

Length of output: 1412


🏁 Script executed:

# Check the actual file content
cat -n backend/src/settlement/settlement.repository.ts 2>/dev/null || echo "File not found, searching for settlement.repository.ts"
fd "settlement.repository.ts"

Repository: OlufunbiIK/StellarSplit

Length of output: 883


Use TypeORM FindOperators; MongoDB-style $lt is invalid for relational databases.

In TypeORM 0.3.28, { snoozedUntil: { $lt: now } } syntax is not supported for PostgreSQL and will generate invalid SQL. Use LessThan(now) and IsNull() instead to ensure expired snoozes re-activate correctly.

Suggested fix
+import { IsNull, LessThan } from "typeorm";
+
 export async function findActiveParticipants(): Promise<User[]> {
   const now = new Date();
   return userRepo.find({
     where: [
-      { snoozedUntil: null },
-      { snoozedUntil: { $lt: now } }
+      { snoozedUntil: IsNull() },
+      { snoozedUntil: LessThan(now) },
     ]
   });
 }
📝 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 async function findActiveParticipants(): Promise<User[]> {
const now = new Date();
return userRepo.find({
where: [
{ snoozedUntil: null },
{ snoozedUntil: { $lt: now } }
]
});
import { IsNull, LessThan } from "typeorm";
export async function findActiveParticipants(): Promise<User[]> {
const now = new Date();
return userRepo.find({
where: [
{ snoozedUntil: IsNull() },
{ snoozedUntil: LessThan(now) }
]
});
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/settlement/settlement.repository.ts` around lines 6 - 13, The
findActiveParticipants function uses a MongoDB-style filter ({ snoozedUntil: {
$lt: now } }) which is invalid for TypeORM; update the userRepo.find call to use
TypeORM FindOperator helpers—replace the array where clause with conditions
using IsNull() for snoozedUntil and LessThan(now) to match expired snoozes
(refer to the findActiveParticipants function and the snoozedUntil property in
the userRepo.find call) so the query becomes a proper TypeORM query that returns
users with snoozedUntil IS NULL or snoozedUntil < now.

Comment on lines +15 to 24
// Example deterministic logic
const total = participants.reduce((sum, u) => sum + Object.values(u.balances).reduce((a, b) => a + b, 0), 0);

return {
owes: parseFloat(stats.owes || 0),
owed: parseFloat(stats.owed || 0),
net: parseFloat(stats.owed || 0) - parseFloat(stats.owes || 0),
};
if (total === 0) {
suggestions.push({ participants: participants.map(p => p.id), status: "completed", details: "All balances settled" });
} else if (total > 0) {
suggestions.push({ participants: participants.map(p => p.id), status: "partial", details: "Some balances remain unsettled" });
} else {
suggestions.push({ participants: participants.map(p => p.id), status: "invalid", details: "Balances mismatch" });
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

A single numeric total cannot tell you whether settlement is complete.

This folds every entry in balances into one number and treats exact 0 as completed. Opposing balances in different assets—or even no participants at all—can be reported as settled, and === 0 is brittle for money. Compute settlement per asset with decimal-safe math.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/settlement/settlement.service.ts` around lines 15 - 24, The
current reduction using participants.reduce and summing
Object.values(u.balances) into a single numeric total (and then using total ===
0) is incorrect and brittle for money and multi-asset balances; update the logic
in the settlement calculation (the block that currently computes total and calls
suggestions.push) to: aggregate balances per asset key across all participants
using a decimal-safe approach (e.g., Big.js or integer smallest-unit arithmetic)
instead of a single Number, treat an empty participants array explicitly,
determine settlement status by checking each per-asset aggregate is exactly zero
(using the decimal-safe equality) and push suggestions with status "completed"
only if every asset total is zero, "partial" if some assets are non-zero, and
"invalid" if mismatched conditions apply; reference the existing symbols
participants, u.balances, and suggestions.push when making the change.

Comment on lines +15 to +17
<p>Transaction Hash: {result.txHash}</p>
<button onClick={() => onBackToSplit(result.txHash)}>Back to Split</button>
</div>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use split id for navigation, not transaction hash.

The success action currently passes txHash into onBackToSplit, which breaks split routing.

Suggested update
 interface Props {
   result: PaymentResult;
+  splitId?: string;
   onRetry: () => void;
   onBackToSplit: (splitId?: string) => void;
 }
 
-export const PaymentResult: React.FC<Props> = ({ result, onRetry, onBackToSplit }) => {
+export const PaymentResult: React.FC<Props> = ({ result, splitId, onRetry, onBackToSplit }) => {
   if (result.success) {
     return (
       <div>
         <h2>Payment Successful</h2>
         <p>Transaction Hash: {result.txHash}</p>
-        <button onClick={() => onBackToSplit(result.txHash)}>Back to Split</button>
+        <button onClick={() => onBackToSplit(splitId)}>Back to Split</button>
       </div>
     );
   }
📝 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
<p>Transaction Hash: {result.txHash}</p>
<button onClick={() => onBackToSplit(result.txHash)}>Back to Split</button>
</div>
interface Props {
result: PaymentResult;
splitId?: string;
onRetry: () => void;
onBackToSplit: (splitId?: string) => void;
}
export const PaymentResult: React.FC<Props> = ({ result, splitId, onRetry, onBackToSplit }) => {
if (result.success) {
return (
<div>
<h2>Payment Successful</h2>
<p>Transaction Hash: {result.txHash}</p>
<button onClick={() => onBackToSplit(splitId)}>Back to Split</button>
</div>
);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/Payment/PaymentResult.tsx` around lines 15 - 17, In
PaymentResult, the Back to Split button is calling onBackToSplit(result.txHash)
which breaks routing; change it to pass the split identifier (e.g.,
result.splitId or result.split_id depending on the payload) instead of txHash
and update the onBackToSplit prop type/signature if necessary so PaymentResult
and its parent expect a splitId; locate the PaymentResult component and the
onBackToSplit usage to replace result.txHash with the correct split id field and
ensure any caller/handler (router/navigation) uses that split id for navigation.

Comment on lines +13 to +17
const uri = window.location.search; // assume ?uri=...
if (!request && uri) {
const parsed = parsePaymentUri(new URLSearchParams(uri).get("uri") || "");
setRequest(parsed);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify state updates are not performed during render and are moved into an effect.
rg -nP 'setRequest\(' frontend/src/pages/PaymentURIPage.tsx -C2
rg -nP 'useEffect\s*\(' frontend/src/pages/PaymentURIPage.tsx -C2

Repository: OlufunbiIK/StellarSplit

Length of output: 214


🏁 Script executed:

# Get more context about the component structure and parsePaymentUri
cat -n frontend/src/pages/PaymentURIPage.tsx | head -40

Repository: OlufunbiIK/StellarSplit

Length of output: 1758


🏁 Script executed:

# Find and examine parsePaymentUri to understand error handling
rg -n "parsePaymentUri" --type tsx --type ts -A 5 | head -50

Repository: OlufunbiIK/StellarSplit

Length of output: 94


🏁 Script executed:

# Check if React.StrictMode is used in the app (which would amplify render phase errors)
rg -n "StrictMode" --type tsx --type ts

Repository: OlufunbiIK/StellarSplit

Length of output: 94


🏁 Script executed:

# Find parsePaymentUri without type restriction
rg -n "parsePaymentUri" -A 10 | head -60

Repository: OlufunbiIK/StellarSplit

Length of output: 2868


🏁 Script executed:

# Check for StrictMode usage without type restriction
rg -n "StrictMode"

Repository: OlufunbiIK/StellarSplit

Length of output: 279


🏁 Script executed:

# Check error handling in parsePaymentUri and whether it can throw
rg -n "throw\|Error\|catch" frontend/src/utils/stellar/paymentUri.ts

Repository: OlufunbiIK/StellarSplit

Length of output: 49


Move URI parsing/state update out of render and guard parse failures.

Calling setRequest while rendering is unstable and causes unhandled errors. The parsePaymentUri function calls new URL(uri) which throws on invalid or empty input, crashing the component instead of gracefully handling invalid payment requests.

Suggested update
-import React, { useState } from "react";
+import React, { useEffect, useState } from "react";
@@
-  const uri = window.location.search; // assume ?uri=...
-  if (!request && uri) {
-    const parsed = parsePaymentUri(new URLSearchParams(uri).get("uri") || "");
-    setRequest(parsed);
-  }
+  useEffect(() => {
+    const rawUri = new URLSearchParams(window.location.search).get("uri");
+    if (!rawUri) return;
+    try {
+      setRequest(parsePaymentUri(rawUri));
+    } catch {
+      setRequest(null);
+    }
+  }, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/pages/PaymentURIPage.tsx` around lines 13 - 17, The component is
calling parsePaymentUri and setRequest during render which can throw
(parsePaymentUri uses new URL) and cause crashes; move the URI extraction and
state update into a useEffect that runs once (or when window.location.search
changes) and only attempt parsing when the uri param exists, wrapping
parsePaymentUri in a try/catch to guard against invalid/empty input and avoid
calling setRequest on parse failure; refer to the parsePaymentUri function and
the setRequest state updater (and the local variable uri / request) when making
these changes so the effect reads new
URLSearchParams(window.location.search).get("uri"), tries parsing, and calls
setRequest(parsed) only on success.

Comment on lines +22 to +24
// TODO: integrate Stellar SDK transaction submission
const txHash = "mock-tx-hash";
setResult({ success: true, txHash });
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Replace mocked tx hash with real submission call.

This path is still stubbed, so the “submit payment” flow is not production-ready and cannot return real success/failure outcomes.

Suggested update
+import { signAndSubmitPayment } from "../utils/stellar/wallet";
@@
-      // TODO: integrate Stellar SDK transaction submission
-      const txHash = "mock-tx-hash"; 
-      setResult({ success: true, txHash });
+      const submission = await signAndSubmitPayment(request.amount, request.to);
+      if (!submission.success) {
+        throw new Error("Transaction submission failed");
+      }
+      setResult({ success: true, txHash: submission.txHash, splitId: request.splitId });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/pages/PaymentURIPage.tsx` around lines 22 - 24, In
PaymentURIPage.tsx replace the stubbed txHash ("mock-tx-hash") and the immediate
setResult call with a real Stellar SDK submission: build or decode the
transaction (where the mock is currently set), call server or use the
StellarSdk.Server().submitTransaction(...) to submit the XDR, await the
response, then call setResult({ success: true, txHash: response.hash }) on
success or setResult({ success: false, error: err.message || err }) on failure;
ensure you catch and log/propagate submission errors and keep existing UI state
handling around the submit handler in this component.

Comment on lines +10 to +14
export interface PaymentResult {
success: boolean;
txHash?: string;
error?: string;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Include splitId in PaymentResult contract.

The result model should carry navigation context; otherwise consumers tend to repurpose txHash for split routing.

Suggested update
 export interface PaymentResult {
   success: boolean;
   txHash?: string;
   error?: string;
+  splitId?: string;
 }
📝 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 interface PaymentResult {
success: boolean;
txHash?: string;
error?: string;
}
export interface PaymentResult {
success: boolean;
txHash?: string;
error?: string;
splitId?: string;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/types/payment.ts` around lines 10 - 14, The PaymentResult
interface is missing splitId, causing callers to reuse txHash for routing;
update the PaymentResult type (export interface PaymentResult) to include a
splitId?: string property (optional string) and then update any related
consumers/return sites to populate and consume splitId instead of overloading
txHash; verify functions that construct or return PaymentResult (e.g., payment
handlers, createPaymentResult helpers) are updated to set splitId where
available.

Comment on lines +5 to 13
const url = new URL(uri);
return {
uri,
destination,
amount,
assetCode,
assetIssuer,
memo: params.get('memo') ?? undefined,
memoType: memoTypeRaw as StellarPaymentRequest['memoType'],
message: params.get('msg') ?? undefined,
callback: params.get('callback') ?? undefined,
splitId: params.get('split_id') ?? undefined,
asset: url.searchParams.get("asset") || "XLM",
amount: Number(url.searchParams.get("amount")),
memo: url.searchParams.get("memo") || undefined,
from: url.searchParams.get("from") || "",
to: url.searchParams.get("to") || "",
splitId: url.searchParams.get("splitId") || undefined,
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate required URI fields before returning PaymentRequest.

amount and destination are not validated. Invalid/missing input can flow through as NaN/empty destination and break downstream payment handling.

Suggested update
 export function parsePaymentUri(uri: string): PaymentRequest {
   // Example: stellar:pay?asset=USD&amount=50&memo=Lunch&to=GABC...
-  const url = new URL(uri);
+  const url = new URL(uri);
+  const amountRaw = url.searchParams.get("amount");
+  const amount = Number(amountRaw);
+  const to = url.searchParams.get("to") || "";
+
+  if (!Number.isFinite(amount) || amount <= 0) {
+    throw new Error("Invalid payment amount");
+  }
+  if (!to) {
+    throw new Error("Missing payment destination");
+  }
+
   return {
     asset: url.searchParams.get("asset") || "XLM",
-    amount: Number(url.searchParams.get("amount")),
+    amount,
     memo: url.searchParams.get("memo") || undefined,
     from: url.searchParams.get("from") || "",
-    to: url.searchParams.get("to") || "",
+    to,
     splitId: url.searchParams.get("splitId") || undefined,
   };
 }
📝 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 url = new URL(uri);
return {
uri,
destination,
amount,
assetCode,
assetIssuer,
memo: params.get('memo') ?? undefined,
memoType: memoTypeRaw as StellarPaymentRequest['memoType'],
message: params.get('msg') ?? undefined,
callback: params.get('callback') ?? undefined,
splitId: params.get('split_id') ?? undefined,
asset: url.searchParams.get("asset") || "XLM",
amount: Number(url.searchParams.get("amount")),
memo: url.searchParams.get("memo") || undefined,
from: url.searchParams.get("from") || "",
to: url.searchParams.get("to") || "",
splitId: url.searchParams.get("splitId") || undefined,
};
const url = new URL(uri);
const amountRaw = url.searchParams.get("amount");
const amount = Number(amountRaw);
const to = url.searchParams.get("to") || "";
if (!Number.isFinite(amount) || amount <= 0) {
throw new Error("Invalid payment amount");
}
if (!to) {
throw new Error("Missing payment destination");
}
return {
asset: url.searchParams.get("asset") || "XLM",
amount,
memo: url.searchParams.get("memo") || undefined,
from: url.searchParams.get("from") || "",
to,
splitId: url.searchParams.get("splitId") || undefined,
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/utils/stellar/paymentUri.ts` around lines 5 - 13, The current
parsing returns amount and to without validation so NaN/empty strings can leak;
update the parsing function (where URL is constructed into `url` and the
returned object with properties asset, amount, memo, from, to, splitId) to
validate that `amount` is present and a finite positive number (use Number(...)
and Number.isFinite / > 0) and that `to` (destination) is a non-empty string; if
validation fails, throw or return a clearly typed error (or a Result/nullable)
so downstream payment handlers won't receive NaN or empty destination values.

@OlufunbiIK OlufunbiIK merged commit 4900882 into OlufunbiIK:main Mar 28, 2026
3 of 6 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.

Build a Production-Ready Payment Request and Settlement UX

2 participants