stashing code
This commit is contained in:
7
packages/logic/core/array.utils.ts
Normal file
7
packages/logic/core/array.utils.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export function chunk<T>(arr: T[], size: number): T[][] {
|
||||
const result = [];
|
||||
for (let i = 0; i < arr.length; i += size) {
|
||||
result.push(arr.slice(i, i + size));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
8
packages/logic/core/currency.utils.ts
Normal file
8
packages/logic/core/currency.utils.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export function formatCurrency(amount: number, code: string) {
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: code,
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
}).format(amount);
|
||||
}
|
||||
264
packages/logic/core/data/countries.ts
Normal file
264
packages/logic/core/data/countries.ts
Normal file
@@ -0,0 +1,264 @@
|
||||
export const COUNTRIES = [
|
||||
{ id: "1", name: "Afghanistan", code: "AF" },
|
||||
{ id: "2", name: "Albania", code: "AL" },
|
||||
{ id: "3", name: "Algeria", code: "DZ" },
|
||||
{ id: "4", name: "American Samoa", code: "AS" },
|
||||
{ id: "5", name: "Andorra", code: "AD" },
|
||||
{ id: "6", name: "Angola", code: "AO" },
|
||||
{ id: "7", name: "Anguilla", code: "AI" },
|
||||
{ id: "8", name: "Antarctica", code: "AQ" },
|
||||
{ id: "9", name: "Antigua and Barbuda", code: "AG" },
|
||||
{ id: "10", name: "Argentina", code: "AR" },
|
||||
{ id: "11", name: "Armenia", code: "AM" },
|
||||
{ id: "12", name: "Aruba", code: "AW" },
|
||||
{ id: "13", name: "Australia", code: "AU" },
|
||||
{ id: "14", name: "Austria", code: "AT" },
|
||||
{ id: "15", name: "Azerbaijan", code: "AZ" },
|
||||
{ id: "16", name: "Bahamas", code: "BS" },
|
||||
{ id: "17", name: "Bahrain", code: "BH" },
|
||||
{ id: "18", name: "Bangladesh", code: "BD" },
|
||||
{ id: "19", name: "Barbados", code: "BB" },
|
||||
{ id: "20", name: "Belarus", code: "BY" },
|
||||
{ id: "21", name: "Belgium", code: "BE" },
|
||||
{ id: "22", name: "Belize", code: "BZ" },
|
||||
{ id: "23", name: "Benin", code: "BJ" },
|
||||
{ id: "24", name: "Bermuda", code: "BM" },
|
||||
{ id: "25", name: "Bhutan", code: "BT" },
|
||||
{ id: "26", name: "Bolivia", code: "BO" },
|
||||
{ id: "27", name: "Bosnia and Herzegovina", code: "BA" },
|
||||
{ id: "28", name: "Botswana", code: "BW" },
|
||||
{ id: "29", name: "Bouvet Island", code: "BV" },
|
||||
{ id: "30", name: "Brazil", code: "BR" },
|
||||
{ id: "31", name: "British Indian Ocean Territory", code: "IO" },
|
||||
{ id: "32", name: "British Virgin Islands", code: "VG" },
|
||||
{ id: "33", name: "Brunei", code: "BN" },
|
||||
{ id: "34", name: "Bulgaria", code: "BG" },
|
||||
{ id: "35", name: "Burkina Faso", code: "BF" },
|
||||
{ id: "36", name: "Burundi", code: "BI" },
|
||||
{ id: "37", name: "Cambodia", code: "KH" },
|
||||
{ id: "38", name: "Cameroon", code: "CM" },
|
||||
{ id: "39", name: "Canada", code: "CA" },
|
||||
{ id: "40", name: "Cape Verde", code: "CV" },
|
||||
{ id: "41", name: "Caribbean Netherlands", code: "BQ" },
|
||||
{ id: "42", name: "Cayman Islands", code: "KY" },
|
||||
{ id: "43", name: "Central African Republic", code: "CF" },
|
||||
{ id: "44", name: "Chad", code: "TD" },
|
||||
{ id: "45", name: "Chile", code: "CL" },
|
||||
{ id: "46", name: "China", code: "CN" },
|
||||
{ id: "47", name: "Christmas Island", code: "CX" },
|
||||
{ id: "48", name: "Cocos (Keeling) Islands", code: "CC" },
|
||||
{ id: "49", name: "Colombia", code: "CO" },
|
||||
{ id: "50", name: "Comoros", code: "KM" },
|
||||
{ id: "51", name: "Cook Islands", code: "CK" },
|
||||
{ id: "52", name: "Costa Rica", code: "CR" },
|
||||
{ id: "53", name: "Croatia", code: "HR" },
|
||||
{ id: "54", name: "Cuba", code: "CU" },
|
||||
{ id: "55", name: "Curaçao", code: "CW" },
|
||||
{ id: "56", name: "Cyprus", code: "CY" },
|
||||
{ id: "57", name: "Czechia", code: "CZ" },
|
||||
{ id: "58", name: "DR Congo", code: "CD" },
|
||||
{ id: "59", name: "Denmark", code: "DK" },
|
||||
{ id: "60", name: "Djibouti", code: "DJ" },
|
||||
{ id: "61", name: "Dominica", code: "DM" },
|
||||
{ id: "62", name: "Dominican Republic", code: "DO" },
|
||||
{ id: "63", name: "Ecuador", code: "EC" },
|
||||
{ id: "64", name: "Egypt", code: "EG" },
|
||||
{ id: "65", name: "El Salvador", code: "SV" },
|
||||
{ id: "66", name: "Equatorial Guinea", code: "GQ" },
|
||||
{ id: "67", name: "Eritrea", code: "ER" },
|
||||
{ id: "68", name: "Estonia", code: "EE" },
|
||||
{ id: "69", name: "Eswatini", code: "SZ" },
|
||||
{ id: "70", name: "Ethiopia", code: "ET" },
|
||||
{ id: "71", name: "Falkland Islands", code: "FK" },
|
||||
{ id: "72", name: "Faroe Islands", code: "FO" },
|
||||
{ id: "73", name: "Fiji", code: "FJ" },
|
||||
{ id: "74", name: "Finland", code: "FI" },
|
||||
{ id: "75", name: "France", code: "FR" },
|
||||
{ id: "76", name: "French Guiana", code: "GF" },
|
||||
{ id: "77", name: "French Polynesia", code: "PF" },
|
||||
{ id: "78", name: "French Southern and Antarctic Lands", code: "TF" },
|
||||
{ id: "79", name: "Gabon", code: "GA" },
|
||||
{ id: "80", name: "Gambia", code: "GM" },
|
||||
{ id: "81", name: "Georgia", code: "GE" },
|
||||
{ id: "82", name: "Germany", code: "DE" },
|
||||
{ id: "83", name: "Ghana", code: "GH" },
|
||||
{ id: "84", name: "Gibraltar", code: "GI" },
|
||||
{ id: "85", name: "Greece", code: "GR" },
|
||||
{ id: "86", name: "Greenland", code: "GL" },
|
||||
{ id: "87", name: "Grenada", code: "GD" },
|
||||
{ id: "88", name: "Guadeloupe", code: "GP" },
|
||||
{ id: "89", name: "Guam", code: "GU" },
|
||||
{ id: "90", name: "Guatemala", code: "GT" },
|
||||
{ id: "91", name: "Guernsey", code: "GG" },
|
||||
{ id: "92", name: "Guinea", code: "GN" },
|
||||
{ id: "93", name: "Guinea-Bissau", code: "GW" },
|
||||
{ id: "94", name: "Guyana", code: "GY" },
|
||||
{ id: "95", name: "Haiti", code: "HT" },
|
||||
{ id: "96", name: "Heard Island and McDonald Islands", code: "HM" },
|
||||
{ id: "97", name: "Honduras", code: "HN" },
|
||||
{ id: "98", name: "Hong Kong", code: "HK" },
|
||||
{ id: "99", name: "Hungary", code: "HU" },
|
||||
{ id: "100", name: "Iceland", code: "IS" },
|
||||
{ id: "101", name: "India", code: "IN" },
|
||||
{ id: "102", name: "Indonesia", code: "ID" },
|
||||
{ id: "103", name: "Iran", code: "IR" },
|
||||
{ id: "104", name: "Iraq", code: "IQ" },
|
||||
{ id: "105", name: "Ireland", code: "IE" },
|
||||
{ id: "106", name: "Isle of Man", code: "IM" },
|
||||
{ id: "107", name: "Israel", code: "IL" },
|
||||
{ id: "108", name: "Italy", code: "IT" },
|
||||
{ id: "109", name: "Ivory Coast", code: "CI" },
|
||||
{ id: "110", name: "Jamaica", code: "JM" },
|
||||
{ id: "111", name: "Japan", code: "JP" },
|
||||
{ id: "112", name: "Jersey", code: "JE" },
|
||||
{ id: "113", name: "Jordan", code: "JO" },
|
||||
{ id: "114", name: "Kazakhstan", code: "KZ" },
|
||||
{ id: "115", name: "Kenya", code: "KE" },
|
||||
{ id: "116", name: "Kiribati", code: "KI" },
|
||||
{ id: "117", name: "Kosovo", code: "XK" },
|
||||
{ id: "118", name: "Kuwait", code: "KW" },
|
||||
{ id: "119", name: "Kyrgyzstan", code: "KG" },
|
||||
{ id: "120", name: "Laos", code: "LA" },
|
||||
{ id: "121", name: "Latvia", code: "LV" },
|
||||
{ id: "122", name: "Lebanon", code: "LB" },
|
||||
{ id: "123", name: "Lesotho", code: "LS" },
|
||||
{ id: "124", name: "Liberia", code: "LR" },
|
||||
{ id: "125", name: "Libya", code: "LY" },
|
||||
{ id: "126", name: "Liechtenstein", code: "LI" },
|
||||
{ id: "127", name: "Lithuania", code: "LT" },
|
||||
{ id: "128", name: "Luxembourg", code: "LU" },
|
||||
{ id: "129", name: "Macau", code: "MO" },
|
||||
{ id: "130", name: "Madagascar", code: "MG" },
|
||||
{ id: "131", name: "Malawi", code: "MW" },
|
||||
{ id: "132", name: "Malaysia", code: "MY" },
|
||||
{ id: "133", name: "Maldives", code: "MV" },
|
||||
{ id: "134", name: "Mali", code: "ML" },
|
||||
{ id: "135", name: "Malta", code: "MT" },
|
||||
{ id: "136", name: "Marshall Islands", code: "MH" },
|
||||
{ id: "137", name: "Martinique", code: "MQ" },
|
||||
{ id: "138", name: "Mauritania", code: "MR" },
|
||||
{ id: "139", name: "Mauritius", code: "MU" },
|
||||
{ id: "140", name: "Mayotte", code: "YT" },
|
||||
{ id: "141", name: "Mexico", code: "MX" },
|
||||
{ id: "142", name: "Micronesia", code: "FM" },
|
||||
{ id: "143", name: "Moldova", code: "MD" },
|
||||
{ id: "144", name: "Monaco", code: "MC" },
|
||||
{ id: "145", name: "Mongolia", code: "MN" },
|
||||
{ id: "146", name: "Montenegro", code: "ME" },
|
||||
{ id: "147", name: "Montserrat", code: "MS" },
|
||||
{ id: "148", name: "Morocco", code: "MA" },
|
||||
{ id: "149", name: "Mozambique", code: "MZ" },
|
||||
{ id: "150", name: "Myanmar", code: "MM" },
|
||||
{ id: "151", name: "Namibia", code: "NA" },
|
||||
{ id: "152", name: "Nauru", code: "NR" },
|
||||
{ id: "153", name: "Nepal", code: "NP" },
|
||||
{ id: "154", name: "Netherlands", code: "NL" },
|
||||
{ id: "155", name: "New Caledonia", code: "NC" },
|
||||
{ id: "156", name: "New Zealand", code: "NZ" },
|
||||
{ id: "157", name: "Nicaragua", code: "NI" },
|
||||
{ id: "158", name: "Niger", code: "NE" },
|
||||
{ id: "159", name: "Nigeria", code: "NG" },
|
||||
{ id: "160", name: "Niue", code: "NU" },
|
||||
{ id: "161", name: "Norfolk Island", code: "NF" },
|
||||
{ id: "162", name: "North Korea", code: "KP" },
|
||||
{ id: "163", name: "North Macedonia", code: "MK" },
|
||||
{ id: "164", name: "Northern Mariana Islands", code: "MP" },
|
||||
{ id: "165", name: "Norway", code: "NO" },
|
||||
{ id: "166", name: "Oman", code: "OM" },
|
||||
{ id: "167", name: "Pakistan", code: "PK" },
|
||||
{ id: "168", name: "Palau", code: "PW" },
|
||||
{ id: "169", name: "Palestine", code: "PS" },
|
||||
{ id: "170", name: "Panama", code: "PA" },
|
||||
{ id: "171", name: "Papua New Guinea", code: "PG" },
|
||||
{ id: "172", name: "Paraguay", code: "PY" },
|
||||
{ id: "173", name: "Peru", code: "PE" },
|
||||
{ id: "174", name: "Philippines", code: "PH" },
|
||||
{ id: "175", name: "Pitcairn Islands", code: "PN" },
|
||||
{ id: "176", name: "Poland", code: "PL" },
|
||||
{ id: "177", name: "Portugal", code: "PT" },
|
||||
{ id: "178", name: "Puerto Rico", code: "PR" },
|
||||
{ id: "179", name: "Qatar", code: "QA" },
|
||||
{ id: "180", name: "Republic of the Congo", code: "CG" },
|
||||
{ id: "181", name: "Romania", code: "RO" },
|
||||
{ id: "182", name: "Russia", code: "RU" },
|
||||
{ id: "183", name: "Rwanda", code: "RW" },
|
||||
{ id: "184", name: "Réunion", code: "RE" },
|
||||
{ id: "185", name: "Saint Barthélemy", code: "BL" },
|
||||
{
|
||||
id: "186",
|
||||
name: "Saint Helena, Ascension and Tristan da Cunha",
|
||||
code: "SH",
|
||||
},
|
||||
{ id: "187", name: "Saint Kitts and Nevis", code: "KN" },
|
||||
{ id: "188", name: "Saint Lucia", code: "LC" },
|
||||
{ id: "189", name: "Saint Martin", code: "MF" },
|
||||
{ id: "190", name: "Saint Pierre and Miquelon", code: "PM" },
|
||||
{ id: "191", name: "Saint Vincent and the Grenadines", code: "VC" },
|
||||
{ id: "192", name: "Samoa", code: "WS" },
|
||||
{ id: "193", name: "San Marino", code: "SM" },
|
||||
{ id: "194", name: "Saudi Arabia", code: "SA" },
|
||||
{ id: "195", name: "Senegal", code: "SN" },
|
||||
{ id: "196", name: "Serbia", code: "RS" },
|
||||
{ id: "197", name: "Seychelles", code: "SC" },
|
||||
{ id: "198", name: "Sierra Leone", code: "SL" },
|
||||
{ id: "199", name: "Singapore", code: "SG" },
|
||||
{ id: "200", name: "Sint Maarten", code: "SX" },
|
||||
{ id: "201", name: "Slovakia", code: "SK" },
|
||||
{ id: "202", name: "Slovenia", code: "SI" },
|
||||
{ id: "203", name: "Solomon Islands", code: "SB" },
|
||||
{ id: "204", name: "Somalia", code: "SO" },
|
||||
{ id: "205", name: "South Africa", code: "ZA" },
|
||||
{ id: "206", name: "South Georgia", code: "GS" },
|
||||
{ id: "207", name: "South Korea", code: "KR" },
|
||||
{ id: "208", name: "South Sudan", code: "SS" },
|
||||
{ id: "209", name: "Spain", code: "ES" },
|
||||
{ id: "210", name: "Sri Lanka", code: "LK" },
|
||||
{ id: "211", name: "Sudan", code: "SD" },
|
||||
{ id: "212", name: "Suriname", code: "SR" },
|
||||
{ id: "213", name: "Svalbard and Jan Mayen", code: "SJ" },
|
||||
{ id: "214", name: "Sweden", code: "SE" },
|
||||
{ id: "215", name: "Switzerland", code: "CH" },
|
||||
{ id: "216", name: "Syria", code: "SY" },
|
||||
{ id: "217", name: "São Tomé and Príncipe", code: "ST" },
|
||||
{ id: "218", name: "Taiwan", code: "TW" },
|
||||
{ id: "219", name: "Tajikistan", code: "TJ" },
|
||||
{ id: "220", name: "Tanzania", code: "TZ" },
|
||||
{ id: "221", name: "Thailand", code: "TH" },
|
||||
{ id: "222", name: "Timor-Leste", code: "TL" },
|
||||
{ id: "223", name: "Togo", code: "TG" },
|
||||
{ id: "224", name: "Tokelau", code: "TK" },
|
||||
{ id: "225", name: "Tonga", code: "TO" },
|
||||
{ id: "226", name: "Trinidad and Tobago", code: "TT" },
|
||||
{ id: "227", name: "Tunisia", code: "TN" },
|
||||
{ id: "228", name: "Turkey", code: "TR" },
|
||||
{ id: "229", name: "Turkmenistan", code: "TM" },
|
||||
{ id: "230", name: "Turks and Caicos Islands", code: "TC" },
|
||||
{ id: "231", name: "Tuvalu", code: "TV" },
|
||||
{ id: "232", name: "Uganda", code: "UG" },
|
||||
{ id: "233", name: "Ukraine", code: "UA" },
|
||||
{ id: "234", name: "United Arab Emirates", code: "AE" },
|
||||
{ id: "235", name: "United Kingdom", code: "GB" },
|
||||
{ id: "236", name: "United States", code: "US" },
|
||||
{ id: "237", name: "United States Minor Outlying Islands", code: "UM" },
|
||||
{ id: "238", name: "United States Virgin Islands", code: "VI" },
|
||||
{ id: "239", name: "Uruguay", code: "UY" },
|
||||
{ id: "240", name: "Uzbekistan", code: "UZ" },
|
||||
{ id: "241", name: "Vanuatu", code: "VU" },
|
||||
{ id: "242", name: "Vatican City", code: "VA" },
|
||||
{ id: "243", name: "Venezuela", code: "VE" },
|
||||
{ id: "244", name: "Vietnam", code: "VN" },
|
||||
{ id: "245", name: "Wallis and Futuna", code: "WF" },
|
||||
{ id: "246", name: "Western Sahara", code: "EH" },
|
||||
{ id: "247", name: "Yemen", code: "YE" },
|
||||
{ id: "248", name: "Zambia", code: "ZM" },
|
||||
{ id: "249", name: "Zimbabwe", code: "ZW" },
|
||||
{ id: "250", name: "Åland Islands", code: "AX" },
|
||||
];
|
||||
|
||||
export const COUNTRIES_SELECT = COUNTRIES.map((c) => {
|
||||
return {
|
||||
id: c.id,
|
||||
label: `${c.code} (${c.name})`,
|
||||
value: c.name.toLowerCase(),
|
||||
};
|
||||
});
|
||||
1227
packages/logic/core/data/phonecc.ts
Normal file
1227
packages/logic/core/data/phonecc.ts
Normal file
File diff suppressed because it is too large
Load Diff
92
packages/logic/core/date.utils.ts
Normal file
92
packages/logic/core/date.utils.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import type { CalendarDate } from "@internationalized/date";
|
||||
|
||||
export const formatTime = (isoString: string | undefined) => {
|
||||
if (!isoString) return "N/A";
|
||||
try {
|
||||
return formatDistanceToNow(new Date(isoString), { addSuffix: true });
|
||||
} catch (e) {
|
||||
return "Invalid date";
|
||||
}
|
||||
};
|
||||
|
||||
export function formatDuration(ms: number): string {
|
||||
const seconds = Math.floor(ms / 1000);
|
||||
if (seconds < 60) return `${seconds}s`;
|
||||
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
return `${minutes}m ${remainingSeconds}s`;
|
||||
}
|
||||
|
||||
export function formatDateTimeFromIsoString(isoString: string): string {
|
||||
try {
|
||||
const date = new Date(isoString);
|
||||
return new Intl.DateTimeFormat("en-US", {
|
||||
dateStyle: "medium",
|
||||
timeStyle: "short",
|
||||
}).format(date);
|
||||
} catch (e) {
|
||||
return "Invalid date";
|
||||
}
|
||||
}
|
||||
|
||||
export function getJustDateString(d: Date): string {
|
||||
return d.toISOString().split("T")[0];
|
||||
}
|
||||
|
||||
export function formatDateTime(dateTimeStr: string) {
|
||||
const date = new Date(dateTimeStr);
|
||||
return {
|
||||
time: date.toLocaleTimeString("en-US", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
}),
|
||||
date: date.toLocaleDateString("en-US", {
|
||||
weekday: "short",
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export function formatDate(dateStr: string) {
|
||||
return new Date(dateStr).toLocaleDateString("en-US", {
|
||||
weekday: "short",
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
});
|
||||
}
|
||||
|
||||
export function isTimestampMoreThan1MinAgo(ts: string): boolean {
|
||||
const lastPingedDate = new Date(ts);
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - lastPingedDate.getTime();
|
||||
return diff > 60000;
|
||||
}
|
||||
|
||||
export function isTimestampOlderThan(ts: string, seconds: number): boolean {
|
||||
const lastPingedDate = new Date(ts);
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - lastPingedDate.getTime();
|
||||
return diff > seconds * 1000;
|
||||
}
|
||||
|
||||
export function makeDateStringISO(ds: string): string {
|
||||
if (ds.includes("T")) {
|
||||
return `${ds.split("T")[0]}T00:00:00.000Z`;
|
||||
}
|
||||
return `${ds}T00:00:00.000Z`;
|
||||
}
|
||||
|
||||
export function parseCalDateToDateString(v: CalendarDate) {
|
||||
let month: string | number = v.month;
|
||||
if (month < 10) {
|
||||
month = `0${month}`;
|
||||
}
|
||||
let day: string | number = v.day;
|
||||
if (day < 10) {
|
||||
day = `0${day}`;
|
||||
}
|
||||
return `${v.year}-${month}-${day}`;
|
||||
}
|
||||
15
packages/logic/core/enums.ts
Normal file
15
packages/logic/core/enums.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export const PAYMENT_STATUS = {
|
||||
PENDING: "pending",
|
||||
REVIEW: "review",
|
||||
FAILED: "failed",
|
||||
SUCCESS: "success",
|
||||
};
|
||||
export type PaymentStatus = "pending" | "review" | "failed" | "success";
|
||||
|
||||
export const UserRoleMap = {
|
||||
ADMIN: "admin",
|
||||
AGENT: "agent",
|
||||
SUPERVISOR: "supervisor",
|
||||
ENDUSER: "enduser",
|
||||
};
|
||||
export type UserType = "admin" | "agent" | "supervisor" | "enduser";
|
||||
12
packages/logic/core/pagination.utils.ts
Normal file
12
packages/logic/core/pagination.utils.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const paginationModel = z.object({
|
||||
cursor: z.string().optional(),
|
||||
limit: z.number().int().max(100),
|
||||
asc: z.boolean().default(true),
|
||||
totalItemCount: z.number().int().default(0),
|
||||
totalPages: z.number().int(),
|
||||
page: z.number().int(),
|
||||
});
|
||||
|
||||
export type PaginationModel = z.infer<typeof paginationModel>;
|
||||
89
packages/logic/core/string.utils.ts
Normal file
89
packages/logic/core/string.utils.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import type { z } from "zod";
|
||||
|
||||
export function capitalize(input: string, firstOfAllWords?: boolean): string {
|
||||
// capitalize first letter of input
|
||||
if (!firstOfAllWords) {
|
||||
return input.charAt(0).toUpperCase() + input.slice(1);
|
||||
}
|
||||
let out = "";
|
||||
for (const word of input.split(" ")) {
|
||||
out += word.charAt(0).toUpperCase() + word.slice(1) + " ";
|
||||
}
|
||||
return out.slice(0, -1);
|
||||
}
|
||||
|
||||
export function camelToSpacedPascal(input: string): string {
|
||||
let result = "";
|
||||
let previousChar = "";
|
||||
for (const char of input) {
|
||||
if (char === char.toUpperCase() && previousChar !== " ") {
|
||||
result += " ";
|
||||
}
|
||||
result += char;
|
||||
previousChar = char;
|
||||
}
|
||||
return result.charAt(0).toUpperCase() + result.slice(1);
|
||||
}
|
||||
|
||||
export function snakeToCamel(input: string): string {
|
||||
if (!input) {
|
||||
return input;
|
||||
}
|
||||
// also account for numbers and kebab-case
|
||||
const splits = input.split(/[-_]/);
|
||||
let result = splits[0];
|
||||
for (const split of splits.slice(1)) {
|
||||
result += capitalize(split, true);
|
||||
}
|
||||
return result ?? "";
|
||||
}
|
||||
|
||||
export function snakeToSpacedPascal(input: string): string {
|
||||
return camelToSpacedPascal(snakeToCamel(input));
|
||||
}
|
||||
|
||||
export function spacedPascalToSnake(input: string): string {
|
||||
return input.split(" ").join("_").toLowerCase();
|
||||
}
|
||||
|
||||
export function convertDashedLowerToTitleCase(input: string): string {
|
||||
return input
|
||||
.split("-")
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||
.join(" "); // Join the words with a space
|
||||
}
|
||||
|
||||
export function encodeCursor<T>(cursor: T): string {
|
||||
try {
|
||||
// Convert the object to a JSON string
|
||||
const jsonString = JSON.stringify(cursor);
|
||||
// Convert to UTF-8 bytes, then base64
|
||||
return btoa(
|
||||
encodeURIComponent(jsonString).replace(/%([0-9A-F]{2})/g, (_, p1) =>
|
||||
String.fromCharCode(parseInt(p1, 16)),
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error encoding cursor:", error);
|
||||
throw new Error("Failed to encode cursor");
|
||||
}
|
||||
}
|
||||
|
||||
export function decodeCursor<T>(cursor: string, parser: z.AnyZodObject) {
|
||||
try {
|
||||
// Decode base64 back to UTF-8 string
|
||||
const decoded = decodeURIComponent(
|
||||
Array.prototype.map
|
||||
.call(atob(cursor), (c) => {
|
||||
return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
|
||||
})
|
||||
.join(""),
|
||||
);
|
||||
// Parse back to object
|
||||
const parsedData = JSON.parse(decoded);
|
||||
return parser.safeParse(parsedData) as z.SafeParseReturnType<any, T>;
|
||||
} catch (error) {
|
||||
console.error("Error decoding cursor:", error);
|
||||
return { error: new Error("Failed to decode cursor"), data: undefined };
|
||||
}
|
||||
}
|
||||
52
packages/logic/domains/account/data/entities.ts
Normal file
52
packages/logic/domains/account/data/entities.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const emailAccountPayloadModel = z.object({
|
||||
email: z.string().email().min(6).max(128),
|
||||
password: z.string().max(128),
|
||||
agentId: z.string().optional(),
|
||||
orderId: z.number().int().nullish().optional(),
|
||||
});
|
||||
export type EmailAccountPayload = z.infer<typeof emailAccountPayloadModel>;
|
||||
|
||||
export const emailAccountModel = emailAccountPayloadModel
|
||||
.pick({ email: true, agentId: true, orderId: true })
|
||||
.merge(
|
||||
z.object({
|
||||
id: z.number().int(),
|
||||
used: z.boolean().default(false),
|
||||
lastActiveCheckAt: z.coerce.string().optional(),
|
||||
createdAt: z.coerce.string(),
|
||||
updatedAt: z.coerce.string(),
|
||||
}),
|
||||
);
|
||||
export type EmailAccount = z.infer<typeof emailAccountModel>;
|
||||
|
||||
export const emailAccountFullModel = emailAccountPayloadModel.merge(
|
||||
z.object({
|
||||
id: z.number().int(),
|
||||
used: z.boolean().default(false),
|
||||
createdAt: z.coerce.string(),
|
||||
updatedAt: z.coerce.string(),
|
||||
}),
|
||||
);
|
||||
export type EmailAccountFull = z.infer<typeof emailAccountFullModel>;
|
||||
|
||||
export const inboxModel = z.object({
|
||||
id: z.number(),
|
||||
emailId: z.string().default(""),
|
||||
|
||||
from: z.string(),
|
||||
to: z.string().optional(),
|
||||
cc: z.string().optional(),
|
||||
subject: z.string(),
|
||||
body: z.string(),
|
||||
|
||||
attachments: z.any().optional(),
|
||||
emailAccountId: z.number(),
|
||||
|
||||
dated: z.coerce.string(),
|
||||
|
||||
createdAt: z.coerce.string(),
|
||||
updatedAt: z.coerce.string(),
|
||||
});
|
||||
export type InboxModel = z.infer<typeof inboxModel>;
|
||||
34
packages/logic/domains/airport/data/entities.ts
Normal file
34
packages/logic/domains/airport/data/entities.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
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;
|
||||
});
|
||||
|
||||
export const airportModel = z.object({
|
||||
id: z.coerce.number().int(),
|
||||
ident: z.string().nullable().optional(),
|
||||
type: z.string(),
|
||||
name: z.string(),
|
||||
latitudeDeg: numberModel.default(0.0),
|
||||
longitudeDeg: numberModel.default(0.0),
|
||||
elevationFt: numberModel.default(0.0),
|
||||
continent: z.string(),
|
||||
isoCountry: z.string(),
|
||||
country: z.string(),
|
||||
isoRegion: z.string(),
|
||||
municipality: z.string(),
|
||||
scheduledService: z.string(),
|
||||
gpsCode: z.coerce.string().default("----"),
|
||||
iataCode: z.coerce.string().min(1),
|
||||
localCode: z.coerce.string().default("----"),
|
||||
createdAt: z.coerce.date(),
|
||||
updatedAt: z.coerce.date(),
|
||||
});
|
||||
export type Airport = z.infer<typeof airportModel>;
|
||||
21
packages/logic/domains/auth/data/entities.ts
Normal file
21
packages/logic/domains/auth/data/entities.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { authClient } from "../config/client";
|
||||
import { z } from "zod";
|
||||
|
||||
export const passwordModel = z.string().min(6).max(128);
|
||||
|
||||
export const authPayloadModel = z.object({
|
||||
username: z.string().min(4).max(128),
|
||||
password: passwordModel,
|
||||
});
|
||||
|
||||
export type AuthPayloadModel = z.infer<typeof authPayloadModel>;
|
||||
export type Session = typeof authClient.$Infer.Session;
|
||||
|
||||
export const changePasswordPayloadModel = z.object({
|
||||
oldPassword: passwordModel,
|
||||
newPassword: passwordModel,
|
||||
});
|
||||
|
||||
export type ChangePasswordPayloadModel = z.infer<
|
||||
typeof changePasswordPayloadModel
|
||||
>;
|
||||
11
packages/logic/domains/auth/session/data/entities.ts
Normal file
11
packages/logic/domains/auth/session/data/entities.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const sessionModel = z.object({
|
||||
id: z.string(),
|
||||
userId: z.coerce.number().int(),
|
||||
userAgent: z.string(),
|
||||
ipAddress: z.string(),
|
||||
expiresAt: z.coerce.date(),
|
||||
});
|
||||
export type SessionModel = z.infer<typeof sessionModel>;
|
||||
export type Session = SessionModel & { id: string };
|
||||
158
packages/logic/domains/ckflow/data/entities.ts
Normal file
158
packages/logic/domains/ckflow/data/entities.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
PassengerPII,
|
||||
passengerPIIModel,
|
||||
} from "../../passengerinfo/data/entities";
|
||||
import {
|
||||
PaymentDetailsPayload,
|
||||
paymentDetailsPayloadModel,
|
||||
} from "../../paymentinfo/data/entities";
|
||||
import { CheckoutStep } from "../../ticket/data/entities";
|
||||
|
||||
// Define action types for the checkout flow
|
||||
export enum CKActionType {
|
||||
PutInVerification = "PUT_IN_VERIFICATION",
|
||||
ShowVerificationScreen = "SHOW_VERIFICATION_SCREEN",
|
||||
RequestOTP = "REQUEST_OTP",
|
||||
CompleteOrder = "COMPLETE_ORDER",
|
||||
BackToPII = "BACK_TO_PII",
|
||||
BackToPayment = "BACK_TO_PAYMENT",
|
||||
TerminateSession = "TERMINATE_SESSION",
|
||||
}
|
||||
|
||||
export enum SessionOutcome {
|
||||
PENDING = "PENDING",
|
||||
COMPLETED = "COMPLETED",
|
||||
ABANDONED = "ABANDONED",
|
||||
TERMINATED = "TERMINATED",
|
||||
EXPIRED = "EXPIRED",
|
||||
PAYMENT_FAILED = "PAYMENT_FAILED",
|
||||
PAYMENT_SUCCESSFUL = "PAYMENT_SUCCESSFUL",
|
||||
VERIFICATION_FAILED = "VERIFICATION_FAILED",
|
||||
SYSTEM_ERROR = "SYSTEM_ERROR",
|
||||
}
|
||||
|
||||
export enum PaymentErrorType {
|
||||
DO_NOT_HONOR = "DO_NOT_HONOR",
|
||||
REFER_TO_ISSUER = "REFER_TO_ISSUER",
|
||||
TRANSACTION_DENIED = "TRANSACTION_DENIED",
|
||||
CAPTURE_CARD_ERROR = "CAPTURE_CARD_ERROR",
|
||||
CUSTOM_ERROR = "CUSTOM_ERROR",
|
||||
}
|
||||
|
||||
export interface PaymentErrorMessage {
|
||||
type: PaymentErrorType;
|
||||
message: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
// Model for pending actions in the flow
|
||||
export const pendingActionModel = z.object({
|
||||
id: z.string(),
|
||||
type: z.nativeEnum(CKActionType),
|
||||
data: z.record(z.string(), z.any()).default({}),
|
||||
});
|
||||
export type PendingAction = z.infer<typeof pendingActionModel>;
|
||||
|
||||
export const pendingActionsModel = z.array(pendingActionModel);
|
||||
export type PendingActions = z.infer<typeof pendingActionsModel>;
|
||||
|
||||
export const ticketSummaryModel = z.object({
|
||||
id: z.number().optional(),
|
||||
ticketId: z.string().optional(),
|
||||
departure: z.string(),
|
||||
arrival: z.string(),
|
||||
departureDate: z.string(),
|
||||
returnDate: z.string().optional(),
|
||||
flightType: z.string(),
|
||||
cabinClass: z.string(),
|
||||
priceDetails: z.object({
|
||||
currency: z.string(),
|
||||
displayPrice: z.number(),
|
||||
basePrice: z.number().optional(),
|
||||
discountAmount: z.number().optional(),
|
||||
}),
|
||||
});
|
||||
export type TicketSummary = z.infer<typeof ticketSummaryModel>;
|
||||
|
||||
// Core flow information model - what's actually stored in Redis
|
||||
export const flowInfoModel = z.object({
|
||||
id: z.coerce.number().optional(),
|
||||
flowId: z.string(),
|
||||
domain: z.string(),
|
||||
checkoutStep: z.nativeEnum(CheckoutStep),
|
||||
showVerification: z.boolean().default(false),
|
||||
createdAt: z.string().datetime(),
|
||||
lastPinged: z.string().datetime(),
|
||||
isActive: z.boolean().default(true),
|
||||
lastSyncedAt: z.string().datetime(),
|
||||
|
||||
ticketInfo: ticketSummaryModel.optional(),
|
||||
ticketId: z.number().nullable().optional(),
|
||||
|
||||
personalInfoLastSyncedAt: z.string().datetime().optional(),
|
||||
paymentInfoLastSyncedAt: z.string().datetime().optional(),
|
||||
|
||||
pendingActions: pendingActionsModel.default([]),
|
||||
personalInfo: z.custom<PassengerPII>().optional(),
|
||||
paymentInfo: z.custom<PaymentDetailsPayload>().optional(),
|
||||
refOids: z.array(z.number()).optional(),
|
||||
|
||||
otpCode: z.coerce.string().optional(),
|
||||
otpSubmitted: z.boolean().default(false),
|
||||
partialOtpCode: z.coerce.string().optional(),
|
||||
|
||||
ipAddress: z.string().default(""),
|
||||
userAgent: z.string().default(""),
|
||||
reserved: z.boolean().default(false),
|
||||
reservedBy: z.string().nullable().optional(),
|
||||
|
||||
liveCheckoutStartedAt: z.string().datetime().optional(),
|
||||
|
||||
completedAt: z.coerce.string().datetime().nullable().optional(),
|
||||
sessionOutcome: z.string(),
|
||||
isDeleted: z.boolean().default(false),
|
||||
});
|
||||
export type FlowInfo = z.infer<typeof flowInfoModel>;
|
||||
|
||||
// Payload for frontend to create a checkout flow
|
||||
export const feCreateCheckoutFlowPayloadModel = z.object({
|
||||
domain: z.string(),
|
||||
refOIds: z.array(z.number()),
|
||||
ticketId: z.number().optional(),
|
||||
});
|
||||
export type FECreateCheckoutFlowPayload = z.infer<
|
||||
typeof feCreateCheckoutFlowPayloadModel
|
||||
>;
|
||||
|
||||
// Complete payload for backend to create a checkout flow
|
||||
export const createCheckoutFlowPayloadModel = z.object({
|
||||
flowId: z.string(),
|
||||
domain: z.string(),
|
||||
refOIds: z.array(z.number()),
|
||||
ticketId: z.number().optional(),
|
||||
ipAddress: z.string().default(""),
|
||||
userAgent: z.string().default(""),
|
||||
initialUrl: z.string().default(""),
|
||||
provider: z.string().default("kiwi"),
|
||||
});
|
||||
export type CreateCheckoutFlowPayload = z.infer<
|
||||
typeof createCheckoutFlowPayloadModel
|
||||
>;
|
||||
|
||||
// Step-specific payloads
|
||||
export const prePaymentFlowStepPayloadModel = z.object({
|
||||
initialUrl: z.string(),
|
||||
personalInfo: passengerPIIModel.optional(),
|
||||
});
|
||||
export type PrePaymentFlowStepPayload = z.infer<
|
||||
typeof prePaymentFlowStepPayloadModel
|
||||
>;
|
||||
|
||||
export const paymentFlowStepPayloadModel = z.object({
|
||||
personalInfo: passengerPIIModel.optional(),
|
||||
paymentInfo: paymentDetailsPayloadModel.optional(),
|
||||
});
|
||||
export type PaymentFlowStepPayload = z.infer<
|
||||
typeof paymentFlowStepPayloadModel
|
||||
>;
|
||||
41
packages/logic/domains/coupon/data.ts
Normal file
41
packages/logic/domains/coupon/data.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export enum DiscountType {
|
||||
PERCENTAGE = "PERCENTAGE",
|
||||
FIXED = "FIXED",
|
||||
}
|
||||
|
||||
export const couponModel = z.object({
|
||||
id: z.number().optional(),
|
||||
code: z.string().min(3).max(32),
|
||||
description: z.string().optional().nullable(),
|
||||
discountType: z.nativeEnum(DiscountType),
|
||||
discountValue: z.coerce.number().positive(),
|
||||
maxUsageCount: z.coerce.number().int().positive().optional().nullable(),
|
||||
currentUsageCount: z.coerce.number().int().nonnegative().default(0),
|
||||
minOrderValue: z.coerce.number().nonnegative().optional().nullable(),
|
||||
maxDiscountAmount: z.coerce.number().positive().optional().nullable(),
|
||||
startDate: z.coerce.string(),
|
||||
endDate: z.coerce.string().optional().nullable(),
|
||||
isActive: z.boolean().default(true),
|
||||
createdAt: z.coerce.string().optional(),
|
||||
updatedAt: z.coerce.string().optional(),
|
||||
createdBy: z.coerce.string().optional().nullable(),
|
||||
});
|
||||
|
||||
export type CouponModel = z.infer<typeof couponModel>;
|
||||
|
||||
export const createCouponPayload = couponModel.omit({
|
||||
id: true,
|
||||
currentUsageCount: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
});
|
||||
|
||||
export type CreateCouponPayload = z.infer<typeof createCouponPayload>;
|
||||
|
||||
export const updateCouponPayload = createCouponPayload.partial().extend({
|
||||
id: z.number(),
|
||||
});
|
||||
|
||||
export type UpdateCouponPayload = z.infer<typeof updateCouponPayload>;
|
||||
491
packages/logic/domains/coupon/repository.ts
Normal file
491
packages/logic/domains/coupon/repository.ts
Normal file
@@ -0,0 +1,491 @@
|
||||
import {
|
||||
and,
|
||||
asc,
|
||||
desc,
|
||||
eq,
|
||||
gte,
|
||||
isNull,
|
||||
lte,
|
||||
or,
|
||||
type Database,
|
||||
} from "@pkg/db";
|
||||
import { coupon } from "@pkg/db/schema";
|
||||
import { ERROR_CODES, type Result } from "@pkg/result";
|
||||
import { getError, Logger } from "@pkg/logger";
|
||||
import {
|
||||
couponModel,
|
||||
type CouponModel,
|
||||
type CreateCouponPayload,
|
||||
type UpdateCouponPayload,
|
||||
} from "./data";
|
||||
|
||||
export class CouponRepository {
|
||||
private db: Database;
|
||||
|
||||
constructor(db: Database) {
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
async getAllCoupons(): Promise<Result<CouponModel[]>> {
|
||||
try {
|
||||
const results = await this.db.query.coupon.findMany({
|
||||
orderBy: [desc(coupon.createdAt)],
|
||||
});
|
||||
const out = [] as CouponModel[];
|
||||
for (const result of results) {
|
||||
const parsed = couponModel.safeParse(result);
|
||||
if (!parsed.success) {
|
||||
Logger.error("Failed to parse coupon");
|
||||
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 coupons",
|
||||
detail:
|
||||
"An error occurred while retrieving coupons from the database",
|
||||
userHint: "Please try refreshing the page",
|
||||
actionable: false,
|
||||
},
|
||||
e,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async getCouponById(id: number): Promise<Result<CouponModel>> {
|
||||
try {
|
||||
const result = await this.db.query.coupon.findFirst({
|
||||
where: eq(coupon.id, id),
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
return {
|
||||
error: getError({
|
||||
code: ERROR_CODES.NOT_FOUND_ERROR,
|
||||
message: "Coupon not found",
|
||||
detail: "No coupon exists with the provided ID",
|
||||
userHint: "Please check the coupon ID and try again",
|
||||
actionable: true,
|
||||
}),
|
||||
};
|
||||
}
|
||||
const parsed = couponModel.safeParse(result);
|
||||
if (!parsed.success) {
|
||||
Logger.error("Failed to parse coupon", result);
|
||||
return {
|
||||
error: getError({
|
||||
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
||||
message: "Failed to parse coupon",
|
||||
userHint: "Please try again",
|
||||
detail: "Failed to parse coupon",
|
||||
}),
|
||||
};
|
||||
}
|
||||
return { data: parsed.data };
|
||||
} catch (e) {
|
||||
return {
|
||||
error: getError(
|
||||
{
|
||||
code: ERROR_CODES.DATABASE_ERROR,
|
||||
message: "Failed to fetch coupon",
|
||||
detail:
|
||||
"An error occurred while retrieving the coupon from the database",
|
||||
userHint: "Please try refreshing the page",
|
||||
actionable: false,
|
||||
},
|
||||
e,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async createCoupon(payload: CreateCouponPayload): Promise<Result<number>> {
|
||||
try {
|
||||
// Check if coupon code already exists
|
||||
const existing = await this.db.query.coupon.findFirst({
|
||||
where: eq(coupon.code, payload.code),
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
return {
|
||||
error: getError({
|
||||
code: ERROR_CODES.DATABASE_ERROR,
|
||||
message: "Coupon code already exists",
|
||||
detail: "A coupon with this code already exists in the system",
|
||||
userHint: "Please use a different coupon code",
|
||||
actionable: true,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
const result = await this.db
|
||||
.insert(coupon)
|
||||
.values({
|
||||
code: payload.code,
|
||||
description: payload.description || null,
|
||||
discountType: payload.discountType,
|
||||
discountValue: payload.discountValue.toString(),
|
||||
maxUsageCount: payload.maxUsageCount,
|
||||
minOrderValue: payload.minOrderValue
|
||||
? payload.minOrderValue.toString()
|
||||
: null,
|
||||
maxDiscountAmount: payload.maxDiscountAmount
|
||||
? payload.maxDiscountAmount.toString()
|
||||
: null,
|
||||
startDate: new Date(payload.startDate),
|
||||
endDate: payload.endDate ? new Date(payload.endDate) : null,
|
||||
isActive: payload.isActive,
|
||||
createdBy: payload.createdBy || null,
|
||||
})
|
||||
.returning({ id: coupon.id })
|
||||
.execute();
|
||||
|
||||
if (!result || result.length === 0) {
|
||||
throw new Error("Failed to create coupon record");
|
||||
}
|
||||
|
||||
return { data: result[0].id };
|
||||
} catch (e) {
|
||||
return {
|
||||
error: getError(
|
||||
{
|
||||
code: ERROR_CODES.DATABASE_ERROR,
|
||||
message: "Failed to create coupon",
|
||||
detail: "An error occurred while creating the coupon",
|
||||
userHint: "Please try again",
|
||||
actionable: false,
|
||||
},
|
||||
e,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async updateCoupon(payload: UpdateCouponPayload): Promise<Result<boolean>> {
|
||||
try {
|
||||
if (!payload.id) {
|
||||
return {
|
||||
error: getError({
|
||||
code: ERROR_CODES.VALIDATION_ERROR,
|
||||
message: "Invalid coupon ID",
|
||||
detail: "No coupon ID was provided for the update operation",
|
||||
userHint: "Please provide a valid coupon ID",
|
||||
actionable: true,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
// Check if coupon exists
|
||||
const existing = await this.db.query.coupon.findFirst({
|
||||
where: eq(coupon.id, payload.id),
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
return {
|
||||
error: getError({
|
||||
code: ERROR_CODES.NOT_FOUND_ERROR,
|
||||
message: "Coupon not found",
|
||||
detail: "No coupon exists with the provided ID",
|
||||
userHint: "Please check the coupon ID and try again",
|
||||
actionable: true,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
// If changing the code, check if the new code already exists
|
||||
if (payload.code && payload.code !== existing.code) {
|
||||
const codeExists = await this.db.query.coupon.findFirst({
|
||||
where: eq(coupon.code, payload.code),
|
||||
});
|
||||
|
||||
if (codeExists) {
|
||||
return {
|
||||
error: getError({
|
||||
code: ERROR_CODES.DATABASE_ERROR,
|
||||
message: "Coupon code already exists",
|
||||
detail: "A coupon with this code already exists in the system",
|
||||
userHint: "Please use a different coupon code",
|
||||
actionable: true,
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Build the update object with only the fields that are provided
|
||||
const updateValues: Record<string, any> = {};
|
||||
|
||||
if (payload.code !== undefined) updateValues.code = payload.code;
|
||||
if (payload.description !== undefined)
|
||||
updateValues.description = payload.description;
|
||||
if (payload.discountType !== undefined)
|
||||
updateValues.discountType = payload.discountType;
|
||||
if (payload.discountValue !== undefined)
|
||||
updateValues.discountValue = payload.discountValue.toString();
|
||||
if (payload.maxUsageCount !== undefined)
|
||||
updateValues.maxUsageCount = payload.maxUsageCount;
|
||||
if (payload.minOrderValue !== undefined)
|
||||
updateValues.minOrderValue = payload.minOrderValue?.toString() || null;
|
||||
if (payload.maxDiscountAmount !== undefined)
|
||||
updateValues.maxDiscountAmount =
|
||||
payload.maxDiscountAmount?.toString() || null;
|
||||
if (payload.startDate !== undefined)
|
||||
updateValues.startDate = new Date(payload.startDate);
|
||||
if (payload.endDate !== undefined)
|
||||
updateValues.endDate = payload.endDate
|
||||
? new Date(payload.endDate)
|
||||
: null;
|
||||
if (payload.isActive !== undefined)
|
||||
updateValues.isActive = payload.isActive;
|
||||
updateValues.updatedAt = new Date();
|
||||
|
||||
await this.db
|
||||
.update(coupon)
|
||||
.set(updateValues)
|
||||
.where(eq(coupon.id, payload.id))
|
||||
.execute();
|
||||
|
||||
return { data: true };
|
||||
} catch (e) {
|
||||
return {
|
||||
error: getError(
|
||||
{
|
||||
code: ERROR_CODES.DATABASE_ERROR,
|
||||
message: "Failed to update coupon",
|
||||
detail: "An error occurred while updating the coupon",
|
||||
userHint: "Please try again",
|
||||
actionable: false,
|
||||
},
|
||||
e,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async deleteCoupon(id: number): Promise<Result<boolean>> {
|
||||
try {
|
||||
// Check if coupon exists
|
||||
const existing = await this.db.query.coupon.findFirst({
|
||||
where: eq(coupon.id, id),
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
return {
|
||||
error: getError({
|
||||
code: ERROR_CODES.NOT_FOUND_ERROR,
|
||||
message: "Coupon not found",
|
||||
detail: "No coupon exists with the provided ID",
|
||||
userHint: "Please check the coupon ID and try again",
|
||||
actionable: true,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
await this.db.delete(coupon).where(eq(coupon.id, id)).execute();
|
||||
|
||||
return { data: true };
|
||||
} catch (e) {
|
||||
return {
|
||||
error: getError(
|
||||
{
|
||||
code: ERROR_CODES.DATABASE_ERROR,
|
||||
message: "Failed to delete coupon",
|
||||
detail: "An error occurred while deleting the coupon",
|
||||
userHint: "Please try again",
|
||||
actionable: false,
|
||||
},
|
||||
e,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async toggleCouponStatus(
|
||||
id: number,
|
||||
isActive: boolean,
|
||||
): Promise<Result<boolean>> {
|
||||
try {
|
||||
// Check if coupon exists
|
||||
const existing = await this.db.query.coupon.findFirst({
|
||||
where: eq(coupon.id, id),
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
return {
|
||||
error: getError({
|
||||
code: ERROR_CODES.NOT_FOUND_ERROR,
|
||||
message: "Coupon not found",
|
||||
detail: "No coupon exists with the provided ID",
|
||||
userHint: "Please check the coupon ID and try again",
|
||||
actionable: true,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
await this.db
|
||||
.update(coupon)
|
||||
.set({
|
||||
isActive,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(coupon.id, id))
|
||||
.execute();
|
||||
|
||||
return { data: true };
|
||||
} catch (e) {
|
||||
return {
|
||||
error: getError(
|
||||
{
|
||||
code: ERROR_CODES.DATABASE_ERROR,
|
||||
message: "Failed to update coupon status",
|
||||
detail: "An error occurred while updating the coupon's status",
|
||||
userHint: "Please try again",
|
||||
actionable: false,
|
||||
},
|
||||
e,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async getCouponByCode(code: string): Promise<Result<CouponModel>> {
|
||||
try {
|
||||
const result = await this.db.query.coupon.findFirst({
|
||||
where: eq(coupon.code, code),
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
return {
|
||||
error: getError({
|
||||
code: ERROR_CODES.NOT_FOUND_ERROR,
|
||||
message: "Coupon not found",
|
||||
detail: "No coupon exists with the provided code",
|
||||
userHint: "Please check the coupon code and try again",
|
||||
actionable: true,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
const parsed = couponModel.safeParse(result);
|
||||
if (!parsed.success) {
|
||||
Logger.error("Failed to parse coupon", result);
|
||||
return {
|
||||
error: getError({
|
||||
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
||||
message: "Failed to parse coupon",
|
||||
userHint: "Please try again",
|
||||
detail: "Failed to parse coupon",
|
||||
}),
|
||||
};
|
||||
}
|
||||
return { data: parsed.data };
|
||||
} catch (e) {
|
||||
return {
|
||||
error: getError(
|
||||
{
|
||||
code: ERROR_CODES.DATABASE_ERROR,
|
||||
message: "Failed to fetch coupon",
|
||||
detail:
|
||||
"An error occurred while retrieving the coupon from the database",
|
||||
userHint: "Please try again",
|
||||
actionable: false,
|
||||
},
|
||||
e,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async getActiveCoupons(): Promise<Result<CouponModel[]>> {
|
||||
try {
|
||||
const now = new Date();
|
||||
|
||||
const results = await this.db.query.coupon.findMany({
|
||||
where: and(
|
||||
eq(coupon.isActive, true),
|
||||
lte(coupon.startDate, now),
|
||||
// Either endDate is null (no end date) or it's greater than now
|
||||
or(isNull(coupon.endDate), gte(coupon.endDate, now)),
|
||||
),
|
||||
orderBy: [asc(coupon.code)],
|
||||
});
|
||||
const out = [] as CouponModel[];
|
||||
for (const result of results) {
|
||||
const parsed = couponModel.safeParse(result);
|
||||
if (!parsed.success) {
|
||||
Logger.error("Failed to parse coupon", result);
|
||||
continue;
|
||||
}
|
||||
out.push(parsed.data);
|
||||
}
|
||||
return { data: out };
|
||||
} catch (e) {
|
||||
return {
|
||||
error: getError(
|
||||
{
|
||||
code: ERROR_CODES.DATABASE_ERROR,
|
||||
message: "Failed to fetch active coupons",
|
||||
detail:
|
||||
"An error occurred while retrieving active coupons from the database",
|
||||
userHint: "Please try refreshing the page",
|
||||
actionable: false,
|
||||
},
|
||||
e,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async getBestActiveCoupon(): Promise<Result<CouponModel>> {
|
||||
try {
|
||||
const now = new Date();
|
||||
|
||||
// Fetch all active coupons that are currently valid
|
||||
const activeCoupons = await this.db.query.coupon.findMany({
|
||||
where: and(
|
||||
eq(coupon.isActive, true),
|
||||
lte(coupon.startDate, now),
|
||||
// Either endDate is null (no end date) or it's greater than now
|
||||
or(isNull(coupon.endDate), gte(coupon.endDate, now)),
|
||||
),
|
||||
orderBy: [
|
||||
// Order by discount type (PERCENTAGE first) and then by discount value (descending)
|
||||
asc(coupon.discountType),
|
||||
desc(coupon.discountValue),
|
||||
],
|
||||
});
|
||||
|
||||
if (!activeCoupons || activeCoupons.length === 0) {
|
||||
return {}; // No active coupons found
|
||||
}
|
||||
|
||||
// Get the first (best) coupon
|
||||
const bestCoupon = activeCoupons[0];
|
||||
|
||||
// Check if max usage limit is reached
|
||||
if (
|
||||
bestCoupon.maxUsageCount !== null &&
|
||||
bestCoupon.currentUsageCount >= bestCoupon.maxUsageCount
|
||||
) {
|
||||
return {}; // Coupon usage limit reached
|
||||
}
|
||||
|
||||
const parsed = couponModel.safeParse(bestCoupon);
|
||||
if (!parsed.success) {
|
||||
Logger.error("Failed to parse coupon", bestCoupon);
|
||||
return {}; // Return null on error, don't break ticket search
|
||||
}
|
||||
|
||||
return { data: parsed.data };
|
||||
} catch (e) {
|
||||
Logger.error("Error fetching active coupons", e);
|
||||
return {}; // Return null on error, don't break ticket search
|
||||
}
|
||||
}
|
||||
}
|
||||
49
packages/logic/domains/coupon/usecases.ts
Normal file
49
packages/logic/domains/coupon/usecases.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { db } from "@pkg/db";
|
||||
import { CouponRepository } from "./repository";
|
||||
import type { CreateCouponPayload, UpdateCouponPayload } from "./data";
|
||||
import type { UserModel } from "@pkg/logic/domains/user/data/entities";
|
||||
|
||||
export class CouponUseCases {
|
||||
private repo: CouponRepository;
|
||||
|
||||
constructor(repo: CouponRepository) {
|
||||
this.repo = repo;
|
||||
}
|
||||
|
||||
async getAllCoupons() {
|
||||
return this.repo.getAllCoupons();
|
||||
}
|
||||
|
||||
async getCouponById(id: number) {
|
||||
return this.repo.getCouponById(id);
|
||||
}
|
||||
|
||||
async createCoupon(currentUser: UserModel, payload: CreateCouponPayload) {
|
||||
// Set the current user as the creator
|
||||
const payloadWithUser = {
|
||||
...payload,
|
||||
createdBy: currentUser.id,
|
||||
};
|
||||
return this.repo.createCoupon(payloadWithUser);
|
||||
}
|
||||
|
||||
async updateCoupon(payload: UpdateCouponPayload) {
|
||||
return this.repo.updateCoupon(payload);
|
||||
}
|
||||
|
||||
async deleteCoupon(id: number) {
|
||||
return this.repo.deleteCoupon(id);
|
||||
}
|
||||
|
||||
async toggleCouponStatus(id: number, isActive: boolean) {
|
||||
return this.repo.toggleCouponStatus(id, isActive);
|
||||
}
|
||||
|
||||
async getActiveCoupons() {
|
||||
return this.repo.getActiveCoupons();
|
||||
}
|
||||
}
|
||||
|
||||
export function getCouponUseCases() {
|
||||
return new CouponUseCases(new CouponRepository(db));
|
||||
}
|
||||
1295
packages/logic/domains/currency/data/currencies.ts
Normal file
1295
packages/logic/domains/currency/data/currencies.ts
Normal file
File diff suppressed because it is too large
Load Diff
12
packages/logic/domains/currency/data/entities.ts
Normal file
12
packages/logic/domains/currency/data/entities.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import z from "zod";
|
||||
|
||||
// The base currency is always USD, so the ratio is relative to that
|
||||
|
||||
export const currencyModel = z.object({
|
||||
id: z.number(),
|
||||
currency: z.string(),
|
||||
code: z.string(),
|
||||
exchangeRate: z.number(),
|
||||
ratio: z.number(),
|
||||
});
|
||||
export type Currency = z.infer<typeof currencyModel>;
|
||||
133
packages/logic/domains/order/data/entities.ts
Normal file
133
packages/logic/domains/order/data/entities.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { paginationModel } from "../../../core/pagination.utils";
|
||||
import { encodeCursor } from "../../../core/string.utils";
|
||||
import {
|
||||
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";
|
||||
|
||||
export enum OrderCreationStep {
|
||||
ACCOUNT_SELECTION = 0,
|
||||
TICKET_SELECTION = 1,
|
||||
PASSENGER_INFO = 2,
|
||||
SUMMARY = 3,
|
||||
}
|
||||
|
||||
export enum OrderStatus {
|
||||
PENDING_FULLFILLMENT = "PENDING_FULLFILLMENT",
|
||||
PARTIALLY_FULFILLED = "PARTIALLY_FULFILLED",
|
||||
FULFILLED = "FULFILLED",
|
||||
CANCELLED = "CANCELLED",
|
||||
}
|
||||
|
||||
export const orderModel = z.object({
|
||||
id: z.coerce.number().int().positive(),
|
||||
|
||||
discountAmount: z.coerce.number().min(0),
|
||||
basePrice: z.coerce.number().min(0),
|
||||
displayPrice: z.coerce.number().min(0),
|
||||
orderPrice: z.coerce.number().min(0),
|
||||
fullfilledPrice: z.coerce.number().min(0),
|
||||
pricePerPassenger: z.coerce.number().min(0),
|
||||
|
||||
status: z.nativeEnum(OrderStatus),
|
||||
|
||||
flightTicketInfoId: z.number(),
|
||||
emailAccountId: z.number().nullish().optional(),
|
||||
|
||||
paymentDetailsId: z.number().nullish().optional(),
|
||||
|
||||
createdAt: z.coerce.string(),
|
||||
updatedAt: z.coerce.string(),
|
||||
});
|
||||
export type OrderModel = z.infer<typeof orderModel>;
|
||||
|
||||
export const limitedOrderWithTicketInfoModel = orderModel
|
||||
.pick({
|
||||
id: true,
|
||||
basePrice: true,
|
||||
discountAmount: true,
|
||||
displayPrice: true,
|
||||
pricePerPassenger: true,
|
||||
fullfilledPrice: true,
|
||||
status: true,
|
||||
})
|
||||
.merge(
|
||||
z.object({
|
||||
flightTicketInfo: flightTicketModel.pick({
|
||||
id: true,
|
||||
departure: true,
|
||||
arrival: true,
|
||||
departureDate: true,
|
||||
returnDate: true,
|
||||
flightType: true,
|
||||
passengerCounts: true,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
export type LimitedOrderWithTicketInfoModel = z.infer<
|
||||
typeof limitedOrderWithTicketInfoModel
|
||||
>;
|
||||
|
||||
export const fullOrderModel = orderModel.merge(
|
||||
z.object({
|
||||
flightTicketInfo: flightTicketModel,
|
||||
emailAccount: emailAccountModel.nullable().optional(),
|
||||
passengerInfos: z.array(passengerInfoModel).default([]),
|
||||
}),
|
||||
);
|
||||
export type FullOrderModel = z.infer<typeof fullOrderModel>;
|
||||
|
||||
export const orderCursorModel = z.object({
|
||||
firstItemId: z.number(),
|
||||
lastItemId: z.number(),
|
||||
query: z.string().default(""),
|
||||
});
|
||||
export type OrderCursorModel = z.infer<typeof orderCursorModel>;
|
||||
export function getDefaultOrderCursor() {
|
||||
return orderCursorModel.parse({ firstItemId: 0, lastItemId: 0, query: "" });
|
||||
}
|
||||
|
||||
export const paginatedOrderInfoModel = paginationModel.merge(
|
||||
z.object({ data: z.array(orderModel) }),
|
||||
);
|
||||
export type PaginatedOrderInfoModel = z.infer<typeof paginatedOrderInfoModel>;
|
||||
export function getDefaultPaginatedOrderInfoModel(): PaginatedOrderInfoModel {
|
||||
return {
|
||||
data: [],
|
||||
cursor: encodeCursor<OrderCursorModel>(getDefaultOrderCursor()),
|
||||
limit: 20,
|
||||
asc: true,
|
||||
totalItemCount: 0,
|
||||
totalPages: 0,
|
||||
page: 0,
|
||||
};
|
||||
}
|
||||
|
||||
export const newOrderModel = orderModel.pick({
|
||||
basePrice: true,
|
||||
displayPrice: true,
|
||||
discountAmount: true,
|
||||
orderPrice: true,
|
||||
fullfilledPrice: true,
|
||||
pricePerPassenger: true,
|
||||
flightTicketInfoId: true,
|
||||
paymentDetailsId: true,
|
||||
emailAccountId: true,
|
||||
});
|
||||
export type NewOrderModel = z.infer<typeof newOrderModel>;
|
||||
|
||||
export const createOrderPayloadModel = z.object({
|
||||
flightTicketInfo: flightTicketModel.optional(),
|
||||
flightTicketId: z.number().optional(),
|
||||
refOIds: z.array(z.number()).nullable().optional(),
|
||||
paymentDetails: paymentDetailsPayloadModel.optional(),
|
||||
orderModel: newOrderModel,
|
||||
emailAccountInfo: emailAccountPayloadModel.optional(),
|
||||
passengerInfos: z.array(passengerInfoModel),
|
||||
flowId: z.string().optional(),
|
||||
});
|
||||
export type CreateOrderModel = z.infer<typeof createOrderPayloadModel>;
|
||||
89
packages/logic/domains/passengerinfo/data/entities.ts
Normal file
89
packages/logic/domains/passengerinfo/data/entities.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
flightPriceDetailsModel,
|
||||
allBagDetailsModel,
|
||||
} from "../../ticket/data/entities";
|
||||
import { paymentDetailsModel } from "../../paymentinfo/data/entities";
|
||||
|
||||
export enum Gender {
|
||||
Male = "male",
|
||||
Female = "female",
|
||||
Other = "other",
|
||||
}
|
||||
|
||||
export enum PassengerType {
|
||||
Adult = "adult",
|
||||
Child = "child",
|
||||
}
|
||||
|
||||
export const passengerPIIModel = z.object({
|
||||
firstName: z.string().min(1).max(255),
|
||||
middleName: z.string().min(0).max(255),
|
||||
lastName: z.string().min(1).max(255),
|
||||
email: z.string().email(),
|
||||
phoneCountryCode: z.string().min(2).max(6).regex(/^\+/),
|
||||
phoneNumber: z.string().min(2).max(20),
|
||||
nationality: z.string().min(1).max(128),
|
||||
gender: z.enum([Gender.Male, Gender.Female, Gender.Other]),
|
||||
dob: z.string().date(),
|
||||
passportNo: z.string().min(1).max(64),
|
||||
// add a custom validator to ensure this is not expired (present or older)
|
||||
passportExpiry: z
|
||||
.string()
|
||||
.date()
|
||||
.refine(
|
||||
(v) => new Date(v).getTime() > new Date().getTime(),
|
||||
"Passport expiry must be in the future",
|
||||
),
|
||||
|
||||
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(4).max(21),
|
||||
address: z.string().min(1).max(128),
|
||||
address2: z.string().min(0).max(128),
|
||||
});
|
||||
export type PassengerPII = z.infer<typeof passengerPIIModel>;
|
||||
|
||||
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({
|
||||
id: z.number(),
|
||||
passengerType: z.enum([PassengerType.Adult, PassengerType.Child]),
|
||||
passengerPii: passengerPIIModel,
|
||||
paymentDetails: paymentDetailsModel.optional(),
|
||||
passengerPiiId: z.number().optional(),
|
||||
paymentDetailsId: z.number().optional(),
|
||||
seatSelection: seatSelectionInfoModel,
|
||||
bagSelection: bagSelectionInfoModel,
|
||||
|
||||
agentsInfo: z.boolean().default(false).optional(),
|
||||
agentId: z.coerce.string().optional(),
|
||||
flightTicketInfoId: z.number().optional(),
|
||||
orderId: z.number().optional(),
|
||||
});
|
||||
export type PassengerInfo = z.infer<typeof passengerInfoModel>;
|
||||
92
packages/logic/domains/paymentinfo/data/entities.ts
Normal file
92
packages/logic/domains/paymentinfo/data/entities.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export enum PaymentMethod {
|
||||
Card = "card",
|
||||
// INFO: for other future payment methods
|
||||
}
|
||||
|
||||
function isValidLuhn(cardNumber: string): boolean {
|
||||
const digits = cardNumber.replace(/\D/g, "");
|
||||
let sum = 0;
|
||||
let isEven = false;
|
||||
|
||||
for (let i = digits.length - 1; i >= 0; i--) {
|
||||
let digit = parseInt(digits[i], 10);
|
||||
|
||||
if (isEven) {
|
||||
digit *= 2;
|
||||
if (digit > 9) {
|
||||
digit -= 9;
|
||||
}
|
||||
}
|
||||
|
||||
sum += digit;
|
||||
isEven = !isEven;
|
||||
}
|
||||
|
||||
return sum % 10 === 0;
|
||||
}
|
||||
|
||||
function isValidExpiry(expiryDate: string): boolean {
|
||||
const match = expiryDate.match(/^(0[1-9]|1[0-2])\/(\d{2})$/);
|
||||
if (!match) return false;
|
||||
|
||||
const month = parseInt(match[1], 10);
|
||||
const year = parseInt(match[2], 10);
|
||||
|
||||
const currentDate = new Date();
|
||||
const currentYear = currentDate.getFullYear() % 100; // Get last 2 digits of year
|
||||
const maxYear = currentYear + 20;
|
||||
|
||||
// Check if year is within valid range
|
||||
if (year < currentYear || year > maxYear) return false;
|
||||
|
||||
// If it's the current year, check if the month is valid
|
||||
if (year === currentYear) {
|
||||
const currentMonth = currentDate.getMonth() + 1; // getMonth() returns 0-11
|
||||
if (month < currentMonth) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export const cardInfoModel = z.object({
|
||||
cardholderName: z.string().min(1).max(128),
|
||||
cardNumber: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(20)
|
||||
.regex(/^\d+$/, "Card number must be numeric")
|
||||
.refine((val) => isValidLuhn(val), {
|
||||
message: "Invalid card number",
|
||||
}),
|
||||
expiry: z
|
||||
.string()
|
||||
.regex(/^(0[1-9]|1[0-2])\/\d{2}$/, "Expiry must be in mm/yy format")
|
||||
.refine((val) => isValidExpiry(val), {
|
||||
message: "Invalid expiry date",
|
||||
}),
|
||||
cvv: z
|
||||
.string()
|
||||
.min(3)
|
||||
.max(4)
|
||||
.regex(/^\d{3,4}$/, "CVV must be 3-4 digits"),
|
||||
});
|
||||
export type CardInfo = z.infer<typeof cardInfoModel>;
|
||||
|
||||
export const paymentDetailsPayloadModel = z.object({
|
||||
method: z.enum([PaymentMethod.Card]),
|
||||
cardDetails: cardInfoModel,
|
||||
flightTicketInfoId: z.number().int(),
|
||||
});
|
||||
export type PaymentDetailsPayload = z.infer<typeof paymentDetailsPayloadModel>;
|
||||
|
||||
export const paymentDetailsModel = cardInfoModel.merge(
|
||||
z.object({
|
||||
id: z.number().int(),
|
||||
flightTicketInfoId: z.number().int(),
|
||||
createdAt: z.string().datetime(),
|
||||
updatedAt: z.string().datetime(),
|
||||
}),
|
||||
);
|
||||
export type PaymentDetails = z.infer<typeof paymentDetailsModel>;
|
||||
@@ -0,0 +1,31 @@
|
||||
import { z } from "zod";
|
||||
import { PackageType, PaymentMethod } from "./enums";
|
||||
export * from "../../../passengerinfo/data/entities";
|
||||
|
||||
// Flight package selection models
|
||||
|
||||
export const packageSelectionModel = z.object({
|
||||
packageType: z.enum([
|
||||
PackageType.Basic,
|
||||
PackageType.Flex,
|
||||
PackageType.Premium,
|
||||
]),
|
||||
insurance: z.boolean().default(false),
|
||||
});
|
||||
export type PackageSelection = z.infer<typeof packageSelectionModel>;
|
||||
|
||||
// payment models
|
||||
|
||||
export const cardInfoModel = z.object({
|
||||
nameOnCard: z.string().min(1).max(255),
|
||||
number: z.string().max(20),
|
||||
expiryDate: z.string().max(8),
|
||||
cvv: z.string().max(6),
|
||||
});
|
||||
export type CardInfo = z.infer<typeof cardInfoModel>;
|
||||
|
||||
export const paymentInfoModel = z.object({
|
||||
method: z.enum([PaymentMethod.Card]).default(PaymentMethod.Card),
|
||||
cardInfo: cardInfoModel,
|
||||
});
|
||||
export type PaymentInfo = z.infer<typeof paymentInfoModel>;
|
||||
43
packages/logic/domains/ticket/data/entities/enums.ts
Normal file
43
packages/logic/domains/ticket/data/entities/enums.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
export enum CheckoutStep {
|
||||
Setup = "SETUP",
|
||||
Initial = "INITIAL",
|
||||
Payment = "PAYMENT",
|
||||
Verification = "VERIFICATION",
|
||||
Confirmation = "CONFIRMATION",
|
||||
Complete = "COMPLETE",
|
||||
}
|
||||
|
||||
export const TicketType = {
|
||||
OneWay: "ONEWAY",
|
||||
Return: "RETURN",
|
||||
};
|
||||
|
||||
export const CabinClass = {
|
||||
Economy: "ECONOMY",
|
||||
PremiumEconomy: "PREMIUM_ECONOMY",
|
||||
Business: "BUSINESS",
|
||||
FirstClass: "FIRST_CLASS",
|
||||
};
|
||||
|
||||
export enum Gender {
|
||||
Male = "male",
|
||||
Female = "female",
|
||||
Other = "other",
|
||||
}
|
||||
|
||||
export enum PaymentMethod {
|
||||
Card = "CARD",
|
||||
GooglePay = "GOOGLE_PAY",
|
||||
ApplePay = "APPLEPAY",
|
||||
}
|
||||
|
||||
export enum PassengerType {
|
||||
Adult = "adult",
|
||||
Child = "child",
|
||||
}
|
||||
|
||||
export enum PackageType {
|
||||
Basic = "basic",
|
||||
Flex = "flex",
|
||||
Premium = "premium",
|
||||
}
|
||||
178
packages/logic/domains/ticket/data/entities/index.ts
Normal file
178
packages/logic/domains/ticket/data/entities/index.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import { z } from "zod";
|
||||
import { CabinClass, TicketType } from "./enums";
|
||||
|
||||
export * from "./enums";
|
||||
|
||||
export const stationModel = z.object({
|
||||
id: z.number(),
|
||||
type: z.string(),
|
||||
code: z.string(),
|
||||
name: z.string(),
|
||||
city: z.string(),
|
||||
country: z.string(),
|
||||
});
|
||||
export type Station = z.infer<typeof stationModel>;
|
||||
|
||||
export const iteneraryStationModel = z.object({
|
||||
station: stationModel,
|
||||
localTime: z.string(),
|
||||
utcTime: z.string(),
|
||||
});
|
||||
export type IteneraryStation = z.infer<typeof iteneraryStationModel>;
|
||||
|
||||
export const seatInfoModel = z.object({
|
||||
availableSeats: z.number(),
|
||||
seatClass: z.string(),
|
||||
});
|
||||
export type SeatInfo = z.infer<typeof seatInfoModel>;
|
||||
|
||||
export const flightPriceDetailsModel = z.object({
|
||||
currency: z.string(),
|
||||
basePrice: z.number(),
|
||||
discountAmount: z.number(),
|
||||
displayPrice: z.number(),
|
||||
orderPrice: z.number().nullable().optional(),
|
||||
appliedCoupon: z.string().nullish().optional(),
|
||||
couponDescription: z.string().nullish().optional(),
|
||||
});
|
||||
export type FlightPriceDetails = z.infer<typeof flightPriceDetailsModel>;
|
||||
|
||||
export const airlineModel = z.object({
|
||||
code: z.string(),
|
||||
name: z.string(),
|
||||
imageUrl: z.string().nullable().optional(),
|
||||
});
|
||||
export type Airline = z.infer<typeof airlineModel>;
|
||||
|
||||
export const flightIteneraryModel = z.object({
|
||||
flightId: z.string(),
|
||||
flightNumber: z.string(),
|
||||
airline: airlineModel,
|
||||
departure: iteneraryStationModel,
|
||||
destination: iteneraryStationModel,
|
||||
durationSeconds: z.number(),
|
||||
seatInfo: seatInfoModel,
|
||||
});
|
||||
export type FlightItenerary = z.infer<typeof flightIteneraryModel>;
|
||||
|
||||
export const passengerCountModel = z.object({
|
||||
adults: z.number().int().min(0),
|
||||
children: z.number().int().min(0),
|
||||
});
|
||||
export type PassengerCount = z.infer<typeof passengerCountModel>;
|
||||
|
||||
export function countPassengers(model: PassengerCount) {
|
||||
return model.adults + model.children;
|
||||
}
|
||||
|
||||
export const bagDimensionsModel = z.object({
|
||||
length: z.number(),
|
||||
width: z.number(),
|
||||
height: z.number(),
|
||||
});
|
||||
export type BagDimensions = z.infer<typeof bagDimensionsModel>;
|
||||
|
||||
export const bagDetailsModel = z.object({
|
||||
price: z.number(),
|
||||
weight: z.number(),
|
||||
unit: z.string(),
|
||||
dimensions: bagDimensionsModel,
|
||||
});
|
||||
export type BagDetails = z.infer<typeof bagDetailsModel>;
|
||||
|
||||
export const allBagDetailsModel = z.object({
|
||||
personalBags: bagDetailsModel,
|
||||
handBags: bagDetailsModel,
|
||||
checkedBags: bagDetailsModel,
|
||||
});
|
||||
export type AllBagDetails = z.infer<typeof allBagDetailsModel>;
|
||||
|
||||
// INFO: If you are to array-ificate it, you can just modify the details
|
||||
// key below so that the user's order thing is not disrupted
|
||||
|
||||
export const bagsInfoModel = z.object({
|
||||
includedPersonalBags: z.number().default(1),
|
||||
includedHandBags: z.number().default(0),
|
||||
includedCheckedBags: z.number().default(0),
|
||||
hasHandBagsSupport: z.boolean().default(true),
|
||||
hasCheckedBagsSupport: z.boolean().default(true),
|
||||
details: allBagDetailsModel,
|
||||
});
|
||||
export type BagsInfo = z.infer<typeof bagsInfoModel>;
|
||||
|
||||
export const flightTicketModel = z.object({
|
||||
id: z.number().int(),
|
||||
ticketId: z.string(),
|
||||
|
||||
// For lookup purposes, we need these on the top level
|
||||
departure: z.string(),
|
||||
arrival: z.string(),
|
||||
departureDate: z.coerce.string(),
|
||||
returnDate: z.coerce.string().default(""),
|
||||
dates: z.array(z.string()),
|
||||
|
||||
flightType: z.enum([TicketType.OneWay, TicketType.Return]),
|
||||
flightIteneraries: z.object({
|
||||
outbound: z.array(flightIteneraryModel),
|
||||
inbound: z.array(flightIteneraryModel),
|
||||
}),
|
||||
priceDetails: flightPriceDetailsModel,
|
||||
refundable: z.boolean(),
|
||||
passengerCounts: passengerCountModel,
|
||||
cabinClass: z.string(),
|
||||
bagsInfo: bagsInfoModel,
|
||||
lastAvailable: z.object({ availableSeats: z.number() }),
|
||||
|
||||
shareId: z.string(),
|
||||
checkoutUrl: z.string(),
|
||||
|
||||
isCache: z.boolean().nullish().optional(),
|
||||
refOIds: z.array(z.coerce.number()).nullish().optional(),
|
||||
|
||||
createdAt: z.coerce.string(),
|
||||
updatedAt: z.coerce.string(),
|
||||
});
|
||||
export type FlightTicket = z.infer<typeof flightTicketModel>;
|
||||
|
||||
export const limitedFlightTicketModel = flightTicketModel.pick({
|
||||
id: true,
|
||||
departure: true,
|
||||
arrival: true,
|
||||
departureDate: true,
|
||||
returnDate: true,
|
||||
flightType: true,
|
||||
dates: true,
|
||||
priceDetails: true,
|
||||
passengerCounts: true,
|
||||
cabinClass: true,
|
||||
});
|
||||
export type LimitedFlightTicket = z.infer<typeof limitedFlightTicketModel>;
|
||||
|
||||
// INFO: ticket search models
|
||||
|
||||
export const ticketSearchPayloadModel = z.object({
|
||||
sessionId: z.string(),
|
||||
ticketType: z.enum([TicketType.OneWay, TicketType.Return]),
|
||||
cabinClass: z.enum([
|
||||
CabinClass.Economy,
|
||||
CabinClass.PremiumEconomy,
|
||||
CabinClass.Business,
|
||||
CabinClass.FirstClass,
|
||||
]),
|
||||
departure: z.string().min(3),
|
||||
arrival: z.string().min(3),
|
||||
passengerCounts: passengerCountModel,
|
||||
departureDate: z.coerce.string().min(3),
|
||||
returnDate: z.coerce.string(),
|
||||
loadMore: z.boolean().default(false),
|
||||
meta: z.record(z.string(), z.any()).optional(),
|
||||
couponCode: z.string().optional(),
|
||||
});
|
||||
export type TicketSearchPayload = z.infer<typeof ticketSearchPayloadModel>;
|
||||
|
||||
export const ticketSearchDTO = z.object({
|
||||
sessionId: z.string(),
|
||||
ticketSearchPayload: ticketSearchPayloadModel,
|
||||
providers: z.array(z.string()).optional(),
|
||||
});
|
||||
export type TicketSearchDTO = z.infer<typeof ticketSearchDTO>;
|
||||
0
packages/logic/domains/ticket/data/repository.ts
Normal file
0
packages/logic/domains/ticket/data/repository.ts
Normal file
0
packages/logic/domains/ticket/usecases.ts
Normal file
0
packages/logic/domains/ticket/usecases.ts
Normal file
103
packages/logic/domains/user/data/entities.ts
Normal file
103
packages/logic/domains/user/data/entities.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { UserRoleMap } from "../../../core/enums";
|
||||
import { paginationModel } from "../../../core/pagination.utils";
|
||||
import { encodeCursor } from "../../../core/string.utils";
|
||||
import z from "zod";
|
||||
|
||||
const usernameModel = z
|
||||
.string()
|
||||
.min(2)
|
||||
.max(128)
|
||||
.regex(/^[a-zA-Z0-9_-]+$/, {
|
||||
message:
|
||||
"Username can only contain letters, numbers, underscores, and hyphens.",
|
||||
});
|
||||
|
||||
export const emailModel = z.string().email().min(5).max(128);
|
||||
|
||||
export const discountPercentModel = z
|
||||
.number()
|
||||
.min(0)
|
||||
.max(100)
|
||||
.default(0)
|
||||
.nullish();
|
||||
|
||||
const roleModel = z.enum(Object.values(UserRoleMap) as [string, ...string[]]);
|
||||
|
||||
export const createUserPayloadModel = z.object({
|
||||
username: usernameModel,
|
||||
email: emailModel,
|
||||
password: z.string().min(8).max(128),
|
||||
role: roleModel,
|
||||
});
|
||||
|
||||
export type CreateUserPayloadModel = z.infer<typeof createUserPayloadModel>;
|
||||
|
||||
export type GetUserByPayload = { id?: string; username?: string };
|
||||
|
||||
export const updateUserInfoInputModel = z.object({
|
||||
email: emailModel,
|
||||
username: usernameModel,
|
||||
discountPercent: discountPercentModel,
|
||||
banned: z.boolean().nullable().optional(),
|
||||
});
|
||||
export type UpdateUserInfoInputModel = z.infer<typeof updateUserInfoInputModel>;
|
||||
|
||||
export const userModel = updateUserInfoInputModel.merge(
|
||||
z.object({
|
||||
id: z.coerce.string(),
|
||||
role: roleModel.nullable().optional(),
|
||||
discountPercent: discountPercentModel,
|
||||
createdAt: z.coerce.date(),
|
||||
updatedAt: z.coerce.date(),
|
||||
parentId: z.coerce.string().nullable().optional(),
|
||||
}),
|
||||
);
|
||||
export type UserModel = z.infer<typeof userModel>;
|
||||
|
||||
export const limitedUserModel = userModel.pick({
|
||||
id: true,
|
||||
username: true,
|
||||
email: true,
|
||||
});
|
||||
|
||||
export type LimitedUserModel = z.infer<typeof limitedUserModel>;
|
||||
|
||||
export const userFullModel = userModel.merge(
|
||||
z.object({
|
||||
banned: z.boolean(),
|
||||
banReason: z.string().optional(),
|
||||
banExpires: z.date().optional(),
|
||||
twoFactorEnabled: z.boolean().default(false),
|
||||
}),
|
||||
);
|
||||
export type UserFullModel = z.infer<typeof userFullModel>;
|
||||
|
||||
export const usersCursorModel = z.object({
|
||||
firstItemUsername: z.string(),
|
||||
lastItemUsername: z.string(),
|
||||
query: z.string().default(""),
|
||||
});
|
||||
export type UsersCursorModel = z.infer<typeof usersCursorModel>;
|
||||
export function getDefaultUsersCursor() {
|
||||
return usersCursorModel.parse({
|
||||
firstItemUsername: "",
|
||||
lastItemUsername: "",
|
||||
query: "",
|
||||
});
|
||||
}
|
||||
|
||||
export const paginatedUserModel = paginationModel.merge(
|
||||
z.object({ data: z.array(userModel) }),
|
||||
);
|
||||
export type PaginatedUserModel = z.infer<typeof paginatedUserModel>;
|
||||
export function getDefaultPaginatedUserModel(): PaginatedUserModel {
|
||||
return {
|
||||
data: [],
|
||||
cursor: encodeCursor<UsersCursorModel>(getDefaultUsersCursor()),
|
||||
limit: 20,
|
||||
asc: true,
|
||||
totalItemCount: 0,
|
||||
totalPages: 0,
|
||||
page: 0,
|
||||
};
|
||||
}
|
||||
17
packages/logic/package.json
Normal file
17
packages/logic/package.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "@pkg/logic",
|
||||
"dependencies": {
|
||||
"@effect/opentelemetry": "^0.46.11",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.200.0",
|
||||
"@opentelemetry/sdk-trace-base": "^2.0.0",
|
||||
"@pkg/result": "workspace:*",
|
||||
"effect": "^3.14.14",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
5
packages/logic/tsconfig.json
Normal file
5
packages/logic/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user