🔄 cleanup: more order logic cleanup on the admin side mostly

This commit is contained in:
user
2025-10-20 22:39:00 +03:00
parent 10bcbf982a
commit 4ae1957a88
18 changed files with 375 additions and 221 deletions

View File

@@ -1,5 +1,78 @@
<script lang="ts"> <script lang="ts">
let { order }: { order?: any } = $props(); import { Badge } from "$lib/components/ui/badge";
import { Separator } from "$lib/components/ui/separator";
import type { FullOrderModel } from "$lib/domains/order/data/entities";
import { OrderStatus } from "$lib/domains/order/data/entities";
import OrderMainInfo from "./order-main-info.svelte";
import OrderMiscInfo from "./order-misc-info.svelte";
let { order }: { order?: FullOrderModel } = $props();
function getStatusVariant(status: OrderStatus) {
switch (status) {
case OrderStatus.FULFILLED:
return "success";
case OrderStatus.PARTIALLY_FULFILLED:
return "default";
case OrderStatus.PENDING_FULFILLMENT:
return "secondary";
case OrderStatus.CANCELLED:
return "destructive";
default:
return "outline";
}
}
function formatStatus(status: OrderStatus): string {
return status
.split("_")
.map((word) => word.charAt(0) + word.slice(1).toLowerCase())
.join(" ");
}
</script> </script>
<span>show the order details - todo</span> {#if order}
<div class="container mx-auto py-6">
<!-- Order Header -->
<div class="mb-6 flex items-center justify-between">
<div class="flex flex-col gap-2">
<h1 class="text-3xl font-bold">Order #{order.id}</h1>
<p class="text-gray-600">
Created on {new Date(order.createdAt).toLocaleDateString(
"en-US",
{
year: "numeric",
month: "long",
day: "numeric",
},
)}
</p>
</div>
<Badge
variant={getStatusVariant(order.status)}
class="px-4 py-2 text-base"
>
{formatStatus(order.status)}
</Badge>
</div>
<Separator class="my-6" />
<!-- Order Content Grid -->
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
<!-- Main Info Column -->
<div class="flex flex-col gap-6">
<OrderMainInfo {order} />
</div>
<!-- Misc Info Column -->
<div class="flex flex-col gap-6">
<OrderMiscInfo {order} />
</div>
</div>
</div>
{:else}
<div class="container mx-auto py-12 text-center">
<p class="text-xl text-gray-500">Order not found</p>
</div>
{/if}

View File

@@ -2,11 +2,11 @@
import Icon from "$lib/components/atoms/icon.svelte"; import Icon from "$lib/components/atoms/icon.svelte";
import Title from "$lib/components/atoms/title.svelte"; import Title from "$lib/components/atoms/title.svelte";
import { Badge } from "$lib/components/ui/badge"; import { Badge } from "$lib/components/ui/badge";
import CustomerDetailsCard from "$lib/domains/customerinfo/view/customer-details-card.svelte";
import type { FullOrderModel } from "$lib/domains/order/data/entities"; import type { FullOrderModel } from "$lib/domains/order/data/entities";
import TicketLegsOverview from "$lib/domains/ticket/view/ticket/ticket-legs-overview.svelte"; import ProductIcon from "~icons/solar/box-broken";
import CreditCardIcon from "~icons/solar/card-broken"; import CreditCardIcon from "~icons/solar/card-broken";
import EmailIcon from "~icons/solar/letter-broken"; import EmailIcon from "~icons/solar/letter-broken";
import TicketIcon from "~icons/solar/ticket-broken";
let { order }: { order: FullOrderModel } = $props(); let { order }: { order: FullOrderModel } = $props();
@@ -17,38 +17,59 @@
</script> </script>
<div class="flex flex-col gap-6"> <div class="flex flex-col gap-6">
{#if order.emailAccountId && order.emailAccount} {#if order.emailAccountId}
<!-- Email Account Info --> <!-- Email Account Info -->
<div class={cardStyle}> <div class={cardStyle}>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Icon icon={EmailIcon} cls="w-5 h-5" /> <Icon icon={EmailIcon} cls="w-5 h-5" />
<Title size="h5" color="black">Account Information</Title> <Title size="h5" color="black">Account Information</Title>
</div> </div>
<p class="text-gray-800">{order.emailAccount?.email}</p> <p class="text-gray-800">Email Account ID: #{order.emailAccountId}</p>
</div> </div>
{/if} {/if}
<!-- Flight Ticket Info --> <!-- Product Info -->
<div class={cardStyle}> <div class={cardStyle}>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Icon icon={TicketIcon} cls="w-5 h-5" /> <Icon icon={ProductIcon} cls="w-5 h-5" />
<Title size="h5" color="black">Flight Details</Title> <Title size="h5" color="black">Product Details</Title>
</div> </div>
<div class="flex gap-2"> {#if order.product.discountPrice > 0 && order.product.discountPrice < order.product.price}
<Badge variant="outline"> <Badge variant="success">Discounted</Badge>
{order.flightTicketInfo.flightType} {/if}
</Badge> </div>
<Badge variant="secondary">
{order.flightTicketInfo.cabinClass} <div class="flex flex-col gap-3">
</Badge> <div>
<span class="text-sm text-gray-500">Product Name</span>
<p class="font-medium">{order.product.title}</p>
</div>
<div>
<span class="text-sm text-gray-500">Description</span>
<p class="text-gray-700">{order.product.description}</p>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<span class="text-sm text-gray-500">Regular Price</span>
<p class="font-medium">${order.product.price.toFixed(2)}</p>
</div>
{#if order.product.discountPrice > 0}
<div>
<span class="text-sm text-gray-500">Discount Price</span>
<p class="font-medium text-green-600">
${order.product.discountPrice.toFixed(2)}
</p>
</div>
{/if}
</div>
</div> </div>
</div> </div>
<TicketLegsOverview data={order.flightTicketInfo} /> <!-- Price Summary -->
</div>
<div class={cardStyle}> <div class={cardStyle}>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Icon icon={CreditCardIcon} cls="w-5 h-5" /> <Icon icon={CreditCardIcon} cls="w-5 h-5" />
@@ -58,6 +79,11 @@
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<span>Base Price</span> <span>Base Price</span>
<span>${order.basePrice.toFixed(2)}</span>
</div>
<div class="flex items-center justify-between">
<span>Display Price</span>
<span>${order.displayPrice.toFixed(2)}</span> <span>${order.displayPrice.toFixed(2)}</span>
</div> </div>
@@ -71,9 +97,19 @@
{/if} {/if}
<div class="mt-2 flex items-center justify-between border-t pt-2"> <div class="mt-2 flex items-center justify-between border-t pt-2">
<span class="font-medium">Total Price</span> <span class="font-medium">Order Price</span>
<span class="font-medium">${order.orderPrice.toFixed(2)}</span> <span class="font-medium">${order.orderPrice.toFixed(2)}</span>
</div> </div>
<div class="flex items-center justify-between">
<span class="text-sm text-gray-500">Fulfilled</span>
<span class="text-sm">${order.fullfilledPrice.toFixed(2)}</span>
</div> </div>
</div> </div>
</div>
{#if order.customerInfo}
<!-- Customer Information -->
<CustomerDetailsCard customerInfo={order.customerInfo} />
{/if}
</div> </div>

View File

@@ -1,118 +1,115 @@
<script lang="ts"> <script lang="ts">
import Icon from "$lib/components/atoms/icon.svelte"; import Icon from "$lib/components/atoms/icon.svelte";
import Title from "$lib/components/atoms/title.svelte"; import Title from "$lib/components/atoms/title.svelte";
import { adminSiteNavMap } from "$lib/core/constants"; import { Badge } from "$lib/components/ui/badge";
import { capitalize } from "$lib/core/string.utils";
import CinfoCard from "$lib/domains/customerinfo/view/cinfo-card.svelte";
import type { FullOrderModel } from "$lib/domains/order/data/entities"; import type { FullOrderModel } from "$lib/domains/order/data/entities";
import SuitcaseIcon from "~icons/bi/suitcase2"; import { OrderStatus } from "$lib/domains/order/data/entities";
import BagIcon from "~icons/lucide/briefcase"; import CalendarIcon from "~icons/solar/calendar-broken";
import BackpackIcon from "~icons/solar/backpack-linear"; import InfoIcon from "~icons/solar/info-circle-broken";
import PackageIcon from "~icons/solar/box-broken";
import UsersIcon from "~icons/solar/users-group-rounded-broken";
let { order }: { order: FullOrderModel } = $props(); let { order }: { order: FullOrderModel } = $props();
const cardStyle = const cardStyle =
"flex flex-col gap-4 rounded-lg border border-gray-200 bg-white p-6 shadow-md"; "flex flex-col gap-4 rounded-lg border border-gray-200 bg-white p-6 shadow-md";
function getStatusVariant(status: OrderStatus) {
switch (status) {
case OrderStatus.FULFILLED:
return "success";
case OrderStatus.PARTIALLY_FULFILLED:
return "default";
case OrderStatus.PENDING_FULFILLMENT:
return "secondary";
case OrderStatus.CANCELLED:
return "destructive";
default:
return "outline";
}
}
function formatStatus(status: OrderStatus): string {
return status
.split("_")
.map((word) => word.charAt(0) + word.slice(1).toLowerCase())
.join(" ");
}
function formatDate(dateString: string): string {
const date = new Date(dateString);
return date.toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
});
}
</script> </script>
<div class="flex flex-col gap-6"> <div class="flex flex-col gap-6">
<!-- Passenger Information --> <!-- Order Status -->
<div class={cardStyle}> <div class={cardStyle}>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Icon icon={UsersIcon} cls="w-5 h-5" /> <Icon icon={InfoIcon} cls="w-5 h-5" />
<Title size="h5" color="black">Passengers</Title> <Title size="h5" color="black">Order Status</Title>
</div> </div>
{#each order.passengerInfos as passenger, index}
<div class="flex flex-col gap-4">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<span class="font-semibold"> <span class="text-gray-600">Current Status</span>
Passenger {index + 1} ({capitalize( <Badge variant={getStatusVariant(order.status)}>
passenger.passengerType, {formatStatus(order.status)}
)}) </Badge>
</span>
</div> </div>
<!-- Personal Info --> <div class="flex flex-col gap-2 text-sm">
<div class="rounded-lg border bg-gray-50 p-4"> <div class="flex items-center justify-between">
<div class="grid grid-cols-2 gap-3 text-sm md:grid-cols-3"> <span class="text-gray-600">Order ID</span>
<div> <span class="font-medium">#{order.id}</span>
<span class="text-gray-500">Name</span>
<p class="font-medium">
{passenger.passengerPii.firstName}
{passenger.passengerPii.middleName}
{passenger.passengerPii.lastName}
</p>
</div>
<div>
<span class="text-gray-500">Nationality</span>
<p class="font-medium">
{capitalize(passenger.passengerPii.nationality)}
</p>
</div>
<div>
<span class="text-gray-500">Date of Birth</span>
<p class="font-medium">
{passenger.passengerPii.dob}
</p>
</div> </div>
</div> </div>
</div> </div>
<!-- Baggage Info --> <!-- Timestamps -->
<div class="flex flex-wrap gap-4 text-sm"> <div class={cardStyle}>
{#if passenger.bagSelection.personalBags > 0}
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Icon <Icon icon={CalendarIcon} cls="w-5 h-5" />
icon={BackpackIcon} <Title size="h5" color="black">Timeline</Title>
cls="h-5 w-5 text-gray-600"
/>
<span
>{passenger.bagSelection.personalBags}x Personal
Item</span
>
</div> </div>
{/if}
{#if passenger.bagSelection.handBags > 0} <div class="flex flex-col gap-3">
<div class="flex items-center gap-2"> <div>
<Icon <span class="text-sm text-gray-500">Created At</span>
icon={SuitcaseIcon} <p class="font-medium">{formatDate(order.createdAt)}</p>
cls="h-5 w-5 text-gray-600"
/>
<span
>{passenger.bagSelection.handBags}x Cabin Bag</span
>
</div> </div>
{/if}
{#if passenger.bagSelection.checkedBags > 0} <div>
<div class="flex items-center gap-2"> <span class="text-sm text-gray-500">Last Updated</span>
<Icon icon={BagIcon} cls="h-5 w-5 text-gray-600" /> <p class="font-medium">{formatDate(order.updatedAt)}</p>
<span
>{passenger.bagSelection.checkedBags}x Checked Bag</span
>
</div> </div>
{/if}
</div> </div>
</div> </div>
{#if index < order.passengerInfos.length - 1} <!-- Additional Info -->
<div class="border-b border-dashed"></div> {#if order.emailAccountId}
{/if} <div class={cardStyle}>
{/each} <div class="flex items-center gap-2">
<Icon icon={InfoIcon} cls="w-5 h-5" />
<Title size="h5" color="black">Additional Information</Title>
</div> </div>
{#if order.flightTicketInfo.refOIds} <div class="flex flex-col gap-2 text-sm">
{#each order.flightTicketInfo.refOIds as refOId} <div class="flex items-center justify-between">
<CinfoCard icon={PackageIcon} title="Order"> <span class="text-gray-600">Email Account ID</span>
<a <span class="font-medium">#{order.emailAccountId}</span>
href={`${adminSiteNavMap.orders}/${refOId}`} </div>
class="mt-1 inline-block font-medium text-primary hover:underline"
> {#if order.paymentInfoId}
Reference Order #{refOId} <div class="flex items-center justify-between">
</a> <span class="text-gray-600">Payment Info ID</span>
</CinfoCard> <span class="font-medium">#{order.paymentInfoId}</span>
{/each} </div>
{/if}
</div>
</div>
{/if} {/if}
</div> </div>

View File

@@ -1,14 +1,14 @@
import { get } from "svelte/store"; import { encodeCursor } from "$lib/core/string.utils";
import {
getDefaultOrderCursor,
type PaginatedOrderInfoModel,
type OrderCursorModel,
getDefaultPaginatedOrderInfoModel,
} from "../data/entities";
import { trpcApiStore } from "$lib/stores/api"; import { trpcApiStore } from "$lib/stores/api";
import type { Result } from "@pkg/result"; import type { Result } from "@pkg/result";
import { toast } from "svelte-sonner"; import { toast } from "svelte-sonner";
import { encodeCursor } from "$lib/core/string.utils"; import { get } from "svelte/store";
import {
getDefaultOrderCursor,
getDefaultPaginatedOrderInfoModel,
type OrderCursorModel,
type PaginatedOrderInfoModel,
} from "../data/entities";
export class OrderViewModel { export class OrderViewModel {
orderInfo = $state<PaginatedOrderInfoModel>( orderInfo = $state<PaginatedOrderInfoModel>(
@@ -82,16 +82,7 @@ export class OrderViewModel {
this.resetOrderInfo(); this.resetOrderInfo();
return; return;
} }
this.orderInfo = { this.orderInfo = { ...res.data, data: res.data.data };
...res.data,
data: res.data.data.map((item) => {
return {
...item,
createdAt: new Date(item.createdAt),
updatedAt: new Date(item.updatedAt),
};
}),
};
} }
async searchOrders() { async searchOrders() {

View File

@@ -1,25 +1,25 @@
<script lang="ts"> <script lang="ts">
import { import { goto } from "$app/navigation";
getCoreRowModel, import Title from "$lib/components/atoms/title.svelte";
getPaginationRowModel, import DataTableActions from "$lib/components/molecules/data-table/data-table-actions.svelte";
getFilteredRowModel, import DataTable from "$lib/components/molecules/data-table/data-table.svelte";
type ColumnDef,
type PaginationState,
type ColumnFiltersState,
} from "@tanstack/table-core";
import { import {
createSvelteTable, createSvelteTable,
renderComponent, renderComponent,
} from "$lib/components/ui/data-table"; } from "$lib/components/ui/data-table";
import DataTable from "$lib/components/molecules/data-table/data-table.svelte";
import DataTableActions from "$lib/components/molecules/data-table/data-table-actions.svelte";
import Title from "$lib/components/atoms/title.svelte";
import { goto } from "$app/navigation";
import { adminBasePath } from "$lib/core/constants"; import { adminBasePath } from "$lib/core/constants";
import { snakeToSpacedPascal } from "$lib/core/string.utils";
import {
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
type ColumnDef,
type ColumnFiltersState,
type PaginationState,
} from "@tanstack/table-core";
import { useDebounce } from "runed"; import { useDebounce } from "runed";
import type { FullOrderModel } from "../data/entities"; import type { FullOrderModel } from "../data/entities";
import { orderVM } from "./order.vm.svelte"; import { orderVM } from "./order.vm.svelte";
import { capitalize, snakeToSpacedPascal } from "$lib/core/string.utils";
let { data }: { data: FullOrderModel[] } = $props(); let { data }: { data: FullOrderModel[] } = $props();
@@ -33,34 +33,45 @@
}, },
}, },
{ {
header: "Places", header: "Order ID",
accessorKey: "direction", accessorKey: "orderId",
cell: ({ row }) => { cell: ({ row }) => {
const r = row.original as FullOrderModel; const r = row.original as FullOrderModel;
return `${r.flightTicketInfo.departure} - ${r.flightTicketInfo.arrival}`; return `#${r.id}`;
}, },
}, },
{ {
header: "Type", header: "Product",
accessorKey: "type", accessorKey: "product",
cell: ({ row }) => { cell: ({ row }) => {
const r = row.original as FullOrderModel; const r = row.original as FullOrderModel;
return capitalize(r.flightTicketInfo.flightType.toLowerCase()); return r.product.title;
}, },
}, },
{ {
header: "Passenger(s)", header: "Customer",
accessorKey: "passengers", accessorKey: "customer",
cell: ({ row }) => { cell: ({ row }) => {
const _o = (row.original as FullOrderModel).flightTicketInfo const r = row.original as FullOrderModel;
.passengerCounts; if (!r.customerInfo) return "N/A";
return `${_o.adults + _o.children}`; return `${r.customerInfo.firstName} ${r.customerInfo.lastName}`;
},
},
{
header: "Customer Email",
accessorKey: "customerEmail",
cell: ({ row }) => {
const r = row.original as FullOrderModel;
return r.customerInfo?.email || "N/A";
}, },
}, },
{ {
header: "Price", header: "Price",
accessorKey: "orderPrice", accessorKey: "orderPrice",
cell: ({ row }) => (row.original as FullOrderModel).orderPrice!, cell: ({ row }) => {
const r = row.original as FullOrderModel;
return `$${r.orderPrice.toFixed(2)}`;
},
}, },
{ {
header: "Status", header: "Status",
@@ -141,7 +152,7 @@
() => debounceDuration, () => debounceDuration,
); );
let pageCount = $derived(2); let pageCount = $derived(Math.ceil(data.length / pagination.pageSize) || 1);
</script> </script>
{#if data.length > 0} {#if data.length > 0}
@@ -162,7 +173,7 @@
orderVM.query = q; orderVM.query = q;
debouncedSearch(); debouncedSearch();
}} }}
filterFieldPlaceholder="Search users..." filterFieldPlaceholder="Search orders..."
/> />
{:else} {:else}
<div class="grid h-full place-items-center p-4 py-12 md:p-8 md:py-32"> <div class="grid h-full place-items-center p-4 py-12 md:p-8 md:py-32">

View File

@@ -1,34 +1,80 @@
<script lang="ts"> <script lang="ts">
import { import { goto } from "$app/navigation";
getCoreRowModel, import Title from "$lib/components/atoms/title.svelte";
getPaginationRowModel, import DataTableActions from "$lib/components/molecules/data-table/data-table-actions.svelte";
getFilteredRowModel, import DataTable from "$lib/components/molecules/data-table/data-table.svelte";
type ColumnDef,
type PaginationState,
type ColumnFiltersState,
} from "@tanstack/table-core";
import { import {
createSvelteTable, createSvelteTable,
renderComponent, renderComponent,
} from "$lib/components/ui/data-table"; } from "$lib/components/ui/data-table";
import DataTable from "$lib/components/molecules/data-table/data-table.svelte";
import DataTableActions from "$lib/components/molecules/data-table/data-table-actions.svelte";
import Title from "$lib/components/atoms/title.svelte";
import { goto } from "$app/navigation";
import { adminBasePath } from "$lib/core/constants"; import { adminBasePath } from "$lib/core/constants";
import { orderVM } from "./order.vm.svelte"; import {
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
type ColumnDef,
type ColumnFiltersState,
type PaginationState,
} from "@tanstack/table-core";
import { useDebounce } from "runed"; import { useDebounce } from "runed";
import { OrderStatus } from "../data/entities";
import { orderVM } from "./order.vm.svelte";
function getStatusVariant(status: OrderStatus) {
switch (status) {
case OrderStatus.FULFILLED:
return "success";
case OrderStatus.PARTIALLY_FULFILLED:
return "default";
case OrderStatus.PENDING_FULFILLMENT:
return "secondary";
case OrderStatus.CANCELLED:
return "destructive";
default:
return "outline";
}
}
function formatStatus(status: OrderStatus): string {
return status
.split("_")
.map((word) => word.charAt(0) + word.slice(1).toLowerCase())
.join(" ");
}
// Define columns // Define columns
const columns: ColumnDef<any>[] = [ const columns: ColumnDef<any>[] = [
{ {
header: "Username", header: "Order ID",
accessorKey: "username", accessorKey: "id",
cell: ({ row }) => `#${row.original.id}`,
}, },
{ {
header: "Email", header: "Product",
accessorKey: "email", id: "product",
cell: ({ getValue }) => getValue<string>().toLowerCase(), cell: ({ row }) => {
return row.original.product.title;
},
},
{
header: "Price",
id: "price",
cell: ({ row }) => {
const basePrice = row.original.displayPrice;
const finalPrice = row.original.basePrice;
const hasDiscount = row.original.discountAmount > 0;
return hasDiscount
? `$${finalPrice.toFixed(2)} (was $${basePrice.toFixed(2)})`
: `$${finalPrice.toFixed(2)}`;
},
},
{
header: "Status",
accessorKey: "status",
cell: ({ row }) => {
return formatStatus(row.original.status);
},
}, },
{ {
header: "Action", header: "Action",
@@ -37,9 +83,11 @@
return renderComponent(DataTableActions, { return renderComponent(DataTableActions, {
actions: [ actions: [
{ {
title: "View agent", title: "View Order",
action: () => { action: () => {
goto(`${adminBasePath}/users/${row.original.id}`); goto(
`${adminBasePath}/orders/${row.original.id}`,
);
}, },
}, },
], ],
@@ -113,7 +161,7 @@
orderVM.query = q; orderVM.query = q;
debouncedSearch(); debouncedSearch();
}} }}
filterFieldPlaceholder="Search users..." filterFieldPlaceholder="Search orders..."
/> />
{:else} {:else}
<div class="grid place-items-center p-4 py-12 md:p-8 md:py-24"> <div class="grid place-items-center p-4 py-12 md:p-8 md:py-24">

View File

@@ -2,17 +2,17 @@ import { eq, type Database } from "@pkg/db";
import { paymentInfo } from "@pkg/db/schema"; import { paymentInfo } from "@pkg/db/schema";
import { Logger } from "@pkg/logger"; import { Logger } from "@pkg/logger";
import type { Result } from "@pkg/result"; import type { Result } from "@pkg/result";
import { paymentDetailsModel, type PaymentDetails } from "./data"; import { paymentInfoModel, type PaymentInfo } from "./data";
export class PaymentInfoRepository { export class PaymentInfoRepository {
constructor(private db: Database) {} constructor(private db: Database) {}
async getPaymentInfo(id: number): Promise<Result<PaymentDetails>> { async getPaymentInfo(id: number): Promise<Result<PaymentInfo>> {
Logger.info(`Getting payment info with id ${id}`); Logger.info(`Getting payment info with id ${id}`);
const out = await this.db.query.paymentInfo.findFirst({ const out = await this.db.query.paymentInfo.findFirst({
where: eq(paymentInfo.id, id), where: eq(paymentInfo.id, id),
}); });
const parsed = paymentDetailsModel.safeParse(out); const parsed = paymentInfoModel.safeParse(out);
if (parsed.error) { if (parsed.error) {
Logger.error(parsed.error); Logger.error(parsed.error);
return {}; return {};

View File

@@ -1,5 +1,5 @@
import type { CustomerInfo } from "$lib/domains/passengerinfo/data/entities"; import type { CustomerInfo } from "$lib/domains/passengerinfo/data/entities";
import type { PaymentDetailsPayload } from "$lib/domains/paymentinfo/data/entities"; import type { PaymentInfoPayload } from "$lib/domains/paymentinfo/data/entities";
import { CheckoutStep } from "$lib/domains/ticket/data/entities"; import { CheckoutStep } from "$lib/domains/ticket/data/entities";
import type { Database } from "@pkg/db"; import type { Database } from "@pkg/db";
import { and, eq } from "@pkg/db"; import { and, eq } from "@pkg/db";
@@ -254,7 +254,7 @@ export class CheckoutFlowRepository {
async syncPaymentInfo( async syncPaymentInfo(
flowId: string, flowId: string,
paymentInfo: PaymentDetailsPayload, paymentInfo: PaymentInfoPayload,
): Promise<Result<boolean>> { ): Promise<Result<boolean>> {
try { try {
const existingSession = await this.db const existingSession = await this.db

View File

@@ -3,8 +3,8 @@ import {
type CustomerInfo, type CustomerInfo,
} from "$lib/domains/passengerinfo/data/entities"; } from "$lib/domains/passengerinfo/data/entities";
import { import {
paymentDetailsPayloadModel, paymentInfoPayloadModel,
type PaymentDetailsPayload, type PaymentInfoPayload,
} from "$lib/domains/paymentinfo/data/entities"; } from "$lib/domains/paymentinfo/data/entities";
import { CheckoutStep } from "$lib/domains/ticket/data/entities"; import { CheckoutStep } from "$lib/domains/ticket/data/entities";
import { createTRPCRouter, publicProcedure } from "$lib/trpc/t"; import { createTRPCRouter, publicProcedure } from "$lib/trpc/t";
@@ -86,7 +86,7 @@ export const ckflowRouter = createTRPCRouter({
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
return getCKUseCases().syncPaymentInfo( return getCKUseCases().syncPaymentInfo(
input.flowId, input.flowId,
input.paymentInfo as PaymentDetailsPayload, input.paymentInfo as PaymentInfoPayload,
); );
}), }),
@@ -119,7 +119,7 @@ export const ckflowRouter = createTRPCRouter({
flowId: z.string(), flowId: z.string(),
payload: z.object({ payload: z.object({
personalInfo: customerInfoModel.optional(), personalInfo: customerInfoModel.optional(),
paymentInfo: paymentDetailsPayloadModel.optional(), paymentInfo: paymentInfoPayloadModel.optional(),
}), }),
}), }),
) )

View File

@@ -1,5 +1,5 @@
import type { CustomerInfo } from "$lib/domains/passengerinfo/data/entities"; import type { CustomerInfo } from "$lib/domains/passengerinfo/data/entities";
import type { PaymentDetailsPayload } from "$lib/domains/paymentinfo/data/entities"; import type { PaymentInfoPayload } from "$lib/domains/paymentinfo/data/entities";
import { db } from "@pkg/db"; import { db } from "@pkg/db";
import { isTimestampMoreThan1MinAgo } from "@pkg/logic/core/date.utils"; import { isTimestampMoreThan1MinAgo } from "@pkg/logic/core/date.utils";
import type { import type {
@@ -58,7 +58,7 @@ export class CheckoutFlowUseCases {
return this.repo.syncPersonalInfo(flowId, personalInfo); return this.repo.syncPersonalInfo(flowId, personalInfo);
} }
async syncPaymentInfo(flowId: string, paymentInfo: PaymentDetailsPayload) { async syncPaymentInfo(flowId: string, paymentInfo: PaymentInfoPayload) {
return this.repo.syncPaymentInfo(flowId, paymentInfo); return this.repo.syncPaymentInfo(flowId, paymentInfo);
} }

View File

@@ -11,7 +11,7 @@ import {
type CustomerInfo, type CustomerInfo,
} from "$lib/domains/passengerinfo/data/entities"; } from "$lib/domains/passengerinfo/data/entities";
import { passengerInfoVM } from "$lib/domains/passengerinfo/view/passenger.info.vm.svelte"; import { passengerInfoVM } from "$lib/domains/passengerinfo/view/passenger.info.vm.svelte";
import type { PaymentDetailsPayload } from "$lib/domains/paymentinfo/data/entities"; import type { PaymentInfoPayload } from "$lib/domains/paymentinfo/data/entities";
import { PaymentMethod } from "$lib/domains/paymentinfo/data/entities"; import { PaymentMethod } from "$lib/domains/paymentinfo/data/entities";
import { import {
CheckoutStep, CheckoutStep,
@@ -302,7 +302,7 @@ export class CKFlowViewModel {
} }
} }
async syncPaymentInfo(paymentInfo: PaymentDetailsPayload) { async syncPaymentInfo(paymentInfo: PaymentInfoPayload) {
if (!this.flowId || !this.setupDone || !paymentInfo.cardDetails) { if (!this.flowId || !this.setupDone || !paymentInfo.cardDetails) {
return; return;
} }

View File

@@ -3,9 +3,9 @@ import { paymentInfo } from "@pkg/db/schema";
import { Logger } from "@pkg/logger"; import { Logger } from "@pkg/logger";
import type { Result } from "@pkg/result"; import type { Result } from "@pkg/result";
import { import {
paymentDetailsModel, paymentInfoModel,
type PaymentDetails, type PaymentInfo,
type PaymentDetailsPayload, type PaymentInfoPayload,
} from "./entities"; } from "./entities";
export class PaymentInfoRepository { export class PaymentInfoRepository {
@@ -14,9 +14,7 @@ export class PaymentInfoRepository {
this.db = db; this.db = db;
} }
async createPaymentInfo( async createPaymentInfo(data: PaymentInfoPayload): Promise<Result<number>> {
data: PaymentDetailsPayload,
): Promise<Result<number>> {
const out = await this.db const out = await this.db
.insert(paymentInfo) .insert(paymentInfo)
.values({ .values({
@@ -34,12 +32,12 @@ export class PaymentInfoRepository {
return { data: out[0]?.id }; return { data: out[0]?.id };
} }
async getPaymentInfo(id: number): Promise<Result<PaymentDetails>> { async getPaymentInfo(id: number): Promise<Result<PaymentInfo>> {
Logger.info(`Getting payment info with id ${id}`); Logger.info(`Getting payment info with id ${id}`);
const out = await this.db.query.paymentInfo.findFirst({ const out = await this.db.query.paymentInfo.findFirst({
where: eq(paymentInfo.id, id), where: eq(paymentInfo.id, id),
}); });
const parsed = paymentDetailsModel.safeParse(out); const parsed = paymentInfoModel.safeParse(out);
if (parsed.error) { if (parsed.error) {
Logger.error(parsed.error); Logger.error(parsed.error);
return {}; return {};

View File

@@ -1,4 +1,4 @@
import type { PaymentDetailsPayload } from "../data/entities"; import type { PaymentInfoPayload } from "../data/entities";
import type { PaymentInfoRepository } from "../data/repository"; import type { PaymentInfoRepository } from "../data/repository";
export class PaymentInfoUseCases { export class PaymentInfoUseCases {
@@ -8,7 +8,7 @@ export class PaymentInfoUseCases {
this.repo = repo; this.repo = repo;
} }
async createPaymentInfo(payload: PaymentDetailsPayload) { async createPaymentInfo(payload: PaymentInfoPayload) {
return this.repo.createPaymentInfo(payload); return this.repo.createPaymentInfo(payload);
} }

View File

@@ -2,7 +2,7 @@ import { ckFlowVM } from "$lib/domains/ckflow/view/ckflow.vm.svelte";
import { newOrderModel } from "$lib/domains/order/data/entities"; import { newOrderModel } from "$lib/domains/order/data/entities";
import { passengerInfoVM } from "$lib/domains/passengerinfo/view/passenger.info.vm.svelte"; import { passengerInfoVM } from "$lib/domains/passengerinfo/view/passenger.info.vm.svelte";
import { import {
paymentDetailsPayloadModel, paymentInfoPayloadModel,
PaymentMethod, PaymentMethod,
} from "$lib/domains/paymentinfo/data/entities"; } from "$lib/domains/paymentinfo/data/entities";
import { trpcApiStore } from "$lib/stores/api"; import { trpcApiStore } from "$lib/stores/api";
@@ -105,7 +105,7 @@ class TicketCheckoutViewModel {
return false; return false;
} }
const pInfoParsed = paymentDetailsPayloadModel.safeParse({ const pInfoParsed = paymentInfoPayloadModel.safeParse({
method: PaymentMethod.Card, method: PaymentMethod.Card,
cardDetails: paymentInfoVM.cardDetails, cardDetails: paymentInfoVM.cardDetails,
flightTicketInfoId: ticket.id, flightTicketInfoId: ticket.id,

View File

@@ -5,8 +5,8 @@ import {
customerInfoModel, customerInfoModel,
} from "../../passengerinfo/data/entities"; } from "../../passengerinfo/data/entities";
import { import {
PaymentDetailsPayload, PaymentInfoPayload,
paymentDetailsPayloadModel, paymentInfoPayloadModel,
} from "../../paymentinfo/data/entities"; } from "../../paymentinfo/data/entities";
import { productModel } from "../../product/data"; import { productModel } from "../../product/data";
@@ -78,7 +78,7 @@ export const flowInfoModel = z.object({
pendingActions: pendingActionsModel.default([]), pendingActions: pendingActionsModel.default([]),
personalInfo: z.custom<CustomerInfo>().optional(), personalInfo: z.custom<CustomerInfo>().optional(),
paymentInfo: z.custom<PaymentDetailsPayload>().optional(), paymentInfo: z.custom<PaymentInfoPayload>().optional(),
refOids: z.array(z.number()).optional(), refOids: z.array(z.number()).optional(),
otpCode: z.coerce.string().optional(), otpCode: z.coerce.string().optional(),
@@ -134,7 +134,7 @@ export type PrePaymentFlowStepPayload = z.infer<
export const paymentFlowStepPayloadModel = z.object({ export const paymentFlowStepPayloadModel = z.object({
personalInfo: customerInfoModel.optional(), personalInfo: customerInfoModel.optional(),
paymentInfo: paymentDetailsPayloadModel.optional(), paymentInfo: paymentInfoPayloadModel.optional(),
}); });
export type PaymentFlowStepPayload = z.infer< export type PaymentFlowStepPayload = z.infer<
typeof paymentFlowStepPayloadModel typeof paymentFlowStepPayloadModel

View File

@@ -2,7 +2,7 @@ import { z } from "zod";
import { paginationModel } from "../../../core/pagination.utils"; import { paginationModel } from "../../../core/pagination.utils";
import { encodeCursor } from "../../../core/string.utils"; import { encodeCursor } from "../../../core/string.utils";
import { customerInfoModel } from "../../customerinfo/data"; import { customerInfoModel } from "../../customerinfo/data";
import { paymentDetailsPayloadModel } from "../../paymentinfo/data/entities"; import { paymentInfoPayloadModel } from "../../paymentinfo/data/entities";
import { productModel } from "../../product/data"; import { productModel } from "../../product/data";
export enum OrderCreationStep { export enum OrderCreationStep {
@@ -35,7 +35,7 @@ export const orderModel = z.object({
productId: z.number(), productId: z.number(),
customerInfoId: z.number().nullish().optional(), customerInfoId: z.number().nullish().optional(),
emailAccountId: z.number().nullish().optional(), emailAccountId: z.number().nullish().optional(),
paymentDetailsId: z.number().nullish().optional(), paymentInfoId: z.number().nullish().optional(),
createdAt: z.coerce.string(), createdAt: z.coerce.string(),
updatedAt: z.coerce.string(), updatedAt: z.coerce.string(),
@@ -108,7 +108,7 @@ export const newOrderModel = orderModel.pick({
fullfilledPrice: true, fullfilledPrice: true,
productId: true, productId: true,
customerInfoId: true, customerInfoId: true,
paymentDetailsId: true, paymentInfoId: true,
emailAccountId: true, emailAccountId: true,
}); });
export type NewOrderModel = z.infer<typeof newOrderModel>; export type NewOrderModel = z.infer<typeof newOrderModel>;
@@ -117,7 +117,7 @@ export const createOrderPayloadModel = z.object({
product: productModel.optional(), product: productModel.optional(),
productId: z.number().optional(), productId: z.number().optional(),
customerInfo: customerInfoModel, customerInfo: customerInfoModel,
paymentDetails: paymentDetailsPayloadModel.optional(), paymentInfo: paymentInfoPayloadModel.optional(),
orderModel: newOrderModel, orderModel: newOrderModel,
}); });
export type CreateOrderModel = z.infer<typeof createOrderPayloadModel>; export type CreateOrderModel = z.infer<typeof createOrderPayloadModel>;

View File

@@ -1,5 +1,5 @@
import { z } from "zod"; import { z } from "zod";
import { paymentDetailsModel } from "../../paymentinfo/data/entities"; import { paymentInfoModel } from "../../paymentinfo/data/entities";
export enum Gender { export enum Gender {
Male = "male", Male = "male",
@@ -45,9 +45,9 @@ export const passengerInfoModel = z.object({
id: z.number(), id: z.number(),
passengerType: z.enum([PassengerType.Adult, PassengerType.Child]), passengerType: z.enum([PassengerType.Adult, PassengerType.Child]),
passengerPii: customerInfoModel, passengerPii: customerInfoModel,
paymentDetails: paymentDetailsModel.optional(), paymentInfo: paymentInfoModel.optional(),
passengerPiiId: z.number().optional(), passengerPiiId: z.number().optional(),
paymentDetailsId: z.number().optional(), paymentInfoId: z.number().optional(),
seatSelection: z.any(), seatSelection: z.any(),
bagSelection: z.any(), bagSelection: z.any(),

View File

@@ -74,15 +74,15 @@ export const cardInfoModel = z.object({
}); });
export type CardInfo = z.infer<typeof cardInfoModel>; export type CardInfo = z.infer<typeof cardInfoModel>;
export const paymentDetailsPayloadModel = z.object({ export const paymentInfoPayloadModel = z.object({
method: z.enum([PaymentMethod.Card]), method: z.enum([PaymentMethod.Card]),
cardDetails: cardInfoModel, cardDetails: cardInfoModel,
productId: z.number().int(), productId: z.number().int(),
orderId: z.number().int(), orderId: z.number().int(),
}); });
export type PaymentDetailsPayload = z.infer<typeof paymentDetailsPayloadModel>; export type PaymentInfoPayload = z.infer<typeof paymentInfoPayloadModel>;
export const paymentDetailsModel = cardInfoModel.merge( export const paymentInfoModel = cardInfoModel.merge(
z.object({ z.object({
id: z.number().int(), id: z.number().int(),
productId: z.number().int(), productId: z.number().int(),
@@ -91,4 +91,4 @@ export const paymentDetailsModel = cardInfoModel.merge(
updatedAt: z.string().datetime(), updatedAt: z.string().datetime(),
}), }),
); );
export type PaymentDetails = z.infer<typeof paymentDetailsModel>; export type PaymentInfo = z.infer<typeof paymentInfoModel>;