From af0d5bc74ab051e07d73d0d400f8f8d2a877859b Mon Sep 17 00:00:00 2001 From: wisdom uzoma Date: Wed, 25 Mar 2026 21:32:46 +0100 Subject: [PATCH 1/2] feat: setup global request validation and payment DTOs --- .../payments/dto/create-payment-intent.dto.ts | 19 +++++++++++++ apps/api/src/payments/enums/currency.enum.ts | 5 ++++ apps/api/src/payments/payments.controller.ts | 19 +++++++++++++ apps/api/src/payments/payments.module.ts | 9 +++++++ apps/api/src/payments/payments.service.ts | 27 +++++++++++++++++++ 5 files changed, 79 insertions(+) create mode 100644 apps/api/src/payments/dto/create-payment-intent.dto.ts create mode 100644 apps/api/src/payments/enums/currency.enum.ts create mode 100644 apps/api/src/payments/payments.controller.ts create mode 100644 apps/api/src/payments/payments.module.ts create mode 100644 apps/api/src/payments/payments.service.ts diff --git a/apps/api/src/payments/dto/create-payment-intent.dto.ts b/apps/api/src/payments/dto/create-payment-intent.dto.ts new file mode 100644 index 0000000..6267273 --- /dev/null +++ b/apps/api/src/payments/dto/create-payment-intent.dto.ts @@ -0,0 +1,19 @@ +import { IsEnum, IsNumber, IsObject, IsOptional, Min } from 'class-validator'; +import { Type } from 'class-transformer'; +import { Currency } from '../enums/currency.enum.js'; + +export class CreatePaymentIntentDto { + @IsNumber() + @Min(0.01, { message: 'amount must be at least 0.01' }) + @Type(() => Number) + amount!: number; + + @IsEnum(Currency, { + message: `currency must be one of: ${Object.values(Currency).join(', ')}`, + }) + currency!: Currency; + + @IsOptional() + @IsObject() + metadata?: Record; +} diff --git a/apps/api/src/payments/enums/currency.enum.ts b/apps/api/src/payments/enums/currency.enum.ts new file mode 100644 index 0000000..38e8b7e --- /dev/null +++ b/apps/api/src/payments/enums/currency.enum.ts @@ -0,0 +1,5 @@ +export enum Currency { + USDC = 'USDC', + EURC = 'EURC', + XLM = 'XLM', +} diff --git a/apps/api/src/payments/payments.controller.ts b/apps/api/src/payments/payments.controller.ts new file mode 100644 index 0000000..0bab15c --- /dev/null +++ b/apps/api/src/payments/payments.controller.ts @@ -0,0 +1,19 @@ +import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common'; +import { CurrentMerchant } from '../auth/decorators/current-merchant.decorator.js'; +import { type MerchantUser } from '../auth/interfaces/merchant-user.interface.js'; +import { CreatePaymentIntentDto } from './dto/create-payment-intent.dto.js'; +import { PaymentsService, type PaymentIntent } from './payments.service.js'; + +@Controller('payments') +export class PaymentsController { + constructor(private readonly paymentsService: PaymentsService) {} + + @Post('intents') + @HttpCode(HttpStatus.CREATED) + createPaymentIntent( + @Body() dto: CreatePaymentIntentDto, + @CurrentMerchant() merchant: MerchantUser, + ): PaymentIntent { + return this.paymentsService.createPaymentIntent(dto, merchant.merchant_id); + } +} diff --git a/apps/api/src/payments/payments.module.ts b/apps/api/src/payments/payments.module.ts new file mode 100644 index 0000000..c4167a1 --- /dev/null +++ b/apps/api/src/payments/payments.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { PaymentsController } from './payments.controller.js'; +import { PaymentsService } from './payments.service.js'; + +@Module({ + controllers: [PaymentsController], + providers: [PaymentsService], +}) +export class PaymentsModule {} diff --git a/apps/api/src/payments/payments.service.ts b/apps/api/src/payments/payments.service.ts new file mode 100644 index 0000000..dcb1e74 --- /dev/null +++ b/apps/api/src/payments/payments.service.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@nestjs/common'; +import { type CreatePaymentIntentDto } from './dto/create-payment-intent.dto.js'; + +export interface PaymentIntent { + id: string; + merchantId: string; + amount: number; + currency: string; + metadata?: Record; + status: 'pending'; + createdAt: string; +} + +@Injectable() +export class PaymentsService { + createPaymentIntent(dto: CreatePaymentIntentDto, merchantId: string): PaymentIntent { + return { + id: crypto.randomUUID(), + merchantId, + amount: dto.amount, + currency: dto.currency, + metadata: dto.metadata, + status: 'pending', + createdAt: new Date().toISOString(), + }; + } +} From 7e81624dd860395e9c269716dabb14d1ef90c598 Mon Sep 17 00:00:00 2001 From: wisdom uzoma Date: Sun, 29 Mar 2026 22:50:17 +0100 Subject: [PATCH 2/2] fix(frontend): escape apostrophes and remove Math.random from render --- apps/frontend/src/app/checkout/page.tsx | 4 +- apps/frontend/src/app/dashboard/page.tsx | 150 ++++++++++++----------- 2 files changed, 78 insertions(+), 76 deletions(-) diff --git a/apps/frontend/src/app/checkout/page.tsx b/apps/frontend/src/app/checkout/page.tsx index 98dbd5b..95cb110 100644 --- a/apps/frontend/src/app/checkout/page.tsx +++ b/apps/frontend/src/app/checkout/page.tsx @@ -511,7 +511,7 @@ export default function PaymentCheckout() { onClick={handleBankPayment} className="w-full bg-white hover:bg-zinc-100 text-black h-14 rounded-xl mt-6 text-base transition-all duration-200 shadow-lg shadow-white/10" > - I've Completed the Transfer + I've Completed the Transfer

@@ -611,7 +611,7 @@ export default function PaymentCheckout() { onClick={handleCryptoDetection} className="w-full bg-white hover:bg-zinc-100 text-black h-14 rounded-xl mt-6 text-base transition-all duration-200 shadow-lg shadow-white/10" > - I've Sent the Payment + I've Sent the Payment

diff --git a/apps/frontend/src/app/dashboard/page.tsx b/apps/frontend/src/app/dashboard/page.tsx index 68a7042..ef7b64e 100644 --- a/apps/frontend/src/app/dashboard/page.tsx +++ b/apps/frontend/src/app/dashboard/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { motion } from "motion/react"; +import { motion } from 'motion/react'; import { TrendingUp, TrendingDown, @@ -10,90 +10,90 @@ import { Clock, CheckCircle2, AlertCircle, -} from "lucide-react"; +} from 'lucide-react'; const stats = [ { - label: "Total Volume (30d)", - value: "$12,847,392.45", - change: "+18.2%", - trend: "up", + label: 'Total Volume (30d)', + value: '$12,847,392.45', + change: '+18.2%', + trend: 'up', icon: DollarSign, }, { - label: "Settlement Balance", - value: "$2,103,482.12", - change: "+5.4%", - trend: "up", + label: 'Settlement Balance', + value: '$2,103,482.12', + change: '+5.4%', + trend: 'up', icon: Activity, }, { - label: "Pending Settlements", - value: "47", - change: "-12.3%", - trend: "down", + label: 'Pending Settlements', + value: '47', + change: '-12.3%', + trend: 'down', icon: Clock, }, { - label: "Reserve Ratio", - value: "127.3%", - change: "+2.1%", - trend: "up", + label: 'Reserve Ratio', + value: '127.3%', + change: '+2.1%', + trend: 'up', icon: CheckCircle2, }, ]; const assets = [ - { symbol: "sUSDC", balance: "1,245,382.45", usd: "1,245,382.45", change: "+2.3%" }, - { symbol: "sBTC", balance: "12.4583", usd: "625,847.92", change: "+5.1%" }, - { symbol: "sETH", balance: "145.2341", usd: "232,251.75", change: "-1.2%" }, + { symbol: 'sUSDC', balance: '1,245,382.45', usd: '1,245,382.45', change: '+2.3%' }, + { symbol: 'sBTC', balance: '12.4583', usd: '625,847.92', change: '+5.1%' }, + { symbol: 'sETH', balance: '145.2341', usd: '232,251.75', change: '-1.2%' }, ]; const transactions = [ { - id: "pay_9k2j3n4k5j6h", - type: "Payment", - asset: "sUSDC", - amount: "+12,450.00", - status: "completed", - time: "2m ago", - hash: "0x7a8f9b2c...4e5d6f1a", + id: 'pay_9k2j3n4k5j6h', + type: 'Payment', + asset: 'sUSDC', + amount: '+12,450.00', + status: 'completed', + time: '2m ago', + hash: '0x7a8f9b2c...4e5d6f1a', }, { - id: "pay_8h1j2k3l4m5n", - type: "Redemption", - asset: "sBTC", - amount: "-0.2341", - status: "completed", - time: "5m ago", - hash: "0x3c4d5e6f...7a8b9c0d", + id: 'pay_8h1j2k3l4m5n', + type: 'Redemption', + asset: 'sBTC', + amount: '-0.2341', + status: 'completed', + time: '5m ago', + hash: '0x3c4d5e6f...7a8b9c0d', }, { - id: "pay_7g8h9i0j1k2l", - type: "Payment", - asset: "sETH", - amount: "+5.4321", - status: "pending", - time: "8m ago", - hash: "0x1a2b3c4d...5e6f7g8h", + id: 'pay_7g8h9i0j1k2l', + type: 'Payment', + asset: 'sETH', + amount: '+5.4321', + status: 'pending', + time: '8m ago', + hash: '0x1a2b3c4d...5e6f7g8h', }, { - id: "pay_6f7g8h9i0j1k", - type: "Settlement", - asset: "sUSDC", - amount: "-8,230.50", - status: "completed", - time: "12m ago", - hash: "0x9h8g7f6e...5d4c3b2a", + id: 'pay_6f7g8h9i0j1k', + type: 'Settlement', + asset: 'sUSDC', + amount: '-8,230.50', + status: 'completed', + time: '12m ago', + hash: '0x9h8g7f6e...5d4c3b2a', }, { - id: "pay_5e6f7g8h9i0j", - type: "Payment", - asset: "sBTC", - amount: "+0.1234", - status: "completed", - time: "15m ago", - hash: "0x2b3c4d5e...6f7g8h9i", + id: 'pay_5e6f7g8h9i0j', + type: 'Payment', + asset: 'sBTC', + amount: '+0.1234', + status: 'completed', + time: '15m ago', + hash: '0x2b3c4d5e...6f7g8h9i', }, ]; @@ -109,9 +109,7 @@ export default function OverviewPage() { > Overview -

- Real-time metrics and settlement status -

+

Real-time metrics and settlement status

{/* Stats Grid */} @@ -136,17 +134,17 @@ export default function OverviewPage() { repeatDelay: 2, }} /> - +
- {stat.trend === "up" ? ( + {stat.trend === 'up' ? ( ) : ( @@ -154,7 +152,7 @@ export default function OverviewPage() { {stat.change}
- +
{stat.value}
{stat.label}
@@ -200,7 +198,9 @@ export default function OverviewPage() {
${asset.usd}
-
+
{asset.change}
@@ -211,7 +211,7 @@ export default function OverviewPage() {
@@ -228,7 +228,7 @@ export default function OverviewPage() { transition={{ delay: 0.3 }} >

Reserve Health

- +
{/* Background circle */} @@ -249,9 +249,9 @@ export default function OverviewPage() { stroke="rgba(255,255,255,0.3)" strokeWidth="12" strokeLinecap="round" - initial={{ strokeDasharray: "0 440" }} - animate={{ strokeDasharray: "350 440" }} - transition={{ duration: 1.5, ease: "easeOut" }} + initial={{ strokeDasharray: '0 440' }} + animate={{ strokeDasharray: '350 440' }} + transition={{ duration: 1.5, ease: 'easeOut' }} />
@@ -336,18 +336,20 @@ export default function OverviewPage() { {tx.asset} - + {tx.amount} - {tx.status === "completed" ? ( + {tx.status === 'completed' ? ( ) : (