This commit is contained in:
Roba Boru
2026-06-24 11:56:30 +03:00
9 changed files with 426 additions and 324 deletions

View File

@@ -1,5 +1,6 @@
import { Body, Controller, Delete, Get, Param, Post, Patch, UseGuards, Query, Req, BadRequestException } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth, ApiResponse, ApiQuery, ApiBody } from '@nestjs/swagger';
import { IsPublic } from '@tria-plc/api-common/modules/auth/decorators/public.decorator';
import { BookingsService } from './bookings.service';
import { GuestBookingService } from './guest-booking.service';
import { CreateBookingDto, ModifyBookingDto, CancelBookingDto } from './bookings.dto';
@@ -44,7 +45,8 @@ export class BookingsController {
}
@Get('by-device')
@ApiOperation({
@IsPublic()
@ApiOperation({
summary: 'Get bookings by device ID',
description: 'Returns all bookings associated with a device ID (for guest users). Includes saved passenger details and booking history.'
})
@@ -98,7 +100,8 @@ export class BookingsController {
}
@Post('guest')
@ApiOperation({
@IsPublic()
@ApiOperation({
summary: 'Create guest booking — ONE_WAY | ROUND_TRIP | TRANSIT | ROUND_TRIP_TRANSIT (no login required)',
description: `Creates a booking without requiring login. Supports all four booking types.

View File

@@ -17,6 +17,7 @@ import {
ApiOkResponse,
ApiProduces,
} from "@nestjs/swagger";
import { IsPublic } from "@tria-plc/api-common/modules/auth/decorators/public.decorator";
import { Response } from "express";
import { PaymentsService } from "./payments.service";
import {
@@ -62,6 +63,7 @@ export class PaymentsController {
}
@Post("initiate")
@IsPublic()
@ApiOperation({
summary: "Initiate payment with nationality-based payment methods",
description: `Initiates payment for a booking with support for multiple payment providers:\n\n**Ethiopian Payment Methods:**\n- TELEBIRR - Ethiopia's leading mobile money\n- CBE_BIRR - Commercial Bank of Ethiopia\n- EBIRR - Electronic payment gateway\n\n**Djiboutian Payment Methods:**\n- WAAFI - Djibouti's mobile money service\n\n**International Payment Methods:**\n- CARD - Visa, Mastercard\n- WALLET - Internal wallet balance\n\n**Multi-Currency:**\n- All transactions processed in ETB\n- Display amounts in ETB, DJF, or USD\n- Real-time exchange rate conversion`,
@@ -71,12 +73,14 @@ export class PaymentsController {
}
@Get("intents/:bookingId")
@IsPublic()
@ApiOperation({ summary: "Get payment intent status for a booking" })
getIntent(@Param("bookingId") bookingId: string) {
return this.service.getIntentByBookingId(bookingId);
}
@Get("waafi/return")
@IsPublic()
@ApiOperation({
summary:
"DEMO ONLY — confirm a Waafi payment from the browser-return params and return JSON for the " +
@@ -117,6 +121,7 @@ export class PaymentsController {
}
@Get("methods")
@IsPublic()
@ApiOperation({
summary: "List payment systems supported by the platform",
description:
@@ -129,6 +134,7 @@ export class PaymentsController {
}
@Get("checkout")
@IsPublic()
@ApiOperation({
summary: "Browser checkout redirect",
description:

View File

@@ -1,5 +1,6 @@
import { Body, Controller, Delete, Get, Param, Patch, Post, Query, ParseIntPipe, UseGuards } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth, ApiParam, ApiQuery, ApiResponse } from '@nestjs/swagger';
import { IsPublic } from '@tria-plc/api-common/modules/auth/decorators/public.decorator';
import { SchedulesService } from './schedules.service';
import { CreateScheduleDto, UpdateScheduleDto, CreateFareRuleDto, UpdateScheduleStatusDto, UpdateStopTimeDto, ListSchedulesDto, BulkCreateSchedulesDto, BulkSchedulesResponseDto } from './schedules.dto';
import { JwtGuard } from '../../common/jwt.guard';
@@ -23,6 +24,7 @@ export class SchedulesController {
createSchedule(@Body() dto: CreateScheduleDto) { return this.service.createSchedule(dto); }
@Get()
@IsPublic()
@ApiOperation({ summary: 'List schedules with optional filters' })
@ApiQuery({ name: 'date', required: false })
@ApiQuery({ name: 'routeId', required: false })
@@ -67,6 +69,7 @@ export class SchedulesController {
createSegmentFareRule(@Body() dto: any) { return this.service.createSegmentFareRule(dto); }
@Get('routes/:routeId/segment-fares')
@IsPublic()
@ApiOperation({ summary: 'List all segment fare rules for a route' })
@ApiParam({ name: 'routeId', description: 'Route UUID' })
getSegmentFares(@Param('routeId') routeId: string) { return this.service.getSegmentFares(routeId); }
@@ -86,6 +89,7 @@ export class SchedulesController {
// ===== PARAMETRIZED ROUTES (generic :id routes come AFTER specific routes) =====
@Get(':id')
@IsPublic()
@ApiOperation({ summary: 'Get schedule detail' })
@ApiParam({ name: 'id', description: 'TrainSchedule UUID' })
getSchedule(@Param('id') id: string) { return this.service.getSchedule(id); }
@@ -113,6 +117,7 @@ export class SchedulesController {
deleteSchedule(@Param('id') id: string) { return this.service.deleteSchedule(id); }
@Get(':id/stops')
@IsPublic()
@ApiOperation({ summary: 'List all stops for a schedule' })
@ApiParam({ name: 'id', description: 'TrainSchedule UUID' })
getStops(@Param('id') id: string) { return this.service.getStops(id); }

View File

@@ -1,10 +1,12 @@
import { Body, Controller, Post } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
import { IsPublic } from '@tria-plc/api-common/modules/auth/decorators/public.decorator';
import { SearchService } from './search.service';
import { SearchTripsDto, FareQuoteDto } from './search.dto';
@ApiTags('Search')
@Controller('search')
@IsPublic()
export class SearchController {
constructor(private service: SearchService) {}

View File

@@ -1,5 +1,6 @@
import { Body, Controller, Delete, Get, Param, Patch, Post, UseGuards } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth, ApiParam, ApiResponse, ApiBody } from '@nestjs/swagger';
import { IsPublic } from '@tria-plc/api-common/modules/auth/decorators/public.decorator';
import { SeatClassesService } from './seat-classes.service';
import { CreateSeatClassDto, UpdateSeatClassDto } from './seat-classes.dto';
import { JwtGuard } from '../../common/jwt.guard';
@@ -10,11 +11,13 @@ export class SeatClassesController {
constructor(private service: SeatClassesService) {}
@Get()
@IsPublic()
@ApiOperation({ summary: 'List all seat classes' })
@ApiResponse({ status: 200, description: 'Returns all seat classes with their coaches' })
listSeatClasses() { return this.service.listSeatClasses(); }
@Get(':id')
@IsPublic()
@ApiOperation({ summary: 'Get a seat class by ID' })
@ApiParam({ name: 'id', description: 'Seat class UUID' })
@ApiResponse({ status: 200, description: 'Returns seat class with its coaches' })

View File

@@ -1,36 +1,64 @@
import { Body, Controller, Delete, Get, Param, Post, Patch, Query, UseGuards } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth, ApiParam, ApiQuery, ApiResponse } from '@nestjs/swagger';
import { SeatsService } from './seats.service';
import { HoldSeatsDto } from './seats.dto';
import { JwtGuard } from '../../common/jwt.guard';
import { IamGuard } from '../../common/iam-adapter';
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Patch,
Query,
UseGuards,
} from "@nestjs/common";
import {
ApiTags,
ApiOperation,
ApiBearerAuth,
ApiParam,
ApiQuery,
ApiResponse,
} from "@nestjs/swagger";
import { IsPublic } from "@tria-plc/api-common/modules/auth/decorators/public.decorator";
import { SeatsService } from "./seats.service";
import { HoldSeatsDto } from "./seats.dto";
import { JwtGuard } from "../../common/jwt.guard";
import { IamGuard } from "../../common/iam-adapter";
@ApiTags('Seats')
@Controller('seats')
@ApiTags("Seats")
@Controller("seats")
export class SeatsController {
constructor(private service: SeatsService) {}
// ── Seat Map ──────────────────────────────────────────────────────────────
@Get('seatmap/:scheduleId')
@Get("seatmap/:scheduleId")
@ApiOperation({
summary: 'Get seat map filtered by coach type',
description: `Returns all coaches of the given coachTypeId assigned to the schedule, each with their full seat list and real-time availability. Origin and destination are derived from the schedule. Omit coachTypeId to get all coaches.`
summary: "Get seat map filtered by coach type",
description: `Returns all coaches of the given coachTypeId assigned to the schedule, each with their full seat list and real-time availability. Origin and destination are derived from the schedule. Omit coachTypeId to get all coaches.`,
})
@ApiParam({ name: "scheduleId", description: "TrainSchedule UUID" })
@ApiQuery({
name: "coachTypeId",
required: false,
description:
"Filter by CoachType UUID — returns all coaches of that type (e.g. all Economy coaches)",
})
@ApiResponse({
status: 200,
description:
"List of coaches of the given type with their seats and availability",
})
@ApiParam({ name: 'scheduleId', description: 'TrainSchedule UUID' })
@ApiQuery({ name: 'coachTypeId', required: false, description: 'Filter by CoachType UUID — returns all coaches of that type (e.g. all Economy coaches)' })
@ApiResponse({ status: 200, description: 'List of coaches of the given type with their seats and availability' })
getSeatMap(
@Param('scheduleId') scheduleId: string,
@Query('coachTypeId') coachTypeId?: string,
@Param("scheduleId") scheduleId: string,
@Query("coachTypeId") coachTypeId?: string,
) {
return this.service.getSeatMap(scheduleId, coachTypeId);
}
// ── Hold / Release ────────────────────────────────────────────────────────
@Get('holds')
@UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth')
@Get("holds")
@UseGuards(JwtGuard)
@ApiBearerAuth("JWT-auth")
@ApiOperation({
summary: 'List active seat holds with full leg context',
summary: "List active seat holds with full leg context",
description: `Returns all non-expired holds enriched with:
- **schedule**: train number, departure/arrival, full route origin→destination
- **leg**: the specific origin→destination this hold covers (station name, code, stop sequence)
@@ -39,25 +67,45 @@ export class SeatsController {
This makes it clear which segment of the route each seat is held for, enabling segment-based reuse of the same seat on non-overlapping legs.`,
})
@ApiQuery({ name: 'scheduleId', required: false, description: 'Filter by TrainSchedule UUID' })
@ApiQuery({ name: 'passengerId', required: false, description: 'Filter by Passenger UUID' })
@ApiResponse({ status: 200, description: 'Active holds with schedule, leg, and seat details' })
@ApiQuery({
name: "scheduleId",
required: false,
description: "Filter by TrainSchedule UUID",
})
@ApiQuery({
name: "passengerId",
required: false,
description: "Filter by Passenger UUID",
})
@ApiResponse({
status: 200,
description: "Active holds with schedule, leg, and seat details",
})
getHolds(
@Query('scheduleId') scheduleId?: string,
@Query('passengerId') passengerId?: string,
) { return this.service.getHolds(scheduleId, passengerId); }
@Query("scheduleId") scheduleId?: string,
@Query("passengerId") passengerId?: string,
) {
return this.service.getHolds(scheduleId, passengerId);
}
@Get('holds/:holdId')
@UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth')
@ApiOperation({ summary: 'Get a single hold with full leg context' })
@ApiParam({ name: 'holdId', description: 'SeatHold UUID' })
@ApiResponse({ status: 200, description: 'Hold with schedule, leg, and seat details' })
@ApiResponse({ status: 404, description: 'Hold not found' })
getHold(@Param('holdId') holdId: string) { return this.service.getHold(holdId); }
@Get("holds/:holdId")
@UseGuards(JwtGuard)
@ApiBearerAuth("JWT-auth")
@ApiOperation({ summary: "Get a single hold with full leg context" })
@ApiParam({ name: "holdId", description: "SeatHold UUID" })
@ApiResponse({
status: 200,
description: "Hold with schedule, leg, and seat details",
})
@ApiResponse({ status: 404, description: "Hold not found" })
getHold(@Param("holdId") holdId: string) {
return this.service.getHold(holdId);
}
@Post('hold')
@ApiOperation({
summary: 'Hold seats for 15 minutes before booking (Public - Guest booking supported)',
@Post("hold")
@ApiOperation({
summary:
"Hold seats for 15 minutes before booking (Public - Guest booking supported)",
description: `Temporarily reserves seats for a passenger to complete booking.
**Features:**
@@ -65,74 +113,107 @@ This makes it clear which segment of the route each seat is held for, enabling s
- Auto-release after expiry
- Prevents double booking
- Required before creating booking
- **Public endpoint** - No authentication required (supports guest booking)`
- **Public endpoint** - No authentication required (supports guest booking)`,
})
@ApiResponse({ status: 201, description: 'Seats held successfully with holdId' })
@ApiResponse({ status: 409, description: 'One or more seats unavailable' })
holdSeats(@Body() dto: HoldSeatsDto) { return this.service.holdSeats(dto); }
@ApiResponse({
status: 201,
description: "Seats held successfully with holdId",
})
@ApiResponse({ status: 409, description: "One or more seats unavailable" })
holdSeats(@Body() dto: HoldSeatsDto) {
return this.service.holdSeats(dto);
}
@Delete('hold/:holdId')
@UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth')
@ApiOperation({ summary: 'Release a seat hold' })
@ApiParam({ name: 'holdId', description: 'Hold UUID' })
@ApiResponse({ status: 200, description: 'Hold released' })
@ApiResponse({ status: 404, description: 'Hold not found' })
releaseHold(@Param('holdId') holdId: string) { return this.service.releaseHold(holdId); }
@Delete("hold/:holdId")
@UseGuards(JwtGuard)
@ApiBearerAuth("JWT-auth")
@ApiOperation({ summary: "Release a seat hold" })
@ApiParam({ name: "holdId", description: "Hold UUID" })
@ApiResponse({ status: 200, description: "Hold released" })
@ApiResponse({ status: 404, description: "Hold not found" })
releaseHold(@Param("holdId") holdId: string) {
return this.service.releaseHold(holdId);
}
// ── Seat Block / Unblock ───────────────────────────────────────────────────
@Post(':seatId/block')
@UseGuards(IamGuard) @ApiBearerAuth('IAM-auth')
@ApiOperation({ summary: 'Block a seat (e.g., maintenance, damage)' })
@ApiParam({ name: 'seatId', description: 'Seat UUID' })
@ApiResponse({ status: 200, description: 'Seat blocked' })
blockSeat(@Param('seatId') seatId: string, @Body() body: { reason: string }) {
@Post(":seatId/block")
@UseGuards(IamGuard)
@ApiBearerAuth("IAM-auth")
@ApiOperation({ summary: "Block a seat (e.g., maintenance, damage)" })
@ApiParam({ name: "seatId", description: "Seat UUID" })
@ApiResponse({ status: 200, description: "Seat blocked" })
blockSeat(@Param("seatId") seatId: string, @Body() body: { reason: string }) {
return this.service.blockSeat(seatId, body.reason);
}
@Delete(':seatId/block')
@UseGuards(IamGuard) @ApiBearerAuth('IAM-auth')
@ApiOperation({ summary: 'Unblock a seat' })
@ApiParam({ name: 'seatId', description: 'Seat UUID' })
@ApiResponse({ status: 200, description: 'Seat unblocked' })
unblockSeat(@Param('seatId') seatId: string) {
@Delete(":seatId/block")
@UseGuards(IamGuard)
@ApiBearerAuth("IAM-auth")
@ApiOperation({ summary: "Unblock a seat" })
@ApiParam({ name: "seatId", description: "Seat UUID" })
@ApiResponse({ status: 200, description: "Seat unblocked" })
unblockSeat(@Param("seatId") seatId: string) {
return this.service.unblockSeat(seatId);
}
// ── Remove Seat ────────────────────────────────────────────────────────────
@Patch(':seatId/remove')
@UseGuards(IamGuard) @ApiBearerAuth('IAM-auth')
@ApiOperation({ summary: 'Remove a seat by marking with negative seatNumber' })
@ApiParam({ name: 'seatId', description: 'Seat UUID' })
@ApiResponse({ status: 200, description: 'Seat removed (seatNumber negated), shows as empty space' })
@ApiResponse({ status: 404, description: 'Seat not found' })
removeSeat(@Param('seatId') seatId: string) {
@Patch(":seatId/remove")
@UseGuards(IamGuard)
@ApiBearerAuth("IAM-auth")
@ApiOperation({
summary: "Remove a seat by marking with negative seatNumber",
})
@ApiParam({ name: "seatId", description: "Seat UUID" })
@ApiResponse({
status: 200,
description: "Seat removed (seatNumber negated), shows as empty space",
})
@ApiResponse({ status: 404, description: "Seat not found" })
removeSeat(@Param("seatId") seatId: string) {
return this.service.removeSeat(seatId);
}
@Patch(':seatId/undo-remove')
@UseGuards(IamGuard) @ApiBearerAuth('IAM-auth')
@ApiOperation({ summary: 'Undo seat removal by restoring original seatNumber' })
@ApiParam({ name: 'seatId', description: 'Seat UUID' })
@ApiResponse({ status: 200, description: 'Seat restored (negative seatNumber removed)' })
@ApiResponse({ status: 404, description: 'Seat not found' })
@ApiResponse({ status: 400, description: 'Seat is not removed' })
undoRemoveSeat(@Param('seatId') seatId: string) {
@Patch(":seatId/undo-remove")
@UseGuards(IamGuard)
@ApiBearerAuth("IAM-auth")
@ApiOperation({
summary: "Undo seat removal by restoring original seatNumber",
})
@ApiParam({ name: "seatId", description: "Seat UUID" })
@ApiResponse({
status: 200,
description: "Seat restored (negative seatNumber removed)",
})
@ApiResponse({ status: 404, description: "Seat not found" })
@ApiResponse({ status: 400, description: "Seat is not removed" })
undoRemoveSeat(@Param("seatId") seatId: string) {
return this.service.undoRemoveSeat(seatId);
}
@Get('export/csv/:scheduleId') @UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Export seats as CSV' })
async exportCSV(@Param('scheduleId') scheduleId: string) {
@Get("export/csv/:scheduleId")
@UseGuards(JwtGuard)
@ApiBearerAuth("JWT-auth")
@ApiOperation({ summary: "Export seats as CSV" })
async exportCSV(@Param("scheduleId") scheduleId: string) {
const csv = await this.service.exportSeatsCSV(scheduleId);
return { csv, filename: `seats-${scheduleId}.csv` };
}
@Post('import/preview') @UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Preview CSV import' })
@Post("import/preview")
@UseGuards(JwtGuard)
@ApiBearerAuth("JWT-auth")
@ApiOperation({ summary: "Preview CSV import" })
previewCSV(@Body() body: { csv: string }) {
return this.service.previewSeatsCSV(body.csv);
}
@Post('import/commit') @UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Commit CSV import' })
importCSV(@Body() body: { scheduleId: string; csv: string; commit: boolean }) {
@Post("import/commit")
@UseGuards(JwtGuard)
@ApiBearerAuth("JWT-auth")
@ApiOperation({ summary: "Commit CSV import" })
importCSV(
@Body() body: { scheduleId: string; csv: string; commit: boolean },
) {
return this.service.importSeatsCSV(body.scheduleId, body.csv, body.commit);
}
}

View File

@@ -1,5 +1,6 @@
import { Body, Controller, Get, Param, Post, Patch, Delete, UseGuards, Query } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth, ApiQuery, ApiResponse } from '@nestjs/swagger';
import { IsPublic } from '@tria-plc/api-common/modules/auth/decorators/public.decorator';
import { StationsService } from './stations.service';
import { CreateStationDto } from './stations.dto';
import { JwtGuard } from '../../common/jwt.guard';
@@ -10,7 +11,8 @@ export class StationsController {
constructor(private service: StationsService) {}
@Get()
@ApiOperation({
@IsPublic()
@ApiOperation({
summary: 'List all stations with country information',
description: 'Returns all stations on the Ethio-Djibouti Railway with country codes (ET for Ethiopia, DJ for Djibouti)'
})
@@ -48,7 +50,8 @@ export class StationsController {
}
@Get(':id')
@ApiOperation({
@IsPublic()
@ApiOperation({
summary: 'Get station details by ID',
description: 'Returns station information including name, code, country, coordinates, and facilities'
})

View File

@@ -0,0 +1,85 @@
'use client';
import { useEffect, useState } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { useBookingStore } from '@/lib/booking-store';
import { usePaymentStore } from '@/lib/payment-store';
import { apiClient } from '@/lib/api-client';
import { CheckCircle, Loader2 } from 'lucide-react';
import { Suspense } from 'react';
function DmoneySuccessContent() {
const router = useRouter();
const searchParams = useSearchParams();
const { bookingId } = useBookingStore();
const { updateStatus } = usePaymentStore();
const [status, setStatus] = useState<'processing' | 'done' | 'error'>('processing');
// D-Money callback query params (mirrors Telebirr)
const orderid = searchParams.get('orderid') || '';
const trxRef = searchParams.get('trxRef') || searchParams.get('outTradeNo') || '';
const bookingIdQp = searchParams.get('bookingId') || bookingId || '';
useEffect(() => {
const confirm = async () => {
try {
if (bookingIdQp) {
await apiClient.patch(`/bookings/${bookingIdQp}/confirm`, {
paymentReference: orderid || trxRef,
paymentMethod: 'DMONEY',
});
}
updateStatus('SUCCEEDED');
setStatus('done');
setTimeout(() => router.push('/booking/confirmation'), 1500);
} catch (err: any) {
updateStatus('SUCCEEDED');
setStatus('done');
setTimeout(() => router.push('/booking/confirmation'), 1500);
}
};
confirm();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 flex items-center justify-center px-4">
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-xl p-8 max-w-md w-full text-center">
{status === 'processing' && (
<>
<Loader2 className="w-14 h-14 text-primary animate-spin mx-auto mb-4" />
<h1 className="text-xl font-bold text-gray-900 dark:text-white mb-2">Confirming payment</h1>
<p className="text-sm text-gray-500 dark:text-gray-400">Please wait while we confirm your D-Money payment.</p>
</>
)}
{status === 'done' && (
<>
<CheckCircle className="w-14 h-14 text-green-500 mx-auto mb-4" />
<h1 className="text-xl font-bold text-gray-900 dark:text-white mb-2">Payment Successful!</h1>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">Your D-Money payment was received.</p>
{orderid && <p className="text-xs text-gray-400">Order ID: {orderid}</p>}
{trxRef && <p className="text-xs text-gray-400">Transaction Ref: {trxRef}</p>}
<p className="text-xs text-gray-400 mt-3">Redirecting to your booking confirmation</p>
</>
)}
{status === 'error' && (
<>
<div className="w-14 h-14 rounded-full bg-red-100 flex items-center justify-center mx-auto mb-4">
<span className="text-3xl"></span>
</div>
<h1 className="text-xl font-bold text-gray-900 dark:text-white mb-2">Something went wrong</h1>
<p className="text-sm text-red-500 mb-4">Unable to confirm payment</p>
<button onClick={() => router.push('/booking/confirmation')}
className="btn-primary w-full">Go to confirmation</button>
</>
)}
</div>
</div>
);
}
export default function DmoneySuccessPage() {
return <Suspense fallback={<div className="min-h-screen flex items-center justify-center"><Loader2 className="w-10 h-10 animate-spin text-primary" /></div>}><DmoneySuccessContent /></Suspense>;
}