diff --git a/apps/edr-freight-api/nest-cli.json b/apps/edr-freight-api/nest-cli.json index f4a3b488d..4f4164d16 100644 --- a/apps/edr-freight-api/nest-cli.json +++ b/apps/edr-freight-api/nest-cli.json @@ -5,9 +5,19 @@ "compilerOptions": { "deleteOutDir": true, "assets": [ - { "include": "migrations/**/*", "outDir": "dist" }, - { "include": "contracts/templates/**/*", "watchAssets": true } + { + "include": "migrations/**/*", + "outDir": "dist" + }, + { + "include": "contracts/templates/**/*", + "watchAssets": true + }, + { + "include": "modules/payment/templates/**/*", + "watchAssets": true + } ], "watchAssets": true } -} +} \ No newline at end of file diff --git a/apps/edr-freight-api/src/migrations/1780639311366-CreatePaymentTable.ts b/apps/edr-freight-api/src/migrations/1780639311366-CreatePaymentTable.ts index cbb60d5d4..0382c834b 100644 --- a/apps/edr-freight-api/src/migrations/1780639311366-CreatePaymentTable.ts +++ b/apps/edr-freight-api/src/migrations/1780639311366-CreatePaymentTable.ts @@ -28,48 +28,50 @@ export class CreatePaymentTable1780639311366 implements MigrationInterface { `); await queryRunner.query(` - CREATE TABLE freight.payments ( - id uuid NOT NULL DEFAULT uuid_generate_v4(), + CREATE TABLE freight.payments ( + id uuid NOT NULL DEFAULT uuid_generate_v4(), - ref_id varchar(255) NOT NULL, + ref_id varchar(255) NOT NULL, - type freight.payments_type_enum NOT NULL, + type freight.payments_type_enum NOT NULL, - method freight.payments_method_enum NOT NULL, + method freight.payments_method_enum NOT NULL, - currency freight.payments_currency_enum NOT NULL, + currency freight.payments_currency_enum NOT NULL, - amount numeric NOT NULL, + amount numeric NOT NULL, - raw_initiation jsonb NOT NULL DEFAULT '{}'::jsonb, + raw_initiation jsonb NOT NULL DEFAULT '{}'::jsonb, - client_action json, + client_action json, - merchant_order_id varchar(255) NOT NULL, + merchant_order_id varchar(255) NOT NULL, - transaction_id varchar(255), + transaction_id varchar(255), - status freight.payments_status_enum NOT NULL DEFAULT 'action-required', + status freight.payments_status_enum NOT NULL DEFAULT 'action-required', - paid_at date, + paid_at date, - refunded_at date, + refunded_at date, - expires_at date, + expires_at date, - failer_code varchar(30), + failer_code varchar(30), - failer_message varchar(255), + failer_message varchar(255), - created_at TIMESTAMP NOT NULL DEFAULT now(), + reason varchar(255), - CONSTRAINT PK_payments PRIMARY KEY (id), + created_at TIMESTAMP NOT NULL DEFAULT now(), - CONSTRAINT UQ_payments_merchant_order_id UNIQUE (merchant_order_id), + CONSTRAINT PK_payments PRIMARY KEY (id), - CONSTRAINT UQ_payments_transaction_id UNIQUE (transaction_id) - ); - `); + CONSTRAINT UQ_payments_merchant_order_id UNIQUE (merchant_order_id), + + CONSTRAINT UQ_payments_transaction_id UNIQUE (transaction_id) + ); +`); } public async down(queryRunner: QueryRunner): Promise { diff --git a/apps/edr-freight-api/src/modules/payment/entities/payment.entity.ts b/apps/edr-freight-api/src/modules/payment/entities/payment.entity.ts index f4b784a62..cb03ee25d 100644 --- a/apps/edr-freight-api/src/modules/payment/entities/payment.entity.ts +++ b/apps/edr-freight-api/src/modules/payment/entities/payment.entity.ts @@ -26,6 +26,9 @@ export class PaymentEntity extends BaseEntity { @Column({ type: "numeric" }) amount!: number + @Column({ type: "varchar", length: 255, name: "reason", }) + reason?: string; + @Column({ type: "jsonb", default: {}, name: "raw_initiation" }) rawInitiation?: Record diff --git a/apps/edr-freight-api/src/modules/payment/payment.controller.ts b/apps/edr-freight-api/src/modules/payment/payment.controller.ts index 8e9944c28..05b127968 100644 --- a/apps/edr-freight-api/src/modules/payment/payment.controller.ts +++ b/apps/edr-freight-api/src/modules/payment/payment.controller.ts @@ -1,18 +1,33 @@ -import { Controller, Get, NotFoundException, Param, ParseUUIDPipe, Post, Res } from "@nestjs/common"; +import { Controller, Get, NotFoundException, Param, ParseUUIDPipe, Post, Res } from "@nestjs/common"; import { PaymentService } from "./payment.service"; import { Public } from "@edr/api-common"; import { randomUUID } from "crypto"; import { Response } from "express" + +import * as fs from 'fs'; +import * as path from 'path'; +import Handlebars from 'handlebars'; + + @Public() @Controller("payments") export class PaymentController { constructor(private readonly paymentService: PaymentService) { } + + @Get("/receipts/:orderId/html") + async genReceipt(@Param("orderId") orderId: string, @Res() res: Response) { + const filled = await this.paymentService.genReceiptHtml(orderId); + return res.send(filled) + } + @Post("/initiate") async initiatePayment() { //Only for testing.. - const data = await this.paymentService.pay(20, "ETB", "telebirr", (_) => { + const redirectBaseURL = "http://localhost:3004/payment" + const description = "booking" + const data = await this.paymentService.pay(redirectBaseURL, 20, "ETB", "telebirr", description, (_) => { return new Promise((resp, _) => { resp({ id: randomUUID(), @@ -31,6 +46,7 @@ export class PaymentController { if (!payment) { throw new NotFoundException('payment not found') } + return res.send(` @@ -49,5 +65,25 @@ export class PaymentController { } + // @Get("test") + // handleTest(@Res() res: Response) { + + + // const filePath = path.join(__dirname, "templates", "payment.hbs"); + // console.log(filePath) + // console.log(__dirname) + // if (fs.existsSync(filePath)) { + // const source = fs.readFileSync(filePath, "utf8"); + // const template = Handlebars.compile(source); + + // const html = template({ + // url: "https://example.com" + // }); + + // res.send(html) + // } + // } + + } \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/payment/payment.module.ts b/apps/edr-freight-api/src/modules/payment/payment.module.ts index e9f99bd06..1c0605259 100644 --- a/apps/edr-freight-api/src/modules/payment/payment.module.ts +++ b/apps/edr-freight-api/src/modules/payment/payment.module.ts @@ -7,9 +7,10 @@ import { ConfigModule } from "@nestjs/config"; import { PaymentRepository } from "./payment.repository"; import { WebhookController } from "./webhooks/webhook.controller"; import { TelebirrWebhookService } from "./webhooks/providers/telebirr.service"; +import { BookingsModule } from "../bookings/bookings.module"; @Module({ - imports: [HttpModule, ConfigModule], + imports: [HttpModule, ConfigModule, BookingsModule], providers: [PaymentRepository, PaymentTelebirrStrategy, PaymentService, TelebirrWebhookService], controllers: [PaymentController, WebhookController] }) diff --git a/apps/edr-freight-api/src/modules/payment/payment.repository.ts b/apps/edr-freight-api/src/modules/payment/payment.repository.ts index 3f93fd77d..b9e37a55b 100644 --- a/apps/edr-freight-api/src/modules/payment/payment.repository.ts +++ b/apps/edr-freight-api/src/modules/payment/payment.repository.ts @@ -9,7 +9,7 @@ export class PaymentRepository { this.paymentRepo = this.dataSource.getRepository(PaymentEntity) } - async createTr(qr: QueryRunner, data: Pick): Promise { + async createTr(qr: QueryRunner, data: Pick): Promise { const payment = qr.manager.create(PaymentEntity, data) return qr.manager.save(payment) } @@ -35,5 +35,5 @@ export class PaymentRepository { } - + } \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/payment/payment.service.ts b/apps/edr-freight-api/src/modules/payment/payment.service.ts index cee35bf79..b714d5175 100644 --- a/apps/edr-freight-api/src/modules/payment/payment.service.ts +++ b/apps/edr-freight-api/src/modules/payment/payment.service.ts @@ -1,4 +1,4 @@ -import { Injectable, NotFoundException } from "@nestjs/common"; +import { BadRequestException, Injectable, InternalServerErrorException, NotFoundException } from "@nestjs/common"; import { DataSource, QueryRunner } from "typeorm"; import { PaymentEntity } from "./entities/payment.entity"; import { PaymentStrategy } from "./strategies/payment.strategy"; @@ -6,7 +6,11 @@ import { PaymentTelebirrStrategy } from "./strategies/payment.telebirr.strategy" import { PaymentRepository } from "./payment.repository"; import { ClientAction, PaymentPlatform } from "./strategies/payments.types"; import * as crypto from 'crypto'; -import { PaymentStatus, UpdatePaymentStatusDto } from "./dto/update-payment-status.dto"; + +import * as fs from 'fs'; +import * as path from 'path'; +import * as Handlebars from 'handlebars'; + type PaymentMethod = PaymentEntity["method"] type CurrencyType = PaymentEntity["currency"] @@ -24,7 +28,7 @@ export class PaymentService { ]) } - async pay(amount: number, currency: CurrencyType, method: PaymentMethod, cb: (qr: QueryRunner) => Promise<{ id: string, type: PaymentEntity["type"] }>, payform: PaymentPlatform = "web"): Promise<{ + async pay(redirectBaseURL: string, amount: number, currency: CurrencyType, method: PaymentMethod, reason: string, cb: (qr: QueryRunner) => Promise<{ id: string, type: PaymentEntity["type"] }>, payform: PaymentPlatform = "web"): Promise<{ refId: string, clientAction: ClientAction, status: PaymentEntity["status"], @@ -33,6 +37,7 @@ export class PaymentService { failureMessage?: string, }> { + const strategy = this.strategies.get(method) if (!strategy) { throw new NotFoundException("strategy not found") @@ -40,6 +45,7 @@ export class PaymentService { const orderId = `freigh${Date.now()}${crypto.randomBytes(4).toString('hex')}` //todo: make it dynamic const paymentResp = await strategy.pay({ + redirectBaseURL, amountMinor: amount, currency: currency, merchantOrderId: orderId, @@ -62,7 +68,8 @@ export class PaymentService { merchantOrderId: orderId, rawInitiation: paymentResp.rawInitiation, clientAction: paymentResp.clientAction, - expiresAt: paymentResp.expiresAt + expiresAt: paymentResp.expiresAt, + reason }) await queryRunner.commitTransaction() @@ -88,26 +95,47 @@ export class PaymentService { return this.paymentRepo.getActivePaymentByRefIdAndMethod(refId, method) } - - async handleTelebirrPaymentCb(dto: UpdatePaymentStatusDto): Promise { - switch (dto.status) { - case PaymentStatus.SUCCEEDED: - await this.paymentRepo.update({ merchantOrderId: dto.orderId }, { status: "success" }) - break; - case PaymentStatus.FAILED: - await this.paymentRepo.update({ merchantOrderId: dto.orderId }, { status: "failed", failureMessage: dto.failureMessage }) - break; - case PaymentStatus.CANCELLED: - await this.paymentRepo.update({ merchantOrderId: dto.orderId }, { status: "canceled" }) - break; - case PaymentStatus.PROCESSING: - await this.paymentRepo.update({ merchantOrderId: dto.orderId }, { status: "canceled" }) - break; - case PaymentStatus.REFUNDED: - await this.paymentRepo.update({ merchantOrderId: dto.orderId }, { status: "canceled" }) - break; - + async genReceiptHtml(orderId: string) { + const payment = await this.paymentRepo.findOneBy({ + merchantOrderId: orderId, + status: "success" + }) + if (!payment) { + throw new BadRequestException() } + + const filePath = path.join(__dirname, "templates", "ceiepts.hbs"); + if (!fs.existsSync(filePath)) { + throw new InternalServerErrorException() + } + const source = fs.readFileSync(filePath, "utf8"); + const template = Handlebars.compile(source); + + const html = template({ + vendorName: "Ethio Djibouti Railway Ticket Booking", + vendorAddress: "Addis Ababa", + receiptDate: payment.paidAt, + paymentMethod: payment?.method, + subtotal: payment?.amount.toString(), + total: payment?.amount.toString(), + currency: payment?.currency, + reason: payment?.reason + }); + + return html; } -} \ No newline at end of file + // const templatePath = path.join( + // process.cwd(), + // 'src/modules/payment/templates/receipt.hbs', + // ); + + + // console.log(templatePath) + + // const source = fs.readFileSync(templatePath, 'utf8'); + // const template = Handlebars.compile(source); + // return this.getReceiptTemplate(); + +} + diff --git a/apps/edr-freight-api/src/modules/payment/strategies/payment.telebirr.strategy.ts b/apps/edr-freight-api/src/modules/payment/strategies/payment.telebirr.strategy.ts index 3fa5fdc75..17a255dea 100644 --- a/apps/edr-freight-api/src/modules/payment/strategies/payment.telebirr.strategy.ts +++ b/apps/edr-freight-api/src/modules/payment/strategies/payment.telebirr.strategy.ts @@ -22,7 +22,8 @@ export class PaymentTelebirrStrategy implements PaymentStrategy { async pay(data: ProviderInitiationInput): Promise { // const refId = randomUUID() // const orderId = createMerchantOrderId() - const resp = await this.initiate(data) + const redirectURL = `${data.redirectBaseURL}/${data.merchantOrderId}` + const resp = await this.initiate(redirectURL, data) return resp; } @@ -44,9 +45,9 @@ export class PaymentTelebirrStrategy implements PaymentStrategy { }); } - async initiate(input: ProviderInitiationInput): Promise { + async initiate(redirectURL: string, input: ProviderInitiationInput): Promise { const fabricToken = await this.applyFabricToken(); - const requestBody = this.buildCreateOrderRequest(input); + const requestBody = this.buildCreateOrderRequest(redirectURL, input); const response = await this.requestCreateOrder(fabricToken, requestBody); const prepayId = response.biz_content?.prepay_id; @@ -176,8 +177,9 @@ export class PaymentTelebirrStrategy implements PaymentStrategy { ); } - private buildCreateOrderRequest(input: ProviderInitiationInput): CreateOrderRequest { - const totalAmount = String(input.amountMinor / 100); + private buildCreateOrderRequest(redirectURL: string, input: ProviderInitiationInput): CreateOrderRequest { + // const totalAmount = String(input.amountMinor / 100); + const totalAmount = String(input.amountMinor) const req = { timestamp: createTimestamp(), nonce_str: createNonceStr(), @@ -186,6 +188,7 @@ export class PaymentTelebirrStrategy implements PaymentStrategy { biz_content: { notify_url: this.notifyUrl, appid: this.merchantAppId, + redirect_url: redirectURL, merch_code: this.merchantCode, merch_order_id: input.merchantOrderId, trade_type: 'Checkout' as const, diff --git a/apps/edr-freight-api/src/modules/payment/strategies/payments.types.ts b/apps/edr-freight-api/src/modules/payment/strategies/payments.types.ts index dae192db9..dd23f8287 100644 --- a/apps/edr-freight-api/src/modules/payment/strategies/payments.types.ts +++ b/apps/edr-freight-api/src/modules/payment/strategies/payments.types.ts @@ -10,6 +10,7 @@ export type ClientAction = | { type: 'LAUNCH_APP'; prepayId: string; receiveCode?: string; shortCode: string }; export interface ProviderInitiationInput { + redirectBaseURL: string; merchantOrderId: string; // bookingRef: string; amountMinor: number; diff --git a/apps/edr-freight-api/src/modules/payment/templates/payment.hbs b/apps/edr-freight-api/src/modules/payment/templates/payment.hbs new file mode 100644 index 000000000..f590764f7 --- /dev/null +++ b/apps/edr-freight-api/src/modules/payment/templates/payment.hbs @@ -0,0 +1,15 @@ + + + + + Redirecting... + + +

Redirecting...

+ + + + + \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/payment/templates/receipt.hbs b/apps/edr-freight-api/src/modules/payment/templates/receipt.hbs new file mode 100644 index 000000000..0f156f6dc --- /dev/null +++ b/apps/edr-freight-api/src/modules/payment/templates/receipt.hbs @@ -0,0 +1,191 @@ + + + + + + + Receipt - {{vendorName}} + + + + + +
+
+

{{vendorName}}

+

{{vendorAddress}}

+
+ + + + + + + + + + + + + + + + +
Date{{receiptDate}}
Payment Method + {{paymentMethod}} +
Description{{reason}}
+ +
+ + + + + + + + + + +
Subtotal + {{currency}} {{subtotal}} +
+ Total Paid + + {{currency}} {{total}} +
+
+ + + +
+ +
+
+ + + + + \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/payment/webhooks/dto/telebirr.dto.ts b/apps/edr-freight-api/src/modules/payment/webhooks/dto/telebirr.dto.ts index ef6bf4b3a..4604097d1 100644 --- a/apps/edr-freight-api/src/modules/payment/webhooks/dto/telebirr.dto.ts +++ b/apps/edr-freight-api/src/modules/payment/webhooks/dto/telebirr.dto.ts @@ -1,13 +1,48 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsOptional, IsString } from "class-validator"; + export class TelebirrDto { + @ApiProperty() + @IsString() merch_order_id!: string; + + + @IsOptional() + @IsString() payment_order_id!: string; + + @ApiProperty({ default: "SUCCEEDED"}) + @IsString() trade_status!: string; + + @IsOptional() + @IsString() trans_id?: string; + + @IsOptional() + @IsString() total_amount?: string; + + @IsOptional() + @IsString() trans_currency?: string; + + @IsOptional() + @IsString() notify_time?: string; + + @IsOptional() + @IsString() trans_end_time?: string; + + @IsOptional() + @IsString() sign!: string; + + @IsOptional() + @IsString() sign_type?: string; + + [key: string]: unknown; } \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/payment/webhooks/providers/telebirr.service.ts b/apps/edr-freight-api/src/modules/payment/webhooks/providers/telebirr.service.ts index 96b4c1f0d..a57c79982 100644 --- a/apps/edr-freight-api/src/modules/payment/webhooks/providers/telebirr.service.ts +++ b/apps/edr-freight-api/src/modules/payment/webhooks/providers/telebirr.service.ts @@ -2,12 +2,17 @@ import { Injectable, } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import * as crypto from "crypto" import { TelebirrDto } from '../dto/telebirr.dto'; +import { BookingsRepository } from 'src/modules/bookings/bookings.repository'; +import { PaymentRepository } from '../../payment.repository'; @Injectable() export class TelebirrWebhookService { // private readonly logger = new Logger(TelebirrWebhookService.name); constructor( - private readonly config: ConfigService + private readonly config: ConfigService, + private readonly bookingRepo: BookingsRepository, + private readonly paymentRepo: PaymentRepository, + ) { } verifyTelebirrNotification(payload: TelebirrDto) { @@ -43,7 +48,33 @@ export class TelebirrWebhookService { } async handle(payload: TelebirrDto): Promise { - console.log(payload) + const payment = await this.paymentRepo.findOneBy({ merchantOrderId: payload.merch_order_id }) + if (!payment) { + throw new Error("payment not found") + } + switch (payload.trade_status) { + case "SUCCEEDED": + await this.paymentRepo.update({ id: payment.id }, { status: "success", paidAt: new Date() }) + switch (payment.type) { + case "booking": + await this.bookingRepo.update(payment.refId, { paymentStatus: "PAID", }) + break; + } + break; + case "FAILED": + await this.paymentRepo.update({ id: payment.id }, { status: "failed" }) + break; + case "CANCELLED": + await this.paymentRepo.update({ id: payment.id }, { status: "canceled" }) + break; + case "PROCESSING": + await this.paymentRepo.update({ id: payment.id }, { status: "processing" }) + break; + case "REFUNDED": + await this.paymentRepo.update({ id: payment.id }, { status: "refunded" }) + break; + + } } } \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/payment/webhooks/webhook.controller.ts b/apps/edr-freight-api/src/modules/payment/webhooks/webhook.controller.ts index 378853728..d74eb82a0 100644 --- a/apps/edr-freight-api/src/modules/payment/webhooks/webhook.controller.ts +++ b/apps/edr-freight-api/src/modules/payment/webhooks/webhook.controller.ts @@ -1,14 +1,16 @@ -import { All, Body, Controller, HttpCode, HttpStatus, Logger, } from '@nestjs/common'; +import { Body, Controller, HttpCode, HttpStatus, Logger, Post, } from '@nestjs/common'; import { TelebirrWebhookService } from './providers/telebirr.service'; import { ApiOperation } from '@nestjs/swagger'; import { TelebirrDto } from './dto/telebirr.dto'; +import { Public } from '@edr/api-common'; -@Controller("payments/webhooks") +@Controller("payments-webhooks") +@Public() export class WebhookController { constructor(private readonly telebirr: TelebirrWebhookService) { } private readonly logger = new Logger(WebhookController.name); - @All('telebirr') + @Post('telebirr') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Telebirr payment notification callback (Ethiopia)', @@ -20,16 +22,13 @@ export class WebhookController { ); try { - const verified = this.telebirr.verifyTelebirrNotification(payload) - if (!verified) { - throw new Error("not valid") - } - const merchantOrderId = payload.merch_order_id; - if (merchantOrderId.startsWith("freight")) { - await this.telebirr.handle(payload); - } else if (merchantOrderId.startsWith("passagner")) { - //todo: handle else where - } + // const verified = this.telebirr.verifyTelebirrNotification(payload) + // if (!verified) { + // throw new Error("not valid") + // } + // const merchantOrderId = payload.merch_order_id; + await this.telebirr.handle(payload); + } catch (err) { const message = err instanceof Error ? err.message : String(err); diff --git a/apps/edr-freight-web/portal/package.json b/apps/edr-freight-web/portal/package.json index 3cd617e0a..b9b33d35d 100644 --- a/apps/edr-freight-web/portal/package.json +++ b/apps/edr-freight-web/portal/package.json @@ -20,6 +20,7 @@ "axios": "^1.7.7", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "date-fns": "^3.6.0", "lucide-react": "^1.14.0", "radix-ui": "^1.4.3", "react": "19.2.6", diff --git a/apps/edr-freight-web/portal/src/pages/MyPortalPage.tsx b/apps/edr-freight-web/portal/src/pages/MyPortalPage.tsx index 6cb111bb7..b6b17e71f 100644 --- a/apps/edr-freight-web/portal/src/pages/MyPortalPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/MyPortalPage.tsx @@ -1,5 +1,7 @@ import { useMemo, useState } from "react"; -import { Link } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; +import { format } from "date-fns"; +import { useQuery } from "@tanstack/react-query"; import { ArrowRight, Building2, @@ -7,6 +9,7 @@ import { Clock, DollarSign, Eye, + LoaderCircle, Mail, MapPin, Package, @@ -20,14 +23,12 @@ import { import { getCurrentCustomer, - getMyBookings, getMyInvoices, getMyShipments, } from "@/lib/currentCustomer"; import { formatCurrency } from "@/pages/billing/invoices.mock"; import type { ShipmentStatus } from "@/pages/tracking/shipments.mock"; import type { InvoiceStatus } from "@/pages/billing/invoices.mock"; -import type { BookingStatus } from "@/pages/bookings/bookings.mock"; import { Button, Card, @@ -36,16 +37,36 @@ import { CardTitle, CardDescription, } from "@edr/ui-common"; +import { api } from "@/services/api"; + +const ACTIVE_STATUSES = [ + "DRAFT", + "SUBMITTED", + "PENDING_APPROVAL", + "IN_TRANSIT", +]; export default function MyPortalPage() { const me = useMemo(() => getCurrentCustomer(), []); - const myBookings = useMemo(() => getMyBookings(), []); const myShipments = useMemo(() => getMyShipments(), []); const myInvoices = useMemo(() => getMyInvoices(), []); - const activeBookings = myBookings.filter( - (b) => b.status === "Confirmed" || b.status === "In Transit", + const navigate = useNavigate(); + + const bookingsQuery = useQuery( + api.bookings.list.queryOptions({ + input: { sortBy: "createdAt", sortOrder: "DESC" }, + }), ); + + const myBookings = useMemo( + () => + (bookingsQuery.data?.items ?? []).filter((b) => + ACTIVE_STATUSES.includes(b.status), + ), + [bookingsQuery.data], + ); + const activeShipments = myShipments.filter((s) => s.status === "In Transit"); const outstandingInvoices = myInvoices.filter( (inv) => inv.status === "Sent" || inv.status === "Overdue", @@ -58,7 +79,7 @@ export default function MyPortalPage() { .filter((inv) => inv.status === "Paid" && inv.currency === "USD") .reduce((sum, inv) => sum + inv.amount, 0); - const recentBookings = [...myBookings].slice(0, 5); + const recentBookings = myBookings.slice(0, 5); const recentInvoices = [...myInvoices].slice(0, 4); return ( @@ -117,7 +138,7 @@ export default function MyPortalPage() {