diff --git a/apps/edr-passenger-api/src/modules/seats/seats.controller.ts b/apps/edr-passenger-api/src/modules/seats/seats.controller.ts index 4a9783101..fa233f45c 100644 --- a/apps/edr-passenger-api/src/modules/seats/seats.controller.ts +++ b/apps/edr-passenger-api/src/modules/seats/seats.controller.ts @@ -19,7 +19,7 @@ import { ApiResponse, } from "@nestjs/swagger"; import { SeatsService } from "./seats.service"; -import { HoldSeatsDto } from "./seats.dto"; +import { HoldSeatsDto, ReleaseHoldDto } from "./seats.dto"; import { JwtGuard } from "../../common/jwt.guard"; import { IamGuard } from "../../common/iam-adapter"; @@ -182,6 +182,27 @@ This makes it clear which segment of the route each seat is held for, enabling s return this.service.releaseHold(holdId); } + @Post("release") + @SetMetadata('isPublic', true) + @ApiOperation({ + summary: "Release a seat hold by holdId (portal-server use only)", + description: + "Frees a previously-created hold's seats immediately instead of waiting for it to " + + "expire — used when a guest or logged-in user changes their seat selection, so the " + + "stale hold doesn't linger and block that seat for other travellers.\n\n" + + "This is a public endpoint (no JWT), like POST /seats/hold, since guest sessions have " + + "no login to authenticate with. It must ONLY ever be called from the passenger portal's " + + "own Next.js server (a server-side route handler), never directly from browser code — " + + "calling it straight from client JS would let anyone script mass hold-cancellation " + + "against other travellers' in-progress seat selections. The portal's server-side proxy " + + "is what keeps this endpoint's existence out of the browser's network requests.", + }) + @ApiResponse({ status: 200, description: "Hold released" }) + @ApiResponse({ status: 404, description: "Hold not found" }) + releaseSeatById(@Body() dto: ReleaseHoldDto) { + return this.service.releaseHold(dto.holdId); + } + // ── Seat Block / Unblock ─────────────────────────────────────────────────── @Post(":seatId/block") @UseGuards(IamGuard) diff --git a/apps/edr-passenger-api/src/modules/seats/seats.dto.ts b/apps/edr-passenger-api/src/modules/seats/seats.dto.ts index 23c3b3d46..87d47588e 100644 --- a/apps/edr-passenger-api/src/modules/seats/seats.dto.ts +++ b/apps/edr-passenger-api/src/modules/seats/seats.dto.ts @@ -48,3 +48,8 @@ export class HoldSeatsDto { @Type(() => PassengerSeatDto) passengers: PassengerSeatDto[]; } + +export class ReleaseHoldDto { + @ApiProperty({ example: 'hold-uuid', description: 'SeatHold UUID to release' }) + @IsString() holdId: string; +} diff --git a/apps/edr-passenger-web/portal/src/app/api/seats/release-hold/route.ts b/apps/edr-passenger-web/portal/src/app/api/seats/release-hold/route.ts new file mode 100644 index 000000000..c2c1d19a0 --- /dev/null +++ b/apps/edr-passenger-web/portal/src/app/api/seats/release-hold/route.ts @@ -0,0 +1,37 @@ +// Server-side proxy for POST /seats/release on the passenger API. +// +// The browser calls THIS route (same-origin, /api/seats/release-hold) instead of the +// backend's public release endpoint directly. Route handlers run on the Next.js server, not +// in the browser, so the actual backend call — and its URL — never appears in client-side +// JS or network requests a user could copy and script against other travellers' holds. +// Keeping this indirection is the whole point: it doesn't add cryptographic protection (the +// backend endpoint is still public), it just keeps the release capability out of the +// browser's reach so it can't be trivially discovered and abused from client code. + +const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:4000"; + +export async function POST(request: Request) { + let holdId: string | undefined; + try { + ({ holdId } = await request.json()); + } catch { + return Response.json({ message: "Invalid JSON body" }, { status: 400 }); + } + + if (!holdId || typeof holdId !== "string") { + return Response.json({ message: "holdId is required" }, { status: 400 }); + } + + try { + const backendResponse = await fetch(`${API_URL}/seats/release`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ holdId }), + }); + + const data = await backendResponse.json().catch(() => null); + return Response.json(data, { status: backendResponse.status }); + } catch { + return Response.json({ message: "Failed to reach booking service" }, { status: 502 }); + } +} diff --git a/apps/edr-passenger-web/portal/src/app/booking/results/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/results/page.tsx index 77a18380f..66f91dd60 100644 --- a/apps/edr-passenger-web/portal/src/app/booking/results/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/booking/results/page.tsx @@ -1,57 +1,106 @@ -'use client'; +"use client"; -import { useSearchParams, useRouter } from 'next/navigation'; -import { useQuery } from '@tanstack/react-query'; -import { apiClient } from '@/lib/api-client'; -import { useBookingStore } from '@/lib/booking-store'; -import { Schedule } from '@/types'; -import { ArrowRight, Clock, Calendar, Users, ChevronLeft, Check, X, MapPin, Gift, Train, Bed, Armchair, Star } from 'lucide-react'; -import { format } from 'date-fns'; -import { formatTime, getTimePeriod } from '@/utils/format'; -import { useState, useEffect } from 'react'; +import { useSearchParams, useRouter } from "next/navigation"; +import { useQuery } from "@tanstack/react-query"; +import { apiClient } from "@/lib/api-client"; +import { useBookingStore } from "@/lib/booking-store"; +import { Schedule } from "@/types"; +import { + ArrowRight, + Clock, + Calendar, + Users, + ChevronLeft, + Check, + X, + MapPin, + Gift, + Train, + Bed, + Armchair, + Star, +} from "lucide-react"; +import { format } from "date-fns"; +import { formatTime, getTimePeriod } from "@/utils/format"; +import { useState, useEffect } from "react"; export default function ResultsPage() { const router = useRouter(); const searchParams = useSearchParams(); - const { setSelectedSchedule, setOutboundSchedule, setInboundSchedule } = useBookingStore(); - const [selectedCoachTypes, setSelectedCoachTypes] = useState>({}); + const { setSelectedSchedule, setOutboundSchedule, setInboundSchedule } = + useBookingStore(); + const [selectedCoachTypes, setSelectedCoachTypes] = useState< + Record + >({}); const [outboundScheduleData, setOutboundScheduleData] = useState( () => useBookingStore.getState().outboundSchedule, ); const [classModal, setClassModal] = useState(null); - const [promoData, setPromoData] = useState<{ code: string; discount: string; message: string } | null>(null); - const [roundTripStep, setRoundTripStep] = useState<'outbound' | 'inbound'>(() => { - const { outboundSchedule, searchCriteria: sc } = useBookingStore.getState(); - return outboundSchedule && sc?.tripType === 'ROUND_TRIP' ? 'inbound' : 'outbound'; - }); + const [promoData, setPromoData] = useState<{ + code: string; + discount: string; + message: string; + } | null>(null); + const [roundTripStep, setRoundTripStep] = useState<"outbound" | "inbound">( + () => { + const { outboundSchedule, searchCriteria: sc } = + useBookingStore.getState(); + return outboundSchedule && sc?.tripType === "ROUND_TRIP" + ? "inbound" + : "outbound"; + }, + ); const searchCriteria = useBookingStore((s) => s.searchCriteria); const setSearchCriteria = useBookingStore((s) => s.setSearchCriteria); const searchData = { - originStationId: searchParams.get('origin') || searchCriteria?.originStationId || '', - destinationStationId: searchParams.get('destination') || searchCriteria?.destinationStationId || '', - date: searchParams.get('date') || searchCriteria?.departureDate || '', - returnDate: searchParams.get('returnDate') || searchCriteria?.returnDate, - journeyType: (searchParams.get('tripType') ?? searchCriteria?.tripType ?? 'ONE_WAY') === 'ROUND_TRIP' ? 'ROUND_TRIP' : 'ONE_WAY', - adultCount: parseInt(searchParams.get('adults') || '') || searchCriteria?.adultCount || 1, - childCount: parseInt(searchParams.get('children') || '') || searchCriteria?.childCount || 0, - nationality: searchParams.get('nationality') || searchCriteria?.nationality || 'ETHIOPIAN', - promoCode: searchParams.get('promoCode') || searchCriteria?.promoCode || '', + originStationId: + searchParams.get("origin") || searchCriteria?.originStationId || "", + destinationStationId: + searchParams.get("destination") || + searchCriteria?.destinationStationId || + "", + date: searchParams.get("date") || searchCriteria?.departureDate || "", + returnDate: searchParams.get("returnDate") || searchCriteria?.returnDate, + journeyType: + (searchParams.get("tripType") ?? + searchCriteria?.tripType ?? + "ONE_WAY") === "ROUND_TRIP" + ? "ROUND_TRIP" + : "ONE_WAY", + adultCount: + parseInt(searchParams.get("adults") || "") || + searchCriteria?.adultCount || + 1, + childCount: + parseInt(searchParams.get("children") || "") || + searchCriteria?.childCount || + 0, + nationality: + searchParams.get("nationality") || + searchCriteria?.nationality || + "ETHIOPIAN", + promoCode: searchParams.get("promoCode") || searchCriteria?.promoCode || "", }; useEffect(() => { - if (searchParams.get('origin')) { + if (searchParams.get("origin")) { setSearchCriteria({ - tripType: (searchParams.get('tripType') || 'ONE_WAY') as 'ONE_WAY' | 'ROUND_TRIP', - originStationId: searchParams.get('origin')!, - destinationStationId: searchParams.get('destination')!, - departureDate: searchParams.get('date')!, - returnDate: searchParams.get('returnDate') || undefined, - adultCount: parseInt(searchParams.get('adults') || '1'), - childCount: parseInt(searchParams.get('children') || '0'), - nationality: (searchParams.get('nationality') || 'ETHIOPIAN') as 'ETHIOPIAN' | 'DJIBOUTIAN' | 'OTHER', - promoCode: searchParams.get('promoCode') || '', + tripType: (searchParams.get("tripType") || "ONE_WAY") as + | "ONE_WAY" + | "ROUND_TRIP", + originStationId: searchParams.get("origin")!, + destinationStationId: searchParams.get("destination")!, + departureDate: searchParams.get("date")!, + returnDate: searchParams.get("returnDate") || undefined, + adultCount: parseInt(searchParams.get("adults") || "1"), + childCount: parseInt(searchParams.get("children") || "0"), + nationality: (searchParams.get("nationality") || "ETHIOPIAN") as + | "ETHIOPIAN" + | "DJIBOUTIAN" + | "OTHER", + promoCode: searchParams.get("promoCode") || "", }); } }, [searchParams, setSearchCriteria]); @@ -59,13 +108,13 @@ export default function ResultsPage() { useEffect(() => { if (searchData.promoCode) { apiClient - .post('/promos/validate', { code: searchData.promoCode }) + .post("/promos/validate", { code: searchData.promoCode }) .then((response: any) => { if (response.applicable || response.valid) { setPromoData({ code: searchData.promoCode, - discount: response.message || 'Discount applied', - message: response.message || 'Promo code applied successfully!', + discount: response.message || "Discount applied", + message: response.message || "Promo code applied successfully!", }); } }) @@ -90,8 +139,12 @@ export default function ResultsPage() { return `/booking/search?${params}`; }; - const { data: results, isLoading, error } = useQuery({ - queryKey: ['search', searchData], + const { + data: results, + isLoading, + error, + } = useQuery({ + queryKey: ["search", searchData], queryFn: async (): Promise => { const payload: any = { originStationId: searchData.originStationId, @@ -102,15 +155,13 @@ export default function ResultsPage() { nationality: searchData.nationality, journeyType: searchData.journeyType, }; - - if (searchData.journeyType === 'ROUND_TRIP' && searchData.returnDate) { + + if (searchData.journeyType === "ROUND_TRIP" && searchData.returnDate) { payload.returnDate = searchData.returnDate; } - - - const response = await apiClient.post('/search', payload) as any; - - + + const response = (await apiClient.post("/search", payload)) as any; + return response; }, enabled: !!searchData.originStationId && !!searchData.destinationStationId, @@ -118,20 +169,20 @@ export default function ResultsPage() { gcTime: 0, }); - const isRoundTrip = searchData.journeyType === 'ROUND_TRIP'; - + const isRoundTrip = searchData.journeyType === "ROUND_TRIP"; + // Handle both response formats: // 1. One-way: response can be array of schedules OR object with journeyType and outbound // 2. Round-trip: response has journeyType, outbound, inbound properties let outboundSchedules: Schedule[] = []; let inboundSchedules: Schedule[] = []; - + if (results) { - if (results.journeyType === 'ROUND_TRIP') { + if (results.journeyType === "ROUND_TRIP") { // Round trip response format outboundSchedules = results.outbound || []; inboundSchedules = results.inbound || []; - } else if (results.journeyType === 'ONE_WAY' && results.outbound) { + } else if (results.journeyType === "ONE_WAY" && results.outbound) { // One-way response format with outbound array outboundSchedules = results.outbound || []; } else if (Array.isArray(results)) { @@ -142,40 +193,68 @@ export default function ResultsPage() { outboundSchedules = results.data; } } - - // Alternatives are surfaced whenever a leg returns no exact-date results. - const alternativeOutbound: Schedule[] = (!!results && outboundSchedules.length === 0) ? (results?.alternativeOutbound || []) : []; - const alternativeInbound: Schedule[] = (isRoundTrip && !!results && inboundSchedules.length === 0) ? (results?.alternativeInbound || []) : []; - const requestedDate: string = (results && results.requestedDate) || searchData.date; - const requestedReturnDate: string = (results && results.requestedReturnDate) || searchData.returnDate || ''; - const isOneWayNoOutbound = !isRoundTrip && !!results && outboundSchedules.length === 0; + // Alternatives are surfaced whenever a leg returns no exact-date results. + const alternativeOutbound: Schedule[] = + !!results && outboundSchedules.length === 0 + ? results?.alternativeOutbound || [] + : []; + const alternativeInbound: Schedule[] = + isRoundTrip && !!results && inboundSchedules.length === 0 + ? results?.alternativeInbound || [] + : []; + const requestedDate: string = + (results && results.requestedDate) || searchData.date; + const requestedReturnDate: string = + (results && results.requestedReturnDate) || searchData.returnDate || ""; + + const isOneWayNoOutbound = + !isRoundTrip && !!results && outboundSchedules.length === 0; // Round-trip: show results view if either leg has exact results OR alternatives. // One-way: need at least one outbound result. const hasResults = isRoundTrip - ? (outboundSchedules.length > 0 || alternativeOutbound.length > 0) || (inboundSchedules.length > 0 || alternativeInbound.length > 0) + ? outboundSchedules.length > 0 || + alternativeOutbound.length > 0 || + inboundSchedules.length > 0 || + alternativeInbound.length > 0 : outboundSchedules.length > 0; - const handleSelectCoachType = (scheduleId: string, coachTypeId: string, coachTypeCode: string, coachTypeName: string, seatClassName: string) => { - setSelectedCoachTypes(prev => ({ ...prev, [scheduleId]: { id: coachTypeId, code: coachTypeCode, name: coachTypeName, seatClassName } })); + const handleSelectCoachType = ( + scheduleId: string, + coachTypeId: string, + coachTypeCode: string, + coachTypeName: string, + seatClassName: string, + ) => { + setSelectedCoachTypes((prev) => ({ + ...prev, + [scheduleId]: { + id: coachTypeId, + code: coachTypeCode, + name: coachTypeName, + seatClassName, + }, + })); }; const handleSelect = (schedule: Schedule, isOutbound: boolean = false) => { - const scheduleId = schedule.scheduleId || schedule.id || ''; + const scheduleId = schedule.scheduleId || schedule.id || ""; const selectedCoachType = selectedCoachTypes[scheduleId]; - + if (!selectedCoachType) { - alert('Please select a coach type before continuing'); + alert("Please select a coach type before continuing"); return; } // Find the coach type to get pricing info - const coachType = schedule.coachTypes?.find(ct => ct.coachTypeCode === selectedCoachType.code); + const coachType = schedule.coachTypes?.find( + (ct) => ct.coachTypeCode === selectedCoachType.code, + ); // Use displayAmountMinor (passenger's currency) so stored fare matches what the card showed. const minFare = coachType?.classes.length - ? Math.min(...coachType.classes.map(c => c.baseFareMinor)) + ? Math.min(...coachType.classes.map((c) => c.baseFareMinor)) : 0; - const fareCurrency = 'ETB'; + const fareCurrency = "ETB"; const hours = Math.floor((schedule.durationMinutes || 0) / 60); const minutes = (schedule.durationMinutes || 0) % 60; @@ -184,12 +263,13 @@ export default function ResultsPage() { const scheduleData = { id: scheduleId, trainNumber: schedule.trainNumber, - origin: schedule.origin?.name || 'Origin', - destination: schedule.destination?.name || 'Destination', - originStationId: schedule.origin?.id || schedule.originStationId || '', - destinationStationId: schedule.destination?.id || schedule.destinationStationId || '', - departureTime: schedule.departureAt || schedule.departureTime || '', - arrivalTime: schedule.arrivalAt || schedule.arrivalTime || '', + origin: schedule.origin?.name || "Origin", + destination: schedule.destination?.name || "Destination", + originStationId: schedule.origin?.id || schedule.originStationId || "", + destinationStationId: + schedule.destination?.id || schedule.destinationStationId || "", + departureTime: schedule.departureAt || schedule.departureTime || "", + arrivalTime: schedule.arrivalAt || schedule.arrivalTime || "", duration: durationStr, baseFareAdult: minFare, baseFareChild: minFare, @@ -199,7 +279,8 @@ export default function ResultsPage() { selectedCoachTypeId: selectedCoachType.id, selectedCoachTypeCode: selectedCoachType.code, selectedCoachTypeName: selectedCoachType.name, - seatClassName: (selectedCoachType as any).seatClassName || selectedCoachType.name, + seatClassName: + (selectedCoachType as any).seatClassName || selectedCoachType.name, // Retained so the seat map's coach preview can price a switch to a different // coach type without needing a fresh API call. coachTypes: schedule.coachTypes || [], @@ -210,8 +291,8 @@ export default function ResultsPage() { setOutboundScheduleData(scheduleData); setOutboundSchedule(scheduleData); setClassModal(null); - setRoundTripStep('inbound'); - window.scrollTo({ top: 0, behavior: 'smooth' }); + setRoundTripStep("inbound"); + window.scrollTo({ top: 0, behavior: "smooth" }); return; } @@ -223,8 +304,8 @@ export default function ResultsPage() { // For one-way setSelectedSchedule(scheduleData); } - - router.push('/booking/auth-check'); + + router.push("/booking/auth-check"); }; // Shared "Choose Your Coach" drawer — used by both the normal results view and the @@ -233,118 +314,164 @@ export default function ResultsPage() { const renderClassModal = () => { if (!classModal) return null; - const scheduleId = classModal.scheduleId || classModal.id || ''; + const scheduleId = classModal.scheduleId || classModal.id || ""; const selectedCoachType = selectedCoachTypes[scheduleId]; const isOutbound = (classModal as any).isOutbound; // Dining coaches aren't bookable seat/bed classes — exclude them from selection. - const coachTypes = (classModal.coachTypes || []).filter((ct: any) => ct.coachTypeCode !== 'DPC'); + const coachTypes = (classModal.coachTypes || []).filter( + (ct: any) => ct.coachTypeCode !== "DPC", + ); const getCoachIcon = (typeName: string) => { const lower = typeName.toLowerCase(); - if (lower.includes('soft') || lower.includes('vip')) return Star; - if (lower.includes('bed')) return Bed; + if (lower.includes("soft") || lower.includes("vip")) return Star; + if (lower.includes("bed")) return Bed; return Armchair; }; return ( <> -
setClassModal(null)} /> -
setClassModal(null)} + /> +
-
-
-

