🔄 cleanup: useless domain logic on admin

This commit is contained in:
user
2025-10-20 22:41:15 +03:00
parent 4ae1957a88
commit b150095361
13 changed files with 0 additions and 805 deletions

View File

@@ -1,54 +0,0 @@
import { TicketRepository } from "$lib/domains/ticket/data/repository";
import { db } from "@pkg/db";
import { Logger } from "@pkg/logger";
import type { FlightTicket } from "./data/entities";
import { ScrapedTicketsDataSource } from "./data/scrape.data.source";
// if the discount is lower than 1 or more than 100, then we don't apply the discount
function applyDiscount(price: number, discountPercent: number) {
if (discountPercent < 1 || discountPercent > 100) {
return price;
}
const discount = price * (discountPercent / 100);
// Logger.info(`Discounted: ${price} -> ${discount}`);
return price - discount;
}
export class TicketController {
private repo: TicketRepository;
constructor(repo: TicketRepository) {
this.repo = repo;
}
async createTicket(payload: FlightTicket) {
const checkRes = await this.repo.getTicketIdByInfo({
ticketId: "",
departureDate: payload.departureDate,
returnDate: payload.returnDate,
cabinClass: payload.cabinClass,
departure: payload.departure,
arrival: payload.arrival,
});
if (checkRes.error) {
return { error: checkRes.error };
}
if (!checkRes.data) {
Logger.info("Ticket not already found, so making a new one");
return this.repo.createTicket(payload);
}
Logger.info(
"Ticket already exists with similar features, so just returning that",
);
return { data: checkRes.data };
}
async getTicketById(id: number) {
return this.repo.getTicketById(id);
}
}
export function getTC() {
const ds = new ScrapedTicketsDataSource("", "");
return new TicketController(new TicketRepository(db, ds));
}

View File

@@ -1 +0,0 @@
export * from "@pkg/logic/domains/passengerinfo/data/entities";

View File

@@ -1 +0,0 @@
export * from "@pkg/logic/domains/ticket/data/entities/enums";

View File

@@ -1 +0,0 @@
export * from "@pkg/logic/domains/ticket/data/entities/index";

View File

@@ -1,193 +0,0 @@
import { and, eq, or, type Database } from "@pkg/db";
import {
flightTicketModel,
TicketType,
type FlightTicket,
type TicketSearchDTO,
} from "./entities";
import { type Result, ERROR_CODES } from "@pkg/result";
import { getError, Logger } from "@pkg/logger";
import type { ScrapedTicketsDataSource } from "./scrape.data.source";
import { flightTicketInfo } from "@pkg/db/schema";
export class TicketRepository {
private db: Database;
private scraper: ScrapedTicketsDataSource;
constructor(db: Database, scraper: ScrapedTicketsDataSource) {
this.db = db;
this.scraper = scraper;
}
async searchForTickets(
payload: TicketSearchDTO,
): Promise<Result<FlightTicket[]>> {
try {
return this.scraper.searchForTickets(payload);
} catch (err) {
return {
error: getError(
{
code: ERROR_CODES.NETWORK_ERROR,
message: "Failed to search for tickets",
detail: "Could not fetch tickets for the given payload",
userHint: "Please try again later",
error: err,
actionable: false,
},
err,
),
};
}
}
async getTicketById(id: number): Promise<Result<FlightTicket>> {
const out = await this.db.query.flightTicketInfo.findFirst({
where: eq(flightTicketInfo.id, id),
});
if (!out) {
return {
error: getError({
code: ERROR_CODES.NOT_FOUND_ERROR,
message: "Ticket not found",
userHint:
"Please check if the selected ticket is correct, or try again",
detail: "The ticket is not found by the id provided",
}),
};
}
const parsed = flightTicketModel.safeParse(out);
if (!parsed.success) {
return {
error: getError(
{
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
message: "Failed to parse ticket",
userHint: "Please try again",
detail: "Failed to parse ticket",
},
JSON.stringify(parsed.error.errors),
),
};
}
return { data: parsed.data };
}
async getTicketIdByInfo(info: {
ticketId: string;
arrival: string;
departure: string;
cabinClass: string;
departureDate: string;
returnDate: string;
}): Promise<Result<number>> {
try {
const out = await this.db.query.flightTicketInfo.findFirst({
where: or(
eq(flightTicketInfo.ticketId, info.ticketId),
and(
eq(flightTicketInfo.arrival, info.arrival),
eq(flightTicketInfo.departure, info.departure),
eq(flightTicketInfo.cabinClass, info.cabinClass),
eq(
flightTicketInfo.departureDate,
new Date(info.departureDate),
),
),
),
columns: { id: true },
});
if (out && out.id) {
return { data: out?.id };
}
return {};
} catch (e) {
Logger.debug(info);
return {
error: getError(
{
code: ERROR_CODES.DATABASE_ERROR,
message: "Failed to lookup ticket",
detail: "A database error occured while getting ticket by id",
userHint: "Contact us to resolve this issue",
actionable: false,
},
e,
),
};
}
}
async createTicket(payload: FlightTicket): Promise<Result<number>> {
try {
let rd = new Date();
if (
payload.returnDate &&
payload.returnDate.length > 0 &&
payload.flightType !== TicketType.OneWay
) {
rd = new Date(payload.returnDate);
}
const out = await this.db
.insert(flightTicketInfo)
.values({
ticketId: payload.ticketId,
flightType: payload.flightType,
arrival: payload.arrival,
departure: payload.departure,
cabinClass: payload.cabinClass,
departureDate: new Date(payload.departureDate),
returnDate: rd,
priceDetails: payload.priceDetails,
passengerCounts: payload.passengerCounts,
bagsInfo: payload.bagsInfo,
lastAvailable: payload.lastAvailable,
flightIteneraries: payload.flightIteneraries,
checkoutUrl: payload.checkoutUrl ?? "",
dates: payload.dates,
refundable: payload.refundable ?? false,
isCache: payload.isCache ?? false,
shareId: payload.shareId,
})
.returning({ id: flightTicketInfo.id })
.execute();
if (!out || out.length === 0) {
Logger.error("Failed to create ticket");
Logger.debug(out);
Logger.debug(payload);
return {
error: getError({
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
message: "Failed to create ticket",
userHint: "Please try again",
detail: "Failed to create ticket",
}),
};
}
return { data: out[0].id };
} catch (e) {
return {
error: getError(
{
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
message: "An error occured while creating ticket",
userHint: "Please try again later",
actionable: false,
detail: "An error occured while creating ticket",
},
e,
),
};
}
}
}

