big boi refactor to customer inof from passenger info
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -14,10 +14,6 @@ __pycache__
|
|||||||
ot_res.json
|
ot_res.json
|
||||||
out.json
|
out.json
|
||||||
payload.json
|
payload.json
|
||||||
skin.json
|
|
||||||
skyscanner.json
|
|
||||||
skyscanner.har
|
|
||||||
skyscanner-airport-search.har
|
|
||||||
|
|
||||||
screenshots/*.jpeg
|
screenshots/*.jpeg
|
||||||
screenshots/*.png
|
screenshots/*.png
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export * from "@pkg/logic/domains/account/data/entities";
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
import { and, eq, inArray, notInArray, type Database } from "@pkg/db";
|
|
||||||
import { inbox } from "@pkg/db/schema";
|
|
||||||
import { ERROR_CODES, type Result } from "@pkg/result";
|
|
||||||
import { inboxModel, type InboxModel } from "./entities";
|
|
||||||
import { getError, Logger } from "@pkg/logger";
|
|
||||||
|
|
||||||
export class AccountInboxRepository {
|
|
||||||
db: Database;
|
|
||||||
|
|
||||||
constructor(db: Database) {
|
|
||||||
this.db = db;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAccountInbox(
|
|
||||||
accountId: number,
|
|
||||||
ignoreIds: number[] = [],
|
|
||||||
): Promise<Result<InboxModel[]>> {
|
|
||||||
let condition: any = eq(inbox.emailAccountId, accountId);
|
|
||||||
if (ignoreIds.length > 0) {
|
|
||||||
condition = and(condition, notInArray(inbox.id, ignoreIds));
|
|
||||||
}
|
|
||||||
|
|
||||||
const queryRes = await this.db.query.inbox.findMany({
|
|
||||||
where: condition,
|
|
||||||
});
|
|
||||||
|
|
||||||
const out = [] as InboxModel[];
|
|
||||||
|
|
||||||
for (const row of queryRes) {
|
|
||||||
const sParsed = inboxModel.safeParse(row);
|
|
||||||
if (sParsed.success) {
|
|
||||||
out.push(sParsed.data);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
error: getError(
|
|
||||||
{
|
|
||||||
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
|
||||||
message:
|
|
||||||
"An error occured while fetching inbox for account",
|
|
||||||
userHint: "Please try again later, or contact us",
|
|
||||||
detail: "An error occured while fetching inbox for account",
|
|
||||||
actionable: false,
|
|
||||||
},
|
|
||||||
sParsed.error.errors,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return { data: out };
|
|
||||||
}
|
|
||||||
|
|
||||||
// checks which of these are already present in the db
|
|
||||||
async findEmailsAlreadyPresent(
|
|
||||||
emailIds: string[],
|
|
||||||
): Promise<Result<string[]>> {
|
|
||||||
const found = await this.db.query.inbox.findMany({
|
|
||||||
where: inArray(inbox.emailId, emailIds),
|
|
||||||
columns: { emailId: true },
|
|
||||||
});
|
|
||||||
return { data: found.map((e) => e.emailId) };
|
|
||||||
}
|
|
||||||
|
|
||||||
async insertMessages(
|
|
||||||
accountId: number,
|
|
||||||
messages: InboxModel[],
|
|
||||||
): Promise<Result<number[]>> {
|
|
||||||
try {
|
|
||||||
const alreadyPresent = await this.findEmailsAlreadyPresent(
|
|
||||||
messages.map((e) => e.emailId),
|
|
||||||
);
|
|
||||||
if (!alreadyPresent.data) {
|
|
||||||
return { error: alreadyPresent.error };
|
|
||||||
}
|
|
||||||
const payload = messages
|
|
||||||
.filter((e) => !alreadyPresent.data?.includes(e.emailId))
|
|
||||||
.map((m) => {
|
|
||||||
return {
|
|
||||||
emailId: m.emailId,
|
|
||||||
|
|
||||||
from: m.from,
|
|
||||||
to: m.to,
|
|
||||||
cc: m.cc,
|
|
||||||
subject: m.subject,
|
|
||||||
body: m.body,
|
|
||||||
attachments: m.attachments,
|
|
||||||
|
|
||||||
emailAccountId: accountId,
|
|
||||||
|
|
||||||
dated: new Date(m.dated),
|
|
||||||
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
if (payload.length === 0) {
|
|
||||||
Logger.info(
|
|
||||||
`No msgs to insert for acc:${accountId} (${alreadyPresent.data.length} already present)`,
|
|
||||||
);
|
|
||||||
return { data: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.info(
|
|
||||||
`Inserting ${messages.length} msgs for acc:${accountId} (${alreadyPresent.data.length} already present)`,
|
|
||||||
);
|
|
||||||
const out = await this.db
|
|
||||||
.insert(inbox)
|
|
||||||
.values(payload)
|
|
||||||
.returning({ id: inbox.id })
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
return { data: out.map((e) => e.id) };
|
|
||||||
} catch (e) {
|
|
||||||
return {
|
|
||||||
error: getError(
|
|
||||||
{
|
|
||||||
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
|
||||||
message:
|
|
||||||
"An error occured while inserting messages for account",
|
|
||||||
userHint: "Please try again later, or contact us",
|
|
||||||
detail: "An error occured while inserting messages for account",
|
|
||||||
actionable: false,
|
|
||||||
},
|
|
||||||
e,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getInboxMessagesCount(accountId: number): Promise<Result<number>> {
|
|
||||||
const res = await this.db.query.inbox.findMany({
|
|
||||||
where: eq(inbox.emailAccountId, accountId),
|
|
||||||
columns: { id: true },
|
|
||||||
});
|
|
||||||
return { data: res ? res.length : 0 };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
import { eq, type Database } from "@pkg/db";
|
|
||||||
import {
|
|
||||||
emailAccountFullModel,
|
|
||||||
emailAccountModel,
|
|
||||||
type EmailAccount,
|
|
||||||
type EmailAccountFull,
|
|
||||||
} from "./entities";
|
|
||||||
import { ERROR_CODES, type Result } from "@pkg/result";
|
|
||||||
import { Logger } from "@pkg/logger";
|
|
||||||
import { getError } from "@pkg/logger";
|
|
||||||
import { emailAccount } from "@pkg/db/schema";
|
|
||||||
|
|
||||||
export class AccountsRepository {
|
|
||||||
db: Database;
|
|
||||||
constructor(db: Database) {
|
|
||||||
this.db = db;
|
|
||||||
}
|
|
||||||
|
|
||||||
async listAllEmailAccounts(): Promise<Result<EmailAccount[]>> {
|
|
||||||
try {
|
|
||||||
const out = await this.db.query.emailAccount.findMany();
|
|
||||||
Logger.info(out);
|
|
||||||
const parsed = [] as EmailAccount[];
|
|
||||||
for (const each of out) {
|
|
||||||
const pRes = emailAccountModel.safeParse(each);
|
|
||||||
if (pRes.error) {
|
|
||||||
return {
|
|
||||||
error: getError(
|
|
||||||
{
|
|
||||||
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
|
||||||
message:
|
|
||||||
"An error occured while listing email accounts",
|
|
||||||
userHint: "Please try again later",
|
|
||||||
actionable: false,
|
|
||||||
detail: "An error occured while listing email accounts",
|
|
||||||
},
|
|
||||||
pRes.error.errors,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
parsed.push(pRes.data);
|
|
||||||
}
|
|
||||||
return { data: parsed };
|
|
||||||
} catch (e) {
|
|
||||||
return {
|
|
||||||
error: getError(
|
|
||||||
{
|
|
||||||
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
|
||||||
message: "An error occured while listing email accounts",
|
|
||||||
userHint: "Please try again later",
|
|
||||||
actionable: false,
|
|
||||||
detail: "An error occured while listing email accounts",
|
|
||||||
},
|
|
||||||
e,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getEmailAccount(id: number): Promise<Result<EmailAccount>> {
|
|
||||||
try {
|
|
||||||
const out = await this.db.query.emailAccount.findFirst({
|
|
||||||
where: eq(emailAccount.id, id),
|
|
||||||
});
|
|
||||||
if (!out) {
|
|
||||||
Logger.error("Failed to get email account");
|
|
||||||
Logger.debug(out);
|
|
||||||
return {
|
|
||||||
error: getError({
|
|
||||||
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
|
||||||
message: "Failed to get email account",
|
|
||||||
userHint: "Please try again",
|
|
||||||
detail: "Failed to get email account",
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const pRes = emailAccountModel.safeParse(out);
|
|
||||||
if (pRes.error) {
|
|
||||||
return {
|
|
||||||
error: getError(
|
|
||||||
{
|
|
||||||
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
|
||||||
message: "Failed to get email account",
|
|
||||||
userHint: "Please try again",
|
|
||||||
detail: "Failed to get email account",
|
|
||||||
},
|
|
||||||
pRes.error,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return { data: pRes.data };
|
|
||||||
} catch (e) {
|
|
||||||
return {
|
|
||||||
error: getError(
|
|
||||||
{
|
|
||||||
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
|
||||||
message: "An error occured while getting email account",
|
|
||||||
userHint: "Please try again later",
|
|
||||||
actionable: false,
|
|
||||||
detail: "An error occured while getting email account",
|
|
||||||
},
|
|
||||||
e,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getEmailAccountWithPassword(
|
|
||||||
id: number,
|
|
||||||
): Promise<Result<EmailAccountFull>> {
|
|
||||||
try {
|
|
||||||
const out = await this.db.query.emailAccount.findFirst({
|
|
||||||
where: eq(emailAccount.id, id),
|
|
||||||
});
|
|
||||||
if (!out) {
|
|
||||||
Logger.error("Failed to get email account");
|
|
||||||
Logger.debug(out);
|
|
||||||
return {
|
|
||||||
error: getError({
|
|
||||||
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
|
||||||
message: "Failed to get email account",
|
|
||||||
userHint: "Please try again",
|
|
||||||
detail: "Failed to get email account",
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const pRes = emailAccountFullModel.safeParse(out);
|
|
||||||
if (pRes.error) {
|
|
||||||
return {
|
|
||||||
error: getError(
|
|
||||||
{
|
|
||||||
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
|
||||||
message: "Failed to get email account",
|
|
||||||
userHint: "Please try again",
|
|
||||||
detail: "Failed to get email account",
|
|
||||||
},
|
|
||||||
pRes.error,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return { data: pRes.data };
|
|
||||||
} catch (e) {
|
|
||||||
return {
|
|
||||||
error: getError(
|
|
||||||
{
|
|
||||||
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
|
||||||
message: "An error occured while getting email account",
|
|
||||||
userHint: "Please try again later",
|
|
||||||
actionable: false,
|
|
||||||
detail: "An error occured while getting email account",
|
|
||||||
},
|
|
||||||
e,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// selector is either an email or an id
|
|
||||||
async updateLastActiveCheckAt(selector: number | string, ts: string) {
|
|
||||||
Logger.info(`Updating last active check at for ${selector}`);
|
|
||||||
try {
|
|
||||||
await this.db
|
|
||||||
.update(emailAccount)
|
|
||||||
.set({ lastActiveCheckAt: new Date(ts) })
|
|
||||||
.where(
|
|
||||||
typeof selector === "string"
|
|
||||||
? eq(emailAccount.email, selector)
|
|
||||||
: eq(emailAccount.id, selector),
|
|
||||||
)
|
|
||||||
.execute();
|
|
||||||
} catch (e) {
|
|
||||||
return {
|
|
||||||
error: getError(
|
|
||||||
{
|
|
||||||
message: "Failed to update last active check at",
|
|
||||||
code: ERROR_CODES.DATABASE_ERROR,
|
|
||||||
detail: "A database error occured while updating last active check at",
|
|
||||||
userHint: "Please try again later",
|
|
||||||
actionable: false,
|
|
||||||
},
|
|
||||||
e,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import type { AccountInboxRepository } from "../data/inbox.repository";
|
|
||||||
import type { AccountsRepository } from "../data/repository";
|
|
||||||
import { type InboxModel } from "../data/entities";
|
|
||||||
import { type Result } from "@pkg/result";
|
|
||||||
|
|
||||||
export class AccountsController {
|
|
||||||
repo: AccountsRepository;
|
|
||||||
inboxRepo: AccountInboxRepository;
|
|
||||||
|
|
||||||
constructor(repo: AccountsRepository, inboxRepo: AccountInboxRepository) {
|
|
||||||
this.repo = repo;
|
|
||||||
this.inboxRepo = inboxRepo;
|
|
||||||
}
|
|
||||||
|
|
||||||
async listAllEmailAccounts() {
|
|
||||||
return this.repo.listAllEmailAccounts();
|
|
||||||
}
|
|
||||||
|
|
||||||
async getEmailAccount(id: number) {
|
|
||||||
return this.repo.getEmailAccount(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAccountInbox(accountId: number, ignoreIds: number[]) {
|
|
||||||
return this.inboxRepo.getAccountInbox(accountId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async checkAccountActiveStatus(
|
|
||||||
id: number,
|
|
||||||
): Promise<Result<{ ok: boolean; ts: string }>> {
|
|
||||||
return { data: { ok: false, ts: new Date().toISOString() } };
|
|
||||||
}
|
|
||||||
|
|
||||||
async refreshInbox(
|
|
||||||
accountId: number,
|
|
||||||
params?: any,
|
|
||||||
): Promise<Result<InboxModel[]>> {
|
|
||||||
return { data: [] };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import { createTRPCRouter, protectedProcedure } from "$lib/trpc/t";
|
|
||||||
import { db } from "@pkg/db";
|
|
||||||
import { AccountsRepository } from "../data/repository";
|
|
||||||
import { AccountsController } from "./controller";
|
|
||||||
import { AccountInboxRepository } from "../data/inbox.repository";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
function getEAC() {
|
|
||||||
return new AccountsController(
|
|
||||||
new AccountsRepository(db),
|
|
||||||
new AccountInboxRepository(db),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const emailAccountsRouter = createTRPCRouter({
|
|
||||||
listAllEmailAccounts: protectedProcedure.query(async ({ ctx }) => {
|
|
||||||
return getEAC().listAllEmailAccounts();
|
|
||||||
}),
|
|
||||||
|
|
||||||
getEmailAccount: protectedProcedure
|
|
||||||
.input(z.object({ id: z.number() }))
|
|
||||||
.query(async ({ input }) => {
|
|
||||||
return getEAC().getEmailAccount(input.id);
|
|
||||||
}),
|
|
||||||
|
|
||||||
checkAccountActiveStatus: protectedProcedure
|
|
||||||
.input(z.object({ id: z.number() }))
|
|
||||||
.query(async ({ input }) => {
|
|
||||||
return getEAC().checkAccountActiveStatus(input.id);
|
|
||||||
}),
|
|
||||||
|
|
||||||
getAccountInbox: protectedProcedure
|
|
||||||
.input(z.object({ id: z.number(), ignoreIds: z.array(z.number()) }))
|
|
||||||
.query(async ({ input }) => {
|
|
||||||
return getEAC().getAccountInbox(input.id, input.ignoreIds);
|
|
||||||
}),
|
|
||||||
|
|
||||||
refreshInbox: protectedProcedure
|
|
||||||
.input(z.object({ id: z.number() }))
|
|
||||||
.mutation(async ({ input }) => {
|
|
||||||
return getEAC().refreshInbox(input.id);
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
export function parseFrom(from: string) {
|
|
||||||
const match = from.match(/"(.*?)"\s*<(.+?)>/);
|
|
||||||
if (match) {
|
|
||||||
return { name: match[1], email: match[2] };
|
|
||||||
} else {
|
|
||||||
// Handle cases where the name might be missing
|
|
||||||
const emailOnly = from.match(/<(.+?)>/);
|
|
||||||
return { name: null, email: emailOnly ? emailOnly[1] : from };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import BillListIcon from "~icons/solar/bill-list-linear";
|
|
||||||
import Icon from "$lib/components/atoms/icon.svelte";
|
|
||||||
import Button from "$lib/components/ui/button/button.svelte";
|
|
||||||
import { accountsVM } from "./accounts.vm.svelte";
|
|
||||||
import * as Dialog from "$lib/components/ui/dialog/index.js";
|
|
||||||
import Badge from "$lib/components/ui/badge/badge.svelte";
|
|
||||||
import ButtonLoadableText from "$lib/components/atoms/button-loadable-text.svelte";
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Dialog.Root bind:open={accountsVM.showAccountsList}>
|
|
||||||
<Button
|
|
||||||
size="iconSm"
|
|
||||||
variant="outlineWhite"
|
|
||||||
onclick={() => (accountsVM.showAccountsList = true)}
|
|
||||||
>
|
|
||||||
<Icon icon={BillListIcon} cls="h-6 w-6" />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Dialog.Content class="max-w-lg md:max-w-2xl">
|
|
||||||
<Dialog.Header>
|
|
||||||
<Dialog.Title>Agents' Email Accounts</Dialog.Title>
|
|
||||||
</Dialog.Header>
|
|
||||||
<div class="h-[70vh] w-full overflow-y-auto">
|
|
||||||
{#each accountsVM.accounts as acc}
|
|
||||||
{@const isCheckingFor =
|
|
||||||
accountsVM.checkingForId === acc.id &&
|
|
||||||
accountsVM.checkingAccountActiveStatus}
|
|
||||||
<div
|
|
||||||
class="flex w-full items-start justify-between gap-2 rounded-md border border-gray-200 bg-white p-2 shadow-md md:p-4"
|
|
||||||
>
|
|
||||||
<div class="flex h-full flex-col justify-between gap-3">
|
|
||||||
<p class="text-sm font-medium md:text-lg">{acc.email}</p>
|
|
||||||
<div class="h-full justify-end">
|
|
||||||
{#if !isCheckingFor}
|
|
||||||
<p class="text-xs text-gray-600">
|
|
||||||
Last Checked For Activity :
|
|
||||||
{acc.lastActiveCheckAt
|
|
||||||
? new Date(
|
|
||||||
acc.lastActiveCheckAt,
|
|
||||||
).toLocaleString()
|
|
||||||
: "Never"}
|
|
||||||
</p>
|
|
||||||
{:else}
|
|
||||||
<p class="text-xs text-gray-300">checking...</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="flex h-full flex-col items-end justify-between gap-2"
|
|
||||||
>
|
|
||||||
<div class="w-max">
|
|
||||||
<Badge variant="outline">
|
|
||||||
{acc.used ? "Not used" : "Used"}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onclick={() =>
|
|
||||||
accountsVM.checkAccountsActiveStatus(acc.id)}
|
|
||||||
size="sm"
|
|
||||||
variant="outlineWhite"
|
|
||||||
disabled={isCheckingFor}
|
|
||||||
>
|
|
||||||
<ButtonLoadableText
|
|
||||||
loading={isCheckingFor}
|
|
||||||
text={"Check for activity"}
|
|
||||||
loadingText={"Checking"}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</Dialog.Content>
|
|
||||||
</Dialog.Root>
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import {
|
|
||||||
getCoreRowModel,
|
|
||||||
getPaginationRowModel,
|
|
||||||
getFilteredRowModel,
|
|
||||||
type ColumnDef,
|
|
||||||
type PaginationState,
|
|
||||||
type ColumnFiltersState,
|
|
||||||
} from "@tanstack/table-core";
|
|
||||||
import {
|
|
||||||
createSvelteTable,
|
|
||||||
renderComponent,
|
|
||||||
} 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 { adminSiteNavMap } from "$lib/core/constants";
|
|
||||||
import { useDebounce } from "runed";
|
|
||||||
import type { EmailAccount } from "../data/entities";
|
|
||||||
|
|
||||||
let { data }: { data: EmailAccount[] } = $props();
|
|
||||||
|
|
||||||
// Define columns
|
|
||||||
const columns: ColumnDef<any>[] = [
|
|
||||||
{
|
|
||||||
header: "#",
|
|
||||||
accessorKey: "id",
|
|
||||||
cell: ({ row }) => {
|
|
||||||
return Number(row.id) + 1;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "Email",
|
|
||||||
id: "email",
|
|
||||||
cell: ({ row }) => {
|
|
||||||
return row.original.email;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "Action",
|
|
||||||
id: "actions",
|
|
||||||
cell: ({ row }) => {
|
|
||||||
return renderComponent(DataTableActions, {
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
title: "View Details",
|
|
||||||
action: () => {
|
|
||||||
goto(
|
|
||||||
`${adminSiteNavMap.endusers}/${row.original.id}`,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
let pagination = $state<PaginationState>({ pageIndex: 0, pageSize: 10 });
|
|
||||||
let columnFilters = $state<ColumnFiltersState>([]);
|
|
||||||
|
|
||||||
const table = createSvelteTable({
|
|
||||||
get data() {
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
columns,
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
|
||||||
getFilteredRowModel: getFilteredRowModel(),
|
|
||||||
onPaginationChange: (updater) => {
|
|
||||||
if (typeof updater === "function") {
|
|
||||||
pagination = updater(pagination);
|
|
||||||
} else {
|
|
||||||
pagination = updater;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onColumnFiltersChange: (updater) => {
|
|
||||||
if (typeof updater === "function") {
|
|
||||||
columnFilters = updater(columnFilters);
|
|
||||||
} else {
|
|
||||||
columnFilters = updater;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
state: {
|
|
||||||
get pagination() {
|
|
||||||
return pagination;
|
|
||||||
},
|
|
||||||
get columnFilters() {
|
|
||||||
return columnFilters;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
let debounceDuration = $state(1000);
|
|
||||||
const debouncedSearch = useDebounce(
|
|
||||||
() => {
|
|
||||||
console.log("Debounced search - NYI");
|
|
||||||
},
|
|
||||||
() => debounceDuration,
|
|
||||||
);
|
|
||||||
|
|
||||||
let pageCount = $derived(2);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if data.length > 0}
|
|
||||||
<DataTable
|
|
||||||
{table}
|
|
||||||
onNextPageClick={() => {
|
|
||||||
table.nextPage();
|
|
||||||
}}
|
|
||||||
onPreviousPageClick={() => {
|
|
||||||
table.previousPage();
|
|
||||||
}}
|
|
||||||
currPage={pagination.pageIndex + 1}
|
|
||||||
totalPages={pageCount}
|
|
||||||
filterFieldDisabled={false}
|
|
||||||
query={passengerInfoVM.query}
|
|
||||||
hasData={data.length > 0}
|
|
||||||
onQueryChange={(q) => {
|
|
||||||
passengerInfoVM.query = q;
|
|
||||||
debouncedSearch();
|
|
||||||
}}
|
|
||||||
filterFieldPlaceholder="Search users..."
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<div class="grid h-full place-items-center p-4 py-12 md:p-8 md:py-32">
|
|
||||||
<Title size="h5">No Orders found</Title>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
import { trpcApiStore } from "$lib/stores/api";
|
|
||||||
import { get } from "svelte/store";
|
|
||||||
import { type InboxModel, type EmailAccount } from "../data/entities";
|
|
||||||
import { toast } from "svelte-sonner";
|
|
||||||
|
|
||||||
export class AccountsViewModel {
|
|
||||||
accounts = $state([] as EmailAccount[]);
|
|
||||||
|
|
||||||
chosenAccountId = $state<number | undefined>(undefined);
|
|
||||||
|
|
||||||
accountInbox = $state<InboxModel[]>([]);
|
|
||||||
fetchingInbox = $state(false);
|
|
||||||
chosenMessageId = $state<number | undefined>(undefined);
|
|
||||||
|
|
||||||
showAccountsList = $state(false);
|
|
||||||
checkingAccountActiveStatus = $state(false);
|
|
||||||
checkingForId = $state<number | undefined>(undefined);
|
|
||||||
|
|
||||||
loading = $state(false);
|
|
||||||
query = $state("");
|
|
||||||
|
|
||||||
async fetchAccounts() {
|
|
||||||
const api = get(trpcApiStore);
|
|
||||||
if (!api) {
|
|
||||||
return toast("Api not initialized", {
|
|
||||||
description: "Try refreshing the page",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.loading = true;
|
|
||||||
const result = await api.emailAccounts.listAllEmailAccounts.query();
|
|
||||||
this.loading = false;
|
|
||||||
if (result.error) {
|
|
||||||
return toast.error(result.error.message, {
|
|
||||||
description: result.error.userHint,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!!result.data) {
|
|
||||||
this.accounts = result.data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async checkAccountsActiveStatus(id: number) {
|
|
||||||
const api = get(trpcApiStore);
|
|
||||||
if (!api) {
|
|
||||||
return toast("Api not initialized", {
|
|
||||||
description: "Try refreshing the page",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.checkingAccountActiveStatus = true;
|
|
||||||
this.checkingForId = id;
|
|
||||||
const result = await api.emailAccounts.checkAccountActiveStatus.query({
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
this.checkingAccountActiveStatus = false;
|
|
||||||
this.checkingForId = undefined;
|
|
||||||
console.log(result);
|
|
||||||
if (result.error) {
|
|
||||||
return toast.error(result.error.message, {
|
|
||||||
description: result.error.userHint,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!!result.data && !!result.data.ok) {
|
|
||||||
for (let i = 0; i < this.accounts.length; i++) {
|
|
||||||
if (this.accounts[i].id === id) {
|
|
||||||
this.accounts[i].lastActiveCheckAt = result.data.ts;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fetchInbox() {
|
|
||||||
const api = get(trpcApiStore);
|
|
||||||
if (!api) {
|
|
||||||
return toast("Api not initialized", {
|
|
||||||
description: "Try refreshing the page",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.loading = true;
|
|
||||||
if (!this.chosenAccountId) {
|
|
||||||
return toast.error("No account selected", {
|
|
||||||
description: "Please select an account to perform action",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const result = await api.emailAccounts.getAccountInbox.query({
|
|
||||||
id: this.chosenAccountId,
|
|
||||||
ignoreIds: this.accountInbox.map((i) => i.id),
|
|
||||||
});
|
|
||||||
this.loading = false;
|
|
||||||
console.log(result);
|
|
||||||
if (result.error) {
|
|
||||||
return toast.error(result.error.message, {
|
|
||||||
description: result.error.userHint,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!!result.data) {
|
|
||||||
this.upsertMessages(result.data, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async refreshInbox() {
|
|
||||||
const api = get(trpcApiStore);
|
|
||||||
if (!api) {
|
|
||||||
return toast("Api not initialized", {
|
|
||||||
description: "Try refreshing the page",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!this.chosenAccountId) {
|
|
||||||
return toast.error("No account selected", {
|
|
||||||
description: "Please select an account to perform action",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.fetchingInbox = true;
|
|
||||||
const result = await api.emailAccounts.refreshInbox.mutate({
|
|
||||||
id: this.chosenAccountId,
|
|
||||||
});
|
|
||||||
this.fetchingInbox = false;
|
|
||||||
if (result.error) {
|
|
||||||
return toast.error(result.error.message, {
|
|
||||||
description: result.error.userHint,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (result.data) {
|
|
||||||
this.upsertMessages(result.data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private upsertMessages(messages: InboxModel[], reset = false) {
|
|
||||||
if (reset) {
|
|
||||||
this.accountInbox = messages;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const message of messages) {
|
|
||||||
const existing = this.accountInbox.find((m) => m.id === message.id);
|
|
||||||
if (existing) {
|
|
||||||
Object.assign(existing, message);
|
|
||||||
} else {
|
|
||||||
this.accountInbox.push(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const accountsVM = new AccountsViewModel();
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import * as Avatar from "$lib/components/ui/avatar/index.js";
|
|
||||||
import type { InboxModel } from "../data/entities";
|
|
||||||
import Container from "$lib/components/atoms/container.svelte";
|
|
||||||
import { cn } from "$lib/utils";
|
|
||||||
import { CARD_STYLE } from "$lib/core/constants";
|
|
||||||
import { parseFrom } from "../utils";
|
|
||||||
import Title from "$lib/components/atoms/title.svelte";
|
|
||||||
|
|
||||||
let { message }: { message?: InboxModel } = $props();
|
|
||||||
|
|
||||||
let parsedFrom = $derived(parseFrom(message?.from ?? ""));
|
|
||||||
|
|
||||||
function createEmailHTML(content: string) {
|
|
||||||
return `
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<base target="_blank">
|
|
||||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' data: https:; img-src * data: https:;">
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
|
||||||
margin: 0;
|
|
||||||
padding: 16px;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: #333;
|
|
||||||
height: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
img {
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
color: #0066cc;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
table {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
${content}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleIframeLoad(iframe: HTMLIFrameElement) {
|
|
||||||
try {
|
|
||||||
const body = iframe.contentWindow?.document.body;
|
|
||||||
if (body) {
|
|
||||||
// Set iframe height to 100% instead of calculating it
|
|
||||||
iframe.style.height = "100%";
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Failed to adjust iframe:", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if message}
|
|
||||||
<div class="flex h-full flex-col gap-4">
|
|
||||||
<!-- Email Header Section -->
|
|
||||||
<div
|
|
||||||
class={cn("flex flex-col gap-4 rounded-lg bg-white p-4", CARD_STYLE)}
|
|
||||||
>
|
|
||||||
<div class="flex flex-col gap-2 sm:flex-row">
|
|
||||||
<Avatar.Root class="h-10 w-10 rounded-full">
|
|
||||||
<Avatar.Fallback class="rounded-full">
|
|
||||||
{parsedFrom.name?.slice(0, 2).toUpperCase() ??
|
|
||||||
parsedFrom.email?.slice(0, 2).toUpperCase() ??
|
|
||||||
"U"}
|
|
||||||
</Avatar.Fallback>
|
|
||||||
</Avatar.Root>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="flex w-full flex-col items-start gap-2 md:flex-row md:justify-between"
|
|
||||||
>
|
|
||||||
<div class="flex flex-col gap-2 sm:p-1">
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<Title size="h5" color="black" weight="medium">
|
|
||||||
{parsedFrom.name}
|
|
||||||
</Title>
|
|
||||||
|
|
||||||
<small class="text-gray-600">
|
|
||||||
{parsedFrom.email}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p>{message.subject}</p>
|
|
||||||
|
|
||||||
{#if message.to}
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span class="font-medium">To:</span>
|
|
||||||
<span class="text-gray-600">{message.to}</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if message.cc}
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span class="font-medium">CC:</span>
|
|
||||||
<span class="text-gray-600">{message.cc}</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col items-end gap-2">
|
|
||||||
<small class="text-gray-600">
|
|
||||||
{new Date(message.dated).toLocaleString()}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if message.attachments && message.attachments.length > 0}
|
|
||||||
<div class="border-t pt-4">
|
|
||||||
<h3 class="mb-2 font-medium">Attachments:</h3>
|
|
||||||
<div class="flex flex-wrap gap-2">
|
|
||||||
{#each message.attachments as attachment}
|
|
||||||
<div
|
|
||||||
class="flex items-center gap-2 rounded-md border bg-gray-50 px-3 py-2"
|
|
||||||
>
|
|
||||||
<span>{attachment.filename}</span>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Email Body Section with iframe -->
|
|
||||||
<div class={cn("flex-1 rounded-lg bg-white", CARD_STYLE)}>
|
|
||||||
<iframe
|
|
||||||
title="Email Content"
|
|
||||||
srcdoc={createEmailHTML(message.body)}
|
|
||||||
class="w-full border-none"
|
|
||||||
sandbox="allow-same-origin allow-popups allow-popups-to-escape-sandbox"
|
|
||||||
onload={(e) => {
|
|
||||||
// @ts-ignore
|
|
||||||
handleIframeLoad(e.currentTarget);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
</iframe>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div class="flex h-full flex-col gap-4">
|
|
||||||
<Container containerClass="flex flex-col gap-4 p-4 bg-white rounded-lg">
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<h2 class="text-xl font-semibold">No message selected</h2>
|
|
||||||
</div>
|
|
||||||
</Container>
|
|
||||||
</div>
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { accountsVM } from "$lib/domains/account/view/accounts.vm.svelte";
|
|
||||||
import { parseFrom } from "$lib/domains/account/utils";
|
|
||||||
import { cn } from "$lib/utils";
|
|
||||||
import { TRANSITION_ALL } from "$lib/core/constants";
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#each accountsVM.accountInbox.toReversed() as each, idx}
|
|
||||||
{@const parsed = parseFrom(each.from)}
|
|
||||||
<button
|
|
||||||
class={cn(
|
|
||||||
"hover:bg-gray-10 flex w-full cursor-pointer flex-col items-start gap-2 rounded-lg border-2 border-gray-200 bg-white p-2 px-3 text-start hover:drop-shadow-md",
|
|
||||||
TRANSITION_ALL,
|
|
||||||
)}
|
|
||||||
onclick={() => {
|
|
||||||
accountsVM.chosenMessageId = each.id;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class="flex w-full flex-col justify-between">
|
|
||||||
{#if parsed.name}
|
|
||||||
<span class="font-medium">
|
|
||||||
{parsed.name}
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
{#if parsed.email}
|
|
||||||
<span class="text-sm text-gray-400">
|
|
||||||
{parsed.email}
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<small class="text-sm">
|
|
||||||
{each.subject.slice(0, 50)}
|
|
||||||
{each.subject.length > 50 ? "..." : ""}
|
|
||||||
</small>
|
|
||||||
<small class="w-full text-end text-[0.7rem]">
|
|
||||||
{new Date(each.dated).toLocaleString()}
|
|
||||||
</small>
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
1
apps/admin/src/lib/domains/customerinfo/data.ts
Normal file
1
apps/admin/src/lib/domains/customerinfo/data.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "@pkg/logic/domains/customerinfo/data";
|
||||||
1
apps/admin/src/lib/domains/customerinfo/repository.ts
Normal file
1
apps/admin/src/lib/domains/customerinfo/repository.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "@pkg/logic/domains/customerinfo/repository";
|
||||||
47
apps/admin/src/lib/domains/customerinfo/router.ts
Normal file
47
apps/admin/src/lib/domains/customerinfo/router.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { protectedProcedure } from "$lib/server/trpc/t";
|
||||||
|
import { createTRPCRouter } from "$lib/trpc/t";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { createCustomerInfoPayload, updateCustomerInfoPayload } from "./data";
|
||||||
|
import { getCustomerInfoUseCases } from "./usecases";
|
||||||
|
|
||||||
|
export const customerInfoRouter = createTRPCRouter({
|
||||||
|
getAllCustomerInfo: protectedProcedure.query(async ({}) => {
|
||||||
|
const controller = getCustomerInfoUseCases();
|
||||||
|
return controller.getAllCustomerInfo();
|
||||||
|
}),
|
||||||
|
|
||||||
|
getCustomerInfoById: protectedProcedure
|
||||||
|
.input(z.object({ id: z.number() }))
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
const controller = getCustomerInfoUseCases();
|
||||||
|
return controller.getCustomerInfoById(input.id);
|
||||||
|
}),
|
||||||
|
|
||||||
|
getCustomerInfoByOrderId: protectedProcedure
|
||||||
|
.input(z.object({ orderId: z.number() }))
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
const controller = getCustomerInfoUseCases();
|
||||||
|
return controller.getCustomerInfoByOrderId(input.orderId);
|
||||||
|
}),
|
||||||
|
|
||||||
|
createCustomerInfo: protectedProcedure
|
||||||
|
.input(createCustomerInfoPayload)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
const controller = getCustomerInfoUseCases();
|
||||||
|
return controller.createCustomerInfo(input);
|
||||||
|
}),
|
||||||
|
|
||||||
|
updateCustomerInfo: protectedProcedure
|
||||||
|
.input(updateCustomerInfoPayload)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
const controller = getCustomerInfoUseCases();
|
||||||
|
return controller.updateCustomerInfo(input);
|
||||||
|
}),
|
||||||
|
|
||||||
|
deleteCustomerInfo: protectedProcedure
|
||||||
|
.input(z.object({ id: z.number() }))
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
const controller = getCustomerInfoUseCases();
|
||||||
|
return controller.deleteCustomerInfo(input.id);
|
||||||
|
}),
|
||||||
|
});
|
||||||
1
apps/admin/src/lib/domains/customerinfo/usecases.ts
Normal file
1
apps/admin/src/lib/domains/customerinfo/usecases.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "@pkg/logic/domains/customerinfo/usecases";
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import UserIcon from "~icons/solar/user-broken";
|
||||||
|
import type { CustomerInfoModel } from "../data";
|
||||||
|
import InfoCard from "./info-card.svelte";
|
||||||
|
|
||||||
|
let {
|
||||||
|
customerInfo,
|
||||||
|
}: {
|
||||||
|
customerInfo: CustomerInfoModel;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<InfoCard icon={UserIcon} title="Customer Information">
|
||||||
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<span class="text-xs text-gray-500">Full Name</span>
|
||||||
|
<p>
|
||||||
|
{customerInfo.firstName}
|
||||||
|
{#if customerInfo.middleName}
|
||||||
|
{customerInfo.middleName}
|
||||||
|
{/if}
|
||||||
|
{customerInfo.lastName}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-xs text-gray-500">Email</span>
|
||||||
|
<p>{customerInfo.email}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-xs text-gray-500">Phone Number</span>
|
||||||
|
<p>{customerInfo.phoneCountryCode} {customerInfo.phoneNumber}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-xs text-gray-500">City</span>
|
||||||
|
<p>{customerInfo.city}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-xs text-gray-500">State</span>
|
||||||
|
<p>{customerInfo.state}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-xs text-gray-500">Country</span>
|
||||||
|
<p>{customerInfo.country}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-xs text-gray-500">Zip Code</span>
|
||||||
|
<p>{customerInfo.zipCode}</p>
|
||||||
|
</div>
|
||||||
|
<div class="md:col-span-2">
|
||||||
|
<span class="text-xs text-gray-500">Address</span>
|
||||||
|
<p>{customerInfo.address}</p>
|
||||||
|
{#if customerInfo.address2}
|
||||||
|
<p class="text-sm text-gray-600">{customerInfo.address2}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</InfoCard>
|
||||||
@@ -1,29 +1,29 @@
|
|||||||
<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 { adminSiteNavMap } from "$lib/core/constants";
|
import { adminSiteNavMap } from "$lib/core/constants";
|
||||||
|
import {
|
||||||
|
getCoreRowModel,
|
||||||
|
getFilteredRowModel,
|
||||||
|
getPaginationRowModel,
|
||||||
|
type ColumnDef,
|
||||||
|
type ColumnFiltersState,
|
||||||
|
type PaginationState,
|
||||||
|
} from "@tanstack/table-core";
|
||||||
import { useDebounce } from "runed";
|
import { useDebounce } from "runed";
|
||||||
import type { PassengerInfo } from "../data/entities";
|
import type { CustomerInfoModel } from "../data";
|
||||||
import { passengerInfoVM } from "./passengerinfo.vm.svelte";
|
import { customerInfoVM } from "./customerinfo.vm.svelte";
|
||||||
|
|
||||||
let { data }: { data: PassengerInfo[] } = $props();
|
let { data }: { data: CustomerInfoModel[] } = $props();
|
||||||
|
|
||||||
// Define columns
|
// Define columns
|
||||||
const columns: ColumnDef<any>[] = [
|
const columns: ColumnDef<CustomerInfoModel>[] = [
|
||||||
{
|
{
|
||||||
header: "#",
|
header: "#",
|
||||||
accessorKey: "id",
|
accessorKey: "id",
|
||||||
@@ -33,25 +33,30 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: "Email",
|
header: "Email",
|
||||||
id: "email",
|
accessorKey: "email",
|
||||||
cell: ({ row }) => {
|
|
||||||
return row.original.passengerPii.email;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
header: "Phone No",
|
header: "Phone No",
|
||||||
id: "phone",
|
id: "phone",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
return row.original.passengerPii.phoneNumber;
|
return `${row.original.phoneCountryCode} ${row.original.phoneNumber}`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: "Name",
|
header: "Name",
|
||||||
id: "name",
|
id: "name",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const pii = (row.original as PassengerInfo).passengerPii;
|
const middleName = row.original.middleName
|
||||||
return `${pii.firstName} ${pii.lastName}`;
|
? ` ${row.original.middleName}`
|
||||||
|
: "";
|
||||||
|
return `${row.original.firstName}${middleName} ${row.original.lastName}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Location",
|
||||||
|
id: "location",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return `${row.original.city}, ${row.original.state}`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -132,13 +137,13 @@
|
|||||||
currPage={pagination.pageIndex + 1}
|
currPage={pagination.pageIndex + 1}
|
||||||
totalPages={pageCount}
|
totalPages={pageCount}
|
||||||
filterFieldDisabled={false}
|
filterFieldDisabled={false}
|
||||||
query={passengerInfoVM.query}
|
query={customerInfoVM.query}
|
||||||
hasData={data.length > 0}
|
hasData={data.length > 0}
|
||||||
onQueryChange={(q) => {
|
onQueryChange={(q) => {
|
||||||
passengerInfoVM.query = q;
|
customerInfoVM.query = q;
|
||||||
debouncedSearch();
|
debouncedSearch();
|
||||||
}}
|
}}
|
||||||
filterFieldPlaceholder="Search users..."
|
filterFieldPlaceholder="Search customers..."
|
||||||
/>
|
/>
|
||||||
{: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">
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import type { CustomerInfoModel } from "../data";
|
||||||
|
|
||||||
|
export class CustomerInfoViewModel {
|
||||||
|
customerInfos = $state([] as CustomerInfoModel[]);
|
||||||
|
|
||||||
|
loading = $state(false);
|
||||||
|
query = $state("");
|
||||||
|
}
|
||||||
|
|
||||||
|
export const customerInfoVM = new CustomerInfoViewModel();
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { count, eq, type Database } from "@pkg/db";
|
|
||||||
import { ERROR_CODES, type Result } from "$lib/core/data.types";
|
import { ERROR_CODES, type Result } from "$lib/core/data.types";
|
||||||
import { fullOrderModel, type FullOrderModel } from "./entities";
|
import { count, eq, type Database } from "@pkg/db";
|
||||||
|
import { customerInfo, order } from "@pkg/db/schema";
|
||||||
import { getError, Logger } from "@pkg/logger";
|
import { getError, Logger } from "@pkg/logger";
|
||||||
import { order, passengerInfo } from "@pkg/db/schema";
|
import { fullOrderModel, type FullOrderModel } from "./entities";
|
||||||
|
|
||||||
export class OrderRepository {
|
export class OrderRepository {
|
||||||
private db: Database;
|
private db: Database;
|
||||||
@@ -32,7 +32,7 @@ export class OrderRepository {
|
|||||||
for (const each of res) {
|
for (const each of res) {
|
||||||
const parsed = fullOrderModel.safeParse({
|
const parsed = fullOrderModel.safeParse({
|
||||||
...each,
|
...each,
|
||||||
passengerInfos: [],
|
customerInfos: [],
|
||||||
});
|
});
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
Logger.error(JSON.stringify(parsed.error.errors, null, 2));
|
Logger.error(JSON.stringify(parsed.error.errors, null, 2));
|
||||||
@@ -67,13 +67,12 @@ export class OrderRepository {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!out) return {};
|
if (!out) return {};
|
||||||
const relatedPassengerInfos = await this.db.query.passengerInfo.findMany({
|
const relatedCustomerInfos = await this.db.query.customerInfo.findMany({
|
||||||
where: eq(passengerInfo.orderId, oid),
|
where: eq(customerInfo.orderId, oid),
|
||||||
with: { passengerPii: true },
|
|
||||||
});
|
});
|
||||||
const parsed = fullOrderModel.safeParse({
|
const parsed = fullOrderModel.safeParse({
|
||||||
...out,
|
...out,
|
||||||
passengerInfos: relatedPassengerInfos,
|
customerInfo: relatedCustomerInfos,
|
||||||
});
|
});
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export * from "@pkg/logic/domains/passengerinfo/data/entities";
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
import { eq, inArray, type Database } from "@pkg/db";
|
|
||||||
import { passengerInfo } from "@pkg/db/schema";
|
|
||||||
import { ERROR_CODES, type Result } from "@pkg/result";
|
|
||||||
import type { PassengerInfo } from "./entities";
|
|
||||||
import { getError, Logger } from "@pkg/logger";
|
|
||||||
|
|
||||||
export class PassengerInfoRepository {
|
|
||||||
private db: Database;
|
|
||||||
|
|
||||||
constructor(db: Database) {
|
|
||||||
this.db = db;
|
|
||||||
}
|
|
||||||
|
|
||||||
async listAllPassengerInfos(): Promise<Result<PassengerInfo[]>> {
|
|
||||||
try {
|
|
||||||
const out = await this.db.query.passengerInfo.findMany({
|
|
||||||
with: {
|
|
||||||
passengerPii: true,
|
|
||||||
parentAgent: { columns: { username: true } },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return { data: out as any as PassengerInfo[] };
|
|
||||||
} catch (e) {
|
|
||||||
return {
|
|
||||||
error: getError(
|
|
||||||
{
|
|
||||||
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
|
||||||
message: "An error occured while listing passenger infos",
|
|
||||||
userHint: "Please try again later",
|
|
||||||
actionable: false,
|
|
||||||
detail: "An error occured while listing passenger infos",
|
|
||||||
},
|
|
||||||
e,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getPassengerInfo(id: number): Promise<Result<PassengerInfo>> {
|
|
||||||
try {
|
|
||||||
const out = await this.db.query.passengerInfo.findFirst({
|
|
||||||
where: eq(passengerInfo.id, id),
|
|
||||||
with: {
|
|
||||||
passengerPii: true,
|
|
||||||
paymentDetails: true,
|
|
||||||
flightTicketInfo: { columns: { id: true } },
|
|
||||||
order: { columns: { id: true } },
|
|
||||||
parentAgent: { columns: { username: true } },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!out) {
|
|
||||||
Logger.error("Failed to get passenger info");
|
|
||||||
Logger.debug(out);
|
|
||||||
return {
|
|
||||||
error: getError({
|
|
||||||
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
|
||||||
message: "Failed to get passenger info",
|
|
||||||
userHint: "Please try again",
|
|
||||||
detail: "Failed to get passenger info",
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return { data: out as any as PassengerInfo };
|
|
||||||
} catch (e) {
|
|
||||||
return {
|
|
||||||
error: getError(
|
|
||||||
{
|
|
||||||
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
|
||||||
message: "An error occured while getting passenger info",
|
|
||||||
userHint: "Please try again later",
|
|
||||||
actionable: false,
|
|
||||||
detail: "An error occured while getting passenger info",
|
|
||||||
},
|
|
||||||
e,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteAll(ids: number[]): Promise<Result<number>> {
|
|
||||||
Logger.info(`Deleting ${ids.length} passenger info`);
|
|
||||||
const out = await this.db
|
|
||||||
.delete(passengerInfo)
|
|
||||||
.where(inArray(passengerInfo.id, ids));
|
|
||||||
Logger.debug(out);
|
|
||||||
Logger.info(`Deleted ${out.count} passenger info`);
|
|
||||||
return { data: out.count };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import type { Result } from "@pkg/result";
|
|
||||||
import type { PassengerInfo } from "../data/entities";
|
|
||||||
import type { PassengerInfoRepository } from "../data/repository";
|
|
||||||
|
|
||||||
export class PassengerInfoController {
|
|
||||||
repo: PassengerInfoRepository;
|
|
||||||
|
|
||||||
constructor(repo: PassengerInfoRepository) {
|
|
||||||
this.repo = repo;
|
|
||||||
}
|
|
||||||
|
|
||||||
async listAllPassengerInfos() {
|
|
||||||
return this.repo.listAllPassengerInfos();
|
|
||||||
}
|
|
||||||
|
|
||||||
async getPassengerInfo(id: number): Promise<Result<PassengerInfo>> {
|
|
||||||
return this.repo.getPassengerInfo(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import type { PassengerInfo } from "../data/entities";
|
|
||||||
|
|
||||||
export class PassengerInfoViewModel {
|
|
||||||
passengerInfos = $state([] as PassengerInfo[]);
|
|
||||||
|
|
||||||
loading = $state(false);
|
|
||||||
query = $state("");
|
|
||||||
}
|
|
||||||
|
|
||||||
export const passengerInfoVM = new PassengerInfoViewModel();
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import PInfoCard from "./pinfo-card.svelte";
|
|
||||||
import CreditCardIcon from "~icons/solar/card-broken";
|
|
||||||
|
|
||||||
let {
|
|
||||||
paymentDetails,
|
|
||||||
showFullDetails = false,
|
|
||||||
}: {
|
|
||||||
paymentDetails: any;
|
|
||||||
showFullDetails?: boolean;
|
|
||||||
} = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if paymentDetails}
|
|
||||||
<PInfoCard icon={CreditCardIcon} title="Payment Information">
|
|
||||||
<div class="grid grid-cols-1 gap-2 md:grid-cols-2">
|
|
||||||
<div>
|
|
||||||
<span class="text-xs text-gray-500">Cardholder Name</span>
|
|
||||||
<p>{paymentDetails.cardholderName}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="text-xs text-gray-500">Card Number</span>
|
|
||||||
<p>
|
|
||||||
{#if showFullDetails}
|
|
||||||
{paymentDetails.cardNumber}
|
|
||||||
{:else}
|
|
||||||
•••• •••• •••• {paymentDetails.cardNumber.slice(-4)}
|
|
||||||
{/if}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="text-xs text-gray-500">Expiry</span>
|
|
||||||
<p>{paymentDetails.expiry}</p>
|
|
||||||
</div>
|
|
||||||
{#if showFullDetails}
|
|
||||||
<div>
|
|
||||||
<span class="text-xs text-gray-500">CVV</span>
|
|
||||||
<p>{paymentDetails.cvv}</p>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</PInfoCard>
|
|
||||||
{/if}
|
|
||||||
@@ -117,7 +117,9 @@
|
|||||||
<div class="font-medium text-gray-900">
|
<div class="font-medium text-gray-900">
|
||||||
{product.title}
|
{product.title}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-gray-500">
|
<div
|
||||||
|
class="max-w-48 overflow-hidden text-ellipsis text-sm text-gray-500"
|
||||||
|
>
|
||||||
{product.description}
|
{product.description}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { emailAccountsRouter } from "$lib/domains/account/domain/router";
|
|
||||||
import { authRouter } from "$lib/domains/auth/domain/router";
|
import { authRouter } from "$lib/domains/auth/domain/router";
|
||||||
import { ckflowRouter } from "$lib/domains/ckflow/router";
|
import { ckflowRouter } from "$lib/domains/ckflow/router";
|
||||||
import { couponRouter } from "$lib/domains/coupon/router";
|
import { couponRouter } from "$lib/domains/coupon/router";
|
||||||
|
import { customerInfoRouter } from "$lib/domains/customerinfo/router";
|
||||||
import { orderRouter } from "$lib/domains/order/domain/router";
|
import { orderRouter } from "$lib/domains/order/domain/router";
|
||||||
import { productRouter } from "$lib/domains/product/router";
|
import { productRouter } from "$lib/domains/product/router";
|
||||||
import { userRouter } from "$lib/domains/user/domain/router";
|
import { userRouter } from "$lib/domains/user/domain/router";
|
||||||
@@ -11,10 +11,10 @@ export const router = createTRPCRouter({
|
|||||||
auth: authRouter,
|
auth: authRouter,
|
||||||
user: userRouter,
|
user: userRouter,
|
||||||
order: orderRouter,
|
order: orderRouter,
|
||||||
emailAccounts: emailAccountsRouter,
|
|
||||||
ckflow: ckflowRouter,
|
ckflow: ckflowRouter,
|
||||||
coupon: couponRouter,
|
coupon: couponRouter,
|
||||||
product: productRouter,
|
product: productRouter,
|
||||||
|
customerInfo: customerInfoRouter,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type Router = typeof router;
|
export type Router = typeof router;
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
|
import { getCustomerInfoUseCases } from "$lib/domains/customerinfo/usecases";
|
||||||
import { redirect } from "@sveltejs/kit";
|
import { redirect } from "@sveltejs/kit";
|
||||||
import type { PageServerLoad } from "./$types";
|
import type { PageServerLoad } from "./$types";
|
||||||
import { PassengerInfoController } from "$lib/domains/passengerinfo/domain/controller";
|
|
||||||
import { PassengerInfoRepository } from "$lib/domains/passengerinfo/data/repository";
|
|
||||||
import { db } from "@pkg/db";
|
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ locals }) => {
|
export const load: PageServerLoad = async ({ locals }) => {
|
||||||
const sess = locals.session;
|
const sess = locals.session;
|
||||||
if (!sess) {
|
if (!sess) {
|
||||||
return redirect(302, "/auth/login");
|
return redirect(302, "/auth/login");
|
||||||
}
|
}
|
||||||
const pc = new PassengerInfoController(new PassengerInfoRepository(db));
|
const cu = getCustomerInfoUseCases();
|
||||||
const res = await pc.listAllPassengerInfos();
|
const res = await cu.getAllCustomerInfo();
|
||||||
return { data: res.data ?? [], error: res.error };
|
return { data: res.data ?? [], error: res.error };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Container from "$lib/components/atoms/container.svelte";
|
import Container from "$lib/components/atoms/container.svelte";
|
||||||
import Title from "$lib/components/atoms/title.svelte";
|
import Title from "$lib/components/atoms/title.svelte";
|
||||||
import PassengerinfoTable from "$lib/domains/passengerinfo/view/passengerinfo-table.svelte";
|
import CustomerinfoTable from "$lib/domains/customerinfo/view/customerinfo-table.svelte";
|
||||||
import { pageTitle } from "$lib/hooks/page-title.svelte";
|
import { pageTitle } from "$lib/hooks/page-title.svelte";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import type { PageData } from "./$types";
|
|
||||||
import { toast } from "svelte-sonner";
|
import { toast } from "svelte-sonner";
|
||||||
|
import type { PageData } from "./$types";
|
||||||
|
|
||||||
pageTitle.set("Passenger Info");
|
pageTitle.set("Passenger Info");
|
||||||
|
|
||||||
@@ -31,6 +31,6 @@
|
|||||||
<Title size="h3">User Data</Title>
|
<Title size="h3">User Data</Title>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PassengerinfoTable data={data.data} />
|
<CustomerinfoTable data={data.data} />
|
||||||
</Container>
|
</Container>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import type { PageServerLoad } from "./$types";
|
import { getCustomerInfoUseCases } from "$lib/domains/customerinfo/usecases";
|
||||||
import { PassengerInfoRepository } from "$lib/domains/passengerinfo/data/repository";
|
|
||||||
import { db } from "@pkg/db";
|
|
||||||
import { getError } from "@pkg/logger";
|
import { getError } from "@pkg/logger";
|
||||||
import { ERROR_CODES } from "@pkg/result";
|
import { ERROR_CODES } from "@pkg/result";
|
||||||
import { PassengerInfoController } from "$lib/domains/passengerinfo/domain/controller";
|
import type { PageServerLoad } from "./$types";
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ params }) => {
|
export const load: PageServerLoad = async ({ params }) => {
|
||||||
const uid = parseInt(params.uid);
|
const uid = parseInt(params.uid);
|
||||||
@@ -18,7 +16,5 @@ export const load: PageServerLoad = async ({ params }) => {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return new PassengerInfoController(
|
return await getCustomerInfoUseCases().getAllCustomerInfo();
|
||||||
new PassengerInfoRepository(db),
|
|
||||||
).getPassengerInfo(uid);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,30 +1,30 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import { adminSiteNavMap, CARD_STYLE } from "$lib/core/constants";
|
|
||||||
import type { PageData } from "./$types";
|
|
||||||
import Title from "$lib/components/atoms/title.svelte";
|
|
||||||
import Container from "$lib/components/atoms/container.svelte";
|
import Container from "$lib/components/atoms/container.svelte";
|
||||||
import * as Breadcrumb from "$lib/components/ui/breadcrumb/index.js";
|
|
||||||
import { onMount } from "svelte";
|
|
||||||
import Icon from "$lib/components/atoms/icon.svelte";
|
import Icon from "$lib/components/atoms/icon.svelte";
|
||||||
import EmailIcon from "~icons/solar/letter-broken";
|
import Title from "$lib/components/atoms/title.svelte";
|
||||||
import PhoneIcon from "~icons/solar/phone-broken";
|
import * as Breadcrumb from "$lib/components/ui/breadcrumb/index.js";
|
||||||
import PassportIcon from "~icons/solar/passport-broken";
|
import { adminSiteNavMap, CARD_STYLE } from "$lib/core/constants";
|
||||||
import LocationIcon from "~icons/solar/map-point-broken";
|
|
||||||
import ClipboardIcon from "~icons/solar/clipboard-list-broken";
|
|
||||||
import GenderIcon from "~icons/mdi/gender-male-female";
|
|
||||||
import CalendarIcon from "~icons/solar/calendar-broken";
|
|
||||||
import UserIdIcon from "~icons/solar/user-id-broken";
|
|
||||||
import DocumentIcon from "~icons/solar/document-text-broken";
|
|
||||||
import CubeIcon from "~icons/solar/box-minimalistic-broken";
|
|
||||||
import PackageIcon from "~icons/solar/box-broken";
|
|
||||||
import CreditCardIcon from "~icons/solar/wallet-money-broken";
|
|
||||||
import CardUserIcon from "~icons/solar/user-id-linear";
|
|
||||||
import CardNumberIcon from "~icons/solar/card-recive-broken";
|
|
||||||
import CalendarCheckIcon from "~icons/solar/calendar-linear";
|
|
||||||
import LockKeyIcon from "~icons/solar/lock-keyhole-minimalistic-broken";
|
|
||||||
import { capitalize } from "$lib/core/string.utils";
|
import { capitalize } from "$lib/core/string.utils";
|
||||||
import PinfoCard from "$lib/domains/passengerinfo/view/pinfo-card.svelte";
|
import CinfoCard from "$lib/domains/customerinfo/view/cinfo-card.svelte";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import GenderIcon from "~icons/mdi/gender-male-female";
|
||||||
|
import PackageIcon from "~icons/solar/box-broken";
|
||||||
|
import CubeIcon from "~icons/solar/box-minimalistic-broken";
|
||||||
|
import CalendarIcon from "~icons/solar/calendar-broken";
|
||||||
|
import CalendarCheckIcon from "~icons/solar/calendar-linear";
|
||||||
|
import CardNumberIcon from "~icons/solar/card-recive-broken";
|
||||||
|
import ClipboardIcon from "~icons/solar/clipboard-list-broken";
|
||||||
|
import DocumentIcon from "~icons/solar/document-text-broken";
|
||||||
|
import EmailIcon from "~icons/solar/letter-broken";
|
||||||
|
import LockKeyIcon from "~icons/solar/lock-keyhole-minimalistic-broken";
|
||||||
|
import LocationIcon from "~icons/solar/map-point-broken";
|
||||||
|
import PassportIcon from "~icons/solar/passport-broken";
|
||||||
|
import PhoneIcon from "~icons/solar/phone-broken";
|
||||||
|
import UserIdIcon from "~icons/solar/user-id-broken";
|
||||||
|
import CardUserIcon from "~icons/solar/user-id-linear";
|
||||||
|
import CreditCardIcon from "~icons/solar/wallet-money-broken";
|
||||||
|
import type { PageData } from "./$types";
|
||||||
|
|
||||||
let { data }: { data: PageData } = $props();
|
let { data }: { data: PageData } = $props();
|
||||||
|
|
||||||
@@ -179,9 +179,9 @@
|
|||||||
class="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-1 lg:grid-cols-3"
|
class="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-1 lg:grid-cols-3"
|
||||||
>
|
>
|
||||||
{#each piiData as { icon, title, value }}
|
{#each piiData as { icon, title, value }}
|
||||||
<PinfoCard {icon} {title}>
|
<CinfoCard {icon} {title}>
|
||||||
<p class="break-all font-medium">{value}</p>
|
<p class="break-all font-medium">{value}</p>
|
||||||
</PinfoCard>
|
</CinfoCard>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -195,9 +195,9 @@
|
|||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4 md:grid-cols-3">
|
<div class="grid grid-cols-2 gap-4 md:grid-cols-3">
|
||||||
{#each addressInfo as { title, value }}
|
{#each addressInfo as { title, value }}
|
||||||
<PinfoCard {title}>
|
<CinfoCard {title}>
|
||||||
<p class="font-medium">{value}</p>
|
<p class="font-medium">{value}</p>
|
||||||
</PinfoCard>
|
</CinfoCard>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -212,9 +212,9 @@
|
|||||||
|
|
||||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3">
|
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3">
|
||||||
{#each cardInfo as { icon, title, value }}
|
{#each cardInfo as { icon, title, value }}
|
||||||
<PinfoCard {icon} {title}>
|
<CinfoCard {icon} {title}>
|
||||||
<p class="break-all font-medium">{value}</p>
|
<p class="break-all font-medium">{value}</p>
|
||||||
</PinfoCard>
|
</CinfoCard>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -229,14 +229,14 @@
|
|||||||
|
|
||||||
<div class={`grid grid-cols-2 gap-4`}>
|
<div class={`grid grid-cols-2 gap-4`}>
|
||||||
{#if data.data?.orderId}
|
{#if data.data?.orderId}
|
||||||
<PinfoCard icon={PackageIcon} title="Order">
|
<CinfoCard icon={PackageIcon} title="Order">
|
||||||
<a
|
<a
|
||||||
href={`${adminSiteNavMap.orders}/${data.data.orderId}`}
|
href={`${adminSiteNavMap.orders}/${data.data.orderId}`}
|
||||||
class="mt-1 inline-block font-medium text-primary hover:underline"
|
class="mt-1 inline-block font-medium text-primary hover:underline"
|
||||||
>
|
>
|
||||||
View Order #{data.data.orderId}
|
View Order #{data.data.orderId}
|
||||||
</a>
|
</a>
|
||||||
</PinfoCard>
|
</CinfoCard>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
"@better-fetch/fetch": "^1.1.12",
|
"@better-fetch/fetch": "^1.1.12",
|
||||||
"@opentelemetry/api": "^1.9.0",
|
"@opentelemetry/api": "^1.9.0",
|
||||||
"@pkg/email": "workspace:*",
|
"@pkg/email": "workspace:*",
|
||||||
"@pkg/airports-db": "workspace:*",
|
|
||||||
"@pkg/db": "workspace:*",
|
"@pkg/db": "workspace:*",
|
||||||
"@pkg/logger": "workspace:*",
|
"@pkg/logger": "workspace:*",
|
||||||
"@pkg/logic": "workspace:*",
|
"@pkg/logic": "workspace:*",
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
|
import type { CustomerInfo } from "$lib/domains/passengerinfo/data/entities";
|
||||||
|
import type { PaymentDetailsPayload } from "$lib/domains/paymentinfo/data/entities";
|
||||||
|
import { CheckoutStep } from "$lib/domains/ticket/data/entities";
|
||||||
|
import type { Database } from "@pkg/db";
|
||||||
|
import { and, eq } from "@pkg/db";
|
||||||
|
import { checkoutFlowSession } from "@pkg/db/schema";
|
||||||
|
import { getError, Logger } from "@pkg/logger";
|
||||||
import { ERROR_CODES, type Result } from "@pkg/result";
|
import { ERROR_CODES, type Result } from "@pkg/result";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
import {
|
import {
|
||||||
flowInfoModel,
|
flowInfoModel,
|
||||||
SessionOutcome,
|
SessionOutcome,
|
||||||
@@ -7,14 +15,6 @@ import {
|
|||||||
type PaymentFlowStepPayload,
|
type PaymentFlowStepPayload,
|
||||||
type PrePaymentFlowStepPayload,
|
type PrePaymentFlowStepPayload,
|
||||||
} from "./entities";
|
} from "./entities";
|
||||||
import { getError, Logger } from "@pkg/logger";
|
|
||||||
import { nanoid } from "nanoid";
|
|
||||||
import { CheckoutStep } from "$lib/domains/ticket/data/entities";
|
|
||||||
import type { PassengerPII } from "$lib/domains/passengerinfo/data/entities";
|
|
||||||
import type { PaymentDetailsPayload } from "$lib/domains/paymentinfo/data/entities";
|
|
||||||
import type { Database } from "@pkg/db";
|
|
||||||
import { and, eq } from "@pkg/db";
|
|
||||||
import { checkoutFlowSession } from "@pkg/db/schema";
|
|
||||||
|
|
||||||
export class CheckoutFlowRepository {
|
export class CheckoutFlowRepository {
|
||||||
constructor(private db: Database) {}
|
constructor(private db: Database) {}
|
||||||
@@ -202,7 +202,7 @@ export class CheckoutFlowRepository {
|
|||||||
|
|
||||||
async syncPersonalInfo(
|
async syncPersonalInfo(
|
||||||
flowId: string,
|
flowId: string,
|
||||||
personalInfo: PassengerPII,
|
personalInfo: CustomerInfo,
|
||||||
): Promise<Result<boolean>> {
|
): Promise<Result<boolean>> {
|
||||||
try {
|
try {
|
||||||
const existingSession = await this.db
|
const existingSession = await this.db
|
||||||
|
|||||||
@@ -1,4 +1,14 @@
|
|||||||
|
import {
|
||||||
|
customerInfoModel,
|
||||||
|
type CustomerInfo,
|
||||||
|
} from "$lib/domains/passengerinfo/data/entities";
|
||||||
|
import {
|
||||||
|
paymentDetailsPayloadModel,
|
||||||
|
type PaymentDetailsPayload,
|
||||||
|
} from "$lib/domains/paymentinfo/data/entities";
|
||||||
|
import { CheckoutStep } from "$lib/domains/ticket/data/entities";
|
||||||
import { createTRPCRouter, publicProcedure } from "$lib/trpc/t";
|
import { createTRPCRouter, publicProcedure } from "$lib/trpc/t";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
import {
|
||||||
feCreateCheckoutFlowPayloadModel,
|
feCreateCheckoutFlowPayloadModel,
|
||||||
@@ -8,18 +18,6 @@ import {
|
|||||||
type PrePaymentFlowStepPayload,
|
type PrePaymentFlowStepPayload,
|
||||||
} from "../data/entities";
|
} from "../data/entities";
|
||||||
import { getCKUseCases } from "./usecases";
|
import { getCKUseCases } from "./usecases";
|
||||||
import { nanoid } from "nanoid";
|
|
||||||
import {
|
|
||||||
passengerPIIModel,
|
|
||||||
type PassengerInfo,
|
|
||||||
type PassengerPII,
|
|
||||||
} from "$lib/domains/passengerinfo/data/entities";
|
|
||||||
import {
|
|
||||||
paymentDetailsPayloadModel,
|
|
||||||
type PaymentDetails,
|
|
||||||
type PaymentDetailsPayload,
|
|
||||||
} from "$lib/domains/paymentinfo/data/entities";
|
|
||||||
import { CheckoutStep } from "$lib/domains/ticket/data/entities";
|
|
||||||
|
|
||||||
function getIPFromHeaders(headers: Headers): string {
|
function getIPFromHeaders(headers: Headers): string {
|
||||||
const ip = headers.get("x-forwarded-for");
|
const ip = headers.get("x-forwarded-for");
|
||||||
@@ -74,7 +72,7 @@ export const ckflowRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
return getCKUseCases().syncPersonalInfo(
|
return getCKUseCases().syncPersonalInfo(
|
||||||
input.flowId,
|
input.flowId,
|
||||||
input.personalInfo as PassengerPII,
|
input.personalInfo as CustomerInfo,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -104,7 +102,7 @@ export const ckflowRouter = createTRPCRouter({
|
|||||||
flowId: z.string(),
|
flowId: z.string(),
|
||||||
payload: z.object({
|
payload: z.object({
|
||||||
initialUrl: z.string(),
|
initialUrl: z.string(),
|
||||||
personalInfo: passengerPIIModel.optional(),
|
personalInfo: customerInfoModel.optional(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -120,7 +118,7 @@ export const ckflowRouter = createTRPCRouter({
|
|||||||
z.object({
|
z.object({
|
||||||
flowId: z.string(),
|
flowId: z.string(),
|
||||||
payload: z.object({
|
payload: z.object({
|
||||||
personalInfo: passengerPIIModel.optional(),
|
personalInfo: customerInfoModel.optional(),
|
||||||
paymentInfo: paymentDetailsPayloadModel.optional(),
|
paymentInfo: paymentDetailsPayloadModel.optional(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { getRedisInstance } from "$lib/server/redis";
|
import type { CustomerInfo } from "$lib/domains/passengerinfo/data/entities";
|
||||||
|
import type { PaymentDetailsPayload } from "$lib/domains/paymentinfo/data/entities";
|
||||||
|
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 {
|
||||||
CreateCheckoutFlowPayload,
|
CreateCheckoutFlowPayload,
|
||||||
@@ -7,9 +9,6 @@ import type {
|
|||||||
PrePaymentFlowStepPayload,
|
PrePaymentFlowStepPayload,
|
||||||
} from "../data/entities";
|
} from "../data/entities";
|
||||||
import { CheckoutFlowRepository } from "../data/repository";
|
import { CheckoutFlowRepository } from "../data/repository";
|
||||||
import type { PassengerPII } from "$lib/domains/passengerinfo/data/entities";
|
|
||||||
import type { PaymentDetailsPayload } from "$lib/domains/paymentinfo/data/entities";
|
|
||||||
import { db } from "@pkg/db";
|
|
||||||
|
|
||||||
export class CheckoutFlowUseCases {
|
export class CheckoutFlowUseCases {
|
||||||
constructor(private repo: CheckoutFlowRepository) {}
|
constructor(private repo: CheckoutFlowRepository) {}
|
||||||
@@ -55,7 +54,7 @@ export class CheckoutFlowUseCases {
|
|||||||
return this.repo.executePaymentStep(flowId, payload);
|
return this.repo.executePaymentStep(flowId, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
async syncPersonalInfo(flowId: string, personalInfo: PassengerPII) {
|
async syncPersonalInfo(flowId: string, personalInfo: CustomerInfo) {
|
||||||
return this.repo.syncPersonalInfo(flowId, personalInfo);
|
return this.repo.syncPersonalInfo(flowId, personalInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { get } from "svelte/store";
|
import { page } from "$app/state";
|
||||||
import {
|
import {
|
||||||
CKActionType,
|
CKActionType,
|
||||||
SessionOutcome,
|
SessionOutcome,
|
||||||
@@ -6,25 +6,25 @@ import {
|
|||||||
type PendingAction,
|
type PendingAction,
|
||||||
type PendingActions,
|
type PendingActions,
|
||||||
} from "$lib/domains/ckflow/data/entities";
|
} from "$lib/domains/ckflow/data/entities";
|
||||||
import { trpcApiStore } from "$lib/stores/api";
|
import {
|
||||||
import { toast } from "svelte-sonner";
|
customerInfoModel,
|
||||||
import { flightTicketStore } from "$lib/domains/ticket/data/store";
|
type CustomerInfo,
|
||||||
|
} 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 { paymentInfoVM } from "$lib/domains/ticket/view/checkout/payment-info-section/payment.info.vm.svelte";
|
import type { PaymentDetailsPayload } from "$lib/domains/paymentinfo/data/entities";
|
||||||
import { ticketCheckoutVM } from "$lib/domains/ticket/view/checkout/flight-checkout.vm.svelte";
|
import { PaymentMethod } from "$lib/domains/paymentinfo/data/entities";
|
||||||
import {
|
import {
|
||||||
CheckoutStep,
|
CheckoutStep,
|
||||||
type FlightPriceDetails,
|
type FlightPriceDetails,
|
||||||
} from "$lib/domains/ticket/data/entities";
|
} from "$lib/domains/ticket/data/entities";
|
||||||
import { PaymentMethod } from "$lib/domains/paymentinfo/data/entities";
|
import { flightTicketStore } from "$lib/domains/ticket/data/store";
|
||||||
import { page } from "$app/state";
|
import { ticketCheckoutVM } from "$lib/domains/ticket/view/checkout/flight-checkout.vm.svelte";
|
||||||
import { ClientLogger } from "@pkg/logger/client";
|
|
||||||
import { billingDetailsVM } from "$lib/domains/ticket/view/checkout/payment-info-section/billing.details.vm.svelte";
|
import { billingDetailsVM } from "$lib/domains/ticket/view/checkout/payment-info-section/billing.details.vm.svelte";
|
||||||
import {
|
import { paymentInfoVM } from "$lib/domains/ticket/view/checkout/payment-info-section/payment.info.vm.svelte";
|
||||||
passengerPIIModel,
|
import { trpcApiStore } from "$lib/stores/api";
|
||||||
type PassengerPII,
|
import { ClientLogger } from "@pkg/logger/client";
|
||||||
} from "$lib/domains/passengerinfo/data/entities";
|
import { toast } from "svelte-sonner";
|
||||||
import type { PaymentDetailsPayload } from "$lib/domains/paymentinfo/data/entities";
|
import { get } from "svelte/store";
|
||||||
|
|
||||||
class ActionRunner {
|
class ActionRunner {
|
||||||
async run(actions: PendingActions) {
|
async run(actions: PendingActions) {
|
||||||
@@ -248,7 +248,7 @@ export class CKFlowViewModel {
|
|||||||
this.setupDone = true;
|
this.setupDone = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
debouncePersonalInfoSync(personalInfo: PassengerPII) {
|
debouncePersonalInfoSync(personalInfo: CustomerInfo) {
|
||||||
this.clearPersonalInfoDebounce();
|
this.clearPersonalInfoDebounce();
|
||||||
this.personalInfoDebounceTimer = setTimeout(() => {
|
this.personalInfoDebounceTimer = setTimeout(() => {
|
||||||
this.syncPersonalInfo(personalInfo);
|
this.syncPersonalInfo(personalInfo);
|
||||||
@@ -276,12 +276,12 @@ export class CKFlowViewModel {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
isPersonalInfoValid(personalInfo: PassengerPII): boolean {
|
isPersonalInfoValid(personalInfo: CustomerInfo): boolean {
|
||||||
const parsed = passengerPIIModel.safeParse(personalInfo);
|
const parsed = customerInfoModel.safeParse(personalInfo);
|
||||||
return !parsed.error && !!parsed.data;
|
return !parsed.error && !!parsed.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async syncPersonalInfo(personalInfo: PassengerPII) {
|
async syncPersonalInfo(personalInfo: CustomerInfo) {
|
||||||
if (!this.flowId || !this.setupDone) {
|
if (!this.flowId || !this.setupDone) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import type { EmailAccountPayload } from "@pkg/logic/domains/account/data/entities";
|
import { ckFlowVM } from "$lib/domains/ckflow/view/ckflow.vm.svelte";
|
||||||
import { get } from "svelte/store";
|
|
||||||
import {
|
import {
|
||||||
createOrderPayloadModel,
|
createOrderPayloadModel,
|
||||||
OrderCreationStep,
|
OrderCreationStep,
|
||||||
} from "$lib/domains/order/data/entities";
|
} from "$lib/domains/order/data/entities";
|
||||||
import { trpcApiStore } from "$lib/stores/api";
|
|
||||||
import type { FlightTicket } from "$lib/domains/ticket/data/entities";
|
|
||||||
import { toast } from "svelte-sonner";
|
|
||||||
import { passengerInfoVM } from "$lib/domains/passengerinfo/view/passenger.info.vm.svelte";
|
import { passengerInfoVM } from "$lib/domains/passengerinfo/view/passenger.info.vm.svelte";
|
||||||
import { ckFlowVM } from "$lib/domains/ckflow/view/ckflow.vm.svelte";
|
import type { FlightTicket } from "$lib/domains/ticket/data/entities";
|
||||||
|
import { trpcApiStore } from "$lib/stores/api";
|
||||||
|
import type { EmailAccountPayload } from "@pkg/logic/domains/account/data/entities";
|
||||||
|
import { toast } from "svelte-sonner";
|
||||||
|
import { get } from "svelte/store";
|
||||||
|
|
||||||
export class CreateOrderViewModel {
|
export class CreateOrderViewModel {
|
||||||
orderStep = $state(OrderCreationStep.ACCOUNT_SELECTION);
|
orderStep = $state(OrderCreationStep.ACCOUNT_SELECTION);
|
||||||
@@ -40,8 +40,8 @@ export class CreateOrderViewModel {
|
|||||||
if (this.orderStep === OrderCreationStep.ACCOUNT_SELECTION) {
|
if (this.orderStep === OrderCreationStep.ACCOUNT_SELECTION) {
|
||||||
this.orderStep = OrderCreationStep.TICKET_SELECTION;
|
this.orderStep = OrderCreationStep.TICKET_SELECTION;
|
||||||
} else if (this.orderStep === OrderCreationStep.TICKET_SELECTION) {
|
} else if (this.orderStep === OrderCreationStep.TICKET_SELECTION) {
|
||||||
this.orderStep = OrderCreationStep.PASSENGER_INFO;
|
this.orderStep = OrderCreationStep.CUSTOMER_INFO;
|
||||||
} else if (this.orderStep === OrderCreationStep.PASSENGER_INFO) {
|
} else if (this.orderStep === OrderCreationStep.CUSTOMER_INFO) {
|
||||||
this.orderStep = OrderCreationStep.SUMMARY;
|
this.orderStep = OrderCreationStep.SUMMARY;
|
||||||
} else {
|
} else {
|
||||||
this.orderStep = OrderCreationStep.ACCOUNT_SELECTION;
|
this.orderStep = OrderCreationStep.ACCOUNT_SELECTION;
|
||||||
@@ -50,8 +50,8 @@ export class CreateOrderViewModel {
|
|||||||
|
|
||||||
setPrevStep() {
|
setPrevStep() {
|
||||||
if (this.orderStep === OrderCreationStep.SUMMARY) {
|
if (this.orderStep === OrderCreationStep.SUMMARY) {
|
||||||
this.orderStep = OrderCreationStep.PASSENGER_INFO;
|
this.orderStep = OrderCreationStep.CUSTOMER_INFO;
|
||||||
} else if (this.orderStep === OrderCreationStep.PASSENGER_INFO) {
|
} else if (this.orderStep === OrderCreationStep.CUSTOMER_INFO) {
|
||||||
this.orderStep = OrderCreationStep.TICKET_SELECTION;
|
this.orderStep = OrderCreationStep.TICKET_SELECTION;
|
||||||
} else {
|
} else {
|
||||||
this.orderStep = OrderCreationStep.ACCOUNT_SELECTION;
|
this.orderStep = OrderCreationStep.ACCOUNT_SELECTION;
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { eq, inArray, type Database } from "@pkg/db";
|
import { eq, inArray, type Database } from "@pkg/db";
|
||||||
import { order, passengerInfo, passengerPII } from "@pkg/db/schema";
|
import { passengerInfo, passengerPII } from "@pkg/db/schema";
|
||||||
|
import { getError, Logger } from "@pkg/logger";
|
||||||
import { ERROR_CODES, type Result } from "@pkg/result";
|
import { ERROR_CODES, type Result } from "@pkg/result";
|
||||||
import {
|
import {
|
||||||
passengerInfoModel,
|
passengerInfoModel,
|
||||||
|
type CustomerInfo,
|
||||||
type PassengerInfo,
|
type PassengerInfo,
|
||||||
type PassengerPII,
|
|
||||||
} from "./entities";
|
} from "./entities";
|
||||||
import { getError, Logger } from "@pkg/logger";
|
|
||||||
|
|
||||||
export class PassengerInfoRepository {
|
export class PassengerInfoRepository {
|
||||||
private db: Database;
|
private db: Database;
|
||||||
@@ -15,7 +15,7 @@ export class PassengerInfoRepository {
|
|||||||
this.db = db;
|
this.db = db;
|
||||||
}
|
}
|
||||||
|
|
||||||
async createPassengerPii(payload: PassengerPII): Promise<Result<number>> {
|
async createPassengerPii(payload: CustomerInfo): Promise<Result<number>> {
|
||||||
try {
|
try {
|
||||||
const out = await this.db
|
const out = await this.db
|
||||||
.insert(passengerPII)
|
.insert(passengerPII)
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import LabelWrapper from "$lib/components/atoms/label-wrapper.svelte";
|
import LabelWrapper from "$lib/components/atoms/label-wrapper.svelte";
|
||||||
|
import Title from "$lib/components/atoms/title.svelte";
|
||||||
import Input from "$lib/components/ui/input/input.svelte";
|
import Input from "$lib/components/ui/input/input.svelte";
|
||||||
import * as Select from "$lib/components/ui/select";
|
import * as Select from "$lib/components/ui/select";
|
||||||
import { COUNTRIES_SELECT } from "$lib/core/countries";
|
import { COUNTRIES_SELECT } from "$lib/core/countries";
|
||||||
import type { SelectOption } from "$lib/core/data.types";
|
import type { SelectOption } from "$lib/core/data.types";
|
||||||
import { capitalize } from "$lib/core/string.utils";
|
import { capitalize } from "$lib/core/string.utils";
|
||||||
|
import type { CustomerInfo } from "$lib/domains/ticket/data/entities/create.entities";
|
||||||
import { Gender } from "$lib/domains/ticket/data/entities/index";
|
import { Gender } from "$lib/domains/ticket/data/entities/index";
|
||||||
import type { PassengerPII } from "$lib/domains/ticket/data/entities/create.entities";
|
|
||||||
import Title from "$lib/components/atoms/title.svelte";
|
|
||||||
import { passengerInfoVM } from "./passenger.info.vm.svelte";
|
|
||||||
import { PHONE_COUNTRY_CODES } from "@pkg/logic/core/data/phonecc";
|
import { PHONE_COUNTRY_CODES } from "@pkg/logic/core/data/phonecc";
|
||||||
|
import { passengerInfoVM } from "./passenger.info.vm.svelte";
|
||||||
|
|
||||||
let { info = $bindable(), idx }: { info: PassengerPII; idx: number } =
|
let { info = $bindable(), idx }: { info: CustomerInfo; idx: number } =
|
||||||
$props();
|
$props();
|
||||||
|
|
||||||
const genderOpts = [
|
const genderOpts = [
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
import {
|
||||||
|
customerInfoModel,
|
||||||
|
type BagSelectionInfo,
|
||||||
|
type CustomerInfo,
|
||||||
|
type PassengerInfo,
|
||||||
|
type SeatSelectionInfo,
|
||||||
|
} from "$lib/domains/ticket/data/entities/create.entities";
|
||||||
import {
|
import {
|
||||||
Gender,
|
Gender,
|
||||||
PassengerType,
|
PassengerType,
|
||||||
@@ -5,19 +12,12 @@ import {
|
|||||||
type FlightPriceDetails,
|
type FlightPriceDetails,
|
||||||
type PassengerCount,
|
type PassengerCount,
|
||||||
} from "$lib/domains/ticket/data/entities/index";
|
} from "$lib/domains/ticket/data/entities/index";
|
||||||
import {
|
|
||||||
passengerPIIModel,
|
|
||||||
type BagSelectionInfo,
|
|
||||||
type PassengerInfo,
|
|
||||||
type PassengerPII,
|
|
||||||
type SeatSelectionInfo,
|
|
||||||
} from "$lib/domains/ticket/data/entities/create.entities";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export class PassengerInfoViewModel {
|
export class PassengerInfoViewModel {
|
||||||
passengerInfos = $state<PassengerInfo[]>([]);
|
passengerInfos = $state<PassengerInfo[]>([]);
|
||||||
|
|
||||||
piiErrors = $state<Array<Partial<Record<keyof PassengerPII, string>>>>([]);
|
piiErrors = $state<Array<Partial<Record<keyof CustomerInfo, string>>>>([]);
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
this.passengerInfos = [];
|
this.passengerInfos = [];
|
||||||
@@ -47,7 +47,7 @@ export class PassengerInfoViewModel {
|
|||||||
// zipCode: "123098",
|
// zipCode: "123098",
|
||||||
// address: "address",
|
// address: "address",
|
||||||
// address2: "",
|
// address2: "",
|
||||||
// } as PassengerPII;
|
// } as CustomerInfo;
|
||||||
|
|
||||||
const _defaultPiiObj = {
|
const _defaultPiiObj = {
|
||||||
firstName: "",
|
firstName: "",
|
||||||
@@ -67,7 +67,7 @@ export class PassengerInfoViewModel {
|
|||||||
zipCode: "",
|
zipCode: "",
|
||||||
address: "",
|
address: "",
|
||||||
address2: "",
|
address2: "",
|
||||||
} as PassengerPII;
|
} as CustomerInfo;
|
||||||
|
|
||||||
const _defaultPriceObj = {
|
const _defaultPriceObj = {
|
||||||
currency: "",
|
currency: "",
|
||||||
@@ -137,20 +137,20 @@ export class PassengerInfoViewModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
validatePII(info: PassengerPII, idx: number) {
|
validatePII(info: CustomerInfo, idx: number) {
|
||||||
try {
|
try {
|
||||||
const result = passengerPIIModel.parse(info);
|
const result = customerInfoModel.parse(info);
|
||||||
this.piiErrors[idx] = {};
|
this.piiErrors[idx] = {};
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof z.ZodError) {
|
if (error instanceof z.ZodError) {
|
||||||
this.piiErrors[idx] = error.errors.reduce(
|
this.piiErrors[idx] = error.errors.reduce(
|
||||||
(acc, curr) => {
|
(acc, curr) => {
|
||||||
const path = curr.path[0] as keyof PassengerPII;
|
const path = curr.path[0] as keyof CustomerInfo;
|
||||||
acc[path] = curr.message;
|
acc[path] = curr.message;
|
||||||
return acc;
|
return acc;
|
||||||
},
|
},
|
||||||
{} as Record<keyof PassengerPII, string>,
|
{} as Record<keyof CustomerInfo, string>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,618 +0,0 @@
|
|||||||
import { ERROR_CODES, type Result } from "@pkg/result";
|
|
||||||
import type { BagsInfo, FlightTicket, TicketSearchDTO } from "./entities";
|
|
||||||
import { betterFetch } from "@better-fetch/fetch";
|
|
||||||
import { getError, Logger } from "@pkg/logger";
|
|
||||||
import { CabinClass, TicketType, type FlightPriceDetails } from "./entities";
|
|
||||||
import type Redis from "ioredis";
|
|
||||||
|
|
||||||
interface AmadeusAPIRequest {
|
|
||||||
currencyCode: string;
|
|
||||||
originDestinations: Array<{
|
|
||||||
id: string;
|
|
||||||
originLocationCode: string;
|
|
||||||
destinationLocationCode: string;
|
|
||||||
departureDateTimeRange: {
|
|
||||||
date: string;
|
|
||||||
time?: string;
|
|
||||||
};
|
|
||||||
arrivalDateTimeRange?: {
|
|
||||||
date: string;
|
|
||||||
time?: string;
|
|
||||||
};
|
|
||||||
}>;
|
|
||||||
travelers: Array<{
|
|
||||||
id: string;
|
|
||||||
travelerType: string;
|
|
||||||
}>;
|
|
||||||
sources: string[];
|
|
||||||
searchCriteria: {
|
|
||||||
maxFlightOffers: number;
|
|
||||||
flightFilters?: {
|
|
||||||
cabinRestrictions?: Array<{
|
|
||||||
cabin: string;
|
|
||||||
coverage: string;
|
|
||||||
originDestinationIds: string[];
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TokenResponse {
|
|
||||||
type: string;
|
|
||||||
username: string;
|
|
||||||
application_name: string;
|
|
||||||
client_id: string;
|
|
||||||
token_type: string;
|
|
||||||
access_token: string;
|
|
||||||
expires_in: number;
|
|
||||||
state: string;
|
|
||||||
scope: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class AmadeusTicketsAPIDataSource {
|
|
||||||
apiBaseUrl: string;
|
|
||||||
apiKey: string;
|
|
||||||
apiSecret: string;
|
|
||||||
redis: Redis;
|
|
||||||
|
|
||||||
// Redis key for storing the access token
|
|
||||||
private readonly TOKEN_CACHE_KEY = "amadeus:access_token";
|
|
||||||
private readonly TOKEN_EXPIRY_KEY = "amadeus:token_expiry";
|
|
||||||
|
|
||||||
// Add a buffer time to refresh token before it expires (5 minutes in seconds)
|
|
||||||
private readonly TOKEN_EXPIRY_BUFFER = 300;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
apiBaseUrl: string,
|
|
||||||
apiKey: string,
|
|
||||||
apiSecret: string,
|
|
||||||
redis: Redis,
|
|
||||||
) {
|
|
||||||
this.apiBaseUrl = apiBaseUrl;
|
|
||||||
this.apiKey = apiKey;
|
|
||||||
this.apiSecret = apiSecret;
|
|
||||||
this.redis = redis;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a valid access token, either from cache or by requesting a new one
|
|
||||||
*/
|
|
||||||
private async getAccessToken(): Promise<Result<string>> {
|
|
||||||
try {
|
|
||||||
// Try to get token from cache
|
|
||||||
const cachedToken = await this.redis.get(this.TOKEN_CACHE_KEY);
|
|
||||||
const tokenExpiry = await this.redis.get(this.TOKEN_EXPIRY_KEY);
|
|
||||||
|
|
||||||
// Check if we have a valid token that's not about to expire
|
|
||||||
if (cachedToken && tokenExpiry) {
|
|
||||||
const expiryTime = parseInt(tokenExpiry, 10);
|
|
||||||
const currentTime = Math.floor(Date.now() / 1000);
|
|
||||||
|
|
||||||
// If token is still valid and not about to expire, use it
|
|
||||||
if (expiryTime > currentTime + this.TOKEN_EXPIRY_BUFFER) {
|
|
||||||
Logger.debug("Using cached Amadeus API token");
|
|
||||||
return { data: cachedToken };
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.info(
|
|
||||||
"Amadeus API token is expired or about to expire, requesting a new one",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Request a new token
|
|
||||||
Logger.info("Requesting new Amadeus API access token");
|
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
params.append("grant_type", "client_credentials");
|
|
||||||
params.append("client_id", this.apiKey);
|
|
||||||
params.append("client_secret", this.apiSecret);
|
|
||||||
|
|
||||||
const { data, error } = await betterFetch<TokenResponse>(
|
|
||||||
`${this.apiBaseUrl}/v1/security/oauth2/token`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
|
||||||
},
|
|
||||||
body: params.toString(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (error || !data) {
|
|
||||||
return {
|
|
||||||
error: getError(
|
|
||||||
{
|
|
||||||
code: ERROR_CODES.AUTH_ERROR,
|
|
||||||
message: "Failed to authenticate with Amadeus API",
|
|
||||||
detail: "Could not obtain access token",
|
|
||||||
userHint: "Please try again later",
|
|
||||||
error: error,
|
|
||||||
actionable: false,
|
|
||||||
},
|
|
||||||
error,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cache the token
|
|
||||||
const currentTime = Math.floor(Date.now() / 1000);
|
|
||||||
const expiryTime = currentTime + data.expires_in;
|
|
||||||
|
|
||||||
await this.redis.set(this.TOKEN_CACHE_KEY, data.access_token);
|
|
||||||
await this.redis.set(this.TOKEN_EXPIRY_KEY, expiryTime.toString());
|
|
||||||
await this.redis.expire(this.TOKEN_CACHE_KEY, data.expires_in);
|
|
||||||
await this.redis.expire(this.TOKEN_EXPIRY_KEY, data.expires_in);
|
|
||||||
|
|
||||||
Logger.info(
|
|
||||||
`Successfully obtained and cached Amadeus API token (expires in ${data.expires_in} seconds)`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return { data: data.access_token };
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
return {
|
|
||||||
error: getError(
|
|
||||||
{
|
|
||||||
code: ERROR_CODES.AUTH_ERROR,
|
|
||||||
message: "Failed to authenticate with Amadeus API",
|
|
||||||
detail: "An unexpected error occurred during authentication",
|
|
||||||
userHint: "Please try again later",
|
|
||||||
error: err,
|
|
||||||
actionable: false,
|
|
||||||
},
|
|
||||||
err,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async searchForTickets(
|
|
||||||
payload: TicketSearchDTO,
|
|
||||||
): Promise<Result<FlightTicket[]>> {
|
|
||||||
Logger.info(
|
|
||||||
`Base api url: ${this.apiBaseUrl}, api key: ${this.apiKey}, api secret: ${this.apiSecret}`,
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
const tokenResult = await this.getAccessToken();
|
|
||||||
|
|
||||||
if (tokenResult.error) {
|
|
||||||
return { error: tokenResult.error };
|
|
||||||
}
|
|
||||||
|
|
||||||
const accessToken = tokenResult.data;
|
|
||||||
const request = this.buildAmadeusRequest(payload);
|
|
||||||
|
|
||||||
const from = request.originDestinations[0].originLocationCode;
|
|
||||||
const to = request.originDestinations[0].destinationLocationCode;
|
|
||||||
Logger.info(`Searching Amadeus for flights from ${from} to ${to}`);
|
|
||||||
|
|
||||||
const { data, error } = await betterFetch<any>(
|
|
||||||
`${this.apiBaseUrl}/v2/shopping/flight-offers`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/vnd.amadeus+json",
|
|
||||||
Authorization: `Bearer ${accessToken}`,
|
|
||||||
"X-HTTP-Method-Override": "GET",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(request),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return {
|
|
||||||
error: getError(
|
|
||||||
{
|
|
||||||
code: ERROR_CODES.NETWORK_ERROR,
|
|
||||||
message:
|
|
||||||
"Failed to search for tickets via Amadeus API",
|
|
||||||
detail: "Network error when connecting to Amadeus API",
|
|
||||||
userHint: "Please try again later",
|
|
||||||
error: error,
|
|
||||||
actionable: false,
|
|
||||||
},
|
|
||||||
JSON.stringify(error, null, 4),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.errors) {
|
|
||||||
return {
|
|
||||||
error: getError(
|
|
||||||
{
|
|
||||||
code: ERROR_CODES.API_ERROR,
|
|
||||||
message: "Amadeus API returned an error",
|
|
||||||
detail: data.errors
|
|
||||||
.map((e: any) => `${e.title}: ${e.detail || ""}`)
|
|
||||||
.join(", "),
|
|
||||||
userHint: "Please try with different search criteria",
|
|
||||||
error: data.errors,
|
|
||||||
actionable: false,
|
|
||||||
},
|
|
||||||
JSON.stringify(data.errors, null, 4),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we have data to process
|
|
||||||
if (
|
|
||||||
!data.data ||
|
|
||||||
!Array.isArray(data.data) ||
|
|
||||||
data.data.length === 0
|
|
||||||
) {
|
|
||||||
Logger.info("Amadeus API returned no flight offers");
|
|
||||||
return { data: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transform API response to our FlightTicket format
|
|
||||||
const tickets = this.transformAmadeusResponseToFlightTickets(
|
|
||||||
data,
|
|
||||||
payload,
|
|
||||||
);
|
|
||||||
|
|
||||||
Logger.info(`Amadeus API returned ${tickets.length} tickets`);
|
|
||||||
|
|
||||||
return { data: tickets };
|
|
||||||
} catch (err) {
|
|
||||||
return {
|
|
||||||
error: getError(
|
|
||||||
{
|
|
||||||
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
|
||||||
message: "Failed to search for tickets via Amadeus API",
|
|
||||||
detail: "An unexpected error occurred while processing the Amadeus API response",
|
|
||||||
userHint: "Please try again later",
|
|
||||||
error: err,
|
|
||||||
actionable: false,
|
|
||||||
},
|
|
||||||
JSON.stringify(err, null, 4),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private buildAmadeusRequest(payload: TicketSearchDTO): AmadeusAPIRequest {
|
|
||||||
const { ticketSearchPayload } = payload;
|
|
||||||
const { adults, children } = ticketSearchPayload.passengerCounts;
|
|
||||||
|
|
||||||
// Create travelers array
|
|
||||||
const travelers = [];
|
|
||||||
for (let i = 0; i < adults; i++) {
|
|
||||||
travelers.push({
|
|
||||||
id: (i + 1).toString(),
|
|
||||||
travelerType: "ADULT",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < (children || 0); i++) {
|
|
||||||
travelers.push({
|
|
||||||
id: (adults + i + 1).toString(),
|
|
||||||
travelerType: "CHILD",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map cabin class to Amadeus format
|
|
||||||
const cabinMap: Record<string, string> = {
|
|
||||||
[CabinClass.Economy]: "ECONOMY",
|
|
||||||
[CabinClass.PremiumEconomy]: "PREMIUM_ECONOMY",
|
|
||||||
[CabinClass.Business]: "BUSINESS",
|
|
||||||
[CabinClass.FirstClass]: "FIRST",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Build the request
|
|
||||||
const originDestinations = [];
|
|
||||||
|
|
||||||
// Format dates to ensure ISO 8601 format (YYYY-MM-DD)
|
|
||||||
const departureDate = this.formatDateForAmadeus(
|
|
||||||
ticketSearchPayload.departureDate,
|
|
||||||
);
|
|
||||||
const returnDate = ticketSearchPayload.returnDate
|
|
||||||
? this.formatDateForAmadeus(ticketSearchPayload.returnDate)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
// Add outbound journey
|
|
||||||
originDestinations.push({
|
|
||||||
id: "1",
|
|
||||||
originLocationCode: ticketSearchPayload.departure,
|
|
||||||
destinationLocationCode: ticketSearchPayload.arrival,
|
|
||||||
departureDateTimeRange: {
|
|
||||||
date: departureDate,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add return journey if it's a round trip
|
|
||||||
if (ticketSearchPayload.ticketType === TicketType.Return && returnDate) {
|
|
||||||
originDestinations.push({
|
|
||||||
id: "2",
|
|
||||||
originLocationCode: ticketSearchPayload.arrival,
|
|
||||||
destinationLocationCode: ticketSearchPayload.departure,
|
|
||||||
departureDateTimeRange: {
|
|
||||||
date: returnDate,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
currencyCode: "USD", // Use USD as default currency
|
|
||||||
originDestinations,
|
|
||||||
travelers,
|
|
||||||
sources: ["GDS"],
|
|
||||||
searchCriteria: {
|
|
||||||
maxFlightOffers: 50,
|
|
||||||
flightFilters: {
|
|
||||||
cabinRestrictions: [
|
|
||||||
{
|
|
||||||
cabin:
|
|
||||||
cabinMap[ticketSearchPayload.cabinClass] ||
|
|
||||||
"ECONOMY",
|
|
||||||
coverage: "MOST_SEGMENTS",
|
|
||||||
originDestinationIds: originDestinations.map(
|
|
||||||
(od) => od.id,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper method to ensure date is in ISO 8601 format (YYYY-MM-DD)
|
|
||||||
* @param dateString Date string to format
|
|
||||||
* @returns Formatted date string in YYYY-MM-DD format
|
|
||||||
*/
|
|
||||||
private formatDateForAmadeus(dateString: string): string {
|
|
||||||
try {
|
|
||||||
// Check if already in correct format
|
|
||||||
if (/^\d{4}-\d{2}-\d{2}$/.test(dateString)) {
|
|
||||||
return dateString;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to parse the date and format it
|
|
||||||
const date = new Date(dateString);
|
|
||||||
if (isNaN(date.getTime())) {
|
|
||||||
Logger.error(`Invalid date format provided: ${dateString}`);
|
|
||||||
throw new Error(`Invalid date: ${dateString}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format as YYYY-MM-DD
|
|
||||||
return date.toISOString().split("T")[0];
|
|
||||||
} catch (err) {
|
|
||||||
Logger.error(`Error formatting date: ${dateString}`, err);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private transformAmadeusResponseToFlightTickets(
|
|
||||||
response: any,
|
|
||||||
originalPayload: TicketSearchDTO,
|
|
||||||
): FlightTicket[] {
|
|
||||||
if (
|
|
||||||
!response.data ||
|
|
||||||
!Array.isArray(response.data) ||
|
|
||||||
response.data.length === 0
|
|
||||||
) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const { ticketSearchPayload } = originalPayload;
|
|
||||||
const dictionaries = response.dictionaries || {};
|
|
||||||
|
|
||||||
return response.data.map((offer: any, index: number) => {
|
|
||||||
// Extract basic info from the offer
|
|
||||||
const id = index + 1;
|
|
||||||
const ticketId = offer.id;
|
|
||||||
const departure = ticketSearchPayload.departure;
|
|
||||||
const arrival = ticketSearchPayload.arrival;
|
|
||||||
const departureDate = ticketSearchPayload.departureDate;
|
|
||||||
const returnDate = ticketSearchPayload.returnDate || "";
|
|
||||||
const flightType = ticketSearchPayload.ticketType;
|
|
||||||
const cabinClass = ticketSearchPayload.cabinClass;
|
|
||||||
const passengerCounts = ticketSearchPayload.passengerCounts;
|
|
||||||
|
|
||||||
// Create price details
|
|
||||||
const priceDetails: FlightPriceDetails = {
|
|
||||||
currency: offer.price.currency,
|
|
||||||
basePrice: parseFloat(offer.price.total),
|
|
||||||
displayPrice: parseFloat(offer.price.total),
|
|
||||||
discountAmount: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create baggageInfo - using defaults as Amadeus doesn't provide complete bag info
|
|
||||||
const bagsInfo: BagsInfo = {
|
|
||||||
includedPersonalBags: 1,
|
|
||||||
includedHandBags: 1,
|
|
||||||
includedCheckedBags: this.getIncludedBagsFromOffer(offer),
|
|
||||||
hasHandBagsSupport: true,
|
|
||||||
hasCheckedBagsSupport: true,
|
|
||||||
details: {
|
|
||||||
personalBags: {
|
|
||||||
price: 0,
|
|
||||||
weight: 7,
|
|
||||||
unit: "KG",
|
|
||||||
dimensions: { length: 40, width: 30, height: 20 },
|
|
||||||
},
|
|
||||||
handBags: {
|
|
||||||
price: 20,
|
|
||||||
weight: 12,
|
|
||||||
unit: "KG",
|
|
||||||
dimensions: { length: 55, width: 40, height: 20 },
|
|
||||||
},
|
|
||||||
checkedBags: {
|
|
||||||
price: 40,
|
|
||||||
weight: 23,
|
|
||||||
unit: "KG",
|
|
||||||
dimensions: { length: 90, width: 75, height: 43 },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create flight itineraries
|
|
||||||
const flightIteneraries = {
|
|
||||||
outbound: this.buildSegmentDetails(
|
|
||||||
offer.itineraries[0],
|
|
||||||
dictionaries,
|
|
||||||
0,
|
|
||||||
),
|
|
||||||
inbound:
|
|
||||||
offer.itineraries.length > 1
|
|
||||||
? this.buildSegmentDetails(
|
|
||||||
offer.itineraries[1],
|
|
||||||
dictionaries,
|
|
||||||
1,
|
|
||||||
)
|
|
||||||
: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
// Dates - always include departure date
|
|
||||||
const dates = [departureDate];
|
|
||||||
if (returnDate) {
|
|
||||||
dates.push(returnDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the flight ticket
|
|
||||||
const ticket: FlightTicket = {
|
|
||||||
id,
|
|
||||||
ticketId,
|
|
||||||
departure,
|
|
||||||
arrival,
|
|
||||||
departureDate,
|
|
||||||
returnDate,
|
|
||||||
dates,
|
|
||||||
flightType,
|
|
||||||
flightIteneraries,
|
|
||||||
priceDetails,
|
|
||||||
refOIds: [],
|
|
||||||
refundable: offer.pricingOptions?.refundableFare || false,
|
|
||||||
passengerCounts,
|
|
||||||
cabinClass,
|
|
||||||
bagsInfo,
|
|
||||||
lastAvailable: {
|
|
||||||
availableSeats: offer.numberOfBookableSeats || 10,
|
|
||||||
},
|
|
||||||
shareId: `AMADEUS-${ticketId}`,
|
|
||||||
checkoutUrl: "",
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
updatedAt: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
|
|
||||||
return ticket;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private getIncludedBagsFromOffer(offer: any): number {
|
|
||||||
// Try to extract checked bag info from first traveler and first segment
|
|
||||||
try {
|
|
||||||
const firstTraveler = offer.travelerPricings?.[0];
|
|
||||||
const firstSegment = firstTraveler?.fareDetailsBySegment?.[0];
|
|
||||||
|
|
||||||
if (firstSegment?.includedCheckedBags?.quantity) {
|
|
||||||
return firstSegment.includedCheckedBags.quantity;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If weight is specified without quantity, assume 1 bag
|
|
||||||
if (firstSegment?.includedCheckedBags?.weight) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// Ignore error and use default
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0; // Default: no included checked bags
|
|
||||||
}
|
|
||||||
|
|
||||||
private buildSegmentDetails(
|
|
||||||
itinerary: any,
|
|
||||||
dictionaries: any,
|
|
||||||
directionIndex: number,
|
|
||||||
): any[] {
|
|
||||||
return itinerary.segments.map((segment: any, index: number) => {
|
|
||||||
const depStation = this.enhanceLocationInfo(
|
|
||||||
segment.departure.iataCode,
|
|
||||||
dictionaries,
|
|
||||||
);
|
|
||||||
const arrStation = this.enhanceLocationInfo(
|
|
||||||
segment.arrival.iataCode,
|
|
||||||
dictionaries,
|
|
||||||
);
|
|
||||||
const airlineInfo = this.getAirlineInfo(
|
|
||||||
segment.carrierCode,
|
|
||||||
dictionaries,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
flightId: `${directionIndex}-${index}`,
|
|
||||||
flightNumber: `${segment.carrierCode}${segment.number}`,
|
|
||||||
airline: airlineInfo,
|
|
||||||
departure: {
|
|
||||||
station: depStation,
|
|
||||||
localTime: segment.departure.at,
|
|
||||||
utcTime: segment.departure.at, // Note: Amadeus doesn't provide UTC time
|
|
||||||
},
|
|
||||||
destination: {
|
|
||||||
station: arrStation,
|
|
||||||
localTime: segment.arrival.at,
|
|
||||||
utcTime: segment.arrival.at, // Note: Amadeus doesn't provide UTC time
|
|
||||||
},
|
|
||||||
durationSeconds: this.parseDuration(segment.duration),
|
|
||||||
seatInfo: {
|
|
||||||
availableSeats: 10, // Default value as Amadeus doesn't provide per-segment seat info
|
|
||||||
seatClass: this.mapAmadeusCabinToInternal(
|
|
||||||
segment.travelerPricings?.[0]?.fareDetailsBySegment?.[0]
|
|
||||||
?.cabin || "ECONOMY",
|
|
||||||
),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private enhanceLocationInfo(iataCode: string, dictionaries: any) {
|
|
||||||
const locationInfo = dictionaries?.locations?.[iataCode] || {};
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: 0, // Dummy id as we don't have one from Amadeus
|
|
||||||
type: "AIRPORT",
|
|
||||||
code: iataCode,
|
|
||||||
name: iataCode, // We only have the code
|
|
||||||
city: locationInfo.cityCode || iataCode,
|
|
||||||
country: locationInfo.countryCode || "Unknown",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private getAirlineInfo(carrierCode: string, dictionaries: any) {
|
|
||||||
const airlineName = dictionaries?.carriers?.[carrierCode] || carrierCode;
|
|
||||||
|
|
||||||
return {
|
|
||||||
code: carrierCode,
|
|
||||||
name: airlineName,
|
|
||||||
imageUrl: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private parseDuration(durationStr: string): number {
|
|
||||||
try {
|
|
||||||
// Example format: PT5H30M (5 hours 30 minutes)
|
|
||||||
const hourMatch = durationStr.match(/(\d+)H/);
|
|
||||||
const minuteMatch = durationStr.match(/(\d+)M/);
|
|
||||||
|
|
||||||
const hours = hourMatch ? parseInt(hourMatch[1]) : 0;
|
|
||||||
const minutes = minuteMatch ? parseInt(minuteMatch[1]) : 0;
|
|
||||||
|
|
||||||
return hours * 3600 + minutes * 60;
|
|
||||||
} catch (e) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private mapAmadeusCabinToInternal(amadeusClass: string): string | null {
|
|
||||||
const cabinMap: Record<
|
|
||||||
string,
|
|
||||||
(typeof CabinClass)[keyof typeof CabinClass]
|
|
||||||
> = {
|
|
||||||
ECONOMY: CabinClass.Economy,
|
|
||||||
PREMIUM_ECONOMY: CabinClass.PremiumEconomy,
|
|
||||||
BUSINESS: CabinClass.Business,
|
|
||||||
FIRST: CabinClass.FirstClass,
|
|
||||||
};
|
|
||||||
|
|
||||||
return cabinMap[amadeusClass] || CabinClass.Economy;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import LabelWrapper from "$lib/components/atoms/label-wrapper.svelte";
|
import LabelWrapper from "$lib/components/atoms/label-wrapper.svelte";
|
||||||
import Title from "$lib/components/atoms/title.svelte";
|
|
||||||
import Input from "$lib/components/ui/input/input.svelte";
|
import Input from "$lib/components/ui/input/input.svelte";
|
||||||
import * as Select from "$lib/components/ui/select";
|
import * as Select from "$lib/components/ui/select";
|
||||||
import { COUNTRIES_SELECT } from "$lib/core/countries";
|
import { COUNTRIES_SELECT } from "$lib/core/countries";
|
||||||
import { capitalize } from "$lib/core/string.utils";
|
import { capitalize } from "$lib/core/string.utils";
|
||||||
import type { PassengerPII } from "$lib/domains/ticket/data/entities/create.entities";
|
import type { CustomerInfo } from "$lib/domains/ticket/data/entities/create.entities";
|
||||||
import { billingDetailsVM } from "./billing.details.vm.svelte";
|
import { billingDetailsVM } from "./billing.details.vm.svelte";
|
||||||
|
|
||||||
let { info = $bindable() }: { info: PassengerPII } = $props();
|
let { info = $bindable() }: { info: CustomerInfo } = $props();
|
||||||
|
|
||||||
function onSubmit(e: SubmitEvent) {
|
function onSubmit(e: SubmitEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { Gender } from "$lib/domains/ticket/data/entities/index";
|
|
||||||
import {
|
import {
|
||||||
passengerPIIModel,
|
customerInfoModel,
|
||||||
type PassengerPII,
|
type CustomerInfo,
|
||||||
} from "$lib/domains/ticket/data/entities/create.entities";
|
} from "$lib/domains/ticket/data/entities/create.entities";
|
||||||
|
import { Gender } from "$lib/domains/ticket/data/entities/index";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export class BillingDetailsViewModel {
|
export class BillingDetailsViewModel {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
billingDetails = $state<PassengerPII>(undefined);
|
billingDetails = $state<CustomerInfo>(undefined);
|
||||||
|
|
||||||
piiErrors = $state<Partial<Record<keyof PassengerPII, string>>>({});
|
piiErrors = $state<Partial<Record<keyof CustomerInfo, string>>>({});
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.reset();
|
this.reset();
|
||||||
@@ -34,28 +34,28 @@ export class BillingDetailsViewModel {
|
|||||||
zipCode: "",
|
zipCode: "",
|
||||||
address: "",
|
address: "",
|
||||||
address2: "",
|
address2: "",
|
||||||
} as PassengerPII;
|
} as CustomerInfo;
|
||||||
this.piiErrors = {};
|
this.piiErrors = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
setPII(info: PassengerPII) {
|
setPII(info: CustomerInfo) {
|
||||||
this.billingDetails = info;
|
this.billingDetails = info;
|
||||||
}
|
}
|
||||||
|
|
||||||
validatePII(info: PassengerPII) {
|
validatePII(info: CustomerInfo) {
|
||||||
try {
|
try {
|
||||||
const result = passengerPIIModel.parse(info);
|
const result = customerInfoModel.parse(info);
|
||||||
this.piiErrors = {};
|
this.piiErrors = {};
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof z.ZodError) {
|
if (error instanceof z.ZodError) {
|
||||||
this.piiErrors = error.errors.reduce(
|
this.piiErrors = error.errors.reduce(
|
||||||
(acc, curr) => {
|
(acc, curr) => {
|
||||||
const path = curr.path[0] as keyof PassengerPII;
|
const path = curr.path[0] as keyof CustomerInfo;
|
||||||
acc[path] = curr.message;
|
acc[path] = curr.message;
|
||||||
return acc;
|
return acc;
|
||||||
},
|
},
|
||||||
{} as Record<keyof PassengerPII, string>,
|
{} as Record<keyof CustomerInfo, string>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
19
bun.lock
19
bun.lock
@@ -78,7 +78,6 @@
|
|||||||
"@baselime/sveltekit-opentelemetry-middleware": "^0.1.9",
|
"@baselime/sveltekit-opentelemetry-middleware": "^0.1.9",
|
||||||
"@better-fetch/fetch": "^1.1.12",
|
"@better-fetch/fetch": "^1.1.12",
|
||||||
"@opentelemetry/api": "^1.9.0",
|
"@opentelemetry/api": "^1.9.0",
|
||||||
"@pkg/airports-db": "workspace:*",
|
|
||||||
"@pkg/db": "workspace:*",
|
"@pkg/db": "workspace:*",
|
||||||
"@pkg/email": "workspace:*",
|
"@pkg/email": "workspace:*",
|
||||||
"@pkg/logger": "workspace:*",
|
"@pkg/logger": "workspace:*",
|
||||||
@@ -134,22 +133,6 @@
|
|||||||
"vitest": "^2.0.4",
|
"vitest": "^2.0.4",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/airportsdb": {
|
|
||||||
"name": "@pkg/airports-db",
|
|
||||||
"dependencies": {
|
|
||||||
"dotenv": "^16.4.7",
|
|
||||||
"drizzle-orm": "^0.36.1",
|
|
||||||
"postgres": "^3.4.5",
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/bun": "latest",
|
|
||||||
"@types/pg": "^8.11.10",
|
|
||||||
"drizzle-kit": "^0.28.0",
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"typescript": "^5.0.0",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"packages/db": {
|
"packages/db": {
|
||||||
"name": "@pkg/db",
|
"name": "@pkg/db",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -561,8 +544,6 @@
|
|||||||
|
|
||||||
"@phc/format": ["@phc/format@1.0.0", "", {}, "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ=="],
|
"@phc/format": ["@phc/format@1.0.0", "", {}, "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ=="],
|
||||||
|
|
||||||
"@pkg/airports-db": ["@pkg/airports-db@workspace:packages/airportsdb"],
|
|
||||||
|
|
||||||
"@pkg/db": ["@pkg/db@workspace:packages/db"],
|
"@pkg/db": ["@pkg/db@workspace:packages/db"],
|
||||||
|
|
||||||
"@pkg/email": ["@pkg/email@workspace:packages/email"],
|
"@pkg/email": ["@pkg/email@workspace:packages/email"],
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { relations } from "drizzle-orm";
|
import { relations } from "drizzle-orm";
|
||||||
import {
|
import {
|
||||||
boolean,
|
boolean,
|
||||||
date,
|
|
||||||
decimal,
|
decimal,
|
||||||
integer,
|
integer,
|
||||||
json,
|
json,
|
||||||
@@ -76,19 +75,15 @@ export const product = pgTable("product", {
|
|||||||
updatedAt: timestamp("updated_at").defaultNow(),
|
updatedAt: timestamp("updated_at").defaultNow(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const passengerPII = pgTable("passenger_pii", {
|
export const customerInfo = pgTable("customer_info", {
|
||||||
id: serial("id").primaryKey(),
|
id: serial("id").primaryKey(),
|
||||||
|
|
||||||
firstName: varchar("first_name", { length: 64 }).notNull(),
|
firstName: varchar("first_name", { length: 64 }).notNull(),
|
||||||
middleName: varchar("middle_name", { length: 64 }).notNull(),
|
middleName: varchar("middle_name", { length: 64 }).default(""),
|
||||||
lastName: varchar("last_name", { length: 64 }).notNull(),
|
lastName: varchar("last_name", { length: 64 }).notNull(),
|
||||||
email: varchar("email", { length: 128 }).notNull(),
|
email: varchar("email", { length: 128 }).notNull(),
|
||||||
phoneCountryCode: varchar("phone_country_code", { length: 6 }).notNull(),
|
phoneCountryCode: varchar("phone_country_code", { length: 6 }).notNull(),
|
||||||
phoneNumber: varchar("phone_number", { length: 20 }).notNull(),
|
phoneNumber: varchar("phone_number", { length: 20 }).notNull(),
|
||||||
nationality: varchar("nationality", { length: 32 }).notNull(),
|
|
||||||
gender: varchar("gender", { length: 32 }).notNull(),
|
|
||||||
dob: date("dob").notNull(),
|
|
||||||
passportNo: varchar("passport_no", { length: 64 }).notNull(),
|
|
||||||
passportExpiry: varchar("passport_expiry", { length: 12 }).notNull(),
|
|
||||||
|
|
||||||
country: varchar("country", { length: 128 }).notNull(),
|
country: varchar("country", { length: 128 }).notNull(),
|
||||||
state: varchar("state", { length: 128 }).notNull(),
|
state: varchar("state", { length: 128 }).notNull(),
|
||||||
@@ -97,36 +92,9 @@ export const passengerPII = pgTable("passenger_pii", {
|
|||||||
address: text("address").notNull(),
|
address: text("address").notNull(),
|
||||||
address2: text("address2"),
|
address2: text("address2"),
|
||||||
|
|
||||||
createdAt: timestamp("created_at").defaultNow(),
|
|
||||||
updatedAt: timestamp("updated_at").defaultNow(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const passengerInfo = pgTable("passenger_info", {
|
|
||||||
id: serial("id").primaryKey(),
|
|
||||||
|
|
||||||
passengerPiiId: integer("passenger_pii_id").references(
|
|
||||||
() => passengerPII.id,
|
|
||||||
{ onDelete: "cascade" },
|
|
||||||
),
|
|
||||||
paymentDetailsId: integer("payment_details_id").references(
|
|
||||||
() => paymentDetails.id,
|
|
||||||
{ onDelete: "set null" },
|
|
||||||
),
|
|
||||||
|
|
||||||
passengerType: varchar("passenger_type", { length: 24 }).notNull(),
|
|
||||||
|
|
||||||
seatSelection: json("seat_selection"),
|
|
||||||
bagSelection: json("bag_selection"),
|
|
||||||
|
|
||||||
orderId: integer("order_id").references(() => order.id, {
|
orderId: integer("order_id").references(() => order.id, {
|
||||||
onDelete: "cascade",
|
onDelete: "cascade",
|
||||||
}),
|
}),
|
||||||
flightTicketInfoId: integer("flight_ticket_info_id").references(
|
|
||||||
() => flightTicketInfo.id,
|
|
||||||
{ onDelete: "set null" },
|
|
||||||
),
|
|
||||||
agentsInfo: boolean("agents_info").default(false).notNull(),
|
|
||||||
agentId: text("agent_id").references(() => user.id, { onDelete: "set null" }),
|
|
||||||
|
|
||||||
createdAt: timestamp("created_at").defaultNow(),
|
createdAt: timestamp("created_at").defaultNow(),
|
||||||
updatedAt: timestamp("updated_at").defaultNow(),
|
updatedAt: timestamp("updated_at").defaultNow(),
|
||||||
@@ -300,27 +268,11 @@ export const checkoutFlowSession = pgTable("checkout_flow_session", {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const passengerInfoRelations = relations(passengerInfo, ({ one }) => ({
|
export const customerInfoRelations = relations(customerInfo, ({ one }) => ({
|
||||||
passengerPii: one(passengerPII, {
|
|
||||||
fields: [passengerInfo.passengerPiiId],
|
|
||||||
references: [passengerPII.id],
|
|
||||||
}),
|
|
||||||
paymentDetails: one(paymentDetails, {
|
|
||||||
fields: [passengerInfo.paymentDetailsId],
|
|
||||||
references: [paymentDetails.id],
|
|
||||||
}),
|
|
||||||
order: one(order, {
|
order: one(order, {
|
||||||
fields: [passengerInfo.orderId],
|
fields: [customerInfo.orderId],
|
||||||
references: [order.id],
|
references: [order.id],
|
||||||
}),
|
}),
|
||||||
flightTicketInfo: one(flightTicketInfo, {
|
|
||||||
fields: [passengerInfo.flightTicketInfoId],
|
|
||||||
references: [flightTicketInfo.id],
|
|
||||||
}),
|
|
||||||
parentAgent: one(user, {
|
|
||||||
fields: [passengerInfo.agentId],
|
|
||||||
references: [user.id],
|
|
||||||
}),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const orderRelations = relations(order, ({ one, many }) => ({
|
export const orderRelations = relations(order, ({ one, many }) => ({
|
||||||
@@ -332,7 +284,7 @@ export const orderRelations = relations(order, ({ one, many }) => ({
|
|||||||
fields: [order.flightTicketInfoId],
|
fields: [order.flightTicketInfoId],
|
||||||
references: [flightTicketInfo.id],
|
references: [flightTicketInfo.id],
|
||||||
}),
|
}),
|
||||||
passengerInfos: many(passengerInfo),
|
customerInfos: many(customerInfo),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const inboxRelations = relations(inbox, ({ one }) => ({
|
export const inboxRelations = relations(inbox, ({ one }) => ({
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
export const numberModel = z
|
|
||||||
.union([
|
|
||||||
z.coerce
|
|
||||||
.number()
|
|
||||||
.refine((val) => !isNaN(val), { message: "Must be a valid number" }),
|
|
||||||
z.undefined(),
|
|
||||||
])
|
|
||||||
.transform((value) => {
|
|
||||||
return value !== undefined && isNaN(value) ? undefined : value;
|
|
||||||
});
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
import {
|
||||||
PassengerPII,
|
CustomerInfo,
|
||||||
passengerPIIModel,
|
customerInfoModel,
|
||||||
} from "../../passengerinfo/data/entities";
|
} from "../../passengerinfo/data/entities";
|
||||||
import {
|
import {
|
||||||
PaymentDetailsPayload,
|
PaymentDetailsPayload,
|
||||||
@@ -94,7 +94,7 @@ export const flowInfoModel = z.object({
|
|||||||
paymentInfoLastSyncedAt: z.string().datetime().optional(),
|
paymentInfoLastSyncedAt: z.string().datetime().optional(),
|
||||||
|
|
||||||
pendingActions: pendingActionsModel.default([]),
|
pendingActions: pendingActionsModel.default([]),
|
||||||
personalInfo: z.custom<PassengerPII>().optional(),
|
personalInfo: z.custom<CustomerInfo>().optional(),
|
||||||
paymentInfo: z.custom<PaymentDetailsPayload>().optional(),
|
paymentInfo: z.custom<PaymentDetailsPayload>().optional(),
|
||||||
refOids: z.array(z.number()).optional(),
|
refOids: z.array(z.number()).optional(),
|
||||||
|
|
||||||
@@ -143,14 +143,14 @@ export type CreateCheckoutFlowPayload = z.infer<
|
|||||||
// Step-specific payloads
|
// Step-specific payloads
|
||||||
export const prePaymentFlowStepPayloadModel = z.object({
|
export const prePaymentFlowStepPayloadModel = z.object({
|
||||||
initialUrl: z.string(),
|
initialUrl: z.string(),
|
||||||
personalInfo: passengerPIIModel.optional(),
|
personalInfo: customerInfoModel.optional(),
|
||||||
});
|
});
|
||||||
export type PrePaymentFlowStepPayload = z.infer<
|
export type PrePaymentFlowStepPayload = z.infer<
|
||||||
typeof prePaymentFlowStepPayloadModel
|
typeof prePaymentFlowStepPayloadModel
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export const paymentFlowStepPayloadModel = z.object({
|
export const paymentFlowStepPayloadModel = z.object({
|
||||||
personalInfo: passengerPIIModel.optional(),
|
personalInfo: customerInfoModel.optional(),
|
||||||
paymentInfo: paymentDetailsPayloadModel.optional(),
|
paymentInfo: paymentDetailsPayloadModel.optional(),
|
||||||
});
|
});
|
||||||
export type PaymentFlowStepPayload = z.infer<
|
export type PaymentFlowStepPayload = z.infer<
|
||||||
|
|||||||
46
packages/logic/domains/customerinfo/data.ts
Normal file
46
packages/logic/domains/customerinfo/data.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const customerInfoModel = z.object({
|
||||||
|
id: z.number().optional(),
|
||||||
|
firstName: z.string().min(1).max(64),
|
||||||
|
middleName: z.string().max(64).default(""),
|
||||||
|
lastName: z.string().min(1).max(64),
|
||||||
|
email: z.string().email().max(128),
|
||||||
|
phoneCountryCode: z.string().min(1).max(6),
|
||||||
|
phoneNumber: z.string().min(1).max(20),
|
||||||
|
country: z.string().min(1).max(128),
|
||||||
|
state: z.string().min(1).max(128),
|
||||||
|
city: z.string().min(1).max(128),
|
||||||
|
zipCode: z.string().min(1).max(21),
|
||||||
|
address: z.string().min(1),
|
||||||
|
address2: z.string().optional().nullable(),
|
||||||
|
orderId: z.number().optional().nullable(),
|
||||||
|
createdAt: z.coerce.string().optional(),
|
||||||
|
updatedAt: z.coerce.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CustomerInfoModel = z.infer<typeof customerInfoModel>;
|
||||||
|
|
||||||
|
export const createCustomerInfoPayload = customerInfoModel.omit({
|
||||||
|
id: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CreateCustomerInfoPayload = z.infer<
|
||||||
|
typeof createCustomerInfoPayload
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const updateCustomerInfoPayload = customerInfoModel
|
||||||
|
.omit({
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
})
|
||||||
|
.partial()
|
||||||
|
.extend({
|
||||||
|
id: z.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type UpdateCustomerInfoPayload = z.infer<
|
||||||
|
typeof updateCustomerInfoPayload
|
||||||
|
>;
|
||||||
300
packages/logic/domains/customerinfo/repository.ts
Normal file
300
packages/logic/domains/customerinfo/repository.ts
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
import { desc, eq, type Database } from "@pkg/db";
|
||||||
|
import { customerInfo } from "@pkg/db/schema";
|
||||||
|
import { getError, Logger } from "@pkg/logger";
|
||||||
|
import { ERROR_CODES, type Result } from "@pkg/result";
|
||||||
|
import {
|
||||||
|
customerInfoModel,
|
||||||
|
type CreateCustomerInfoPayload,
|
||||||
|
type CustomerInfoModel,
|
||||||
|
type UpdateCustomerInfoPayload,
|
||||||
|
} from "./data";
|
||||||
|
|
||||||
|
export class CustomerInfoRepository {
|
||||||
|
private db: Database;
|
||||||
|
|
||||||
|
constructor(db: Database) {
|
||||||
|
this.db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllCustomerInfo(): Promise<Result<CustomerInfoModel[]>> {
|
||||||
|
try {
|
||||||
|
const results = await this.db.query.customerInfo.findMany({
|
||||||
|
orderBy: [desc(customerInfo.createdAt)],
|
||||||
|
});
|
||||||
|
const out = [] as CustomerInfoModel[];
|
||||||
|
for (const result of results) {
|
||||||
|
const parsed = customerInfoModel.safeParse(result);
|
||||||
|
if (!parsed.success) {
|
||||||
|
Logger.error("Failed to parse customer info");
|
||||||
|
Logger.error(parsed.error);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out.push(parsed.data);
|
||||||
|
}
|
||||||
|
return { data: out };
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
error: getError(
|
||||||
|
{
|
||||||
|
code: ERROR_CODES.DATABASE_ERROR,
|
||||||
|
message: "Failed to fetch customer information",
|
||||||
|
detail:
|
||||||
|
"An error occurred while retrieving customer information from the database",
|
||||||
|
userHint: "Please try refreshing the page",
|
||||||
|
actionable: false,
|
||||||
|
},
|
||||||
|
e,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCustomerInfoById(id: number): Promise<Result<CustomerInfoModel>> {
|
||||||
|
try {
|
||||||
|
const result = await this.db.query.customerInfo.findFirst({
|
||||||
|
where: eq(customerInfo.id, id),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return {
|
||||||
|
error: getError({
|
||||||
|
code: ERROR_CODES.NOT_FOUND_ERROR,
|
||||||
|
message: "Customer information not found",
|
||||||
|
detail: "No customer information exists with the provided ID",
|
||||||
|
userHint: "Please check the customer ID and try again",
|
||||||
|
actionable: true,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = customerInfoModel.safeParse(result);
|
||||||
|
if (!parsed.success) {
|
||||||
|
Logger.error("Failed to parse customer info", result);
|
||||||
|
return {
|
||||||
|
error: getError({
|
||||||
|
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
||||||
|
message: "Failed to parse customer information",
|
||||||
|
userHint: "Please try again",
|
||||||
|
detail: "Failed to parse customer information",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { data: parsed.data };
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
error: getError(
|
||||||
|
{
|
||||||
|
code: ERROR_CODES.DATABASE_ERROR,
|
||||||
|
message: "Failed to fetch customer information",
|
||||||
|
detail:
|
||||||
|
"An error occurred while retrieving the customer information from the database",
|
||||||
|
userHint: "Please try refreshing the page",
|
||||||
|
actionable: false,
|
||||||
|
},
|
||||||
|
e,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCustomerInfoByOrderId(
|
||||||
|
orderId: number,
|
||||||
|
): Promise<Result<CustomerInfoModel[]>> {
|
||||||
|
try {
|
||||||
|
const results = await this.db.query.customerInfo.findMany({
|
||||||
|
where: eq(customerInfo.orderId, orderId),
|
||||||
|
});
|
||||||
|
|
||||||
|
const out = [] as CustomerInfoModel[];
|
||||||
|
for (const result of results) {
|
||||||
|
const parsed = customerInfoModel.safeParse(result);
|
||||||
|
if (!parsed.success) {
|
||||||
|
Logger.error("Failed to parse customer info", result);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out.push(parsed.data);
|
||||||
|
}
|
||||||
|
return { data: out };
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
error: getError(
|
||||||
|
{
|
||||||
|
code: ERROR_CODES.DATABASE_ERROR,
|
||||||
|
message: "Failed to fetch customer information",
|
||||||
|
detail:
|
||||||
|
"An error occurred while retrieving customer information for the order",
|
||||||
|
userHint: "Please try again",
|
||||||
|
actionable: false,
|
||||||
|
},
|
||||||
|
e,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createCustomerInfo(
|
||||||
|
payload: CreateCustomerInfoPayload,
|
||||||
|
): Promise<Result<number>> {
|
||||||
|
try {
|
||||||
|
const result = await this.db
|
||||||
|
.insert(customerInfo)
|
||||||
|
.values({
|
||||||
|
firstName: payload.firstName,
|
||||||
|
middleName: payload.middleName || "",
|
||||||
|
lastName: payload.lastName,
|
||||||
|
email: payload.email,
|
||||||
|
phoneCountryCode: payload.phoneCountryCode,
|
||||||
|
phoneNumber: payload.phoneNumber,
|
||||||
|
country: payload.country,
|
||||||
|
state: payload.state,
|
||||||
|
city: payload.city,
|
||||||
|
zipCode: payload.zipCode,
|
||||||
|
address: payload.address,
|
||||||
|
address2: payload.address2 || null,
|
||||||
|
orderId: payload.orderId || null,
|
||||||
|
})
|
||||||
|
.returning({ id: customerInfo.id })
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
if (!result || result.length === 0) {
|
||||||
|
throw new Error("Failed to create customer info record");
|
||||||
|
}
|
||||||
|
|
||||||
|
return { data: result[0].id };
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
error: getError(
|
||||||
|
{
|
||||||
|
code: ERROR_CODES.DATABASE_ERROR,
|
||||||
|
message: "Failed to create customer information",
|
||||||
|
detail: "An error occurred while creating the customer information",
|
||||||
|
userHint: "Please try again",
|
||||||
|
actionable: false,
|
||||||
|
},
|
||||||
|
e,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateCustomerInfo(
|
||||||
|
payload: UpdateCustomerInfoPayload,
|
||||||
|
): Promise<Result<boolean>> {
|
||||||
|
try {
|
||||||
|
if (!payload.id) {
|
||||||
|
return {
|
||||||
|
error: getError({
|
||||||
|
code: ERROR_CODES.VALIDATION_ERROR,
|
||||||
|
message: "Invalid customer ID",
|
||||||
|
detail: "No customer ID was provided for the update operation",
|
||||||
|
userHint: "Please provide a valid customer ID",
|
||||||
|
actionable: true,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if customer info exists
|
||||||
|
const existing = await this.db.query.customerInfo.findFirst({
|
||||||
|
where: eq(customerInfo.id, payload.id),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
return {
|
||||||
|
error: getError({
|
||||||
|
code: ERROR_CODES.NOT_FOUND_ERROR,
|
||||||
|
message: "Customer information not found",
|
||||||
|
detail: "No customer information exists with the provided ID",
|
||||||
|
userHint: "Please check the customer ID and try again",
|
||||||
|
actionable: true,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the update object with only the fields that are provided
|
||||||
|
const updateValues: Record<string, any> = {};
|
||||||
|
|
||||||
|
if (payload.firstName !== undefined)
|
||||||
|
updateValues.firstName = payload.firstName;
|
||||||
|
if (payload.middleName !== undefined)
|
||||||
|
updateValues.middleName = payload.middleName;
|
||||||
|
if (payload.lastName !== undefined)
|
||||||
|
updateValues.lastName = payload.lastName;
|
||||||
|
if (payload.email !== undefined) updateValues.email = payload.email;
|
||||||
|
if (payload.phoneCountryCode !== undefined)
|
||||||
|
updateValues.phoneCountryCode = payload.phoneCountryCode;
|
||||||
|
if (payload.phoneNumber !== undefined)
|
||||||
|
updateValues.phoneNumber = payload.phoneNumber;
|
||||||
|
if (payload.country !== undefined) updateValues.country = payload.country;
|
||||||
|
if (payload.state !== undefined) updateValues.state = payload.state;
|
||||||
|
if (payload.city !== undefined) updateValues.city = payload.city;
|
||||||
|
if (payload.zipCode !== undefined) updateValues.zipCode = payload.zipCode;
|
||||||
|
if (payload.address !== undefined) updateValues.address = payload.address;
|
||||||
|
if (payload.address2 !== undefined)
|
||||||
|
updateValues.address2 = payload.address2;
|
||||||
|
if (payload.orderId !== undefined) updateValues.orderId = payload.orderId;
|
||||||
|
updateValues.updatedAt = new Date();
|
||||||
|
|
||||||
|
await this.db
|
||||||
|
.update(customerInfo)
|
||||||
|
.set(updateValues)
|
||||||
|
.where(eq(customerInfo.id, payload.id))
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
return { data: true };
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
error: getError(
|
||||||
|
{
|
||||||
|
code: ERROR_CODES.DATABASE_ERROR,
|
||||||
|
message: "Failed to update customer information",
|
||||||
|
detail: "An error occurred while updating the customer information",
|
||||||
|
userHint: "Please try again",
|
||||||
|
actionable: false,
|
||||||
|
},
|
||||||
|
e,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteCustomerInfo(id: number): Promise<Result<boolean>> {
|
||||||
|
try {
|
||||||
|
// Check if customer info exists
|
||||||
|
const existing = await this.db.query.customerInfo.findFirst({
|
||||||
|
where: eq(customerInfo.id, id),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
return {
|
||||||
|
error: getError({
|
||||||
|
code: ERROR_CODES.NOT_FOUND_ERROR,
|
||||||
|
message: "Customer information not found",
|
||||||
|
detail: "No customer information exists with the provided ID",
|
||||||
|
userHint: "Please check the customer ID and try again",
|
||||||
|
actionable: true,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.db
|
||||||
|
.delete(customerInfo)
|
||||||
|
.where(eq(customerInfo.id, id))
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
return { data: true };
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
error: getError(
|
||||||
|
{
|
||||||
|
code: ERROR_CODES.DATABASE_ERROR,
|
||||||
|
message: "Failed to delete customer information",
|
||||||
|
detail: "An error occurred while deleting the customer information",
|
||||||
|
userHint: "Please try again",
|
||||||
|
actionable: false,
|
||||||
|
},
|
||||||
|
e,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
42
packages/logic/domains/customerinfo/usecases.ts
Normal file
42
packages/logic/domains/customerinfo/usecases.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { db } from "@pkg/db";
|
||||||
|
import type {
|
||||||
|
CreateCustomerInfoPayload,
|
||||||
|
UpdateCustomerInfoPayload,
|
||||||
|
} from "./data";
|
||||||
|
import { CustomerInfoRepository } from "./repository";
|
||||||
|
|
||||||
|
export class CustomerInfoUseCases {
|
||||||
|
private repo: CustomerInfoRepository;
|
||||||
|
|
||||||
|
constructor(repo: CustomerInfoRepository) {
|
||||||
|
this.repo = repo;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllCustomerInfo() {
|
||||||
|
return this.repo.getAllCustomerInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCustomerInfoById(id: number) {
|
||||||
|
return this.repo.getCustomerInfoById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCustomerInfoByOrderId(orderId: number) {
|
||||||
|
return this.repo.getCustomerInfoByOrderId(orderId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createCustomerInfo(payload: CreateCustomerInfoPayload) {
|
||||||
|
return this.repo.createCustomerInfo(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateCustomerInfo(payload: UpdateCustomerInfoPayload) {
|
||||||
|
return this.repo.updateCustomerInfo(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteCustomerInfo(id: number) {
|
||||||
|
return this.repo.deleteCustomerInfo(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCustomerInfoUseCases() {
|
||||||
|
return new CustomerInfoUseCases(new CustomerInfoRepository(db));
|
||||||
|
}
|
||||||
@@ -1,18 +1,14 @@
|
|||||||
|
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 {
|
import { customerInfoModel } from "../../customerinfo/data";
|
||||||
emailAccountModel,
|
|
||||||
emailAccountPayloadModel,
|
|
||||||
} from "../../account/data/entities";
|
|
||||||
import { flightTicketModel } from "../../ticket/data/entities";
|
|
||||||
import { passengerInfoModel } from "../../passengerinfo/data/entities";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { paymentDetailsPayloadModel } from "../../paymentinfo/data/entities";
|
import { paymentDetailsPayloadModel } from "../../paymentinfo/data/entities";
|
||||||
|
import { flightTicketModel } from "../../ticket/data/entities";
|
||||||
|
|
||||||
export enum OrderCreationStep {
|
export enum OrderCreationStep {
|
||||||
ACCOUNT_SELECTION = 0,
|
ACCOUNT_SELECTION = 0,
|
||||||
TICKET_SELECTION = 1,
|
TICKET_SELECTION = 1,
|
||||||
PASSENGER_INFO = 2,
|
CUSTOMER_INFO = 2,
|
||||||
SUMMARY = 3,
|
SUMMARY = 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,7 +27,7 @@ export const orderModel = z.object({
|
|||||||
displayPrice: z.coerce.number().min(0),
|
displayPrice: z.coerce.number().min(0),
|
||||||
orderPrice: z.coerce.number().min(0),
|
orderPrice: z.coerce.number().min(0),
|
||||||
fullfilledPrice: z.coerce.number().min(0),
|
fullfilledPrice: z.coerce.number().min(0),
|
||||||
pricePerPassenger: z.coerce.number().min(0),
|
pricePerCustomer: z.coerce.number().min(0),
|
||||||
|
|
||||||
status: z.nativeEnum(OrderStatus),
|
status: z.nativeEnum(OrderStatus),
|
||||||
|
|
||||||
@@ -51,7 +47,7 @@ export const limitedOrderWithTicketInfoModel = orderModel
|
|||||||
basePrice: true,
|
basePrice: true,
|
||||||
discountAmount: true,
|
discountAmount: true,
|
||||||
displayPrice: true,
|
displayPrice: true,
|
||||||
pricePerPassenger: true,
|
pricePerCustomer: true,
|
||||||
fullfilledPrice: true,
|
fullfilledPrice: true,
|
||||||
status: true,
|
status: true,
|
||||||
})
|
})
|
||||||
@@ -75,8 +71,7 @@ export type LimitedOrderWithTicketInfoModel = z.infer<
|
|||||||
export const fullOrderModel = orderModel.merge(
|
export const fullOrderModel = orderModel.merge(
|
||||||
z.object({
|
z.object({
|
||||||
flightTicketInfo: flightTicketModel,
|
flightTicketInfo: flightTicketModel,
|
||||||
emailAccount: emailAccountModel.nullable().optional(),
|
customerInfos: z.array(customerInfoModel).default([]),
|
||||||
passengerInfos: z.array(passengerInfoModel).default([]),
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
export type FullOrderModel = z.infer<typeof fullOrderModel>;
|
export type FullOrderModel = z.infer<typeof fullOrderModel>;
|
||||||
@@ -113,7 +108,7 @@ export const newOrderModel = orderModel.pick({
|
|||||||
discountAmount: true,
|
discountAmount: true,
|
||||||
orderPrice: true,
|
orderPrice: true,
|
||||||
fullfilledPrice: true,
|
fullfilledPrice: true,
|
||||||
pricePerPassenger: true,
|
pricePerCustomer: true,
|
||||||
flightTicketInfoId: true,
|
flightTicketInfoId: true,
|
||||||
paymentDetailsId: true,
|
paymentDetailsId: true,
|
||||||
emailAccountId: true,
|
emailAccountId: true,
|
||||||
@@ -126,8 +121,7 @@ export const createOrderPayloadModel = z.object({
|
|||||||
refOIds: z.array(z.number()).nullable().optional(),
|
refOIds: z.array(z.number()).nullable().optional(),
|
||||||
paymentDetails: paymentDetailsPayloadModel.optional(),
|
paymentDetails: paymentDetailsPayloadModel.optional(),
|
||||||
orderModel: newOrderModel,
|
orderModel: newOrderModel,
|
||||||
emailAccountInfo: emailAccountPayloadModel.optional(),
|
customerInfos: z.array(customerInfoModel),
|
||||||
passengerInfos: z.array(passengerInfoModel),
|
|
||||||
flowId: z.string().optional(),
|
flowId: z.string().optional(),
|
||||||
});
|
});
|
||||||
export type CreateOrderModel = z.infer<typeof createOrderPayloadModel>;
|
export type CreateOrderModel = z.infer<typeof createOrderPayloadModel>;
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
|
||||||
flightPriceDetailsModel,
|
|
||||||
allBagDetailsModel,
|
|
||||||
} from "../../ticket/data/entities";
|
|
||||||
import { paymentDetailsModel } from "../../paymentinfo/data/entities";
|
import { paymentDetailsModel } from "../../paymentinfo/data/entities";
|
||||||
|
|
||||||
export enum Gender {
|
export enum Gender {
|
||||||
@@ -16,7 +12,7 @@ export enum PassengerType {
|
|||||||
Child = "child",
|
Child = "child",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const passengerPIIModel = z.object({
|
export const customerInfoModel = z.object({
|
||||||
firstName: z.string().min(1).max(255),
|
firstName: z.string().min(1).max(255),
|
||||||
middleName: z.string().min(0).max(255),
|
middleName: z.string().min(0).max(255),
|
||||||
lastName: z.string().min(1).max(255),
|
lastName: z.string().min(1).max(255),
|
||||||
@@ -43,43 +39,17 @@ export const passengerPIIModel = z.object({
|
|||||||
address: z.string().min(1).max(128),
|
address: z.string().min(1).max(128),
|
||||||
address2: z.string().min(0).max(128),
|
address2: z.string().min(0).max(128),
|
||||||
});
|
});
|
||||||
export type PassengerPII = z.infer<typeof passengerPIIModel>;
|
export type CustomerInfo = z.infer<typeof customerInfoModel>;
|
||||||
|
|
||||||
export const seatSelectionInfoModel = z.object({
|
|
||||||
id: z.string(),
|
|
||||||
row: z.string(),
|
|
||||||
number: z.number(),
|
|
||||||
seatLetter: z.string(),
|
|
||||||
available: z.boolean(),
|
|
||||||
reserved: z.boolean(),
|
|
||||||
price: flightPriceDetailsModel,
|
|
||||||
});
|
|
||||||
export type SeatSelectionInfo = z.infer<typeof seatSelectionInfoModel>;
|
|
||||||
|
|
||||||
export const flightSeatMapModel = z.object({
|
|
||||||
flightId: z.string(),
|
|
||||||
seats: z.array(z.array(seatSelectionInfoModel)),
|
|
||||||
});
|
|
||||||
export type FlightSeatMap = z.infer<typeof flightSeatMapModel>;
|
|
||||||
|
|
||||||
export const bagSelectionInfoModel = z.object({
|
|
||||||
id: z.number(),
|
|
||||||
personalBags: z.number().default(1),
|
|
||||||
handBags: z.number().default(0),
|
|
||||||
checkedBags: z.number().default(0),
|
|
||||||
pricing: allBagDetailsModel,
|
|
||||||
});
|
|
||||||
export type BagSelectionInfo = z.infer<typeof bagSelectionInfoModel>;
|
|
||||||
|
|
||||||
export const passengerInfoModel = z.object({
|
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: passengerPIIModel,
|
passengerPii: customerInfoModel,
|
||||||
paymentDetails: paymentDetailsModel.optional(),
|
paymentDetails: paymentDetailsModel.optional(),
|
||||||
passengerPiiId: z.number().optional(),
|
passengerPiiId: z.number().optional(),
|
||||||
paymentDetailsId: z.number().optional(),
|
paymentDetailsId: z.number().optional(),
|
||||||
seatSelection: seatSelectionInfoModel,
|
seatSelection: z.any(),
|
||||||
bagSelection: bagSelectionInfoModel,
|
bagSelection: z.any(),
|
||||||
|
|
||||||
agentsInfo: z.boolean().default(false).optional(),
|
agentsInfo: z.boolean().default(false).optional(),
|
||||||
agentId: z.coerce.string().optional(),
|
agentId: z.coerce.string().optional(),
|
||||||
|
|||||||
Reference in New Issue
Block a user