Choose Your Coach

-

- - {classModal.trainNumber} - · - {classModal.origin?.name} → {classModal.destination?.name} -

-
- +
+
+

+ Choose Your Coach +

+

+ + {classModal.trainNumber} + · + + {classModal.origin?.name} → {classModal.destination?.name} + +

+ +
-
- {coachTypes.length > 0 ? ( -
- {coachTypes.map((coachType: any, index: number) => { - const isSelected = selectedCoachType?.id === coachType.coachTypeId; - const minPrice = coachType.classes.length ? Math.min(...coachType.classes.map((c: any) => c.baseFareMinor)) : 0; - const coachCurrency = 'ETB'; - const CoachIcon = getCoachIcon(coachType.coachTypeName); +
+ {coachTypes.length > 0 ? ( +
+ {coachTypes.map((coachType: any, index: number) => { + const isSelected = + selectedCoachType?.id === coachType.coachTypeId; + const minPrice = coachType.classes.length + ? Math.min( + ...coachType.classes.map((c: any) => c.baseFareMinor), + ) + : 0; + const coachCurrency = "ETB"; + const CoachIcon = getCoachIcon(coachType.coachTypeName); - return ( - - ); - })} -
- ) : ( -
-
- -
-

No coach types available for this journey

+
+ )} +
+ + ); + })} +
+ ) : ( +
+
+
+

+ No coach types available for this journey +

+
+ )} +
+ +
+
+ + {!selectedCoachType && ( +

+ + Select a coach type to continue +

)}
- -
-
- - {!selectedCoachType && ( -

- - Select a coach type to continue -

- )} -
-
+
@@ -2326,8 +2387,9 @@ export default function SeatsPage() {
- {/* Mobile spacer so bottom-sheet doesn't cover last seat */} -
+ {/* Mobile spacer so the bottom-sheet doesn't cover the last seat — sized to + match the sheet's current (collapsed/expanded) height. */} +
diff --git a/packages/payment-providers/src/providers/telebirr/telebirr.provider.ts b/packages/payment-providers/src/providers/telebirr/telebirr.provider.ts index ae24834c1..d732a51e3 100644 --- a/packages/payment-providers/src/providers/telebirr/telebirr.provider.ts +++ b/packages/payment-providers/src/providers/telebirr/telebirr.provider.ts @@ -352,7 +352,7 @@ export class TelebirrProvider implements PaymentProvider { return this.config.get("telebirr.timeoutExpress") ?? "15m"; } private get privateKey(): string { - return `-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC/ZcoOng1sJZ4CegopQVCw3HYqqVRLEudgT+dDpS8fRVy7zBgqZunju2VRCQuHeWs7yWgc9QGd4/8kRSLY+jlvKNeZ60yWcqEY+eKyQMmcjOz2Sn41fcVNgF+HV3DGiV4b23B6BCMjnpEFIb9d99/TsjsFSc7gCPgfl2yWDxE/Y1B2tVE6op2qd63YsMVFQGdre/CQYvFJENpQaBLMq4hHyBDgluUXlF0uA1X7UM0ZjbFC6ZIB/Hn1+pl5Ua8dKYrkVaecolmJT/s7c/+/1JeN+ja8luBoONsoODt2mTeVJHLF9Y3oh5rI+IY8HukIZJ1U6O7/JcjH3aRJTZagXUS9AgMBAAECggEBALBIBx8JcWFfEDZFwuAWeUQ7+VX3mVx/770kOuNx24HYt718D/HV0avfKETHqOfA7AQnz42EF1Yd7Rux1ZO0e3unSVRJhMO4linT1XjJ9ScMISAColWQHk3wY4va/FLPqG7N4L1w3BBtdjIc0A2zRGLNcFDBlxl/CVDHfcqD3CXdLukm/friX6TvnrbTyfAFicYgu0+UtDvfxTL3pRL3u3WTkDvnFK5YXhoazLctNOFrNiiIpCW6dJ7WRYRXuXhz7C0rENHyBtJ0zura1WD5oDbRZ8ON4v1KV4QofWiTFXJpbDgZdEeJJmFmt5HIi+Ny3P5n31WwZpRMHGeHrV23//0CgYEA+2/gYjYWOW3JgMDLX7r8fGPTo1ljkOUHuH98H/a/lE3wnnKKx+2ngRNZX4RfvNG4LLeWTz9plxR2RAqqOTbX8fj/NA/sS4mru9zvzMY1925FcX3WsWKBgKlLryl0vPScq4ejMLSCmypGz4VgLMYZqT4NYIkU2Lo1G1MiDoLy0CcCgYEAwt77exynUhM7AlyjhAA2wSINXLKsdFFF1u976x9kVhOfmbAutfMJPEQWb2WXaOJQMvMpgg2rU5aVsyEcuHsRH/2zatrxrGqLqgxaiqPz4ELINIh1iYK/hdRpr1vATHoebOv1wt8/9qxITNKtQTgQbqYci3KV1lPsOrBAB5S57nsCgYAvw+cagS/jpQmcngOEoh8I+mXgKEET64517DIGWHe4kr3dO+FFbc5eZPCbhqgxVJ3qUM4LK/7BJq/46RXBXLvVSfohR80Z5INtYuFjQ1xJLveeQcuhUxdK+95W3kdBBi8lHtVPkVsmYvekwK+ukcuaLSGZbzE4otcn47kajKHYDQKBgDbQyIbJ+ZsRw8CXVHu2H7DWJlIUBIS3s+CQ/xeVfgDkhjmSIKGX2to0AOeW+S9MseiTE/L8a1wY+MUppE2UeK26DLUbH24zjlPoI7PqCJjl0DFOzVlACSXZKV1lfsNEeriC61/EstZtgezyOkAlSCIH4fGr6tAeTU349Bnt0RtvAoGBAObgxjeH6JGpdLz1BbMj8xUHuYQkbxNeIPhH29CySn0vfhwg9VxAtIoOhvZeCfnsCRTj9OZjepCeUqDiDSoFznglrKhfeKUndHjvg+9kiae92iI6qJudPCHMNwP8wMSphkxUqnXFR3lr9A765GA980818UWZdrhrjLKtIIZdh+X1\n-----END PRIVATE KEY-----` + return this.config.get("telebirr.privateKey") ?? ""; } private get publicKey(): string { return this.config.get("telebirr.publicKey") ?? "";