View File

@@ -1,50 +0,0 @@
import { ERROR_CODES, type Result } from "@pkg/result";
import type { FlightTicket, TicketSearchDTO } from "./entities";
import { betterFetch } from "@better-fetch/fetch";
import { getError, Logger } from "@pkg/logger";
export class ScrapedTicketsDataSource {
scraperUrl: string;
apiKey: string;
constructor(scraperUrl: string, apiKey: string) {
this.scraperUrl = scraperUrl;
this.apiKey = apiKey;
}
async searchForTickets(
payload: TicketSearchDTO,
): Promise<Result<FlightTicket[]>> {
const { data, error } = await betterFetch<Result<FlightTicket[]>>(
`${this.scraperUrl}/api/v1/tickets/search`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.apiKey}`,
},
body: JSON.stringify(payload),
},
);
if (error) {
return {
error: getError(
{
code: ERROR_CODES.NETWORK_ERROR,
message: "Failed to search for tickets",
detail: "Could not fetch tickets for the given payload",
userHint: "Please try again later",
error: error,
actionable: false,
},
JSON.stringify(error, null, 4),
),
};
}
Logger.info(`Returning ${data.data?.length} tickets`);
return { data: data.data ?? [] };
}
}

View File

@@ -1,22 +0,0 @@
import { writable } from "svelte/store";
import {
CabinClass,
TicketType,
type FlightTicket,
type TicketSearchPayload,
} from "./entities";
import { nanoid } from "nanoid";
export const flightTicketStore = writable<FlightTicket>();
export const ticketSearchStore = writable<TicketSearchPayload>({
sessionId: nanoid(),
ticketType: TicketType.Return,
cabinClass: CabinClass.Economy,
passengerCounts: { adults: 1, children: 0 },
departure: "",
arrival: "",
departureDate: "",
returnDate: "",
loadMore: false,
});

View File

@@ -1,207 +0,0 @@
import {
type FlightTicket,
ticketSearchPayloadModel,
} from "$lib/domains/ticket/data/entities/index";
import { trpcApiStore } from "$lib/stores/api";
import { toast } from "svelte-sonner";
import { get } from "svelte/store";
import {
MaxStops,
SortOption,
ticketFiltersStore,
} from "./ticket-filters.vm.svelte";
import { ticketSearchStore } from "../data/store";
export class FlightTicketViewModel {
searching = $state(false);
tickets = $state<FlightTicket[]>([]);
renderedTickets = $state<FlightTicket[]>([]);
async searchForTickets(loadMore = false) {
const api = get(trpcApiStore);
if (!api) {
return toast.error("Please try again by reloading the page", {
description: "Page state not properly initialized",
});
}
let payload = get(ticketSearchStore);
if (!payload) {
return toast.error(
"Could not search for tickets due to invalid payload",
);
}
const parsed = ticketSearchPayloadModel.safeParse(payload);
if (!parsed.success) {
console.log("Enter some parameters to search for tickets");
this.searching = false;
return;
}
payload = parsed.data;
if (loadMore) {
payload.loadMore = true;
}
this.searching = true;
const out = await api.ticket.searchTickets.query(payload);
this.searching = false;
console.log(out);
if (out.error) {
return toast.error(out.error.message, {
description: out.error.userHint,
});
}
if (!out.data) {
this.tickets = [];
return toast.error("No search results", {
description: "Please try again with different parameters",
});
}
this.tickets = out.data;
this.applyFilters();
}
applyFilters() {
this.searching = true;
const filters = get(ticketFiltersStore);
const filteredTickets = this.tickets.filter((ticket) => {
// Price filter
if (filters.priceRange.max > 0) {
if (
ticket.priceDetails.displayPrice < filters.priceRange.min ||
ticket.priceDetails.displayPrice > filters.priceRange.max
) {
return false;
}
}
if (filters.maxStops !== MaxStops.Any) {
// Calculate stops for outbound flight
const outboundStops =
ticket.flightIteneraries.outbound.length - 1;
// Calculate stops for inbound flight
const inboundStops = ticket.flightIteneraries.inbound.length - 1;
// Get the maximum number of stops between outbound and inbound
const maxStopsInJourney = Math.max(outboundStops, inboundStops);
switch (filters.maxStops) {
case MaxStops.Direct:
if (maxStopsInJourney > 0) return false;
break;
case MaxStops.One:
if (maxStopsInJourney > 1) return false;
break;
case MaxStops.Two:
if (maxStopsInJourney > 2) return false;
break;
}
}
// Time range filters
if (filters.time.departure.max > 0 || filters.time.arrival.max > 0) {
const allItineraries = [
...ticket.flightIteneraries.outbound,
...ticket.flightIteneraries.inbound,
];
for (const itinerary of allItineraries) {
const departureHour = new Date(
itinerary.departure.utcTime,
).getHours();
const arrivalHour = new Date(
itinerary.destination.utcTime,
).getHours();
if (filters.time.departure.max > 0) {
if (
departureHour < filters.time.departure.min ||
departureHour > filters.time.departure.max
) {
return false;
}
}
if (filters.time.arrival.max > 0) {
if (
arrivalHour < filters.time.arrival.min ||
arrivalHour > filters.time.arrival.max
) {
return false;
}
}
}
}
// Duration filter
if (filters.duration.max > 0) {
const allItineraries = [
...ticket.flightIteneraries.outbound,
...ticket.flightIteneraries.inbound,
];
const totalDuration = allItineraries.reduce(
(sum, itinerary) => sum + itinerary.durationSeconds,
0,
);
const durationHours = totalDuration / 3600;
if (
durationHours < filters.duration.min ||
durationHours > filters.duration.max
) {
return false;
}
}
// Overnight filter
if (!filters.allowOvernight) {
const allItineraries = [
...ticket.flightIteneraries.outbound,
...ticket.flightIteneraries.inbound,
];
const hasOvernightFlight = allItineraries.some((itinerary) => {
const departureHour = new Date(
itinerary.departure.utcTime,
).getHours();
const arrivalHour = new Date(
itinerary.destination.utcTime,
).getHours();
// Consider a flight overnight if it departs between 8 PM (20) and 6 AM (6)
return (
departureHour >= 20 ||
departureHour <= 6 ||
arrivalHour >= 20 ||
arrivalHour <= 6
);
});
if (hasOvernightFlight) return false;
}
return true;
});
filteredTickets.sort((a, b) => {
switch (filters.sortBy) {
case SortOption.PriceLowToHigh:
return (
a.priceDetails.displayPrice - b.priceDetails.displayPrice
);
case SortOption.PriceHighToLow:
return (
b.priceDetails.displayPrice - a.priceDetails.displayPrice
);
default:
return 0;
}
});
this.renderedTickets = filteredTickets;
this.searching = false;
}
}
export const flightTicketVM = new FlightTicketViewModel();

View File

@@ -1,50 +0,0 @@
<script lang="ts">
import BackpackIcon from "~icons/solar/backpack-broken";
import BagCheckIcon from "~icons/solar/suitcase-broken";
import PersonalBagIcon from "~icons/solar/bag-broken";
import Icon from "$lib/components/atoms/icon.svelte";
import Title from "$lib/components/atoms/title.svelte";
import type { FlightTicket } from "../../data/entities";
let { data }: { data: FlightTicket } = $props();
</script>
{#if data}
<Title size="h5" color="black">Baggage Info</Title>
<div class="flex flex-col gap-4">
<!-- Personal Item - Always show -->
<div class="flex items-center justify-between gap-2">
<div class="flex items-center gap-2">
<Icon icon={PersonalBagIcon} cls="w-6 h-6" />
<span>Personal Item</span>
</div>
<span>{data.bagsInfo.includedPersonalBags} included</span>
</div>
<!-- Cabin Baggage - Always show -->
<div class="flex items-center justify-between gap-2">
<div class="flex items-center gap-2">
<Icon icon={BackpackIcon} cls="w-6 h-6" />
<span>Cabin Baggage</span>
</div>
{#if data.bagsInfo.hasHandBagsSupport}
<span>{data.bagsInfo.includedHandBags} included</span>
{:else}
<span class="text-gray-500">Not available</span>
{/if}
</div>
<!-- Checked Baggage - Always show -->
<div class="flex items-center justify-between gap-2">
<div class="flex items-center gap-2">
<Icon icon={BagCheckIcon} cls="w-6 h-6" />
<span>Checked Baggage</span>
</div>
{#if data.bagsInfo.hasCheckedBagsSupport}
<span>{data.bagsInfo.includedCheckedBags} included</span>
{:else}
<span class="text-gray-500">Not available</span>
{/if}
</div>
</div>
{/if}

View File

@@ -1,25 +0,0 @@
<script lang="ts">
import * as Dialog from "$lib/components/ui/dialog/index.js";
import type { FlightTicket } from "../../data/entities";
import TicketDetails from "./ticket-details.svelte";
let {
data,
onCheckoutBtnClick,
children,
}: {
data: FlightTicket;
children: any;
onCheckoutBtnClick: (tid: number) => void;
} = $props();
</script>
<Dialog.Root>
{@render children()}
<Dialog.Content class="w-full max-w-2xl">
<div class="flex max-h-[80vh] w-full flex-col gap-4 overflow-y-auto">
<TicketDetails {data} {onCheckoutBtnClick} />
</div>
</Dialog.Content>
</Dialog.Root>

View File

@@ -1,22 +0,0 @@
<script lang="ts">
import Title from "$lib/components/atoms/title.svelte";
import Button from "$lib/components/ui/button/button.svelte";
import { type FlightTicket } from "../../data/entities";
import TripDetails from "./trip-details.svelte";
let {
data,
onCheckoutBtnClick,
}: { data: FlightTicket; onCheckoutBtnClick: (tid: number) => void } = $props();
</script>
<TripDetails {data} />
<div class="mt-4 flex items-center justify-end gap-4">
<Title center size="h5" color="black">
${data.priceDetails.displayPrice}
</Title>
<Button variant="default" onclick={() => onCheckoutBtnClick(data.id)}>
Continue to Checkout
</Button>
</div>

View File

@@ -1,91 +0,0 @@
<script lang="ts">
import Title from "$lib/components/atoms/title.svelte";
import { Badge } from "$lib/components/ui/badge/index.js";
import { Plane } from "@lucide/svelte";
interface Props {
departure: string;
destination: string;
durationSeconds: number;
departureTime: string;
arrivalTime: string;
departureDate: string;
arrivalDate: string;
airlineName?: string;
stops?: number;
}
let {
departure,
destination,
durationSeconds,
airlineName,
stops,
departureDate,
departureTime,
arrivalDate,
arrivalTime,
}: Props = $props();
const dH = Math.floor(durationSeconds / 3600);
const dM = Math.floor((durationSeconds % 3600) / 60);
const durationFormatted = `${dH}h ${dM}m`;
</script>
<div class="flex w-full items-center justify-between gap-8 p-2">
<!-- Departure Info -->
<div class="flex flex-col items-start">
<Title color="black" size="h5">{departure}</Title>
<Title color="black" size="p" weight="normal">
{departureTime}
</Title>
<span class="text-start text-sm text-gray-500">
{departureDate}
</span>
</div>
<!-- Flight Duration & Details -->
<div class="flex flex-col items-center gap-2">
<div class="flex items-center gap-2">
<div class="h-2 w-2 rounded-full bg-gray-400"></div>
<div class="relative">
<div class="w-40 border-t-2 border-gray-300"></div>
<Plane
class="absolute -top-[10px] left-1/2 -translate-x-1/2 transform text-primary"
size={20}
/>
</div>
<div class="h-2 w-2 rounded-full bg-gray-400"></div>
</div>
<span class="text-sm font-medium text-gray-600">
{durationFormatted}
</span>
<div class="flex flex-col items-center gap-1">
{#if stops !== undefined}
<Badge
variant={stops > 0 ? "secondary" : "outline"}
class="font-medium"
>
{#if stops > 0}
{stops} {stops === 1 ? "Stop" : "Stops"}
{:else}
Direct Flight
{/if}
</Badge>
{/if}
{#if airlineName}
<span class="text-xs text-gray-500">{airlineName}</span>
{/if}
</div>
</div>
<!-- Arrival Info -->
<div class="flex flex-col items-end">
<Title color="black" size="h5">{destination}</Title>
<Title color="black" size="p" weight="normal">
{arrivalTime}
</Title>
<span class="text-sm text-gray-500">{arrivalDate}</span>
</div>
</div>

View File

@@ -1,88 +0,0 @@
<script lang="ts">
import { TicketType, type FlightTicket } from "../../data/entities";
import { Badge } from "$lib/components/ui/badge/index.js";
import TicketItinerary from "./ticket-itinerary.svelte";
import { formatDateTime } from "@pkg/logic/core/date.utils";
let { data }: { data: FlightTicket } = $props();
const outboundDuration = data.flightIteneraries.outbound.reduce(
(acc, curr) => acc + curr.durationSeconds,
0,
);
const inboundDuration = data.flightIteneraries.inbound.reduce(
(acc, curr) => acc + curr.durationSeconds,
0,
);
const isReturnTicket =
data.flightIteneraries.inbound.length > 0 &&
data.flightType === TicketType.Return;
const outboundFirst = data.flightIteneraries.outbound[0];
const outboundLast =
data.flightIteneraries.outbound[
data.flightIteneraries.outbound.length - 1
];
const inboundFirst = isReturnTicket
? data.flightIteneraries.inbound[0]
: null;
const inboundLast = isReturnTicket
? data.flightIteneraries.inbound[
data.flightIteneraries.inbound.length - 1
]
: null;
const outboundDeparture = formatDateTime(outboundFirst.departure.localTime);
const outboundArrival = formatDateTime(outboundLast.destination.localTime);
const inboundDeparture = inboundFirst
? formatDateTime(inboundFirst.departure.localTime)
: null;
const inboundArrival = inboundLast
? formatDateTime(inboundLast.destination.localTime)
: null;
</script>
{#if isReturnTicket}
<Badge variant="outline" class="w-max">
<span>Outbound</span>
</Badge>
{/if}
<TicketItinerary
departure={data.departure}
destination={data.arrival}
durationSeconds={outboundDuration}
stops={data.flightIteneraries.outbound.length - 1}
departureTime={outboundDeparture.time}
departureDate={outboundDeparture.date}
arrivalTime={outboundArrival.time}
arrivalDate={outboundArrival.date}
airlineName={outboundFirst.airline.name}
/>
{#if isReturnTicket}
<div class="w-full border-t-2 border-dashed border-gray-400"></div>
{#if isReturnTicket}
<Badge variant="outline" class="w-max">
<span>Inbound</span>
</Badge>
{/if}
<TicketItinerary
departure={data.arrival}
destination={data.departure}
durationSeconds={inboundDuration}
stops={data.flightIteneraries.inbound.length - 1}
departureTime={inboundDeparture?.time || ""}
departureDate={inboundDeparture?.date || ""}
arrivalTime={inboundArrival?.time || ""}
arrivalDate={inboundArrival?.date || ""}
airlineName={inboundFirst?.airline.name}
/>
{:else}
<div class="w-full border-t-2 border-dashed border-gray-400"></div>
{/if}