stashing code
This commit is contained in:
175
packages/airportsdb/.gitignore
vendored
Normal file
175
packages/airportsdb/.gitignore
vendored
Normal file
@@ -0,0 +1,175 @@
|
||||
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
|
||||
|
||||
# Logs
|
||||
|
||||
logs
|
||||
_.log
|
||||
npm-debug.log_
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Caches
|
||||
|
||||
.cache
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# Runtime data
|
||||
|
||||
pids
|
||||
_.pid
|
||||
_.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
|
||||
.temp
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
14
packages/airportsdb/drizzle.config.ts
Normal file
14
packages/airportsdb/drizzle.config.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { defineConfig } from "drizzle-kit";
|
||||
|
||||
import "dotenv/config";
|
||||
|
||||
export default defineConfig({
|
||||
schema: "./schema",
|
||||
out: "./migrations",
|
||||
dialect: "postgresql",
|
||||
verbose: true,
|
||||
strict: false,
|
||||
dbCredentials: {
|
||||
url: process.env.AIRPORTS_DB_URL ?? "",
|
||||
},
|
||||
});
|
||||
17
packages/airportsdb/index.ts
Normal file
17
packages/airportsdb/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import "dotenv/config";
|
||||
|
||||
import { drizzle } from "drizzle-orm/postgres-js";
|
||||
import postgres from "postgres";
|
||||
import * as schema from "./schema";
|
||||
|
||||
const dbUrl = process.env.AIRPORTS_DB_URL ?? "";
|
||||
|
||||
const client = postgres(dbUrl);
|
||||
|
||||
const db = drizzle(client, { schema });
|
||||
|
||||
export type AirportsDatabase = typeof db;
|
||||
|
||||
export * from "drizzle-orm";
|
||||
|
||||
export { db as airportsDb, schema };
|
||||
@@ -0,0 +1,30 @@
|
||||
CREATE TABLE IF NOT EXISTS "airport" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"type" varchar(64) NOT NULL,
|
||||
"name" varchar(128) NOT NULL,
|
||||
"gps_code" varchar(64) NOT NULL,
|
||||
"ident" varchar(128),
|
||||
"latitude_deg" numeric(10, 2),
|
||||
"longitude_deg" numeric(10, 2),
|
||||
"elevation_ft" numeric(10, 2),
|
||||
"continent" varchar(64),
|
||||
"country" varchar(172),
|
||||
"iso_country" varchar(4) NOT NULL,
|
||||
"iso_region" varchar(64) NOT NULL,
|
||||
"municipality" varchar(64),
|
||||
"scheduled_service" varchar(64),
|
||||
"iata_code" varchar(16) NOT NULL,
|
||||
"local_code" varchar(16) NOT NULL,
|
||||
"created_at" timestamp DEFAULT now(),
|
||||
"updated_at" timestamp DEFAULT now(),
|
||||
"search_vector" "tsvector" GENERATED ALWAYS AS (
|
||||
setweight(to_tsvector('english', coalesce("airport"."name", '')), 'A') ||
|
||||
setweight(to_tsvector('english', coalesce("airport"."iata_code", '')), 'A') ||
|
||||
setweight(to_tsvector('english', coalesce("airport"."municipality", '')), 'B') ||
|
||||
setweight(to_tsvector('english', coalesce("airport"."country", '')), 'C')
|
||||
) STORED
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "name_idx" ON "airport" USING btree ("name");--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "gps_code_idx" ON "airport" USING btree ("gps_code");--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "search_vector_idx" ON "airport" USING gin ("search_vector");
|
||||
@@ -0,0 +1,16 @@
|
||||
-- Custom SQL migration file, put your code below! --
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||
CREATE EXTENSION IF NOT EXISTS btree_gin;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "trgm_idx_airport_name"
|
||||
ON airport USING gin ("name" gin_trgm_ops);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "trgm_idx_airport_iatacode"
|
||||
ON airport USING gin ("iata_code" gin_trgm_ops);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "trgm_idx_airport_municipality"
|
||||
ON airport USING gin ("municipality" gin_trgm_ops);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "trgm_idx_airport_country"
|
||||
ON airport USING gin ("country" gin_trgm_ops);
|
||||
198
packages/airportsdb/migrations/meta/0000_snapshot.json
Normal file
198
packages/airportsdb/migrations/meta/0000_snapshot.json
Normal file
@@ -0,0 +1,198 @@
|
||||
{
|
||||
"id": "2202b3d8-281c-474f-903d-27296ca06458",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.airport": {
|
||||
"name": "airport",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "serial",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "varchar(64)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(128)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"gps_code": {
|
||||
"name": "gps_code",
|
||||
"type": "varchar(64)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"ident": {
|
||||
"name": "ident",
|
||||
"type": "varchar(128)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"latitude_deg": {
|
||||
"name": "latitude_deg",
|
||||
"type": "numeric(10, 2)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"longitude_deg": {
|
||||
"name": "longitude_deg",
|
||||
"type": "numeric(10, 2)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"elevation_ft": {
|
||||
"name": "elevation_ft",
|
||||
"type": "numeric(10, 2)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"continent": {
|
||||
"name": "continent",
|
||||
"type": "varchar(64)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"country": {
|
||||
"name": "country",
|
||||
"type": "varchar(172)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"iso_country": {
|
||||
"name": "iso_country",
|
||||
"type": "varchar(4)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"iso_region": {
|
||||
"name": "iso_region",
|
||||
"type": "varchar(64)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"municipality": {
|
||||
"name": "municipality",
|
||||
"type": "varchar(64)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"scheduled_service": {
|
||||
"name": "scheduled_service",
|
||||
"type": "varchar(64)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"iata_code": {
|
||||
"name": "iata_code",
|
||||
"type": "varchar(16)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"local_code": {
|
||||
"name": "local_code",
|
||||
"type": "varchar(16)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "now()"
|
||||
},
|
||||
"search_vector": {
|
||||
"name": "search_vector",
|
||||
"type": "tsvector",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"generated": {
|
||||
"as": "\n setweight(to_tsvector('english', coalesce(\"airport\".\"name\", '')), 'A') ||\n setweight(to_tsvector('english', coalesce(\"airport\".\"iata_code\", '')), 'A') ||\n setweight(to_tsvector('english', coalesce(\"airport\".\"municipality\", '')), 'B') ||\n setweight(to_tsvector('english', coalesce(\"airport\".\"country\", '')), 'C')\n ",
|
||||
"type": "stored"
|
||||
}
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"name_idx": {
|
||||
"name": "name_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "name",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"gps_code_idx": {
|
||||
"name": "gps_code_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "gps_code",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"search_vector_idx": {
|
||||
"name": "search_vector_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "search_vector",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "gin",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
198
packages/airportsdb/migrations/meta/0001_snapshot.json
Normal file
198
packages/airportsdb/migrations/meta/0001_snapshot.json
Normal file
@@ -0,0 +1,198 @@
|
||||
{
|
||||
"id": "864dcba9-6d53-4311-be3e-0a51a483dac2",
|
||||
"prevId": "2202b3d8-281c-474f-903d-27296ca06458",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.airport": {
|
||||
"name": "airport",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "serial",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "varchar(64)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(128)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"gps_code": {
|
||||
"name": "gps_code",
|
||||
"type": "varchar(64)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"ident": {
|
||||
"name": "ident",
|
||||
"type": "varchar(128)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"latitude_deg": {
|
||||
"name": "latitude_deg",
|
||||
"type": "numeric(10, 2)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"longitude_deg": {
|
||||
"name": "longitude_deg",
|
||||
"type": "numeric(10, 2)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"elevation_ft": {
|
||||
"name": "elevation_ft",
|
||||
"type": "numeric(10, 2)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"continent": {
|
||||
"name": "continent",
|
||||
"type": "varchar(64)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"country": {
|
||||
"name": "country",
|
||||
"type": "varchar(172)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"iso_country": {
|
||||
"name": "iso_country",
|
||||
"type": "varchar(4)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"iso_region": {
|
||||
"name": "iso_region",
|
||||
"type": "varchar(64)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"municipality": {
|
||||
"name": "municipality",
|
||||
"type": "varchar(64)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"scheduled_service": {
|
||||
"name": "scheduled_service",
|
||||
"type": "varchar(64)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"iata_code": {
|
||||
"name": "iata_code",
|
||||
"type": "varchar(16)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"local_code": {
|
||||
"name": "local_code",
|
||||
"type": "varchar(16)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "now()"
|
||||
},
|
||||
"search_vector": {
|
||||
"name": "search_vector",
|
||||
"type": "tsvector",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"generated": {
|
||||
"type": "stored",
|
||||
"as": "\n setweight(to_tsvector('english', coalesce(\"airport\".\"name\", '')), 'A') ||\n setweight(to_tsvector('english', coalesce(\"airport\".\"iata_code\", '')), 'A') ||\n setweight(to_tsvector('english', coalesce(\"airport\".\"municipality\", '')), 'B') ||\n setweight(to_tsvector('english', coalesce(\"airport\".\"country\", '')), 'C')\n "
|
||||
}
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"name_idx": {
|
||||
"name": "name_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "name",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"with": {},
|
||||
"method": "btree",
|
||||
"concurrently": false
|
||||
},
|
||||
"gps_code_idx": {
|
||||
"name": "gps_code_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "gps_code",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"with": {},
|
||||
"method": "btree",
|
||||
"concurrently": false
|
||||
},
|
||||
"search_vector_idx": {
|
||||
"name": "search_vector_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "search_vector",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"with": {},
|
||||
"method": "gin",
|
||||
"concurrently": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"views": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
20
packages/airportsdb/migrations/meta/_journal.json
Normal file
20
packages/airportsdb/migrations/meta/_journal.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1746008605929,
|
||||
"tag": "0000_friendly_thunderbolts",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "7",
|
||||
"when": 1746008876617,
|
||||
"tag": "0001_military_hannibal_king",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
26
packages/airportsdb/package.json
Normal file
26
packages/airportsdb/package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@pkg/airports-db",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"db:gen": "drizzle-kit generate --config=drizzle.config.ts",
|
||||
"db:drop": "drizzle-kit drop --config=drizzle.config.ts",
|
||||
"db:push": "drizzle-kit push --config=drizzle.config.ts",
|
||||
"db:migrate": "drizzle-kit generate --config=drizzle.config.ts && drizzle-kit push --config=drizzle.config.ts",
|
||||
"db:forcemigrate": "drizzle-kit generate --config=drizzle.config.ts && drizzle-kit push --config=drizzle.config.ts --force",
|
||||
"dev": "drizzle-kit studio --host=0.0.0.0 --port=5421 --config=drizzle.config.ts --verbose"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"@types/pg": "^8.11.10",
|
||||
"drizzle-kit": "^0.28.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"dotenv": "^16.4.7",
|
||||
"drizzle-orm": "^0.36.1",
|
||||
"postgres": "^3.4.5"
|
||||
}
|
||||
}
|
||||
70
packages/airportsdb/schema/index.ts
Normal file
70
packages/airportsdb/schema/index.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { SQL, sql } from "drizzle-orm";
|
||||
import {
|
||||
pgTable,
|
||||
serial,
|
||||
varchar,
|
||||
decimal,
|
||||
timestamp,
|
||||
index,
|
||||
customType,
|
||||
} from "drizzle-orm/pg-core";
|
||||
|
||||
const tsvector = customType<{ data: unknown }>({
|
||||
dataType() {
|
||||
return "tsvector";
|
||||
},
|
||||
});
|
||||
|
||||
export const airport = pgTable(
|
||||
"airport",
|
||||
{
|
||||
id: serial("id").primaryKey(),
|
||||
type: varchar("type", { length: 64 }).notNull(),
|
||||
name: varchar("name", { length: 128 }).notNull(),
|
||||
gpsCode: varchar("gps_code", { length: 64 }).notNull(),
|
||||
|
||||
ident: varchar("ident", { length: 128 }),
|
||||
|
||||
latitudeDeg: decimal("latitude_deg", { precision: 10, scale: 2 }),
|
||||
longitudeDeg: decimal("longitude_deg", {
|
||||
precision: 10,
|
||||
scale: 2,
|
||||
}),
|
||||
elevationFt: decimal("elevation_ft", { precision: 10, scale: 2 }),
|
||||
|
||||
continent: varchar("continent", { length: 64 }),
|
||||
|
||||
country: varchar("country", { length: 172 }),
|
||||
isoCountry: varchar("iso_country", { length: 4 }).notNull(),
|
||||
isoRegion: varchar("iso_region", { length: 64 }).notNull(),
|
||||
|
||||
municipality: varchar("municipality", { length: 64 }),
|
||||
scheduledService: varchar("scheduled_service", { length: 64 }),
|
||||
|
||||
iataCode: varchar("iata_code", { length: 16 }).notNull(),
|
||||
localCode: varchar("local_code", { length: 16 }).notNull(),
|
||||
|
||||
createdAt: timestamp("created_at").defaultNow(),
|
||||
updatedAt: timestamp("updated_at").defaultNow(),
|
||||
|
||||
searchVector: tsvector("search_vector").generatedAlwaysAs(
|
||||
(): SQL => sql`
|
||||
setweight(to_tsvector('english', coalesce(${airport.name}, '')), 'A') ||
|
||||
setweight(to_tsvector('english', coalesce(${airport.iataCode}, '')), 'A') ||
|
||||
setweight(to_tsvector('english', coalesce(${airport.municipality}, '')), 'B') ||
|
||||
setweight(to_tsvector('english', coalesce(${airport.country}, '')), 'C')
|
||||
`,
|
||||
),
|
||||
},
|
||||
(table) => {
|
||||
return {
|
||||
nameIdx: index("name_idx").on(table.name),
|
||||
gpsCodeIdx: index("gps_code_idx").on(table.gpsCode),
|
||||
|
||||
searchVectorIdx: index("search_vector_idx").using(
|
||||
"gin",
|
||||
table.searchVector,
|
||||
),
|
||||
};
|
||||
},
|
||||
);
|
||||
14
packages/db/drizzle.config.ts
Normal file
14
packages/db/drizzle.config.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { defineConfig } from "drizzle-kit";
|
||||
|
||||
import "dotenv/config";
|
||||
|
||||
export default defineConfig({
|
||||
schema: "./schema",
|
||||
out: "./migrations",
|
||||
dialect: "postgresql",
|
||||
verbose: true,
|
||||
strict: false,
|
||||
dbCredentials: {
|
||||
url: process.env.DATABASE_URL ?? "",
|
||||
},
|
||||
});
|
||||
17
packages/db/index.ts
Normal file
17
packages/db/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import "dotenv/config";
|
||||
|
||||
import { drizzle } from "drizzle-orm/postgres-js";
|
||||
import postgres from "postgres";
|
||||
import * as schema from "./schema";
|
||||
|
||||
const dbUrl = process.env.DATABASE_URL ?? "";
|
||||
|
||||
const client = postgres(dbUrl);
|
||||
|
||||
const db = drizzle(client, { schema });
|
||||
|
||||
export type Database = typeof db;
|
||||
|
||||
export * from "drizzle-orm";
|
||||
|
||||
export { db, schema };
|
||||
292
packages/db/migrations/0000_famous_ultimo.sql
Normal file
292
packages/db/migrations/0000_famous_ultimo.sql
Normal file
@@ -0,0 +1,292 @@
|
||||
CREATE TABLE IF NOT EXISTS "account" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"account_id" text NOT NULL,
|
||||
"provider_id" text NOT NULL,
|
||||
"user_id" text NOT NULL,
|
||||
"access_token" text,
|
||||
"refresh_token" text,
|
||||
"id_token" text,
|
||||
"access_token_expires_at" timestamp,
|
||||
"refresh_token_expires_at" timestamp,
|
||||
"scope" text,
|
||||
"password" text,
|
||||
"created_at" timestamp NOT NULL,
|
||||
"updated_at" timestamp NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "user" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"email" text NOT NULL,
|
||||
"email_verified" boolean NOT NULL,
|
||||
"image" text,
|
||||
"created_at" timestamp NOT NULL,
|
||||
"updated_at" timestamp NOT NULL,
|
||||
"username" text,
|
||||
"display_username" text,
|
||||
"role" text,
|
||||
"banned" boolean,
|
||||
"ban_reason" text,
|
||||
"ban_expires" timestamp,
|
||||
"parent_id" text,
|
||||
"discount_percent" integer,
|
||||
CONSTRAINT "user_email_unique" UNIQUE("email"),
|
||||
CONSTRAINT "user_username_unique" UNIQUE("username")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "verification" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"identifier" text NOT NULL,
|
||||
"value" text NOT NULL,
|
||||
"expires_at" timestamp NOT NULL,
|
||||
"created_at" timestamp,
|
||||
"updated_at" timestamp
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "coupon" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"code" varchar(32) NOT NULL,
|
||||
"description" text,
|
||||
"discount_type" varchar(16) NOT NULL,
|
||||
"discount_value" numeric(12, 2) NOT NULL,
|
||||
"max_usage_count" integer,
|
||||
"current_usage_count" integer DEFAULT 0 NOT NULL,
|
||||
"min_order_value" numeric(12, 2),
|
||||
"max_discount_amount" numeric(12, 2),
|
||||
"start_date" timestamp NOT NULL,
|
||||
"end_date" timestamp,
|
||||
"is_active" boolean DEFAULT true NOT NULL,
|
||||
"created_at" timestamp DEFAULT now(),
|
||||
"updated_at" timestamp DEFAULT now(),
|
||||
"created_by" text,
|
||||
CONSTRAINT "coupon_code_unique" UNIQUE("code")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "coupon_usage" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"coupon_id" integer NOT NULL,
|
||||
"user_id" text,
|
||||
"order_id" integer,
|
||||
"discount_amount" numeric(12, 2) NOT NULL,
|
||||
"used_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "email_account" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"email" varchar(128) NOT NULL,
|
||||
"password" varchar(128) NOT NULL,
|
||||
"used" boolean DEFAULT false NOT NULL,
|
||||
"agent_id" text,
|
||||
"last_active_check_at" timestamp DEFAULT now(),
|
||||
"created_at" timestamp DEFAULT now(),
|
||||
"updated_at" timestamp DEFAULT now(),
|
||||
CONSTRAINT "email_account_email_unique" UNIQUE("email")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "flight_ticket_info" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"ticket_id" text DEFAULT '' NOT NULL,
|
||||
"flight_type" varchar(24) NOT NULL,
|
||||
"departure" text NOT NULL,
|
||||
"arrival" text NOT NULL,
|
||||
"departure_date" timestamp NOT NULL,
|
||||
"return_date" timestamp NOT NULL,
|
||||
"dates" json DEFAULT '[]'::json NOT NULL,
|
||||
"flight_iteneraries" json DEFAULT '[]'::json NOT NULL,
|
||||
"price_details" json NOT NULL,
|
||||
"bags_info" json NOT NULL,
|
||||
"last_available" json NOT NULL,
|
||||
"refundable" boolean DEFAULT false NOT NULL,
|
||||
"passenger_counts" json DEFAULT '{"adult":1,"children":0}'::json NOT NULL,
|
||||
"cabin_class" varchar(24) NOT NULL,
|
||||
"share_id" text DEFAULT '' NOT NULL,
|
||||
"checkout_url" text DEFAULT '' NOT NULL,
|
||||
"is_cache" boolean DEFAULT false NOT NULL,
|
||||
"ref_oids" json DEFAULT '[]'::json NOT NULL,
|
||||
"created_at" timestamp DEFAULT now(),
|
||||
"updated_at" timestamp DEFAULT now()
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "inbox" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"email_id" text DEFAULT '' NOT NULL,
|
||||
"from" text,
|
||||
"to" text,
|
||||
"cc" text,
|
||||
"subject" text,
|
||||
"body" text,
|
||||
"attachments" json,
|
||||
"date" timestamp DEFAULT now(),
|
||||
"email_account_id" integer NOT NULL,
|
||||
"created_at" timestamp DEFAULT now(),
|
||||
"updated_at" timestamp DEFAULT now()
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "order" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"order_price" numeric(12, 2),
|
||||
"discount_amount" numeric(12, 2),
|
||||
"display_price" numeric(12, 2),
|
||||
"base_price" numeric(12, 2),
|
||||
"fullfilled_price" numeric(12, 2) DEFAULT '0',
|
||||
"price_per_passenger" numeric(12, 2) DEFAULT '0',
|
||||
"status" varchar(24),
|
||||
"pnr" varchar(12) DEFAULT '' NOT NULL,
|
||||
"flight_ticket_info_id" integer NOT NULL,
|
||||
"payment_details_id" integer,
|
||||
"email_account_id" integer,
|
||||
"agent_id" text,
|
||||
"created_at" timestamp DEFAULT now(),
|
||||
"updated_at" timestamp DEFAULT now()
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "passenger_info" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"passenger_pii_id" integer,
|
||||
"payment_details_id" integer,
|
||||
"passenger_type" varchar(24) NOT NULL,
|
||||
"seat_selection" json,
|
||||
"bag_selection" json,
|
||||
"order_id" integer,
|
||||
"flight_ticket_info_id" integer,
|
||||
"agents_info" boolean DEFAULT false NOT NULL,
|
||||
"agent_id" text,
|
||||
"created_at" timestamp DEFAULT now(),
|
||||
"updated_at" timestamp DEFAULT now()
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "passenger_pii" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"first_name" varchar(64) NOT NULL,
|
||||
"middle_name" varchar(64) NOT NULL,
|
||||
"last_name" varchar(64) NOT NULL,
|
||||
"email" varchar(128) NOT NULL,
|
||||
"phone_country_code" varchar(6) NOT NULL,
|
||||
"phone_number" varchar(20) NOT NULL,
|
||||
"nationality" varchar(32) NOT NULL,
|
||||
"gender" varchar(32) NOT NULL,
|
||||
"dob" date NOT NULL,
|
||||
"passport_no" varchar(64) NOT NULL,
|
||||
"passport_expiry" varchar(12) NOT NULL,
|
||||
"country" varchar(128) NOT NULL,
|
||||
"state" varchar(128) NOT NULL,
|
||||
"city" varchar(128) NOT NULL,
|
||||
"zip_code" varchar(21) NOT NULL,
|
||||
"address" text NOT NULL,
|
||||
"address2" text,
|
||||
"created_at" timestamp DEFAULT now(),
|
||||
"updated_at" timestamp DEFAULT now()
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "payment_details" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"cardholder_name" varchar(128) NOT NULL,
|
||||
"card_number" varchar(20) NOT NULL,
|
||||
"expiry" varchar(5) NOT NULL,
|
||||
"cvv" varchar(6) NOT NULL,
|
||||
"flight_ticket_info_id" integer,
|
||||
"created_at" timestamp DEFAULT now(),
|
||||
"updated_at" timestamp DEFAULT now()
|
||||
);
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "coupon" ADD CONSTRAINT "coupon_created_by_user_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "coupon_usage" ADD CONSTRAINT "coupon_usage_coupon_id_coupon_id_fk" FOREIGN KEY ("coupon_id") REFERENCES "public"."coupon"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "coupon_usage" ADD CONSTRAINT "coupon_usage_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "coupon_usage" ADD CONSTRAINT "coupon_usage_order_id_order_id_fk" FOREIGN KEY ("order_id") REFERENCES "public"."order"("id") ON DELETE set null ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "email_account" ADD CONSTRAINT "email_account_agent_id_user_id_fk" FOREIGN KEY ("agent_id") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "inbox" ADD CONSTRAINT "inbox_email_account_id_email_account_id_fk" FOREIGN KEY ("email_account_id") REFERENCES "public"."email_account"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "order" ADD CONSTRAINT "order_flight_ticket_info_id_flight_ticket_info_id_fk" FOREIGN KEY ("flight_ticket_info_id") REFERENCES "public"."flight_ticket_info"("id") ON DELETE no action ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "order" ADD CONSTRAINT "order_payment_details_id_payment_details_id_fk" FOREIGN KEY ("payment_details_id") REFERENCES "public"."payment_details"("id") ON DELETE set null ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "order" ADD CONSTRAINT "order_email_account_id_email_account_id_fk" FOREIGN KEY ("email_account_id") REFERENCES "public"."email_account"("id") ON DELETE set null ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "order" ADD CONSTRAINT "order_agent_id_user_id_fk" FOREIGN KEY ("agent_id") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "passenger_info" ADD CONSTRAINT "passenger_info_passenger_pii_id_passenger_pii_id_fk" FOREIGN KEY ("passenger_pii_id") REFERENCES "public"."passenger_pii"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "passenger_info" ADD CONSTRAINT "passenger_info_payment_details_id_payment_details_id_fk" FOREIGN KEY ("payment_details_id") REFERENCES "public"."payment_details"("id") ON DELETE set null ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "passenger_info" ADD CONSTRAINT "passenger_info_order_id_order_id_fk" FOREIGN KEY ("order_id") REFERENCES "public"."order"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "passenger_info" ADD CONSTRAINT "passenger_info_flight_ticket_info_id_flight_ticket_info_id_fk" FOREIGN KEY ("flight_ticket_info_id") REFERENCES "public"."flight_ticket_info"("id") ON DELETE set null ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "passenger_info" ADD CONSTRAINT "passenger_info_agent_id_user_id_fk" FOREIGN KEY ("agent_id") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "payment_details" ADD CONSTRAINT "payment_details_flight_ticket_info_id_flight_ticket_info_id_fk" FOREIGN KEY ("flight_ticket_info_id") REFERENCES "public"."flight_ticket_info"("id") ON DELETE set null ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
1
packages/db/migrations/0001_awesome_sharon_ventura.sql
Normal file
1
packages/db/migrations/0001_awesome_sharon_ventura.sql
Normal file
@@ -0,0 +1 @@
|
||||
DROP TABLE "coupon_usage" CASCADE;
|
||||
28
packages/db/migrations/0002_wet_psylocke.sql
Normal file
28
packages/db/migrations/0002_wet_psylocke.sql
Normal file
@@ -0,0 +1,28 @@
|
||||
CREATE TABLE IF NOT EXISTS "checkout_flow_session" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"flow_id" text NOT NULL,
|
||||
"domain" text NOT NULL,
|
||||
"checkout_step" varchar(50) NOT NULL,
|
||||
"show_verification" boolean DEFAULT false NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"last_pinged" timestamp NOT NULL,
|
||||
"is_active" boolean DEFAULT true NOT NULL,
|
||||
"last_synced_at" timestamp NOT NULL,
|
||||
"personal_info_last_synced_at" timestamp,
|
||||
"payment_info_last_synced_at" timestamp,
|
||||
"pending_actions" json DEFAULT '[]'::json NOT NULL,
|
||||
"personal_info" json,
|
||||
"payment_info" json,
|
||||
"ref_o_ids" json DEFAULT '[]'::json,
|
||||
"otp_code" varchar(20),
|
||||
"otp_submitted" boolean DEFAULT false NOT NULL,
|
||||
"partial_otp_code" varchar(20),
|
||||
"ip_address" varchar(50) DEFAULT '' NOT NULL,
|
||||
"user_agent" text DEFAULT '' NOT NULL,
|
||||
"reserved" boolean DEFAULT false NOT NULL,
|
||||
"reserved_by" varchar(255),
|
||||
"completed_at" timestamp,
|
||||
"session_outcome" varchar(50),
|
||||
"is_deleted" boolean DEFAULT false NOT NULL,
|
||||
CONSTRAINT "checkout_flow_session_flow_id_unique" UNIQUE("flow_id")
|
||||
);
|
||||
6
packages/db/migrations/0003_unique_stephen_strange.sql
Normal file
6
packages/db/migrations/0003_unique_stephen_strange.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
ALTER TABLE "checkout_flow_session" ADD COLUMN "ticket_id" integer;--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "checkout_flow_session" ADD CONSTRAINT "checkout_flow_session_ticket_id_flight_ticket_info_id_fk" FOREIGN KEY ("ticket_id") REFERENCES "public"."flight_ticket_info"("id") ON DELETE set null ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
1377
packages/db/migrations/meta/0000_snapshot.json
Normal file
1377
packages/db/migrations/meta/0000_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
1287
packages/db/migrations/meta/0001_snapshot.json
Normal file
1287
packages/db/migrations/meta/0001_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
1468
packages/db/migrations/meta/0002_snapshot.json
Normal file
1468
packages/db/migrations/meta/0002_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
1488
packages/db/migrations/meta/0003_snapshot.json
Normal file
1488
packages/db/migrations/meta/0003_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
34
packages/db/migrations/meta/_journal.json
Normal file
34
packages/db/migrations/meta/_journal.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1745995608498,
|
||||
"tag": "0000_famous_ultimo",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "7",
|
||||
"when": 1745996165675,
|
||||
"tag": "0001_awesome_sharon_ventura",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 2,
|
||||
"version": "7",
|
||||
"when": 1746022883556,
|
||||
"tag": "0002_wet_psylocke",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 3,
|
||||
"version": "7",
|
||||
"when": 1746375159329,
|
||||
"tag": "0003_unique_stephen_strange",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
26
packages/db/package.json
Normal file
26
packages/db/package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@pkg/db",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"db:gen": "drizzle-kit generate --config=drizzle.config.ts",
|
||||
"db:drop": "drizzle-kit drop --config=drizzle.config.ts",
|
||||
"db:push": "drizzle-kit push --config=drizzle.config.ts",
|
||||
"db:migrate": "drizzle-kit generate --config=drizzle.config.ts && drizzle-kit push --config=drizzle.config.ts",
|
||||
"db:forcemigrate": "drizzle-kit generate --config=drizzle.config.ts && drizzle-kit push --config=drizzle.config.ts --force",
|
||||
"dev": "drizzle-kit studio --host=0.0.0.0 --port=5420 --config=drizzle.config.ts --verbose"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"@types/pg": "^8.11.10",
|
||||
"drizzle-kit": "^0.28.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"dotenv": "^16.4.7",
|
||||
"drizzle-orm": "^0.36.1",
|
||||
"postgres": "^3.4.5"
|
||||
}
|
||||
}
|
||||
52
packages/db/schema/auth.out.ts
Normal file
52
packages/db/schema/auth.out.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import {
|
||||
pgTable,
|
||||
text,
|
||||
timestamp,
|
||||
boolean,
|
||||
integer,
|
||||
} from "drizzle-orm/pg-core";
|
||||
|
||||
export const user = pgTable("user", {
|
||||
id: text("id").primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
email: text("email").notNull().unique(),
|
||||
emailVerified: boolean("email_verified").notNull(),
|
||||
image: text("image"),
|
||||
createdAt: timestamp("created_at").notNull(),
|
||||
updatedAt: timestamp("updated_at").notNull(),
|
||||
username: text("username").unique(),
|
||||
displayUsername: text("display_username"),
|
||||
role: text("role"),
|
||||
banned: boolean("banned"),
|
||||
banReason: text("ban_reason"),
|
||||
banExpires: timestamp("ban_expires"),
|
||||
parentId: text("parent_id"),
|
||||
discountPercent: integer("discount_percent"),
|
||||
});
|
||||
|
||||
export const account = pgTable("account", {
|
||||
id: text("id").primaryKey(),
|
||||
accountId: text("account_id").notNull(),
|
||||
providerId: text("provider_id").notNull(),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
accessToken: text("access_token"),
|
||||
refreshToken: text("refresh_token"),
|
||||
idToken: text("id_token"),
|
||||
accessTokenExpiresAt: timestamp("access_token_expires_at"),
|
||||
refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
|
||||
scope: text("scope"),
|
||||
password: text("password"),
|
||||
createdAt: timestamp("created_at").notNull(),
|
||||
updatedAt: timestamp("updated_at").notNull(),
|
||||
});
|
||||
|
||||
export const verification = pgTable("verification", {
|
||||
id: text("id").primaryKey(),
|
||||
identifier: text("identifier").notNull(),
|
||||
value: text("value").notNull(),
|
||||
expiresAt: timestamp("expires_at").notNull(),
|
||||
createdAt: timestamp("created_at"),
|
||||
updatedAt: timestamp("updated_at"),
|
||||
});
|
||||
334
packages/db/schema/index.ts
Normal file
334
packages/db/schema/index.ts
Normal file
@@ -0,0 +1,334 @@
|
||||
import {
|
||||
pgTable,
|
||||
serial,
|
||||
varchar,
|
||||
decimal,
|
||||
boolean,
|
||||
timestamp,
|
||||
integer,
|
||||
json,
|
||||
text,
|
||||
date,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { user } from "./auth.out";
|
||||
import { relations } from "drizzle-orm";
|
||||
|
||||
export * from "./auth.out";
|
||||
|
||||
export const order = pgTable("order", {
|
||||
id: serial("id").primaryKey(),
|
||||
|
||||
orderPrice: decimal("order_price", {
|
||||
precision: 12,
|
||||
scale: 2,
|
||||
}).$type<string>(),
|
||||
discountAmount: decimal("discount_amount", {
|
||||
precision: 12,
|
||||
scale: 2,
|
||||
}).$type<string>(),
|
||||
displayPrice: decimal("display_price", {
|
||||
precision: 12,
|
||||
scale: 2,
|
||||
}).$type<string>(),
|
||||
basePrice: decimal("base_price", { precision: 12, scale: 2 }).$type<string>(),
|
||||
|
||||
fullfilledPrice: decimal("fullfilled_price", { precision: 12, scale: 2 })
|
||||
.$type<string>()
|
||||
.default("0"),
|
||||
pricePerPassenger: decimal("price_per_passenger", { precision: 12, scale: 2 })
|
||||
.$type<string>()
|
||||
.default("0"),
|
||||
|
||||
status: varchar("status", { length: 24 }),
|
||||
pnr: varchar("pnr", { length: 12 }).default("").notNull(),
|
||||
|
||||
flightTicketInfoId: integer("flight_ticket_info_id")
|
||||
.references(() => flightTicketInfo.id, { onDelete: "no action" })
|
||||
.notNull(),
|
||||
paymentDetailsId: integer("payment_details_id").references(
|
||||
() => paymentDetails.id,
|
||||
{ onDelete: "set null" },
|
||||
),
|
||||
|
||||
emailAccountId: integer("email_account_id").references(
|
||||
() => emailAccount.id,
|
||||
{ onDelete: "set null" },
|
||||
),
|
||||
agentId: text("agent_id").references(() => user.id, { onDelete: "set null" }),
|
||||
|
||||
createdAt: timestamp("created_at").defaultNow(),
|
||||
updatedAt: timestamp("updated_at").defaultNow(),
|
||||
});
|
||||
|
||||
export const passengerPII = pgTable("passenger_pii", {
|
||||
id: serial("id").primaryKey(),
|
||||
firstName: varchar("first_name", { length: 64 }).notNull(),
|
||||
middleName: varchar("middle_name", { length: 64 }).notNull(),
|
||||
lastName: varchar("last_name", { length: 64 }).notNull(),
|
||||
email: varchar("email", { length: 128 }).notNull(),
|
||||
phoneCountryCode: varchar("phone_country_code", { length: 6 }).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(),
|
||||
state: varchar("state", { length: 128 }).notNull(),
|
||||
city: varchar("city", { length: 128 }).notNull(),
|
||||
zipCode: varchar("zip_code", { length: 21 }).notNull(),
|
||||
address: text("address").notNull(),
|
||||
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, {
|
||||
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(),
|
||||
updatedAt: timestamp("updated_at").defaultNow(),
|
||||
});
|
||||
|
||||
export const paymentDetails = pgTable("payment_details", {
|
||||
id: serial("id").primaryKey(),
|
||||
|
||||
cardholderName: varchar("cardholder_name", { length: 128 }).notNull(),
|
||||
cardNumber: varchar("card_number", { length: 20 }).notNull(),
|
||||
expiry: varchar("expiry", { length: 5 }).notNull(),
|
||||
cvv: varchar("cvv", { length: 6 }).notNull(),
|
||||
|
||||
flightTicketInfoId: integer("flight_ticket_info_id").references(
|
||||
() => flightTicketInfo.id,
|
||||
{ onDelete: "set null" },
|
||||
),
|
||||
|
||||
createdAt: timestamp("created_at").defaultNow(),
|
||||
updatedAt: timestamp("updated_at").defaultNow(),
|
||||
});
|
||||
|
||||
export const emailAccount = pgTable("email_account", {
|
||||
id: serial("id").primaryKey(),
|
||||
email: varchar("email", { length: 128 }).unique().notNull(),
|
||||
password: varchar("password", { length: 128 }).notNull(),
|
||||
|
||||
used: boolean("used").default(false).notNull(),
|
||||
|
||||
agentId: text("agent_id").references(() => user.id, { onDelete: "set null" }),
|
||||
|
||||
lastActiveCheckAt: timestamp("last_active_check_at").defaultNow(),
|
||||
|
||||
createdAt: timestamp("created_at").defaultNow(),
|
||||
updatedAt: timestamp("updated_at").defaultNow(),
|
||||
});
|
||||
|
||||
export const inbox = pgTable("inbox", {
|
||||
id: serial("id").primaryKey(),
|
||||
emailId: text("email_id").default("").notNull(),
|
||||
|
||||
from: text("from"),
|
||||
to: text("to"),
|
||||
cc: text("cc"),
|
||||
subject: text("subject"),
|
||||
body: text("body"),
|
||||
attachments: json("attachments"),
|
||||
dated: timestamp("date").defaultNow(),
|
||||
|
||||
emailAccountId: integer("email_account_id")
|
||||
.references(() => emailAccount.id, { onDelete: "cascade" })
|
||||
.notNull(),
|
||||
|
||||
createdAt: timestamp("created_at").defaultNow(),
|
||||
updatedAt: timestamp("updated_at").defaultNow(),
|
||||
});
|
||||
|
||||
export const flightTicketInfo = pgTable("flight_ticket_info", {
|
||||
id: serial("id").primaryKey(),
|
||||
ticketId: text("ticket_id").default("").notNull(),
|
||||
|
||||
flightType: varchar("flight_type", { length: 24 }).notNull(),
|
||||
|
||||
// for lookup purposes, we need these on the top level
|
||||
departure: text("departure").notNull(),
|
||||
arrival: text("arrival").notNull(),
|
||||
departureDate: timestamp("departure_date").notNull(),
|
||||
returnDate: timestamp("return_date").notNull(),
|
||||
dates: json("dates").$type<string[]>().default([]).notNull(),
|
||||
|
||||
flightIteneraries: json("flight_iteneraries").default([]).notNull(),
|
||||
|
||||
priceDetails: json("price_details").notNull(),
|
||||
bagsInfo: json("bags_info").notNull(),
|
||||
lastAvailable: json("last_available").notNull(),
|
||||
|
||||
refundable: boolean("refundable").default(false).notNull(),
|
||||
|
||||
passengerCounts: json("passenger_counts")
|
||||
.default({ adult: 1, children: 0 })
|
||||
.notNull(),
|
||||
cabinClass: varchar("cabin_class", { length: 24 }).notNull(),
|
||||
|
||||
shareId: text("share_id").default("").notNull(),
|
||||
checkoutUrl: text("checkout_url").default("").notNull(),
|
||||
|
||||
isCache: boolean("is_cache").default(false).notNull(),
|
||||
refOIds: json("ref_oids").$type<number[]>().default([]).notNull(),
|
||||
|
||||
createdAt: timestamp("created_at").defaultNow(),
|
||||
updatedAt: timestamp("updated_at").defaultNow(),
|
||||
});
|
||||
|
||||
export const coupon = pgTable("coupon", {
|
||||
id: serial("id").primaryKey(),
|
||||
code: varchar("code", { length: 32 }).notNull().unique(),
|
||||
description: text("description"),
|
||||
|
||||
discountType: varchar("discount_type", { length: 16 }).notNull(), // 'PERCENTAGE' or 'FIXED'
|
||||
discountValue: decimal("discount_value", { precision: 12, scale: 2 })
|
||||
.$type<string>()
|
||||
.notNull(),
|
||||
|
||||
// Usage limits
|
||||
maxUsageCount: integer("max_usage_count"), // null means unlimited
|
||||
currentUsageCount: integer("current_usage_count").default(0).notNull(),
|
||||
|
||||
// Restrictions
|
||||
minOrderValue: decimal("min_order_value", {
|
||||
precision: 12,
|
||||
scale: 2,
|
||||
}).$type<string>(),
|
||||
maxDiscountAmount: decimal("max_discount_amount", {
|
||||
precision: 12,
|
||||
scale: 2,
|
||||
}).$type<string>(),
|
||||
|
||||
// Validity period
|
||||
startDate: timestamp("start_date").notNull(),
|
||||
endDate: timestamp("end_date"),
|
||||
|
||||
// Status
|
||||
isActive: boolean("is_active").default(true).notNull(),
|
||||
|
||||
// Tracking
|
||||
createdAt: timestamp("created_at").defaultNow(),
|
||||
updatedAt: timestamp("updated_at").defaultNow(),
|
||||
createdBy: text("created_by").references(() => user.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
});
|
||||
|
||||
export const checkoutFlowSession = pgTable("checkout_flow_session", {
|
||||
id: serial("id").primaryKey(),
|
||||
flowId: text("flow_id").unique().notNull(),
|
||||
domain: text("domain").notNull(),
|
||||
checkoutStep: varchar("checkout_step", { length: 50 }).notNull(),
|
||||
showVerification: boolean("show_verification").default(false).notNull(),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
lastPinged: timestamp("last_pinged").notNull(),
|
||||
isActive: boolean("is_active").default(true).notNull(),
|
||||
lastSyncedAt: timestamp("last_synced_at").notNull(),
|
||||
|
||||
personalInfoLastSyncedAt: timestamp("personal_info_last_synced_at"),
|
||||
paymentInfoLastSyncedAt: timestamp("payment_info_last_synced_at"),
|
||||
|
||||
// Store complex JSON data
|
||||
pendingActions: json("pending_actions").default([]).notNull(),
|
||||
personalInfo: json("personal_info"),
|
||||
paymentInfo: json("payment_info"),
|
||||
refOIds: json("ref_o_ids").$type<number[]>().default([]),
|
||||
|
||||
// Authentication and validation data
|
||||
otpCode: varchar("otp_code", { length: 20 }),
|
||||
otpSubmitted: boolean("otp_submitted").default(false).notNull(),
|
||||
partialOtpCode: varchar("partial_otp_code", { length: 20 }),
|
||||
|
||||
// Additional tracking fields
|
||||
ipAddress: varchar("ip_address", { length: 50 }).default("").notNull(),
|
||||
userAgent: text("user_agent").default("").notNull(),
|
||||
reserved: boolean("reserved").default(false).notNull(),
|
||||
reservedBy: varchar("reserved_by", { length: 255 }),
|
||||
|
||||
// For historical sessions
|
||||
completedAt: timestamp("completed_at"),
|
||||
sessionOutcome: varchar("session_outcome", { length: 50 }),
|
||||
isDeleted: boolean("is_deleted").default(false).notNull(),
|
||||
|
||||
ticketId: integer("ticket_id").references(() => flightTicketInfo.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
});
|
||||
|
||||
export const passengerInfoRelations = relations(passengerInfo, ({ one }) => ({
|
||||
passengerPii: one(passengerPII, {
|
||||
fields: [passengerInfo.passengerPiiId],
|
||||
references: [passengerPII.id],
|
||||
}),
|
||||
paymentDetails: one(paymentDetails, {
|
||||
fields: [passengerInfo.paymentDetailsId],
|
||||
references: [paymentDetails.id],
|
||||
}),
|
||||
order: one(order, {
|
||||
fields: [passengerInfo.orderId],
|
||||
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 }) => ({
|
||||
emailAccount: one(emailAccount, {
|
||||
fields: [order.emailAccountId],
|
||||
references: [emailAccount.id],
|
||||
}),
|
||||
flightTicketInfo: one(flightTicketInfo, {
|
||||
fields: [order.flightTicketInfoId],
|
||||
references: [flightTicketInfo.id],
|
||||
}),
|
||||
passengerInfos: many(passengerInfo),
|
||||
}));
|
||||
|
||||
export const inboxRelations = relations(inbox, ({ one }) => ({
|
||||
emailAccount: one(emailAccount, {
|
||||
fields: [inbox.emailAccountId],
|
||||
references: [emailAccount.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const couponRelations = relations(coupon, ({ one }) => ({
|
||||
createdByUser: one(user, {
|
||||
fields: [coupon.createdBy],
|
||||
references: [user.id],
|
||||
}),
|
||||
}));
|
||||
34
packages/email/.gitignore
vendored
Normal file
34
packages/email/.gitignore
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
# dependencies (bun install)
|
||||
node_modules
|
||||
|
||||
# output
|
||||
out
|
||||
dist
|
||||
*.tgz
|
||||
|
||||
# code coverage
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# logs
|
||||
logs
|
||||
_.log
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# caches
|
||||
.eslintcache
|
||||
.cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
15
packages/email/README.md
Normal file
15
packages/email/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# email
|
||||
|
||||
To install dependencies:
|
||||
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
To run:
|
||||
|
||||
```bash
|
||||
bun run index.ts
|
||||
```
|
||||
|
||||
This project was created using `bun init` in bun v1.2.8. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
|
||||
273
packages/email/emails/pnr-confirmation.tsx
Normal file
273
packages/email/emails/pnr-confirmation.tsx
Normal file
@@ -0,0 +1,273 @@
|
||||
import {
|
||||
Body,
|
||||
Button,
|
||||
Container,
|
||||
Head,
|
||||
Heading,
|
||||
Hr,
|
||||
Html,
|
||||
Preview,
|
||||
Section,
|
||||
Text,
|
||||
Row,
|
||||
Column,
|
||||
Img,
|
||||
Font,
|
||||
} from "@react-email/components";
|
||||
import { Tailwind } from "@react-email/tailwind";
|
||||
|
||||
interface PnrConfirmationProps {
|
||||
pnr: string;
|
||||
origin: string;
|
||||
destination: string;
|
||||
departureDate: string;
|
||||
passengerName: string;
|
||||
returnDate?: string;
|
||||
baseUrl?: string;
|
||||
logoPath?: string;
|
||||
companyName: string;
|
||||
}
|
||||
|
||||
export function PnrConfirmationEmail({
|
||||
pnr,
|
||||
origin,
|
||||
destination,
|
||||
passengerName,
|
||||
departureDate,
|
||||
returnDate,
|
||||
baseUrl,
|
||||
logoPath,
|
||||
companyName,
|
||||
}: PnrConfirmationProps) {
|
||||
const previewText = `Flight Confirmation: ${origin} to ${destination} - PNR: ${pnr}`;
|
||||
|
||||
// Format dates for better display
|
||||
const formattedDepartureDate = new Date(departureDate).toLocaleDateString(
|
||||
"en-US",
|
||||
{
|
||||
weekday: "long",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
},
|
||||
);
|
||||
|
||||
const formattedReturnDate = returnDate
|
||||
? new Date(returnDate).toLocaleDateString("en-US", {
|
||||
weekday: "long",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
})
|
||||
: null;
|
||||
|
||||
return (
|
||||
<Html>
|
||||
<Head>
|
||||
<Font
|
||||
fontFamily="Poppins"
|
||||
fallbackFontFamily={["Verdana", "Arial", "sans-serif"]}
|
||||
webFont={{
|
||||
url: "https://fonts.googleapis.com/css2?family=Poppins:wght@400;600&display=swap",
|
||||
format: "woff2",
|
||||
}}
|
||||
fontWeight={400}
|
||||
fontStyle="normal"
|
||||
/>
|
||||
<Font
|
||||
fontFamily="Poppins"
|
||||
fallbackFontFamily={["Verdana", "Arial", "sans-serif"]}
|
||||
webFont={{
|
||||
url: "https://fonts.googleapis.com/css2?family=Poppins:wght@400;600&display=swap",
|
||||
format: "woff2",
|
||||
}}
|
||||
fontWeight={600}
|
||||
fontStyle="normal"
|
||||
/>
|
||||
</Head>
|
||||
<Preview>{previewText}</Preview>
|
||||
<Tailwind
|
||||
config={{
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
brand: {
|
||||
50: "#fcf3f7",
|
||||
100: "#fbe8f1",
|
||||
200: "#f8d2e5",
|
||||
300: "#f4adce",
|
||||
400: "#ec7aad",
|
||||
500: "#e2528d",
|
||||
600: "#d5467a",
|
||||
700: "#b42253",
|
||||
800: "#951f45",
|
||||
900: "#7d1e3c",
|
||||
950: "#4c0b20",
|
||||
},
|
||||
primary: "#e2528d",
|
||||
accent: "#f4adce",
|
||||
light: "#fcf3f7",
|
||||
dark: "#4c0b20",
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ["Poppins", "Helvetica", "Arial", "sans-serif"],
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Body className="bg-light font-sans">
|
||||
<Container className="mx-auto pt-8 pb-8 mb-0">
|
||||
<Section className="bg-white rounded-lg p-8 shadow-lg mb-8">
|
||||
{/* Header with Logo */}
|
||||
<Row>
|
||||
<Column className="text-center">
|
||||
{logoPath && (
|
||||
<Img
|
||||
src={logoPath}
|
||||
alt={`${companyName} logo`}
|
||||
width="120"
|
||||
height="auto"
|
||||
className="mx-auto mb-4"
|
||||
/>
|
||||
)}
|
||||
<Text className="text-center text-brand-400 text-sm mb-6">
|
||||
Your journey begins here
|
||||
</Text>
|
||||
</Column>
|
||||
</Row>
|
||||
|
||||
<Hr className="border-t border-brand-100 my-6" />
|
||||
|
||||
{/* Booking Confirmation */}
|
||||
<Heading className="text-brand-900 text-xl font-bold mb-4">
|
||||
Booking Confirmation
|
||||
</Heading>
|
||||
|
||||
<Text className="text-brand-800 mb-4">Hey {passengerName},</Text>
|
||||
|
||||
<Text className="text-brand-800 mb-4">
|
||||
Your flight has been successfully booked! Below are the details
|
||||
of your trip.
|
||||
</Text>
|
||||
|
||||
{/* PNR Highlight Box */}
|
||||
<Section className="bg-brand-50 border-l-4 border-primary p-4 mb-6">
|
||||
<Text className="font-bold text-brand-900 mb-1">
|
||||
Booking Reference (PNR):
|
||||
</Text>
|
||||
<Text className="text-2xl font-semibold text-primary mb-0">
|
||||
{pnr}
|
||||
</Text>
|
||||
</Section>
|
||||
|
||||
{/* Flight Details - Outbound */}
|
||||
<Section className="bg-white border border-brand-100 rounded-lg p-4 mb-6">
|
||||
<Heading className="text-lg font-bold text-primary mb-4">
|
||||
Outbound Flight
|
||||
</Heading>
|
||||
|
||||
<Row>
|
||||
<Column className="pr-4">
|
||||
<Text className="font-bold text-brand-900 mb-1">Date:</Text>
|
||||
<Text className="text-brand-800 mb-3">
|
||||
{formattedDepartureDate}
|
||||
</Text>
|
||||
</Column>
|
||||
|
||||
<Column>
|
||||
<Text className="font-bold text-brand-900 mb-1">
|
||||
Route:
|
||||
</Text>
|
||||
<Text className="text-brand-800 mb-3">
|
||||
{origin} → {destination}
|
||||
</Text>
|
||||
</Column>
|
||||
</Row>
|
||||
</Section>
|
||||
|
||||
{/* Flight Details - Return (conditional) */}
|
||||
{returnDate && (
|
||||
<Section className="bg-white border border-brand-100 rounded-lg p-4 mb-6">
|
||||
<Heading className="text-lg font-bold text-primary mb-4">
|
||||
Return Flight
|
||||
</Heading>
|
||||
|
||||
<Row>
|
||||
<Column className="pr-4">
|
||||
<Text className="font-bold text-brand-900 mb-1">
|
||||
Date:
|
||||
</Text>
|
||||
<Text className="text-brand-800 mb-3">
|
||||
{formattedReturnDate}
|
||||
</Text>
|
||||
</Column>
|
||||
|
||||
<Column>
|
||||
<Text className="font-bold text-brand-900 mb-1">
|
||||
Route:
|
||||
</Text>
|
||||
<Text className="text-brand-800 mb-3">
|
||||
{destination} → {origin}
|
||||
</Text>
|
||||
</Column>
|
||||
</Row>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
<Text className="text-brand-700 mb-4 text-center text-sm">
|
||||
Please note that it may take up to 48 hours for the payment to
|
||||
be processed and your flight to be confirmed with the airline.
|
||||
</Text>
|
||||
|
||||
{/* Call to action */}
|
||||
<Section className="text-center mt-8 mb-6">
|
||||
<Button
|
||||
className="bg-primary text-white font-bold px-6 py-3 rounded-md"
|
||||
href={`${baseUrl}/track?pnr=${pnr}`}
|
||||
>
|
||||
View Booking Online
|
||||
</Button>
|
||||
</Section>
|
||||
|
||||
<Text className="text-brand-700 text-xs mb-4 text-center">
|
||||
You can use the button above or your booking reference to check
|
||||
in online, make changes to your booking, or add additional
|
||||
services.
|
||||
</Text>
|
||||
|
||||
<Hr className="border-t border-brand-100 my-6" />
|
||||
|
||||
{/* Footer */}
|
||||
<Text className="text-brand-500 text-sm text-center">
|
||||
Thank you for choosing {companyName} for your journey. We hope
|
||||
you have a wonderful trip!
|
||||
</Text>
|
||||
</Section>
|
||||
|
||||
{/* Footer disclaimer */}
|
||||
<Text className="text-brand-400 text-xs text-center px-4">
|
||||
This is an automated message. Please do not reply to this email.
|
||||
For any changes to your booking, please use the "Manage My
|
||||
Booking" button above or contact our customer service team.
|
||||
</Text>
|
||||
</Container>
|
||||
</Body>
|
||||
</Tailwind>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
|
||||
PnrConfirmationEmail.PreviewProps = {
|
||||
pnr: "ABC123",
|
||||
origin: "SFO",
|
||||
destination: "JFK",
|
||||
departureDate: "2023-12-15",
|
||||
returnDate: "2023-12-22",
|
||||
passengerName: "John Smith",
|
||||
baseUrl: "https://flytickettravel.com",
|
||||
logoPath: "https://flytickettravel.com/assets/logos/logo-main.svg",
|
||||
companyName: "FlyTicketTravel",
|
||||
};
|
||||
|
||||
export default PnrConfirmationEmail;
|
||||
89
packages/email/index.ts
Normal file
89
packages/email/index.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import {
|
||||
EmailProviders,
|
||||
type EmailProviderTypes,
|
||||
type EmailServiceProvider,
|
||||
type EmailPayload,
|
||||
} from "./src/data";
|
||||
import { ERROR_CODES, type Result } from "@pkg/result";
|
||||
import { getError } from "@pkg/logger";
|
||||
import { ResendEmailSvcProvider } from "./src/resend.provider";
|
||||
|
||||
export class EmailService {
|
||||
private provider: EmailServiceProvider | null = null;
|
||||
|
||||
initialize(apiKey: string): Result<boolean> {
|
||||
try {
|
||||
this.provider = this.createProvider(EmailProviders.Resend, { apiKey });
|
||||
return this.provider.initialize();
|
||||
} catch (e) {
|
||||
return {
|
||||
error: getError(
|
||||
{
|
||||
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
||||
message: "Failed to initialize email provider",
|
||||
userHint: "Please check email configuration",
|
||||
detail: "Error creating email provider",
|
||||
},
|
||||
e,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private createProvider(
|
||||
type: EmailProviderTypes,
|
||||
config: { apiKey: string },
|
||||
): EmailServiceProvider {
|
||||
switch (type) {
|
||||
case EmailProviders.Resend:
|
||||
return new ResendEmailSvcProvider(config.apiKey);
|
||||
default:
|
||||
throw new Error(`Unsupported email provider: ${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
async sendEmail(
|
||||
payload: EmailPayload,
|
||||
config: { apiKey: string },
|
||||
): Promise<Result<boolean>> {
|
||||
if (!this.provider) {
|
||||
const initResult = this.initialize(config.apiKey);
|
||||
if (initResult.error) return initResult;
|
||||
}
|
||||
return await this.provider!.sendEmail(payload);
|
||||
}
|
||||
|
||||
async sendEmailWithReactTemplate(
|
||||
payload: {
|
||||
to: string;
|
||||
subject: string;
|
||||
from: string;
|
||||
cc?: string[];
|
||||
bcc?: string[];
|
||||
attachments?: any[];
|
||||
template: string;
|
||||
templateData: any;
|
||||
},
|
||||
config: { apiKey: string },
|
||||
): Promise<Result<boolean>> {
|
||||
if (!this.provider) {
|
||||
const initResult = this.initialize(config.apiKey);
|
||||
if (initResult.error) return initResult;
|
||||
}
|
||||
|
||||
// This assumes you're using Resend as the provider
|
||||
if (this.provider instanceof ResendEmailSvcProvider) {
|
||||
return await this.provider.sendEmailWithReact(payload);
|
||||
} else {
|
||||
return {
|
||||
error: getError({
|
||||
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
||||
message:
|
||||
"React templates are only supported with the Resend provider",
|
||||
userHint: "Please configure Resend as your email provider",
|
||||
detail: "Current email provider doesn't support React templates",
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
27
packages/email/package.json
Normal file
27
packages/email/package.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "@pkg/email",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"sample-dev": "email dev --port 5069"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"react-email": "4.0.11"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@pkg/logger": "workspace:*",
|
||||
"@pkg/result": "workspace:*",
|
||||
"@react-email/components": "0.0.38",
|
||||
"@react-email/font": "0.0.9",
|
||||
"@react-email/tailwind": "1.0.5",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"resend": "^4.5.0",
|
||||
"zod": "^3.24.3"
|
||||
}
|
||||
}
|
||||
44
packages/email/src/data.ts
Normal file
44
packages/email/src/data.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { Result } from "@pkg/result";
|
||||
import { z } from "zod";
|
||||
|
||||
export interface EmailServiceProvider {
|
||||
initialize(): Result<boolean>;
|
||||
sendEmail(payload: EmailPayload): Promise<Result<boolean>>;
|
||||
sendEmailWithReact(payload: {
|
||||
to: string;
|
||||
subject: string;
|
||||
from: string;
|
||||
cc?: string[];
|
||||
bcc?: string[];
|
||||
attachments?: any[];
|
||||
template: string;
|
||||
templateData: any;
|
||||
}): Promise<Result<boolean>>;
|
||||
}
|
||||
|
||||
export const EmailProviders = {
|
||||
Resend: "Resend",
|
||||
SendGrid: "SendGrid",
|
||||
} as const;
|
||||
export type EmailProviderTypes = keyof typeof EmailProviders;
|
||||
|
||||
export const emailConfigModel = z.object({
|
||||
provider: z.string(),
|
||||
config: z.object({
|
||||
apiKey: z.string(),
|
||||
}),
|
||||
});
|
||||
export type EmailConfig = z.infer<typeof emailConfigModel>;
|
||||
|
||||
export const emailPayloadModel = z.object({
|
||||
to: z.string().email(),
|
||||
subject: z.string(),
|
||||
from: z.string().email().optional(),
|
||||
cc: z.array(z.string().email()).optional(),
|
||||
bcc: z.array(z.string().email()).optional(),
|
||||
attachments: z.array(z.any()).optional(),
|
||||
html: z.string().optional(),
|
||||
text: z.string().optional(),
|
||||
react: z.any(),
|
||||
});
|
||||
export type EmailPayload = z.infer<typeof emailPayloadModel>;
|
||||
127
packages/email/src/resend.provider.ts
Normal file
127
packages/email/src/resend.provider.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { Resend } from "resend";
|
||||
import { type EmailServiceProvider, type EmailPayload } from "./data";
|
||||
import { ERROR_CODES, type Result } from "@pkg/result";
|
||||
import { getError } from "@pkg/logger";
|
||||
import PnrConfirmationEmail from "../emails/pnr-confirmation";
|
||||
import { createElement } from "react";
|
||||
|
||||
export class ResendEmailSvcProvider implements EmailServiceProvider {
|
||||
private client: Resend | null = null;
|
||||
|
||||
constructor(private apiKey: string) {}
|
||||
|
||||
initialize(): Result<boolean> {
|
||||
try {
|
||||
this.client = new Resend(this.apiKey);
|
||||
return { data: true };
|
||||
} catch (e) {
|
||||
return {
|
||||
error: getError(
|
||||
{
|
||||
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
||||
message: "Failed to initialize Resend client",
|
||||
userHint: "Please check API key configuration",
|
||||
detail: "Error initializing Resend client",
|
||||
},
|
||||
e,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async sendEmail(payload: EmailPayload): Promise<Result<boolean>> {
|
||||
try {
|
||||
if (!this.client) {
|
||||
const initResult = this.initialize();
|
||||
if (initResult.error) return initResult;
|
||||
}
|
||||
|
||||
await this.client!.emails.send({
|
||||
from: payload.from || "onboarding@resend.dev",
|
||||
to: payload.to,
|
||||
subject: payload.subject,
|
||||
html: payload.html ?? "",
|
||||
text: payload.text,
|
||||
cc: payload.cc,
|
||||
bcc: payload.bcc,
|
||||
attachments: payload.attachments,
|
||||
});
|
||||
|
||||
return { data: true };
|
||||
} catch (e) {
|
||||
return {
|
||||
error: getError(
|
||||
{
|
||||
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
||||
message: "Failed to send email",
|
||||
userHint: "Please try again later",
|
||||
detail: "Error sending email via Resend",
|
||||
},
|
||||
e,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async sendEmailWithReact(payload: {
|
||||
to: string;
|
||||
subject: string;
|
||||
from: string;
|
||||
cc?: string[];
|
||||
bcc?: string[];
|
||||
attachments?: any[];
|
||||
template: string;
|
||||
templateData: any;
|
||||
}): Promise<Result<boolean>> {
|
||||
try {
|
||||
if (!this.client) {
|
||||
const initResult = this.initialize();
|
||||
if (initResult.error) return initResult;
|
||||
}
|
||||
|
||||
let body = undefined as any;
|
||||
|
||||
switch (payload.template) {
|
||||
case "pnr-confirmation":
|
||||
body = createElement(PnrConfirmationEmail, payload.templateData);
|
||||
break;
|
||||
default:
|
||||
return {
|
||||
error: getError(
|
||||
{
|
||||
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
||||
message: "Unsupported email template",
|
||||
userHint: "Please check email configuration",
|
||||
detail: "Unsupported email template",
|
||||
},
|
||||
new Error("Unsupported email template"),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
await this.client!.emails.send({
|
||||
from: payload.from,
|
||||
to: payload.to,
|
||||
subject: payload.subject,
|
||||
cc: payload.cc,
|
||||
bcc: payload.bcc,
|
||||
attachments: payload.attachments,
|
||||
react: body,
|
||||
});
|
||||
|
||||
return { data: true };
|
||||
} catch (e) {
|
||||
return {
|
||||
error: getError(
|
||||
{
|
||||
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
||||
message: "Failed to send email with React template",
|
||||
userHint: "Please try again later",
|
||||
detail: "Error sending email via Resend",
|
||||
},
|
||||
e,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
28
packages/email/tsconfig.json
Normal file
28
packages/email/tsconfig.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Environment setup & latest features
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
}
|
||||
97
packages/logger/client.ts
Normal file
97
packages/logger/client.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
type LogLevel = "error" | "warn" | "info" | "http" | "debug";
|
||||
|
||||
export interface LogEntry {
|
||||
level: LogLevel;
|
||||
timestamp: string;
|
||||
message: any;
|
||||
metadata?: any;
|
||||
}
|
||||
|
||||
interface Error {
|
||||
code: string;
|
||||
message: string;
|
||||
userHint?: string;
|
||||
detail?: string;
|
||||
error?: any;
|
||||
actionable?: boolean;
|
||||
}
|
||||
|
||||
class BrowserLogger {
|
||||
private async sendLog(entry: LogEntry) {
|
||||
try {
|
||||
// Only send logs to server in production
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
await fetch("/api/logs", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(entry),
|
||||
});
|
||||
}
|
||||
|
||||
// Always log to console in development
|
||||
const consoleMethod = entry.level === "debug" ? "log" : entry.level;
|
||||
// @ts-ignore
|
||||
console[consoleMethod as keyof Console](
|
||||
`[${entry.level}] ${entry.timestamp}: `,
|
||||
entry.message,
|
||||
entry.metadata || "",
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("Failed to send log to server:", err);
|
||||
// @ts-ignore
|
||||
console[entry.level as keyof Console](entry.message);
|
||||
}
|
||||
}
|
||||
|
||||
private createLogEntry(
|
||||
level: LogLevel,
|
||||
message: any,
|
||||
metadata?: any,
|
||||
): LogEntry {
|
||||
return {
|
||||
level,
|
||||
timestamp: new Date().toISOString(),
|
||||
message,
|
||||
metadata,
|
||||
};
|
||||
}
|
||||
|
||||
error(message: any, metadata?: any) {
|
||||
this.sendLog(this.createLogEntry("error", message, metadata));
|
||||
}
|
||||
|
||||
warn(message: any, metadata?: any) {
|
||||
this.sendLog(this.createLogEntry("warn", message, metadata));
|
||||
}
|
||||
|
||||
info(message: any, metadata?: any) {
|
||||
this.sendLog(this.createLogEntry("info", message, metadata));
|
||||
}
|
||||
|
||||
http(message: any, metadata?: any) {
|
||||
this.sendLog(this.createLogEntry("http", message, metadata));
|
||||
}
|
||||
|
||||
debug(message: any, metadata?: any) {
|
||||
this.sendLog(this.createLogEntry("debug", message, metadata));
|
||||
}
|
||||
}
|
||||
|
||||
const ClientLogger = new BrowserLogger();
|
||||
|
||||
function getError(payload: Error, error?: any) {
|
||||
ClientLogger.error(payload);
|
||||
if (error) {
|
||||
ClientLogger.error(error);
|
||||
}
|
||||
return {
|
||||
code: payload.code,
|
||||
message: payload.message,
|
||||
userHint: payload.userHint,
|
||||
detail: payload.detail,
|
||||
error: error,
|
||||
actionable: payload.actionable,
|
||||
} as Error;
|
||||
}
|
||||
|
||||
export { ClientLogger, getError };
|
||||
105
packages/logger/index.ts
Normal file
105
packages/logger/index.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import winston from "winston";
|
||||
import DailyRotateFile from "winston-daily-rotate-file";
|
||||
import path from "path";
|
||||
import type { Err } from "@pkg/result";
|
||||
import util from "util";
|
||||
|
||||
process.on("warning", (warning) => {
|
||||
if (warning.message.includes("punycode")) {
|
||||
return;
|
||||
}
|
||||
console.warn(warning);
|
||||
});
|
||||
|
||||
const levels = {
|
||||
error: 0,
|
||||
warn: 1,
|
||||
info: 2,
|
||||
http: 3,
|
||||
debug: 4,
|
||||
};
|
||||
|
||||
const colors = {
|
||||
error: "red",
|
||||
warn: "yellow",
|
||||
info: "green",
|
||||
http: "magenta",
|
||||
debug: "white",
|
||||
};
|
||||
|
||||
const level = () => {
|
||||
const env = process.env.NODE_ENV || "development";
|
||||
return env === "development" ? "debug" : "warn";
|
||||
};
|
||||
|
||||
const format = winston.format.combine(
|
||||
winston.format.errors({ stack: true }),
|
||||
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss:ms" }),
|
||||
winston.format.colorize({ all: true }),
|
||||
winston.format.printf((info) => {
|
||||
const { level, message, timestamp, ...extra } = info;
|
||||
|
||||
let formattedMessage = "";
|
||||
if (message instanceof Error) {
|
||||
formattedMessage = message.stack || message.message;
|
||||
} else if (typeof message === "object") {
|
||||
formattedMessage = util.inspect(message, { depth: null, colors: true });
|
||||
} else {
|
||||
formattedMessage = message as any as string;
|
||||
}
|
||||
|
||||
// Handle extra fields (if any)
|
||||
const formattedExtra =
|
||||
Object.keys(extra).length > 0
|
||||
? `\n${util.inspect(extra, { depth: null, colors: true })}`
|
||||
: "";
|
||||
|
||||
return `[${level}] ${timestamp}: ${formattedMessage}${formattedExtra}`;
|
||||
}),
|
||||
);
|
||||
|
||||
const transports = [
|
||||
new winston.transports.Console(),
|
||||
|
||||
new DailyRotateFile({
|
||||
filename: path.join("logs", "error-%DATE%.log"),
|
||||
datePattern: "YYYY-MM-DD",
|
||||
level: "error",
|
||||
maxSize: "5m",
|
||||
maxFiles: "14d",
|
||||
format: format,
|
||||
}),
|
||||
|
||||
new DailyRotateFile({
|
||||
filename: path.join("logs", "all-%DATE%.log"),
|
||||
datePattern: "YYYY-MM-DD",
|
||||
maxSize: "5m",
|
||||
maxFiles: "14d",
|
||||
format: format,
|
||||
}),
|
||||
];
|
||||
|
||||
winston.addColors(colors);
|
||||
|
||||
const Logger = winston.createLogger({
|
||||
level: level(),
|
||||
levels,
|
||||
format,
|
||||
transports,
|
||||
});
|
||||
|
||||
const stream = { write: (message: string) => Logger.http(message.trim()) };
|
||||
|
||||
function getError(payload: Err, error?: any) {
|
||||
Logger.error(JSON.stringify({ payload, error }, null, 2));
|
||||
return {
|
||||
code: payload.code,
|
||||
message: payload.message,
|
||||
userHint: payload.userHint,
|
||||
detail: payload.detail,
|
||||
error: error instanceof Error ? error.message : error,
|
||||
actionable: payload.actionable,
|
||||
} as Err;
|
||||
}
|
||||
|
||||
export { Logger, stream, getError };
|
||||
16
packages/logger/package.json
Normal file
16
packages/logger/package.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "@pkg/logger",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pkg/result": "workspace:*",
|
||||
"winston": "^3.17.0",
|
||||
"winston-daily-rotate-file": "^5.0.0"
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
24
packages/result/index.ts
Normal file
24
packages/result/index.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export const ERROR_CODES = {
|
||||
API_ERROR: "API_ERROR",
|
||||
DATABASE_ERROR: "DATABASE_ERROR",
|
||||
NETWORK_ERROR: "NETWORK_ERROR",
|
||||
AUTH_ERROR: "AUTH_ERROR",
|
||||
PERMISSION_ERROR: "PERMISSION_ERROR",
|
||||
VALIDATION_ERROR: "VALIDATION_ERROR",
|
||||
UNKNOWN_ERROR: "UNKNOWN_ERROR",
|
||||
NOT_FOUND_ERROR: "NOT_FOUND_ERROR",
|
||||
INPUT_ERROR: "INPUT_ERROR",
|
||||
INTERNAL_SERVER_ERROR: "INTERNAL_SERVER_ERROR",
|
||||
EXTERNAL_SERVICE_ERROR: "EXTERNAL_SERVICE_ERROR",
|
||||
} as const;
|
||||
|
||||
export type Err = {
|
||||
code: string;
|
||||
message: string;
|
||||
userHint: string;
|
||||
detail: string;
|
||||
actionable?: boolean;
|
||||
error?: any;
|
||||
};
|
||||
|
||||
export type Result<T> = { data?: T; error?: Err };
|
||||
9
packages/result/package.json
Normal file
9
packages/result/package.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "@pkg/result",
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user