feat: server
This commit is contained in:
parent
01c42bf9fc
commit
21f459e695
21
package.json
21
package.json
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "subscriptions-service",
|
"name": "subscriptions-service",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun run --hot src/index.ts",
|
"dev": "bun run --hot src/index.tsx",
|
||||||
"db:generate": "drizzle-kit generate",
|
"db:generate": "drizzle-kit generate",
|
||||||
"db:migrate": "drizzle-kit migrate",
|
"db:migrate": "drizzle-kit migrate",
|
||||||
"db:seed": "bun run src/server/seeds/index.ts"
|
"db:seed": "bun run src/server/seeds/index.ts"
|
||||||
@ -10,31 +10,36 @@
|
|||||||
"@auth/core": "^0.40.0",
|
"@auth/core": "^0.40.0",
|
||||||
"@hono/auth-js": "^1.1.0",
|
"@hono/auth-js": "^1.1.0",
|
||||||
"@hono/node-server": "^1.19.3",
|
"@hono/node-server": "^1.19.3",
|
||||||
|
"@hono/react-renderer": "^1.0.1",
|
||||||
|
"@hono/standard-validator": "^0.1.5",
|
||||||
|
"@hono/swagger-ui": "^0.5.2",
|
||||||
|
"bcryptjs": "^2.4.3",
|
||||||
"bullmq": "^5.58.6",
|
"bullmq": "^5.58.6",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"drizzle-kit": "^0.31.4",
|
"drizzle-kit": "^0.31.4",
|
||||||
"drizzle-orm": "^0.44.5",
|
"drizzle-orm": "^0.44.5",
|
||||||
"hono": "^4.9.8",
|
"hono": "^4.9.8",
|
||||||
|
"hono-openapi": "^1.0.8",
|
||||||
"ioredis": "^5.7.0",
|
"ioredis": "^5.7.0",
|
||||||
|
"lucide-react": "^0.544.0",
|
||||||
"node-cron": "^4.2.1",
|
"node-cron": "^4.2.1",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
"postgres": "^3.4.4",
|
"postgres": "^3.4.4",
|
||||||
"@hono/react-renderer": "^1.0.0",
|
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"bcryptjs": "^2.4.3"
|
"zod": "^4.1.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"@types/node": "^24.5.2",
|
"@types/node": "^24.5.2",
|
||||||
|
"@types/node-cron": "^3.0.11",
|
||||||
|
"@types/react": "^19.1.13",
|
||||||
|
"@types/react-dom": "^19.1.9",
|
||||||
|
"drizzle-kit": "^0.24.0",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.35.0",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"tsx": "^4.20.5",
|
"tsx": "^4.20.5",
|
||||||
"@types/react": "^18.3.5",
|
|
||||||
"@types/react-dom": "^18.3.0",
|
|
||||||
"@types/bcryptjs": "^2.4.6",
|
|
||||||
"@types/node-cron": "^3.0.11",
|
|
||||||
"drizzle-kit": "^0.24.0",
|
|
||||||
"typescript": "^5.5.3"
|
"typescript": "^5.5.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,15 @@ CREATE TABLE "packages" (
|
|||||||
CONSTRAINT "packages_name_unique" UNIQUE("name")
|
CONSTRAINT "packages_name_unique" UNIQUE("name")
|
||||||
);
|
);
|
||||||
--> statement-breakpoint
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "products" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"name" varchar(255) NOT NULL,
|
||||||
|
"description" varchar(255) NOT NULL,
|
||||||
|
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "products_name_unique" UNIQUE("name")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
CREATE TABLE "subscriptions" (
|
CREATE TABLE "subscriptions" (
|
||||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
"protect_key" varchar(255) NOT NULL,
|
"protect_key" varchar(255) NOT NULL,
|
||||||
@ -28,15 +37,5 @@ CREATE TABLE "subscriptions" (
|
|||||||
CONSTRAINT "subscriptions_protect_key_unique" UNIQUE("protect_key")
|
CONSTRAINT "subscriptions_protect_key_unique" UNIQUE("protect_key")
|
||||||
);
|
);
|
||||||
--> statement-breakpoint
|
--> statement-breakpoint
|
||||||
CREATE TABLE "products" (
|
|
||||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
||||||
"name" varchar(255) NOT NULL,
|
|
||||||
"description" varchar(255) NOT NULL,
|
|
||||||
"features" jsonb NOT NULL,
|
|
||||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
|
||||||
"updated_at" timestamp DEFAULT now() NOT NULL,
|
|
||||||
CONSTRAINT "products_name_unique" UNIQUE("name")
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
ALTER TABLE "packages" ADD CONSTRAINT "packages_product_id_products_id_fk" FOREIGN KEY ("product_id") REFERENCES "public"."products"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
ALTER TABLE "packages" ADD CONSTRAINT "packages_product_id_products_id_fk" FOREIGN KEY ("product_id") REFERENCES "public"."products"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
ALTER TABLE "subscriptions" ADD CONSTRAINT "subscriptions_package_id_packages_id_fk" FOREIGN KEY ("package_id") REFERENCES "public"."packages"("id") ON DELETE no action ON UPDATE no action;
|
ALTER TABLE "subscriptions" ADD CONSTRAINT "subscriptions_package_id_packages_id_fk" FOREIGN KEY ("package_id") REFERENCES "public"."packages"("id") ON DELETE no action ON UPDATE no action;
|
||||||
2
src/database/migrations/0001_odd_stingray.sql
Normal file
2
src/database/migrations/0001_odd_stingray.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "products" ALTER COLUMN "description" SET DEFAULT '';--> statement-breakpoint
|
||||||
|
ALTER TABLE "products" ALTER COLUMN "description" DROP NOT NULL;
|
||||||
2
src/database/migrations/0002_sloppy_infant_terrible.sql
Normal file
2
src/database/migrations/0002_sloppy_infant_terrible.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
DROP TYPE "public"."subscriber_type";--> statement-breakpoint
|
||||||
|
CREATE TYPE "public"."subscriber_type" AS ENUM('student', 'company', 'personal');
|
||||||
1
src/database/migrations/0003_shocking_barracuda.sql
Normal file
1
src/database/migrations/0003_shocking_barracuda.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "subscriptions" ADD COLUMN "used_features" jsonb NOT NULL;
|
||||||
2
src/database/migrations/0004_faithful_daredevil.sql
Normal file
2
src/database/migrations/0004_faithful_daredevil.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "subscriptions" ADD COLUMN "product_id" uuid NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "subscriptions" ADD CONSTRAINT "subscriptions_product_id_products_id_fk" FOREIGN KEY ("product_id") REFERENCES "public"."products"("id") ON DELETE no action ON UPDATE no action;
|
||||||
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"id": "edfbe82b-5107-421b-9e3c-8170ec30d17c",
|
"id": "4a2b23d8-2835-4ba3-a254-ff9d24c1bf35",
|
||||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"dialect": "postgresql",
|
"dialect": "postgresql",
|
||||||
@ -91,6 +91,60 @@
|
|||||||
"checkConstraints": {},
|
"checkConstraints": {},
|
||||||
"isRLSEnabled": false
|
"isRLSEnabled": false
|
||||||
},
|
},
|
||||||
|
"public.products": {
|
||||||
|
"name": "products",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"products_name_unique": {
|
||||||
|
"name": "products_name_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"name"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
"public.subscriptions": {
|
"public.subscriptions": {
|
||||||
"name": "subscriptions",
|
"name": "subscriptions",
|
||||||
"schema": "",
|
"schema": "",
|
||||||
@ -198,66 +252,6 @@
|
|||||||
"policies": {},
|
"policies": {},
|
||||||
"checkConstraints": {},
|
"checkConstraints": {},
|
||||||
"isRLSEnabled": false
|
"isRLSEnabled": false
|
||||||
},
|
|
||||||
"public.products": {
|
|
||||||
"name": "products",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "uuid",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"default": "gen_random_uuid()"
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"name": "name",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"name": "description",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"features": {
|
|
||||||
"name": "features",
|
|
||||||
"type": "jsonb",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"default": "now()"
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"name": "updated_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"default": "now()"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"products_name_unique": {
|
|
||||||
"name": "products_name_unique",
|
|
||||||
"nullsNotDistinct": false,
|
|
||||||
"columns": [
|
|
||||||
"name"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"policies": {},
|
|
||||||
"checkConstraints": {},
|
|
||||||
"isRLSEnabled": false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"enums": {
|
"enums": {
|
||||||
|
|||||||
298
src/database/migrations/meta/0001_snapshot.json
Normal file
298
src/database/migrations/meta/0001_snapshot.json
Normal file
@ -0,0 +1,298 @@
|
|||||||
|
{
|
||||||
|
"id": "acf4bf99-bec3-4cfd-a8b1-b53a3df22200",
|
||||||
|
"prevId": "4a2b23d8-2835-4ba3-a254-ff9d24c1bf35",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.packages": {
|
||||||
|
"name": "packages",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"price": {
|
||||||
|
"name": "price",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"product_id": {
|
||||||
|
"name": "product_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"features": {
|
||||||
|
"name": "features",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"support_type": {
|
||||||
|
"name": "support_type",
|
||||||
|
"type": "support_type",
|
||||||
|
"typeSchema": "public",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"packages_product_id_products_id_fk": {
|
||||||
|
"name": "packages_product_id_products_id_fk",
|
||||||
|
"tableFrom": "packages",
|
||||||
|
"tableTo": "products",
|
||||||
|
"columnsFrom": [
|
||||||
|
"product_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"packages_name_unique": {
|
||||||
|
"name": "packages_name_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"name"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.products": {
|
||||||
|
"name": "products",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "''"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"products_name_unique": {
|
||||||
|
"name": "products_name_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"name"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.subscriptions": {
|
||||||
|
"name": "subscriptions",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"protect_key": {
|
||||||
|
"name": "protect_key",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"subscriber": {
|
||||||
|
"name": "subscriber",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"package_id": {
|
||||||
|
"name": "package_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"start_date": {
|
||||||
|
"name": "start_date",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"duration_in_months": {
|
||||||
|
"name": "duration_in_months",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"end_date": {
|
||||||
|
"name": "end_date",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"sent_sms_count": {
|
||||||
|
"name": "sent_sms_count",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "subscription_status",
|
||||||
|
"typeSchema": "public",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'active'"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"subscriptions_package_id_packages_id_fk": {
|
||||||
|
"name": "subscriptions_package_id_packages_id_fk",
|
||||||
|
"tableFrom": "subscriptions",
|
||||||
|
"tableTo": "packages",
|
||||||
|
"columnsFrom": [
|
||||||
|
"package_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"subscriptions_protect_key_unique": {
|
||||||
|
"name": "subscriptions_protect_key_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"protect_key"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {
|
||||||
|
"public.support_type": {
|
||||||
|
"name": "support_type",
|
||||||
|
"schema": "public",
|
||||||
|
"values": [
|
||||||
|
"standard",
|
||||||
|
"24/7",
|
||||||
|
"premium"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"public.subscriber_type": {
|
||||||
|
"name": "subscriber_type",
|
||||||
|
"schema": "public",
|
||||||
|
"values": [
|
||||||
|
"microfinance",
|
||||||
|
"student",
|
||||||
|
"company",
|
||||||
|
"school"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"public.subscription_status": {
|
||||||
|
"name": "subscription_status",
|
||||||
|
"schema": "public",
|
||||||
|
"values": [
|
||||||
|
"active",
|
||||||
|
"expired",
|
||||||
|
"canceled"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
297
src/database/migrations/meta/0002_snapshot.json
Normal file
297
src/database/migrations/meta/0002_snapshot.json
Normal file
@ -0,0 +1,297 @@
|
|||||||
|
{
|
||||||
|
"id": "d4a13631-0f62-40d8-b907-ecd4693eb607",
|
||||||
|
"prevId": "acf4bf99-bec3-4cfd-a8b1-b53a3df22200",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.packages": {
|
||||||
|
"name": "packages",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"price": {
|
||||||
|
"name": "price",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"product_id": {
|
||||||
|
"name": "product_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"features": {
|
||||||
|
"name": "features",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"support_type": {
|
||||||
|
"name": "support_type",
|
||||||
|
"type": "support_type",
|
||||||
|
"typeSchema": "public",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"packages_product_id_products_id_fk": {
|
||||||
|
"name": "packages_product_id_products_id_fk",
|
||||||
|
"tableFrom": "packages",
|
||||||
|
"tableTo": "products",
|
||||||
|
"columnsFrom": [
|
||||||
|
"product_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"packages_name_unique": {
|
||||||
|
"name": "packages_name_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"name"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.products": {
|
||||||
|
"name": "products",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "''"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"products_name_unique": {
|
||||||
|
"name": "products_name_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"name"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.subscriptions": {
|
||||||
|
"name": "subscriptions",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"protect_key": {
|
||||||
|
"name": "protect_key",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"subscriber": {
|
||||||
|
"name": "subscriber",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"package_id": {
|
||||||
|
"name": "package_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"start_date": {
|
||||||
|
"name": "start_date",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"duration_in_months": {
|
||||||
|
"name": "duration_in_months",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"end_date": {
|
||||||
|
"name": "end_date",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"sent_sms_count": {
|
||||||
|
"name": "sent_sms_count",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "subscription_status",
|
||||||
|
"typeSchema": "public",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'active'"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"subscriptions_package_id_packages_id_fk": {
|
||||||
|
"name": "subscriptions_package_id_packages_id_fk",
|
||||||
|
"tableFrom": "subscriptions",
|
||||||
|
"tableTo": "packages",
|
||||||
|
"columnsFrom": [
|
||||||
|
"package_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"subscriptions_protect_key_unique": {
|
||||||
|
"name": "subscriptions_protect_key_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"protect_key"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {
|
||||||
|
"public.support_type": {
|
||||||
|
"name": "support_type",
|
||||||
|
"schema": "public",
|
||||||
|
"values": [
|
||||||
|
"standard",
|
||||||
|
"24/7",
|
||||||
|
"premium"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"public.subscriber_type": {
|
||||||
|
"name": "subscriber_type",
|
||||||
|
"schema": "public",
|
||||||
|
"values": [
|
||||||
|
"student",
|
||||||
|
"company",
|
||||||
|
"personal"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"public.subscription_status": {
|
||||||
|
"name": "subscription_status",
|
||||||
|
"schema": "public",
|
||||||
|
"values": [
|
||||||
|
"active",
|
||||||
|
"expired",
|
||||||
|
"canceled"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
303
src/database/migrations/meta/0003_snapshot.json
Normal file
303
src/database/migrations/meta/0003_snapshot.json
Normal file
@ -0,0 +1,303 @@
|
|||||||
|
{
|
||||||
|
"id": "b42800ef-0563-4446-aa07-e71dc4becb44",
|
||||||
|
"prevId": "d4a13631-0f62-40d8-b907-ecd4693eb607",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.packages": {
|
||||||
|
"name": "packages",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"price": {
|
||||||
|
"name": "price",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"product_id": {
|
||||||
|
"name": "product_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"features": {
|
||||||
|
"name": "features",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"support_type": {
|
||||||
|
"name": "support_type",
|
||||||
|
"type": "support_type",
|
||||||
|
"typeSchema": "public",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"packages_product_id_products_id_fk": {
|
||||||
|
"name": "packages_product_id_products_id_fk",
|
||||||
|
"tableFrom": "packages",
|
||||||
|
"tableTo": "products",
|
||||||
|
"columnsFrom": [
|
||||||
|
"product_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"packages_name_unique": {
|
||||||
|
"name": "packages_name_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"name"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.products": {
|
||||||
|
"name": "products",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "''"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"products_name_unique": {
|
||||||
|
"name": "products_name_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"name"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.subscriptions": {
|
||||||
|
"name": "subscriptions",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"protect_key": {
|
||||||
|
"name": "protect_key",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"subscriber": {
|
||||||
|
"name": "subscriber",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"package_id": {
|
||||||
|
"name": "package_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"start_date": {
|
||||||
|
"name": "start_date",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"duration_in_months": {
|
||||||
|
"name": "duration_in_months",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"end_date": {
|
||||||
|
"name": "end_date",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"sent_sms_count": {
|
||||||
|
"name": "sent_sms_count",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "subscription_status",
|
||||||
|
"typeSchema": "public",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'active'"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"used_features": {
|
||||||
|
"name": "used_features",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"subscriptions_package_id_packages_id_fk": {
|
||||||
|
"name": "subscriptions_package_id_packages_id_fk",
|
||||||
|
"tableFrom": "subscriptions",
|
||||||
|
"tableTo": "packages",
|
||||||
|
"columnsFrom": [
|
||||||
|
"package_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"subscriptions_protect_key_unique": {
|
||||||
|
"name": "subscriptions_protect_key_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"protect_key"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {
|
||||||
|
"public.support_type": {
|
||||||
|
"name": "support_type",
|
||||||
|
"schema": "public",
|
||||||
|
"values": [
|
||||||
|
"standard",
|
||||||
|
"24/7",
|
||||||
|
"premium"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"public.subscriber_type": {
|
||||||
|
"name": "subscriber_type",
|
||||||
|
"schema": "public",
|
||||||
|
"values": [
|
||||||
|
"student",
|
||||||
|
"company",
|
||||||
|
"personal"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"public.subscription_status": {
|
||||||
|
"name": "subscription_status",
|
||||||
|
"schema": "public",
|
||||||
|
"values": [
|
||||||
|
"active",
|
||||||
|
"expired",
|
||||||
|
"canceled"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
322
src/database/migrations/meta/0004_snapshot.json
Normal file
322
src/database/migrations/meta/0004_snapshot.json
Normal file
@ -0,0 +1,322 @@
|
|||||||
|
{
|
||||||
|
"id": "f50b9714-0193-4e22-9a51-8754c3d60fe0",
|
||||||
|
"prevId": "b42800ef-0563-4446-aa07-e71dc4becb44",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.packages": {
|
||||||
|
"name": "packages",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"price": {
|
||||||
|
"name": "price",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"product_id": {
|
||||||
|
"name": "product_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"features": {
|
||||||
|
"name": "features",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"support_type": {
|
||||||
|
"name": "support_type",
|
||||||
|
"type": "support_type",
|
||||||
|
"typeSchema": "public",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"packages_product_id_products_id_fk": {
|
||||||
|
"name": "packages_product_id_products_id_fk",
|
||||||
|
"tableFrom": "packages",
|
||||||
|
"tableTo": "products",
|
||||||
|
"columnsFrom": [
|
||||||
|
"product_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"packages_name_unique": {
|
||||||
|
"name": "packages_name_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"name"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.products": {
|
||||||
|
"name": "products",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "''"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"products_name_unique": {
|
||||||
|
"name": "products_name_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"name"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.subscriptions": {
|
||||||
|
"name": "subscriptions",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"protect_key": {
|
||||||
|
"name": "protect_key",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"subscriber": {
|
||||||
|
"name": "subscriber",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"package_id": {
|
||||||
|
"name": "package_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"product_id": {
|
||||||
|
"name": "product_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"start_date": {
|
||||||
|
"name": "start_date",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"duration_in_months": {
|
||||||
|
"name": "duration_in_months",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"end_date": {
|
||||||
|
"name": "end_date",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"sent_sms_count": {
|
||||||
|
"name": "sent_sms_count",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "subscription_status",
|
||||||
|
"typeSchema": "public",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'active'"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"used_features": {
|
||||||
|
"name": "used_features",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"subscriptions_package_id_packages_id_fk": {
|
||||||
|
"name": "subscriptions_package_id_packages_id_fk",
|
||||||
|
"tableFrom": "subscriptions",
|
||||||
|
"tableTo": "packages",
|
||||||
|
"columnsFrom": [
|
||||||
|
"package_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"subscriptions_product_id_products_id_fk": {
|
||||||
|
"name": "subscriptions_product_id_products_id_fk",
|
||||||
|
"tableFrom": "subscriptions",
|
||||||
|
"tableTo": "products",
|
||||||
|
"columnsFrom": [
|
||||||
|
"product_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"subscriptions_protect_key_unique": {
|
||||||
|
"name": "subscriptions_protect_key_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"protect_key"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {
|
||||||
|
"public.support_type": {
|
||||||
|
"name": "support_type",
|
||||||
|
"schema": "public",
|
||||||
|
"values": [
|
||||||
|
"standard",
|
||||||
|
"24/7",
|
||||||
|
"premium"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"public.subscriber_type": {
|
||||||
|
"name": "subscriber_type",
|
||||||
|
"schema": "public",
|
||||||
|
"values": [
|
||||||
|
"student",
|
||||||
|
"company",
|
||||||
|
"personal"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"public.subscription_status": {
|
||||||
|
"name": "subscription_status",
|
||||||
|
"schema": "public",
|
||||||
|
"values": [
|
||||||
|
"active",
|
||||||
|
"expired",
|
||||||
|
"canceled"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,8 +5,36 @@
|
|||||||
{
|
{
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"when": 1758304299810,
|
"when": 1758426395659,
|
||||||
"tag": "0000_ordinary_micromax",
|
"tag": "0000_lively_ben_grimm",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 1,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1758426941130,
|
||||||
|
"tag": "0001_odd_stingray",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 2,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1758437134222,
|
||||||
|
"tag": "0002_sloppy_infant_terrible",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 3,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1758455474529,
|
||||||
|
"tag": "0003_shocking_barracuda",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 4,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1758458041709,
|
||||||
|
"tag": "0004_faithful_daredevil",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
export * from './schema/packages';
|
export * from './schema/packages';
|
||||||
|
export * from './schema/products';
|
||||||
export * from './schema/subscriptions';
|
export * from './schema/subscriptions';
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
import { jsonb, pgTable, timestamp, uuid, varchar } from 'drizzle-orm/pg-core';
|
import { pgTable, timestamp, uuid, varchar } from 'drizzle-orm/pg-core';
|
||||||
|
|
||||||
export const products = pgTable('products', {
|
export const products = pgTable('products', {
|
||||||
id: uuid('id').primaryKey().defaultRandom(),
|
id: uuid('id').primaryKey().defaultRandom(),
|
||||||
name: varchar('name', { length: 255 }).notNull().unique(),
|
name: varchar('name', { length: 255 }).notNull().unique(),
|
||||||
description: varchar('description', { length: 255 }).notNull(),
|
description: varchar('description', { length: 255 }).default(''),
|
||||||
features: jsonb('features').notNull(),
|
|
||||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||||
updatedAt: timestamp('updated_at').notNull().defaultNow(),
|
updatedAt: timestamp('updated_at').notNull().defaultNow(),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,15 +1,17 @@
|
|||||||
import { integer, jsonb, pgEnum, pgTable, timestamp, uuid, varchar } from 'drizzle-orm/pg-core';
|
import { integer, jsonb, pgEnum, pgTable, timestamp, uuid, varchar } from 'drizzle-orm/pg-core';
|
||||||
import type { Subscriber } from '../../../lib/types';
|
import type { ISubscriber } from '../../../lib/types';
|
||||||
import { packages } from './packages';
|
import { packages } from './packages';
|
||||||
|
import { products } from './products';
|
||||||
|
|
||||||
export const subscriberTypeEnum = pgEnum('subscriber_type', ['microfinance', 'student', 'company', 'school']);
|
export const subscriberTypeEnum = pgEnum('subscriber_type', ['student', 'company', 'personal']);
|
||||||
export const subscriptionStatusEnum = pgEnum('subscription_status', ['active', 'expired', 'canceled']);
|
export const subscriptionStatusEnum = pgEnum('subscription_status', ['active', 'expired', 'canceled']);
|
||||||
|
|
||||||
export const subscriptions = pgTable('subscriptions', {
|
export const subscriptions = pgTable('subscriptions', {
|
||||||
id: uuid('id').primaryKey().defaultRandom(),
|
id: uuid('id').primaryKey().defaultRandom(),
|
||||||
protectKey: varchar('protect_key', { length: 255 }).notNull().unique(),
|
protectKey: varchar('protect_key', { length: 255 }).notNull().unique(),
|
||||||
subscriber: jsonb('subscriber').$type<Subscriber>().notNull(),
|
subscriber: jsonb('subscriber').$type<ISubscriber>().notNull(),
|
||||||
package: uuid('package_id').notNull().references(() => packages.id),
|
package: uuid('package_id').notNull().references(() => packages.id),
|
||||||
|
product: uuid('product_id').notNull().references(() => products.id),
|
||||||
startDate: timestamp('start_date').notNull().defaultNow(),
|
startDate: timestamp('start_date').notNull().defaultNow(),
|
||||||
durationInMonths: integer('duration_in_months').notNull(),
|
durationInMonths: integer('duration_in_months').notNull(),
|
||||||
endDate: timestamp('end_date').notNull(),
|
endDate: timestamp('end_date').notNull(),
|
||||||
@ -17,6 +19,7 @@ export const subscriptions = pgTable('subscriptions', {
|
|||||||
status: subscriptionStatusEnum('status').notNull().default('active'),
|
status: subscriptionStatusEnum('status').notNull().default('active'),
|
||||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||||
updatedAt: timestamp('updated_at').notNull().defaultNow(),
|
updatedAt: timestamp('updated_at').notNull().defaultNow(),
|
||||||
|
usedFeatures: jsonb('used_features').notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type Subscription = typeof subscriptions.$inferSelect;
|
export type Subscription = typeof subscriptions.$inferSelect;
|
||||||
50
src/index.ts
50
src/index.ts
@ -1,50 +0,0 @@
|
|||||||
import GitHub from '@auth/core/providers/github';
|
|
||||||
import { authHandler, initAuthConfig, verifyAuth } from '@hono/auth-js';
|
|
||||||
import { serve } from '@hono/node-server';
|
|
||||||
import { serveStatic } from '@hono/node-server/serve-static';
|
|
||||||
import { Hono } from 'hono';
|
|
||||||
import packageRoutes from './server/routes/packages';
|
|
||||||
import productRoutes from './server/routes/products';
|
|
||||||
import subscriptionRoutes from './server/routes/subscriptions';
|
|
||||||
const app = new Hono()
|
|
||||||
|
|
||||||
// Auth middleware
|
|
||||||
app.use(
|
|
||||||
'*',
|
|
||||||
initAuthConfig((c) => ({
|
|
||||||
secret: process.env.AUTH_SECRET,
|
|
||||||
providers: [
|
|
||||||
GitHub({
|
|
||||||
clientId: process.env.GITHUB_ID,
|
|
||||||
clientSecret: process.env.GITHUB_SECRET,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
|
|
||||||
// Static files
|
|
||||||
app.use('/static/*', serveStatic({root: 'src/public'}));
|
|
||||||
|
|
||||||
// Auth routes
|
|
||||||
app.use('/api/auth/*', authHandler())
|
|
||||||
|
|
||||||
// Protected routes
|
|
||||||
app.use('/api/*', verifyAuth())
|
|
||||||
|
|
||||||
// Routes
|
|
||||||
app.route('/api/packages', packageRoutes)
|
|
||||||
app.route('/api/subscriptions', subscriptionRoutes)
|
|
||||||
app.route('/api/products', productRoutes)
|
|
||||||
|
|
||||||
app.get('/', (c) => {
|
|
||||||
return c.text('Hello Hono!')
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
app.get('*', (c) => {
|
|
||||||
return c.notFound()
|
|
||||||
})
|
|
||||||
|
|
||||||
const port = process.env.PORT || 3000;
|
|
||||||
|
|
||||||
serve({ fetch: app.fetch, port: Number(port) })
|
|
||||||
131
src/index.tsx
Normal file
131
src/index.tsx
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import GitHub from '@auth/core/providers/github';
|
||||||
|
import { authHandler, initAuthConfig, verifyAuth } from '@hono/auth-js';
|
||||||
|
import { serve } from '@hono/node-server';
|
||||||
|
import { serveStatic } from '@hono/node-server/serve-static';
|
||||||
|
import { reactRenderer } from '@hono/react-renderer';
|
||||||
|
import { Hono } from 'hono';
|
||||||
|
import packageRoutes from './server/routes/packages';
|
||||||
|
import productRoutes from './server/routes/products';
|
||||||
|
import subscriptionClientRoutes from './server/routes/subscription-client';
|
||||||
|
import subscriptionRoutes from './server/routes/subscriptions';
|
||||||
|
import DashboardPage from './web/pages/DashboardPage';
|
||||||
|
import HomePage from './web/pages/HomePage';
|
||||||
|
import PackagesPage from './web/pages/PackagesPage';
|
||||||
|
|
||||||
|
// jobs cron
|
||||||
|
require('./server/jobs/cron');
|
||||||
|
|
||||||
|
const app = new Hono()
|
||||||
|
|
||||||
|
// Auth middleware
|
||||||
|
app.use(
|
||||||
|
'*',
|
||||||
|
initAuthConfig((c) => ({
|
||||||
|
secret: process.env.AUTH_SECRET,
|
||||||
|
providers: [
|
||||||
|
GitHub({
|
||||||
|
clientId: process.env.GITHUB_ID,
|
||||||
|
clientSecret: process.env.GITHUB_SECRET,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
// React renderer setup
|
||||||
|
app.use(
|
||||||
|
'*',
|
||||||
|
reactRenderer(({ children, title }: { children: React.ReactNode; title: string }) => {
|
||||||
|
return (
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charSet="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>{title || 'Subscription System'}</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root">{children}</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
// Static files
|
||||||
|
app.use('/static/*', serveStatic({root: 'src/public'}));
|
||||||
|
|
||||||
|
// Auth routes
|
||||||
|
app.use('/api/auth/*', authHandler())
|
||||||
|
|
||||||
|
|
||||||
|
// Protected routes
|
||||||
|
app.use('/api/packages', verifyAuth())
|
||||||
|
app.use('/api/subscriptions', verifyAuth())
|
||||||
|
app.use('/api/products', verifyAuth())
|
||||||
|
|
||||||
|
// Routes
|
||||||
|
app.route('/api/packages', packageRoutes)
|
||||||
|
app.route('/api/subscriptions', subscriptionRoutes)
|
||||||
|
app.route('/api/products', productRoutes)
|
||||||
|
app.route('/api/:productkey', subscriptionClientRoutes)
|
||||||
|
|
||||||
|
app.get('/', verifyAuth(), (c) => {
|
||||||
|
const auth = c.get('authUser')
|
||||||
|
|
||||||
|
const user = auth?.token ? {
|
||||||
|
email: auth?.token?.email || '',
|
||||||
|
name: auth?.token?.name || '',
|
||||||
|
token: auth?.token?.jti || '',
|
||||||
|
} : undefined;
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return c.redirect('/api/auth/signin');
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.render(<HomePage user={user} />, { title: 'Home - Subscription System' });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/packages', verifyAuth(), (c) => {
|
||||||
|
const auth = c.get('authUser');
|
||||||
|
const user = auth?.token ? {
|
||||||
|
email: auth?.token?.email || '',
|
||||||
|
name: auth?.token?.name || '',
|
||||||
|
token: auth?.token?.jti || '',
|
||||||
|
} : undefined
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return c.redirect('/api/auth/signin');
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.render(<PackagesPage user={user} />, { title: 'Packages - Subscription System' });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/dashboard', verifyAuth(), (c) => {
|
||||||
|
const auth = c.get('authUser');
|
||||||
|
const user = auth?.token ? {
|
||||||
|
email: auth?.token?.email || '',
|
||||||
|
name: auth?.token?.name || '',
|
||||||
|
token: auth?.token?.jti || '',
|
||||||
|
} : undefined;
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return c.redirect('/api/auth/signin');
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.render(<DashboardPage user={user} />, { title: 'Dashboard - Subscription System' });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Health check
|
||||||
|
app.get('/health', (c) => {
|
||||||
|
return c.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('*', (c) => {
|
||||||
|
return c.notFound()
|
||||||
|
})
|
||||||
|
|
||||||
|
const port = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
serve({ fetch: app.fetch, port: Number(port) })
|
||||||
14
src/lib/constants/index.ts
Normal file
14
src/lib/constants/index.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { ultracollecteSeedPackage } from "./packages";
|
||||||
|
import { ultracolletProductSeeding } from "./products";
|
||||||
|
|
||||||
|
|
||||||
|
export const seedingData = {
|
||||||
|
products: {
|
||||||
|
ultracollecte: [ultracolletProductSeeding],
|
||||||
|
},
|
||||||
|
packages: {
|
||||||
|
ultracollecte: ultracollecteSeedPackage,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ISeedingData = typeof seedingData
|
||||||
58
src/lib/constants/packages.ts
Normal file
58
src/lib/constants/packages.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { IUltracollecteSeedPackage } from "../types";
|
||||||
|
|
||||||
|
export const ultracollecteSeedPackage: IUltracollecteSeedPackage[] =[
|
||||||
|
{
|
||||||
|
name: 'starter',
|
||||||
|
price: 50000,
|
||||||
|
supportType: 'standard',
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
features: {
|
||||||
|
clientsCount: 1000,
|
||||||
|
agentsCount: 5,
|
||||||
|
adminsCount: 2,
|
||||||
|
agenciesCount: 1,
|
||||||
|
agentLocalization: true,
|
||||||
|
enableCommision: true,
|
||||||
|
smsCount: 250,
|
||||||
|
supervisorsCount: 2,
|
||||||
|
cashiersCount: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'corporate',
|
||||||
|
price: 200000,
|
||||||
|
supportType: '24/7',
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
features: {
|
||||||
|
clientsCount: 10000,
|
||||||
|
agentsCount: 100,
|
||||||
|
adminsCount: 12,
|
||||||
|
agenciesCount: 10,
|
||||||
|
agentLocalization: true,
|
||||||
|
enableCommision: true,
|
||||||
|
smsCount: 2500,
|
||||||
|
supervisorsCount: 22,
|
||||||
|
cashiersCount: 25,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'pro',
|
||||||
|
price: 100000,
|
||||||
|
supportType: '24/7',
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
features: {
|
||||||
|
clientsCount: 2500,
|
||||||
|
agentsCount: 20,
|
||||||
|
adminsCount: 5,
|
||||||
|
agenciesCount: 4,
|
||||||
|
agentLocalization: true,
|
||||||
|
enableCommision: true,
|
||||||
|
smsCount: 1000,
|
||||||
|
supervisorsCount: 10,
|
||||||
|
cashiersCount: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
9
src/lib/constants/products.ts
Normal file
9
src/lib/constants/products.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { IProductSeed } from "../types";
|
||||||
|
|
||||||
|
export const ultracolletProductSeeding: IProductSeed = {
|
||||||
|
name: "ultracollecte",
|
||||||
|
description: "",
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
export const ultracollectePackage = [
|
|
||||||
{
|
|
||||||
name: 'starter',
|
|
||||||
price: 50000,
|
|
||||||
supportType: 'standard',
|
|
||||||
features: {
|
|
||||||
clientsCount: 1000,
|
|
||||||
agentsCount: 5,
|
|
||||||
adminsCount: 2,
|
|
||||||
agenciesCount: 1,
|
|
||||||
agentLocalization: true,
|
|
||||||
enableCommision: true,
|
|
||||||
smsCount: 250,
|
|
||||||
supervisorsCount: 2,
|
|
||||||
cashiersCount: 2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'corporate',
|
|
||||||
price: 200000,
|
|
||||||
supportType: '24/7',
|
|
||||||
features: {
|
|
||||||
clientsCount: 10000,
|
|
||||||
agentsCount: 100,
|
|
||||||
adminsCount: 12,
|
|
||||||
agenciesCount: 10,
|
|
||||||
agentLocalization: true,
|
|
||||||
enableCommision: true,
|
|
||||||
smsCount: 2500,
|
|
||||||
supervisorsCount: 22,
|
|
||||||
cashiersCount: 25,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'pro',
|
|
||||||
price: 100000,
|
|
||||||
supportType: '24/7',
|
|
||||||
features: {
|
|
||||||
clientsCount: 2500,
|
|
||||||
agentsCount: 20,
|
|
||||||
adminsCount: 5,
|
|
||||||
agenciesCount: 4,
|
|
||||||
agentLocalization: true,
|
|
||||||
enableCommision: true,
|
|
||||||
smsCount: 1000,
|
|
||||||
supervisorsCount: 10,
|
|
||||||
cashiersCount: 10,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -1,10 +1,22 @@
|
|||||||
import { Package, } from "../../database/schema";
|
import { SQL } from "drizzle-orm";
|
||||||
|
|
||||||
export type SubscriberType = 'microfinance' | 'student' | 'company' | 'school';
|
export * from "./packages";
|
||||||
export type SupportType = 'standard' | '24/7' | 'premium';
|
export * from "./products";
|
||||||
export type SubscriptionStatus = 'active' | 'expired' | 'canceled';
|
export type SubscriberType = 'student' | 'company' | 'personal';
|
||||||
|
export type ISupportType = "standard" | "24/7" | "premium";
|
||||||
|
export type SubscriptionStatus = "active" | "expired" | "canceled";
|
||||||
|
|
||||||
export interface Subscriber {
|
/**
|
||||||
|
* Represents a single filter condition or a group of conditions combined by an operator.
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
* - A single condition: `eq(users.isActive, true)`
|
||||||
|
* - An 'AND' group: `{ and: [eq(users.age, 30), like(users.email, '%test%')] }`
|
||||||
|
* - An 'OR' group: `{ or: [eq(users.status, 'pending'), eq(users.status, 'rejected')] }`
|
||||||
|
*/
|
||||||
|
export type FilterCondition = SQL | { and: FilterCondition[] } | { or: FilterCondition[] };
|
||||||
|
|
||||||
|
export interface ISubscriber {
|
||||||
type: SubscriberType;
|
type: SubscriberType;
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
@ -13,34 +25,19 @@ export interface Subscriber {
|
|||||||
country: string;
|
country: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ICreateSubscriptionRequest {
|
||||||
export interface PackageSnapshot {
|
|
||||||
name: string;
|
|
||||||
price: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Subscription {
|
|
||||||
id: string;
|
|
||||||
subscriber: Subscriber;
|
|
||||||
package: Package;
|
|
||||||
startDate: Date;
|
|
||||||
durationInMonths: number;
|
|
||||||
endDate: Date;
|
|
||||||
sentSmsCount: number;
|
|
||||||
status: SubscriptionStatus;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CreateSubscriptionRequest {
|
|
||||||
packageId: string;
|
packageId: string;
|
||||||
durationInMonths: number;
|
durationInMonths: number;
|
||||||
subscriber: Subscriber;
|
subscriber: ISubscriber;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiResponse<T = any> {
|
export interface IApiResponse<T = any> {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
data?: T;
|
data?: T;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
totalRecords?: number;
|
||||||
|
totalPages?: number;
|
||||||
|
currentPage?: number;
|
||||||
|
pageSize?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
66
src/lib/types/packages.ts
Normal file
66
src/lib/types/packages.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { z, ZodTypeAny } from "zod";
|
||||||
|
import { Package } from "../../database/schema";
|
||||||
|
|
||||||
|
export interface IPackageSeed extends Omit<Package, 'id'> {}
|
||||||
|
|
||||||
|
export interface IUltracollecteFeatures {
|
||||||
|
clientsCount: number,
|
||||||
|
agentsCount: number,
|
||||||
|
adminsCount: number,
|
||||||
|
agenciesCount: number,
|
||||||
|
agentLocalization: boolean,
|
||||||
|
enableCommision: boolean,
|
||||||
|
smsCount: number,
|
||||||
|
supervisorsCount: number,
|
||||||
|
cashiersCount: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IFeatures {
|
||||||
|
ultracollecte: IUltracollecteFeatures,
|
||||||
|
}
|
||||||
|
|
||||||
|
type ZodField<T> =
|
||||||
|
T extends number ? z.ZodNumber :
|
||||||
|
T extends boolean ? z.ZodBoolean :
|
||||||
|
ZodTypeAny;
|
||||||
|
|
||||||
|
type ZodSchemaFromInterface<T> = {
|
||||||
|
[K in keyof T]: ZodField<T[K]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
function buildSchema<T>(shape: ZodSchemaFromInterface<T>) {
|
||||||
|
return z.object(shape);
|
||||||
|
}
|
||||||
|
|
||||||
|
const productFeaturesShapes = {
|
||||||
|
ultracollecte: {
|
||||||
|
clientsCount: z.number(),
|
||||||
|
agentsCount: z.number(),
|
||||||
|
adminsCount: z.number(),
|
||||||
|
agenciesCount: z.number(),
|
||||||
|
agentLocalization: z.boolean(),
|
||||||
|
enableCommision: z.boolean(),
|
||||||
|
smsCount: z.number(),
|
||||||
|
supervisorsCount: z.number(),
|
||||||
|
cashiersCount: z.number(),
|
||||||
|
},
|
||||||
|
} satisfies {
|
||||||
|
[K in keyof IFeatures]: ZodSchemaFromInterface<IFeatures[K]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const ProductFeaturesSchemas = {
|
||||||
|
ultracollecte: buildSchema<IUltracollecteFeatures>(productFeaturesShapes.ultracollecte),
|
||||||
|
} satisfies {
|
||||||
|
[K in keyof IFeatures]: ReturnType<typeof buildSchema<IFeatures[K]>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FeaturesSchema = z.object(ProductFeaturesSchemas);
|
||||||
|
|
||||||
|
export type Features = z.infer<typeof FeaturesSchema>;
|
||||||
|
|
||||||
|
|
||||||
|
export interface IUltracollecteSeedPackage extends Omit<Package, 'id' | 'product' | 'feature'> {
|
||||||
|
features: IUltracollecteFeatures
|
||||||
|
}
|
||||||
3
src/lib/types/products.ts
Normal file
3
src/lib/types/products.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { Package, Product } from '../../database/schema';
|
||||||
|
|
||||||
|
export interface IProductSeed extends Omit<Product, 'id'> {}
|
||||||
@ -1,11 +0,0 @@
|
|||||||
export const UltracollecteFeatures = [
|
|
||||||
'clientsCount',
|
|
||||||
'agentsCount',
|
|
||||||
'adminsCount',
|
|
||||||
'agenciesCount',
|
|
||||||
'agentLocalization',
|
|
||||||
'enableCommision',
|
|
||||||
'smsCount',
|
|
||||||
'supervisorsCount',
|
|
||||||
'cashiersCount',
|
|
||||||
];
|
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import { type ClassValue, clsx } from 'clsx';
|
import { type ClassValue, clsx } from 'clsx';
|
||||||
|
import { ProductFeaturesSchemas } from '../types';
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return clsx(inputs);
|
return clsx(inputs);
|
||||||
@ -38,3 +39,10 @@ export function getDaysUntilExpiry(endDate: Date): number {
|
|||||||
const diffTime = endDate.getTime() - now.getTime();
|
const diffTime = endDate.getTime() - now.getTime();
|
||||||
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getProductFeaturesSchema<
|
||||||
|
T extends keyof typeof ProductFeaturesSchemas
|
||||||
|
>(productName: T) {
|
||||||
|
return ProductFeaturesSchemas[productName];
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
30
src/server/openapi.ts
Normal file
30
src/server/openapi.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
export const openApiSpec = {
|
||||||
|
openapi: '3.0.0',
|
||||||
|
info: {
|
||||||
|
title: 'My Hono API',
|
||||||
|
version: '1.0.0',
|
||||||
|
},
|
||||||
|
paths: {
|
||||||
|
'/hello': {
|
||||||
|
get: {
|
||||||
|
summary: 'Hello world',
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: 'Success',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
message: { type: 'string' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -1,145 +1,197 @@
|
|||||||
import { Hono } from 'hono';
|
import { eq, gte, ilike, lte } from "drizzle-orm";
|
||||||
import { Package } from '../../database/schema';
|
import { Hono } from "hono";
|
||||||
import type { ApiResponse } from '../../lib/types';
|
import { db } from "../../database/connexion";
|
||||||
import { PackageService } from '../services/packages';
|
import { Package, packages } from "../../database/schema";
|
||||||
|
import type {
|
||||||
|
FilterCondition,
|
||||||
|
IApiResponse,
|
||||||
|
ISupportType,
|
||||||
|
} from "../../lib/types";
|
||||||
|
import { PackageService } from "../services/packages";
|
||||||
|
import { getSeedingData } from "../utils/getSeedingdata";
|
||||||
|
import { paginate } from "../utils/pagination.util";
|
||||||
|
|
||||||
const packageRoutes = new Hono();
|
const packageRoutes = new Hono();
|
||||||
|
|
||||||
// get packages
|
// get packages
|
||||||
packageRoutes.get('/', async (c) => {
|
packageRoutes.get("/", async (c) => {
|
||||||
|
getSeedingData("packages");
|
||||||
try {
|
try {
|
||||||
const packages = await PackageService.getPackages();
|
const page = Number(c.req.query("page")) || 1;
|
||||||
const response: ApiResponse<Package[] | null> = {
|
const limit = Number(c.req.query("limit")) || 10;
|
||||||
|
const product = c.req.query("product");
|
||||||
|
const supportType = c.req.query("supportType");
|
||||||
|
const price = Number(c.req.query("price"));
|
||||||
|
const name = c.req.query("name");
|
||||||
|
const startDate = c.req.query("startDate");
|
||||||
|
const endDate = c.req.query("endDate");
|
||||||
|
|
||||||
|
let complexFilter: FilterCondition[] = [
|
||||||
|
{
|
||||||
|
and: [...(product ? [eq(packages.product, product)] : [])],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
or: [
|
||||||
|
...(supportType
|
||||||
|
? [eq(packages.supportType, supportType as ISupportType)]
|
||||||
|
: []),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
or: [...(price ? [lte(packages.price, price)] : [])],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
or: [...(name ? [ilike(packages.name, `%${name}%`)] : [])],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
or: [...(startDate ? [gte(packages.createdAt, new Date(startDate))] : [])],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
or: [...(endDate ? [lte(packages.createdAt, new Date(endDate))] : [])],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const pkgs = await paginate<typeof packages>({
|
||||||
|
db,
|
||||||
|
table: packages,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
filters: complexFilter,
|
||||||
|
});
|
||||||
|
const response: IApiResponse<Package[] | null> = {
|
||||||
success: true,
|
success: true,
|
||||||
data: packages
|
...pkgs,
|
||||||
|
data: pkgs.data as any,
|
||||||
};
|
};
|
||||||
return c.json(response);
|
return c.json(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const response: ApiResponse = {
|
console.warn(error);
|
||||||
|
const response: IApiResponse = {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Failed to fetch packages'
|
data: error,
|
||||||
|
error: "Failed to fetch packages",
|
||||||
};
|
};
|
||||||
return c.json(response, 500);
|
return c.json(response, 500);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// get package by id
|
// get package by id
|
||||||
packageRoutes.get('/:id', async (c) => {
|
packageRoutes.get("/:id", async (c) => {
|
||||||
try {
|
try {
|
||||||
const id = c.req.param('id');
|
const id = c.req.param("id");
|
||||||
|
|
||||||
const pkg = await PackageService.getPackageById(id);
|
const pkg = await PackageService.getPackageById(id);
|
||||||
|
|
||||||
if (!pkg) {
|
if (!pkg) {
|
||||||
const response: ApiResponse = {
|
const response: IApiResponse = {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Package not found'
|
error: "Package not found",
|
||||||
};
|
};
|
||||||
return c.json(response, 404);
|
return c.json(response, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
const response: ApiResponse<Package> = {
|
const response: IApiResponse<Package> = {
|
||||||
success: true,
|
success: true,
|
||||||
data: pkg
|
data: pkg,
|
||||||
};
|
};
|
||||||
return c.json(response);
|
return c.json(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const response: ApiResponse = {
|
const response: IApiResponse = {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Failed to fetch package'
|
error: "Failed to fetch package",
|
||||||
};
|
};
|
||||||
return c.json(response, 500);
|
return c.json(response, 500);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// get package by name
|
// get package by name
|
||||||
packageRoutes.get('/name/:name', async (c) => {
|
packageRoutes.get("/name/:name", async (c) => {
|
||||||
try {
|
try {
|
||||||
const name = c.req.param('name');
|
const name = c.req.param("name");
|
||||||
|
|
||||||
const pkg = await PackageService.getPackageByName(name);
|
const pkg = await PackageService.getPackageByName(name);
|
||||||
|
|
||||||
if (!pkg) {
|
if (!pkg) {
|
||||||
const response: ApiResponse = {
|
const response: IApiResponse = {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Package not found'
|
error: "Package not found",
|
||||||
};
|
};
|
||||||
return c.json(response, 404);
|
return c.json(response, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
const response: ApiResponse<Package> = {
|
const response: IApiResponse<Package> = {
|
||||||
success: true,
|
success: true,
|
||||||
data: pkg
|
data: pkg,
|
||||||
};
|
};
|
||||||
return c.json(response);
|
return c.json(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const response: ApiResponse = {
|
const response: IApiResponse = {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Failed to fetch package'
|
error: "Failed to fetch package",
|
||||||
};
|
};
|
||||||
return c.json(response, 500);
|
return c.json(response, 500);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// create package
|
// create package
|
||||||
packageRoutes.post('/', async (c) => {
|
packageRoutes.post("/", async (c) => {
|
||||||
try {
|
try {
|
||||||
const packageData: Package = await c.req.json();
|
const packageData: Package = await c.req.json();
|
||||||
|
|
||||||
const newPackage = await PackageService.createPackage(packageData);
|
const newPackage = await PackageService.createPackage(packageData);
|
||||||
|
|
||||||
const response: ApiResponse<Package> = {
|
const response: IApiResponse<Package> = {
|
||||||
success: true,
|
success: true,
|
||||||
data: newPackage
|
data: newPackage,
|
||||||
};
|
};
|
||||||
return c.json(response, 201);
|
return c.json(response, 201);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const response: ApiResponse = {
|
const response: IApiResponse = {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Failed to create package'
|
error: "Failed to create package",
|
||||||
};
|
};
|
||||||
return c.json(response, 500);
|
return c.json(response, 500);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// update package
|
// update package
|
||||||
packageRoutes.put('/:id', async (c) => {
|
packageRoutes.put("/:id", async (c) => {
|
||||||
try {
|
try {
|
||||||
const id = c.req.param('id');
|
const id = c.req.param("id");
|
||||||
const packageData: Package = await c.req.json();
|
const packageData: Package = await c.req.json();
|
||||||
|
|
||||||
const updatedPackage = await PackageService.updatePackage(id, packageData);
|
const updatedPackage = await PackageService.updatePackage(id, packageData);
|
||||||
|
|
||||||
const response: ApiResponse<Package | null> = {
|
const response: IApiResponse<Package | null> = {
|
||||||
success: true,
|
success: true,
|
||||||
data: updatedPackage
|
data: updatedPackage,
|
||||||
};
|
};
|
||||||
return c.json(response);
|
return c.json(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const response: ApiResponse = {
|
const response: IApiResponse = {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Failed to update package'
|
error: "Failed to update package",
|
||||||
};
|
};
|
||||||
return c.json(response, 500);
|
return c.json(response, 500);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// delete package
|
// delete package
|
||||||
packageRoutes.delete('/:id', async (c) => {
|
packageRoutes.delete("/:id", async (c) => {
|
||||||
try {
|
try {
|
||||||
const id = c.req.param('id');
|
const id = c.req.param("id");
|
||||||
|
|
||||||
const deletedPackage = await PackageService.deletePackage(id);
|
const deletedPackage = await PackageService.deletePackage(id);
|
||||||
|
|
||||||
const response: ApiResponse<Package | null> = {
|
const response: IApiResponse<Package | null> = {
|
||||||
success: true,
|
success: true,
|
||||||
data: deletedPackage
|
data: deletedPackage,
|
||||||
};
|
};
|
||||||
return c.json(response);
|
return c.json(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const response: ApiResponse = {
|
const response: IApiResponse = {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Failed to delete package'
|
error: "Failed to delete package",
|
||||||
};
|
};
|
||||||
return c.json(response, 500);
|
return c.json(response, 500);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,21 +1,32 @@
|
|||||||
import { Hono } from 'hono';
|
import { Hono } from 'hono';
|
||||||
import { Product } from '../../database/schema/schema/products';
|
import { db } from '../../database/connexion';
|
||||||
import type { ApiResponse } from '../../lib/types';
|
import { Product, products } from '../../database/schema/schema/products';
|
||||||
import { ProductService } from '../services/prducts';
|
import type { IApiResponse } from '../../lib/types';
|
||||||
|
import { ProductService } from '../services/products';
|
||||||
|
import { paginate } from '../utils/pagination.util';
|
||||||
|
|
||||||
const productRoutes = new Hono();
|
const productRoutes = new Hono();
|
||||||
|
|
||||||
// get products
|
// get products
|
||||||
productRoutes.get('/', async (c) => {
|
productRoutes.get('/', async (c) => {
|
||||||
try {
|
try {
|
||||||
const pts = await ProductService.getProducts();
|
const page = Number(c.req.query('page')) || 1
|
||||||
const response: ApiResponse<Product[] | null> = {
|
const limit = Number(c.req.query('limit')) || 10
|
||||||
|
|
||||||
|
const pts = await paginate<typeof products>({
|
||||||
|
db,
|
||||||
|
table: products,
|
||||||
|
limit,
|
||||||
|
page
|
||||||
|
});
|
||||||
|
const response: IApiResponse<Product[] | null> = {
|
||||||
success: true,
|
success: true,
|
||||||
data: pts
|
...pts,
|
||||||
|
data: pts.data as any
|
||||||
};
|
};
|
||||||
return c.json(response);
|
return c.json(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const response: ApiResponse = {
|
const response: IApiResponse = {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Failed to fetch products'
|
error: 'Failed to fetch products'
|
||||||
};
|
};
|
||||||
@ -28,13 +39,13 @@ productRoutes.get('/:id', async (c) => {
|
|||||||
try {
|
try {
|
||||||
const id = c.req.param('id');
|
const id = c.req.param('id');
|
||||||
const pt = await ProductService.getProductById(id);
|
const pt = await ProductService.getProductById(id);
|
||||||
const response: ApiResponse<Product | null> = {
|
const response: IApiResponse<Product | null> = {
|
||||||
success: true,
|
success: true,
|
||||||
data: pt
|
data: pt
|
||||||
};
|
};
|
||||||
return c.json(response);
|
return c.json(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const response: ApiResponse = {
|
const response: IApiResponse = {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Failed to fetch product'
|
error: 'Failed to fetch product'
|
||||||
};
|
};
|
||||||
@ -47,13 +58,13 @@ productRoutes.get('/name/:name', async (c) => {
|
|||||||
try {
|
try {
|
||||||
const name = c.req.param('name');
|
const name = c.req.param('name');
|
||||||
const pt = await ProductService.getProductByName(name);
|
const pt = await ProductService.getProductByName(name);
|
||||||
const response: ApiResponse<Product | null> = {
|
const response: IApiResponse<Product | null> = {
|
||||||
success: true,
|
success: true,
|
||||||
data: pt
|
data: pt
|
||||||
};
|
};
|
||||||
return c.json(response);
|
return c.json(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const response: ApiResponse = {
|
const response: IApiResponse = {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Failed to fetch product'
|
error: 'Failed to fetch product'
|
||||||
};
|
};
|
||||||
@ -66,13 +77,13 @@ productRoutes.post('/', async (c) => {
|
|||||||
try {
|
try {
|
||||||
const ptData: Product = await c.req.json();
|
const ptData: Product = await c.req.json();
|
||||||
const newPt = await ProductService.createProduct(ptData);
|
const newPt = await ProductService.createProduct(ptData);
|
||||||
const response: ApiResponse<Product> = {
|
const response: IApiResponse<Product> = {
|
||||||
success: true,
|
success: true,
|
||||||
data: newPt
|
data: newPt
|
||||||
};
|
};
|
||||||
return c.json(response, 201);
|
return c.json(response, 201);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const response: ApiResponse = {
|
const response: IApiResponse = {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Failed to create product'
|
error: 'Failed to create product'
|
||||||
};
|
};
|
||||||
@ -86,13 +97,13 @@ productRoutes.put('/:id', async (c) => {
|
|||||||
const id = c.req.param('id');
|
const id = c.req.param('id');
|
||||||
const ptData: Product = await c.req.json();
|
const ptData: Product = await c.req.json();
|
||||||
const updatedPt = await ProductService.updateProduct(id, ptData);
|
const updatedPt = await ProductService.updateProduct(id, ptData);
|
||||||
const response: ApiResponse<Product | null> = {
|
const response: IApiResponse<Product | null> = {
|
||||||
success: true,
|
success: true,
|
||||||
data: updatedPt
|
data: updatedPt
|
||||||
};
|
};
|
||||||
return c.json(response);
|
return c.json(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const response: ApiResponse = {
|
const response: IApiResponse = {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Failed to update product'
|
error: 'Failed to update product'
|
||||||
};
|
};
|
||||||
@ -105,13 +116,13 @@ productRoutes.delete('/:id', async (c) => {
|
|||||||
try {
|
try {
|
||||||
const id = c.req.param('id');
|
const id = c.req.param('id');
|
||||||
const deletedPt = await ProductService.deleteProduct(id);
|
const deletedPt = await ProductService.deleteProduct(id);
|
||||||
const response: ApiResponse<Product | null> = {
|
const response: IApiResponse<Product | null> = {
|
||||||
success: true,
|
success: true,
|
||||||
data: deletedPt
|
data: deletedPt
|
||||||
};
|
};
|
||||||
return c.json(response);
|
return c.json(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const response: ApiResponse = {
|
const response: IApiResponse = {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Failed to delete product'
|
error: 'Failed to delete product'
|
||||||
};
|
};
|
||||||
|
|||||||
78
src/server/routes/subscription-client.ts
Normal file
78
src/server/routes/subscription-client.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { Hono } from "hono";
|
||||||
|
import { Subscription } from "../../database/schema";
|
||||||
|
import { IApiResponse, ProductFeaturesSchemas } from "../../lib/types";
|
||||||
|
import { getProductFeaturesSchema } from "../../lib/utils";
|
||||||
|
import { SubscriptionService } from "../services/subscriptions";
|
||||||
|
|
||||||
|
const subscriptionClientRoutes = new Hono();
|
||||||
|
|
||||||
|
// Get subscription client by protect key
|
||||||
|
subscriptionClientRoutes.get("/:protectKey", async (c) => {
|
||||||
|
try {
|
||||||
|
const protectKey = c.req.param("protectKey");
|
||||||
|
const subscription = await SubscriptionService.getSubscriptionByProtectKey(
|
||||||
|
protectKey as string
|
||||||
|
);
|
||||||
|
const response: IApiResponse<Subscription | null> = {
|
||||||
|
success: true,
|
||||||
|
data: subscription,
|
||||||
|
};
|
||||||
|
return c.json(response);
|
||||||
|
} catch (error) {
|
||||||
|
const response: IApiResponse = {
|
||||||
|
success: false,
|
||||||
|
error: "Failed to fetch subscription",
|
||||||
|
};
|
||||||
|
return c.json(response, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
subscriptionClientRoutes.put("/:protectKey", async (c) => {
|
||||||
|
try {
|
||||||
|
const protectKey = c.req.param("protectKey");
|
||||||
|
const subscriptionData = await c.req.json();
|
||||||
|
const subscriptionObj =
|
||||||
|
await SubscriptionService.getSubscriptionByProtectKey(
|
||||||
|
protectKey as string
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!subscriptionObj) {
|
||||||
|
const response: IApiResponse = {
|
||||||
|
success: false,
|
||||||
|
error: "Subscription not found",
|
||||||
|
};
|
||||||
|
return c.json(response, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate subscription features type
|
||||||
|
const requiredFeatureType = getProductFeaturesSchema(subscriptionObj.product as keyof typeof ProductFeaturesSchemas);
|
||||||
|
if (!requiredFeatureType.safeParse(subscriptionData).success) {
|
||||||
|
const response: IApiResponse = {
|
||||||
|
success: false,
|
||||||
|
error: "Invalid subscription features type",
|
||||||
|
};
|
||||||
|
return c.json(response, 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
const subscription = await SubscriptionService.updateSubscription(
|
||||||
|
{
|
||||||
|
...subscriptionObj,
|
||||||
|
usedFeatures: subscriptionData,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const response: IApiResponse<Subscription | null> = {
|
||||||
|
success: true,
|
||||||
|
data: subscription,
|
||||||
|
};
|
||||||
|
return c.json(response);
|
||||||
|
} catch (error) {
|
||||||
|
const response: IApiResponse = {
|
||||||
|
success: false,
|
||||||
|
error: "Failed to fetch subscriptions",
|
||||||
|
};
|
||||||
|
return c.json(response, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default subscriptionClientRoutes;
|
||||||
@ -1,181 +1,291 @@
|
|||||||
import { eq } from 'drizzle-orm';
|
import { eq, gte, ilike, lte, sql } from "drizzle-orm";
|
||||||
import { Hono } from 'hono';
|
import { Hono } from "hono";
|
||||||
import { db } from '../../database/connexion';
|
import { db } from "../../database/connexion";
|
||||||
import { packages } from '../../database/schema';
|
import { packages, Subscription, subscriptions } from "../../database/schema";
|
||||||
import type { ApiResponse, CreateSubscriptionRequest, Subscription } from '../../lib/types';
|
import type {
|
||||||
import { SubscriptionService } from '../../server/services/subscriptions';
|
FilterCondition,
|
||||||
|
IApiResponse,
|
||||||
|
ICreateSubscriptionRequest,
|
||||||
|
ISupportType,
|
||||||
|
} from "../../lib/types";
|
||||||
|
import { SubscriptionService } from "../../server/services/subscriptions";
|
||||||
|
import { paginate } from "../utils/pagination.util";
|
||||||
|
|
||||||
const subscriptionRoutes = new Hono();
|
const subscriptionRoutes = new Hono();
|
||||||
|
|
||||||
// Create subscription
|
// Create subscription
|
||||||
subscriptionRoutes.post('/', async (c) => {
|
subscriptionRoutes.post("/", async (c) => {
|
||||||
try {
|
try {
|
||||||
const subscriptionData: CreateSubscriptionRequest = await c.req.json();
|
const subscriptionData: ICreateSubscriptionRequest = await c.req.json();
|
||||||
|
|
||||||
// Get package details
|
// Get package details
|
||||||
const [pkg] = await db.select().from(packages).where(eq(packages.id, subscriptionData.packageId));
|
const [pkg] = await db
|
||||||
|
.select()
|
||||||
|
.from(packages)
|
||||||
|
.where(eq(packages.id, subscriptionData.packageId));
|
||||||
|
|
||||||
if (!pkg) {
|
if (!pkg) {
|
||||||
const response: ApiResponse = {
|
const response: IApiResponse = {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Package not found'
|
error: "Package not found",
|
||||||
};
|
};
|
||||||
return c.json(response, 404);
|
return c.json(response, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
const subscription = await SubscriptionService.createSubscription(subscriptionData);
|
const subscription =
|
||||||
|
await SubscriptionService.createSubscription(subscriptionData);
|
||||||
|
|
||||||
const response: ApiResponse<Subscription> = {
|
const response: IApiResponse<Subscription> = {
|
||||||
success: true,
|
success: true,
|
||||||
data: subscription
|
data: subscription,
|
||||||
};
|
};
|
||||||
return c.json(response, 201);
|
return c.json(response, 201);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Create subscription error:', error);
|
console.error("Create subscription error:", error);
|
||||||
const response: ApiResponse = {
|
const response: IApiResponse = {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Failed to create subscription'
|
error: "Failed to create subscription",
|
||||||
};
|
};
|
||||||
return c.json(response, 500);
|
return c.json(response, 500);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get subscription by protect key
|
// Get subscription by protect key
|
||||||
subscriptionRoutes.get('/status/:protectKey', async (c) => {
|
subscriptionRoutes.get("/status/:protectKey", async (c) => {
|
||||||
try {
|
try {
|
||||||
const protectKey = c.req.param('protectKey');
|
const protectKey = c.req.param("protectKey");
|
||||||
|
const subscription =
|
||||||
const subscription = await SubscriptionService.getSubscriptionByProtectKey(protectKey);
|
await SubscriptionService.getSubscriptionByProtectKey(protectKey);
|
||||||
|
|
||||||
if (!subscription) {
|
if (!subscription) {
|
||||||
const response: ApiResponse = {
|
const response: IApiResponse = {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Subscription not found'
|
error: "Subscription not found",
|
||||||
};
|
};
|
||||||
return c.json(response, 404);
|
return c.json(response, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
const response: ApiResponse<Subscription> = {
|
const response: IApiResponse<Subscription> = {
|
||||||
success: true,
|
success: true,
|
||||||
data: subscription
|
data: subscription,
|
||||||
};
|
};
|
||||||
return c.json(response);
|
return c.json(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const response: ApiResponse = {
|
const response: IApiResponse = {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Failed to fetch subscription'
|
error: "Failed to fetch subscription",
|
||||||
};
|
};
|
||||||
return c.json(response, 500);
|
return c.json(response, 500);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// get subscriptions
|
// get subscriptions
|
||||||
subscriptionRoutes.get('/', async (c) => {
|
subscriptionRoutes.get("/", async (c) => {
|
||||||
try {
|
try {
|
||||||
const subscriptions = await SubscriptionService.getSubscriptions();
|
const page = Number(c.req.query("page")) || 1;
|
||||||
const response: ApiResponse<Subscription[]> = {
|
const limit = Number(c.req.query("limit")) || 10;
|
||||||
|
|
||||||
|
// Parameters that likely filter on the 'packages' table
|
||||||
|
const product = c.req.query("product");
|
||||||
|
const supportType = c.req.query("supportType");
|
||||||
|
const price = Number(c.req.query("price"));
|
||||||
|
const packageName = c.req.query("name");
|
||||||
|
|
||||||
|
// Parameters that filter on the 'subscriptions' table
|
||||||
|
const status = c.req.query("status");
|
||||||
|
const protectKey = c.req.query("protectKey");
|
||||||
|
const subscriptionStartDate = c.req.query("subscriptionStartDate");
|
||||||
|
const subscriptionEndDate = c.req.query("subscriptionEndDate");
|
||||||
|
const createdAtStart = c.req.query("createdAtStart");
|
||||||
|
const createdAtEnd = c.req.query("createdAtEnd");
|
||||||
|
|
||||||
|
// New parameters for filtering on ISubscriber fields within the 'subscriber' JSONB column
|
||||||
|
const subscriberType = c.req.query("subscriberType");
|
||||||
|
const subscriberName = c.req.query("subscriberName");
|
||||||
|
const subscriberEmail = c.req.query("subscriberEmail");
|
||||||
|
const subscriberPhone = c.req.query("subscriberPhone");
|
||||||
|
const subscriberAddress = c.req.query("subscriberAddress");
|
||||||
|
const subscriberCountry = c.req.query("subscriberCountry");
|
||||||
|
|
||||||
|
const allConditions: FilterCondition[] = [];
|
||||||
|
|
||||||
|
// 1. Conditions related to the 'packages' table (assuming a join or relationship)
|
||||||
|
if (product) {
|
||||||
|
allConditions.push(eq(packages.product, product));
|
||||||
|
}
|
||||||
|
if (supportType) {
|
||||||
|
allConditions.push(eq(packages.supportType, supportType as ISupportType));
|
||||||
|
}
|
||||||
|
if (price !== undefined && !isNaN(price)) {
|
||||||
|
allConditions.push(lte(packages.price, price));
|
||||||
|
}
|
||||||
|
if (packageName) {
|
||||||
|
allConditions.push(ilike(packages.name, `%${packageName}%`));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Conditions directly on the 'subscriptions' table
|
||||||
|
if (status) {
|
||||||
|
allConditions.push(
|
||||||
|
eq(subscriptions.status, status as Subscription["status"])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (protectKey) {
|
||||||
|
allConditions.push(eq(subscriptions.protectKey, protectKey));
|
||||||
|
}
|
||||||
|
if (subscriptionStartDate) {
|
||||||
|
allConditions.push(
|
||||||
|
gte(subscriptions.startDate, new Date(subscriptionStartDate))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (subscriptionEndDate) {
|
||||||
|
allConditions.push(
|
||||||
|
lte(subscriptions.endDate, new Date(subscriptionEndDate))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (createdAtStart) {
|
||||||
|
allConditions.push(
|
||||||
|
gte(subscriptions.createdAt, new Date(createdAtStart))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (createdAtEnd) {
|
||||||
|
allConditions.push(lte(subscriptions.createdAt, new Date(createdAtEnd)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Conditions related to 'subscriber' (this part requires a join or specific schema setup)
|
||||||
|
if (subscriberType) {
|
||||||
|
allConditions.push(sql`${subscriptions.subscriber} ->> 'type' = ${subscriberType}`);
|
||||||
|
}
|
||||||
|
if (subscriberName) {
|
||||||
|
allConditions.push(sql`${subscriptions.subscriber} ->> 'name' ILIKE ${`%${subscriberName}%`}`);
|
||||||
|
}
|
||||||
|
if (subscriberEmail) {
|
||||||
|
allConditions.push(sql`${subscriptions.subscriber} ->> 'email' ILIKE ${`%${subscriberEmail}%`}`);
|
||||||
|
}
|
||||||
|
if (subscriberPhone) {
|
||||||
|
allConditions.push(sql`${subscriptions.subscriber} ->> 'phone' ILIKE ${`%${subscriberPhone}%`}`);
|
||||||
|
}
|
||||||
|
if (subscriberAddress) {
|
||||||
|
allConditions.push(sql`${subscriptions.subscriber} ->> 'address' ILIKE ${`%${subscriberAddress}%`}`);
|
||||||
|
}
|
||||||
|
if (subscriberCountry) {
|
||||||
|
allConditions.push(sql`${subscriptions.subscriber} ->> 'country' ILIKE ${`%${subscriberCountry}%`}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine all collected conditions.
|
||||||
|
// By default, we'll AND all top-level conditions.
|
||||||
|
const finalFilter: FilterCondition[] =
|
||||||
|
allConditions.length > 0 ? [{ and: allConditions }] : [];
|
||||||
|
|
||||||
|
const subs = await paginate<typeof subscriptions>({
|
||||||
|
db,
|
||||||
|
table: subscriptions,
|
||||||
|
limit,
|
||||||
|
page,
|
||||||
|
filters: finalFilter,
|
||||||
|
});
|
||||||
|
const response: IApiResponse<Subscription[] | null> = {
|
||||||
success: true,
|
success: true,
|
||||||
data: subscriptions
|
...subs,
|
||||||
|
data: subs.data as any,
|
||||||
};
|
};
|
||||||
return c.json(response);
|
return c.json(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const response: ApiResponse = {
|
const response: IApiResponse = {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Failed to fetch subscriptions'
|
error: "Failed to fetch subscriptions",
|
||||||
};
|
};
|
||||||
return c.json(response, 500);
|
return c.json(response, 500);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// get expired subscriptions
|
// get expired subscriptions
|
||||||
subscriptionRoutes.get('/expired', async (c) => {
|
subscriptionRoutes.get("/expired", async (c) => {
|
||||||
try {
|
try {
|
||||||
const subscriptions = await SubscriptionService.getExpiredSubscriptions({
|
const subscriptions = await SubscriptionService.getExpiredSubscriptions({
|
||||||
limit: 10,
|
limit: 10,
|
||||||
page: 1,
|
page: 1,
|
||||||
});
|
});
|
||||||
const response: ApiResponse<Partial<Subscription>[]> = {
|
const response: IApiResponse<Partial<Subscription>[]> = {
|
||||||
success: true,
|
success: true,
|
||||||
data: subscriptions
|
data: subscriptions,
|
||||||
};
|
};
|
||||||
return c.json(response);
|
return c.json(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const response: ApiResponse = {
|
const response: IApiResponse = {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Failed to fetch expired subscriptions'
|
error: "Failed to fetch expired subscriptions",
|
||||||
};
|
};
|
||||||
return c.json(response, 500);
|
return c.json(response, 500);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// increment sms count
|
// increment sms count
|
||||||
subscriptionRoutes.post('/increment-sms-count/:protectKey', async (c) => {
|
subscriptionRoutes.post("/increment-sms-count/:protectKey", async (c) => {
|
||||||
try {
|
try {
|
||||||
const protectKey = c.req.param('protectKey');
|
const protectKey = c.req.param("protectKey");
|
||||||
|
|
||||||
await SubscriptionService.incrementSmsCount(protectKey);
|
await SubscriptionService.incrementSmsCount(protectKey);
|
||||||
|
|
||||||
const response: ApiResponse = {
|
const response: IApiResponse = {
|
||||||
success: true,
|
success: true,
|
||||||
};
|
};
|
||||||
return c.json(response);
|
return c.json(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const response: ApiResponse = {
|
const response: IApiResponse = {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Failed to increment SMS count'
|
error: "Failed to increment SMS count",
|
||||||
};
|
};
|
||||||
return c.json(response, 500);
|
return c.json(response, 500);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// update subscription status
|
// update subscription status
|
||||||
subscriptionRoutes.patch('/status/:id', async (c) => {
|
subscriptionRoutes.patch("/status/:id", async (c) => {
|
||||||
try {
|
try {
|
||||||
const id = c.req.param('id');
|
const id = c.req.param("id");
|
||||||
const status = (await c.req.json()).status;
|
const status = (await c.req.json()).status;
|
||||||
|
|
||||||
const updatedSubscription = await SubscriptionService.updateSubscriptionStatus(id, status);
|
const updatedSubscription =
|
||||||
|
await SubscriptionService.updateSubscriptionStatus(id, status);
|
||||||
|
|
||||||
const response: ApiResponse<Subscription | null> = {
|
const response: IApiResponse<Subscription | null> = {
|
||||||
success: true,
|
success: true,
|
||||||
data: updatedSubscription
|
data: updatedSubscription,
|
||||||
};
|
};
|
||||||
return c.json(response);
|
return c.json(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const response: ApiResponse = {
|
const response: IApiResponse = {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Failed to update subscription status'
|
error: "Failed to update subscription status",
|
||||||
};
|
};
|
||||||
return c.json(response, 500);
|
return c.json(response, 500);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// get by id
|
// get by id
|
||||||
subscriptionRoutes.get('/:id', async (c) => {
|
subscriptionRoutes.get("/:id", async (c) => {
|
||||||
try {
|
try {
|
||||||
const id = c.req.param('id');
|
const id = c.req.param("id");
|
||||||
|
|
||||||
const subscription = await SubscriptionService.getSubscriptionById(id);
|
const subscription = await SubscriptionService.getSubscriptionById(id);
|
||||||
|
|
||||||
if (!subscription) {
|
if (!subscription) {
|
||||||
const response: ApiResponse = {
|
const response: IApiResponse = {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Subscription not found'
|
error: "Subscription not found",
|
||||||
};
|
};
|
||||||
return c.json(response, 404);
|
return c.json(response, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
const response: ApiResponse<Subscription> = {
|
const response: IApiResponse<Subscription> = {
|
||||||
success: true,
|
success: true,
|
||||||
data: subscription
|
data: subscription,
|
||||||
};
|
};
|
||||||
return c.json(response);
|
return c.json(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const response: ApiResponse = {
|
const response: IApiResponse = {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Failed to fetch subscription'
|
error: "Failed to fetch subscription",
|
||||||
};
|
};
|
||||||
return c.json(response, 500);
|
return c.json(response, 500);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
import { packagesSeed } from "./packages";
|
import { packagesSeed } from "./packages";
|
||||||
|
import { productSeed } from "./products";
|
||||||
|
|
||||||
async function seed() {
|
async function seed() {
|
||||||
console.log('🌱 Seeding database...');
|
console.log('🌱 Seeding database...');
|
||||||
|
await productSeed()
|
||||||
await packagesSeed();
|
await packagesSeed();
|
||||||
|
console.log('✅ Database seeded successfully!');
|
||||||
}
|
}
|
||||||
|
|
||||||
seed();
|
seed();
|
||||||
@ -1,68 +1,37 @@
|
|||||||
|
import { count } from 'drizzle-orm';
|
||||||
import { db } from '../../database/connexion';
|
import { db } from '../../database/connexion';
|
||||||
import { packages } from '../../database/schema';
|
import { packages, products } from '../../database/schema';
|
||||||
|
import { IPackageSeed } from '../../lib/types';
|
||||||
|
import { getSeedingData } from '../utils/getSeedingdata';
|
||||||
|
|
||||||
export async function packagesSeed() {
|
export async function packagesSeed() {
|
||||||
console.log('🌱 Seeding database...');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// const allPackages = await db.select().from(packages);
|
const productNames = await db.select({ name: products.name, id: products.id }).from(products);
|
||||||
// if (allPackages.length > 0) {
|
if (productNames.length === 0) {
|
||||||
// console.log('Database already seeded!');
|
console.error('❌ Error: Products not found');
|
||||||
// return;
|
process.exit(1);
|
||||||
// }
|
}
|
||||||
|
|
||||||
|
const totalPackageCount = (await db.select({ count: count() }).from(packages))[0].count;
|
||||||
|
if (totalPackageCount > 0) {
|
||||||
|
console.log('Packages already seeded!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Clear existing data
|
// Clear existing data
|
||||||
await db.delete(packages);
|
await db.delete(packages);
|
||||||
|
|
||||||
|
// get sample
|
||||||
// Insert sample packages
|
// Insert sample packages
|
||||||
const samplePackages = [
|
const addPropertyPerProduct = productNames.map((item) => ({
|
||||||
{
|
productName: item.name,
|
||||||
"name": "Starter",
|
property: 'product',
|
||||||
"price": 50000,
|
value: item.id
|
||||||
"clientsCount": 1000,
|
}))
|
||||||
"agentsCount": 5,
|
const samplePackages = getSeedingData('packages', addPropertyPerProduct)
|
||||||
"adminsCount": 2,
|
|
||||||
"agenciesCount": 1,
|
|
||||||
"agentLocalization": true,
|
|
||||||
"enableCommision": true,
|
|
||||||
"smsCount": 250,
|
|
||||||
"supervisorsCount": 2,
|
|
||||||
"cashiersCount": 2,
|
|
||||||
"supportType": "standard",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Corporate",
|
|
||||||
"price": 200000,
|
|
||||||
"clientsCount": 10000,
|
|
||||||
"agentsCount": 100,
|
|
||||||
"adminsCount": 12,
|
|
||||||
"agenciesCount": 10,
|
|
||||||
"agentLocalization": true,
|
|
||||||
"enableCommision": true,
|
|
||||||
"smsCount": 2500,
|
|
||||||
"supervisorsCount": 22,
|
|
||||||
"cashiersCount": 25,
|
|
||||||
"supportType": "24/7",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Pro",
|
|
||||||
"price": 100000,
|
|
||||||
"clientsCount": 2500,
|
|
||||||
"agentsCount": 20,
|
|
||||||
"adminsCount": 5,
|
|
||||||
"agenciesCount": 4,
|
|
||||||
"agentLocalization": true,
|
|
||||||
"enableCommision": true,
|
|
||||||
"smsCount": 1000,
|
|
||||||
"supervisorsCount": 10,
|
|
||||||
"cashiersCount": 10,
|
|
||||||
"supportType": "24/7",
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// await db.insert(packages).values(samplePackages);
|
await db.insert(packages).values(samplePackages as IPackageSeed[]);
|
||||||
|
|
||||||
console.log('✅ Database seeded successfully!');
|
console.log('Packages seeded successfully! ✅');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Error seeding database:', error);
|
console.error('❌ Error seeding database:', error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
21
src/server/seeds/products.ts
Normal file
21
src/server/seeds/products.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { db } from '../../database/connexion';
|
||||||
|
import { packages, products } from '../../database/schema';
|
||||||
|
import { IProductSeed } from '../../lib/types';
|
||||||
|
import { getSeedingData } from '../utils/getSeedingdata';
|
||||||
|
|
||||||
|
export async function productSeed() {
|
||||||
|
try {
|
||||||
|
// Clear existing data
|
||||||
|
await db.delete(packages);
|
||||||
|
await db.delete(products);
|
||||||
|
|
||||||
|
const sampleData = getSeedingData('products')
|
||||||
|
|
||||||
|
await db.insert(products).values(sampleData as IProductSeed[]);
|
||||||
|
|
||||||
|
console.log('Products seeded successfully! ✅');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error seeding database:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,29 +1,41 @@
|
|||||||
import { eq, lt, sql } from 'drizzle-orm';
|
import { eq, lt, sql } from "drizzle-orm";
|
||||||
import { db } from '../../database/connexion';
|
import { db } from "../../database/connexion";
|
||||||
import { subscriptions } from '../../database/schema';
|
import { products, Subscription, subscriptions } from "../../database/schema";
|
||||||
import type { CreateSubscriptionRequest, Subscription } from '../../lib/types';
|
import { addMonths, generateId } from "../../lib/utils";
|
||||||
import { addMonths, generateId } from '../../lib/utils';
|
|
||||||
|
|
||||||
export class SubscriptionService {
|
export class SubscriptionService {
|
||||||
static async createSubscription(data: CreateSubscriptionRequest) {
|
static async createSubscription(data: Omit<Subscription, 'protectKey' | 'id' | 'endDate'>) {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const newSub = await db.insert(subscriptions).values({
|
const newSub = await db
|
||||||
protectKey: 'USSK_' + generateId(6),
|
.insert(subscriptions)
|
||||||
subscriber: data.subscriber,
|
.values({
|
||||||
package: data.packageId,
|
...data,
|
||||||
startDate: now,
|
protectKey: "USSK_" + generateId(6),
|
||||||
durationInMonths: data.durationInMonths,
|
startDate: data.startDate || now,
|
||||||
endDate: addMonths(now, data.durationInMonths),
|
endDate: addMonths(data.startDate || now, data.durationInMonths),
|
||||||
status: 'active',
|
createdAt: now,
|
||||||
createdAt: now,
|
updatedAt: now,
|
||||||
updatedAt: now,
|
status: data.status || "active",
|
||||||
}).returning();
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return newSub[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
static async updateSubscription(data: Subscription) {
|
||||||
|
const now = new Date();
|
||||||
|
const newSub = await db
|
||||||
|
.update(subscriptions)
|
||||||
|
.set({ ...data, updatedAt: now })
|
||||||
|
.where(eq(subscriptions.protectKey, data.protectKey))
|
||||||
|
.returning();
|
||||||
|
|
||||||
return newSub[0];
|
return newSub[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getSubscriptions(limit = 10, page = 1): Promise<Subscription[]> {
|
static async getSubscriptions(limit = 10, page = 1): Promise<Subscription[]> {
|
||||||
const data= await db.select()
|
const data = await db
|
||||||
|
.select()
|
||||||
.from(subscriptions)
|
.from(subscriptions)
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.offset((page - 1) * limit);
|
.offset((page - 1) * limit);
|
||||||
@ -33,12 +45,13 @@ export class SubscriptionService {
|
|||||||
|
|
||||||
static async updateSubscriptionStatus(
|
static async updateSubscriptionStatus(
|
||||||
subscriptionId: string,
|
subscriptionId: string,
|
||||||
status: 'active' | 'expired' | 'canceled'
|
status: "active" | "expired" | "canceled"
|
||||||
): Promise<Subscription | null> {
|
): Promise<Subscription | null> {
|
||||||
const [updatedSubscription] = await db.update(subscriptions)
|
const [updatedSubscription] = await db
|
||||||
|
.update(subscriptions)
|
||||||
.set({
|
.set({
|
||||||
status,
|
status,
|
||||||
updatedAt: new Date()
|
updatedAt: new Date(),
|
||||||
})
|
})
|
||||||
.where(eq(subscriptions.id, subscriptionId))
|
.where(eq(subscriptions.id, subscriptionId))
|
||||||
.returning();
|
.returning();
|
||||||
@ -46,18 +59,42 @@ export class SubscriptionService {
|
|||||||
return updatedSubscription || null;
|
return updatedSubscription || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getSubscriptionByProtectKey(protectKey: string): Promise<Subscription | null> {
|
static async getSubscriptionByProtectKey(
|
||||||
const [subscription] = await db.select()
|
protectKey: string
|
||||||
|
): Promise<Subscription | null> {
|
||||||
|
try {
|
||||||
|
const [subscription] = await db
|
||||||
|
.select()
|
||||||
.from(subscriptions)
|
.from(subscriptions)
|
||||||
.where(eq(subscriptions.protectKey, protectKey))
|
.where(eq(subscriptions.protectKey, protectKey))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
return subscription || null;
|
if (!subscription) {
|
||||||
|
throw new Error("Subscription not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the product
|
||||||
|
const product = await db.select({
|
||||||
|
name: products.name,
|
||||||
|
}).from(products).where(eq(products.id, subscription.product)).limit(1);
|
||||||
|
|
||||||
|
if (!product) {
|
||||||
|
throw new Error("Product not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
subscription.product = product[0].name;
|
||||||
|
return subscription;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Get subscription by protect key error:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getSubscriptionById(id: string): Promise<Subscription | null> {
|
static async getSubscriptionById(id: string): Promise<Subscription | null> {
|
||||||
const [subscription] = await db.select()
|
const [subscription] = await db
|
||||||
|
.select()
|
||||||
.from(subscriptions)
|
.from(subscriptions)
|
||||||
|
.$dynamic()
|
||||||
.where(eq(subscriptions.id, id))
|
.where(eq(subscriptions.id, id))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
@ -70,7 +107,8 @@ export class SubscriptionService {
|
|||||||
}): Promise<Partial<Subscription>[]> {
|
}): Promise<Partial<Subscription>[]> {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
const expiredSubscriptions = await db.select({
|
const expiredSubscriptions = await db
|
||||||
|
.select({
|
||||||
id: subscriptions.id,
|
id: subscriptions.id,
|
||||||
protectKey: subscriptions.protectKey,
|
protectKey: subscriptions.protectKey,
|
||||||
subscriber: subscriptions.subscriber,
|
subscriber: subscriptions.subscriber,
|
||||||
@ -79,7 +117,7 @@ export class SubscriptionService {
|
|||||||
endDate: subscriptions.endDate,
|
endDate: subscriptions.endDate,
|
||||||
sentSmsCount: subscriptions.sentSmsCount,
|
sentSmsCount: subscriptions.sentSmsCount,
|
||||||
status: subscriptions.status,
|
status: subscriptions.status,
|
||||||
})
|
})
|
||||||
.from(subscriptions)
|
.from(subscriptions)
|
||||||
.where(lt(subscriptions.endDate, now))
|
.where(lt(subscriptions.endDate, now))
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
@ -89,10 +127,11 @@ export class SubscriptionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async incrementSmsCount(protectKey: string): Promise<void> {
|
static async incrementSmsCount(protectKey: string): Promise<void> {
|
||||||
await db.update(subscriptions)
|
await db
|
||||||
|
.update(subscriptions)
|
||||||
.set({
|
.set({
|
||||||
sentSmsCount: sql`${subscriptions.sentSmsCount} + 1`,
|
sentSmsCount: sql`${subscriptions.sentSmsCount} + 1`,
|
||||||
updatedAt: new Date()
|
updatedAt: new Date(),
|
||||||
})
|
})
|
||||||
.where(eq(subscriptions.protectKey, protectKey));
|
.where(eq(subscriptions.protectKey, protectKey));
|
||||||
}
|
}
|
||||||
|
|||||||
36
src/server/utils/getSeedingdata.ts
Normal file
36
src/server/utils/getSeedingdata.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { ISeedingData, seedingData } from "../../lib/constants";
|
||||||
|
import { IPackageSeed, IProductSeed } from "../../lib/types";
|
||||||
|
|
||||||
|
type ISeedDataType = IProductSeed | IPackageSeed;
|
||||||
|
|
||||||
|
export function getSeedingData(slug: keyof ISeedingData, addPropertyPerProduct?: {
|
||||||
|
productName: string,
|
||||||
|
property: string,
|
||||||
|
value: any
|
||||||
|
}[]) {
|
||||||
|
const seedingJson = seedingData[slug]
|
||||||
|
type ISeedingJson = typeof seedingJson
|
||||||
|
let x: ISeedDataType[] = []
|
||||||
|
for (const key in seedingJson){
|
||||||
|
if (!addPropertyPerProduct) {
|
||||||
|
const value = seedingJson[key as keyof ISeedingJson]
|
||||||
|
x = [...x, ...value as ISeedDataType[]]
|
||||||
|
} else {
|
||||||
|
const selected = addPropertyPerProduct.find((item) => item.productName === key)
|
||||||
|
if (selected) {
|
||||||
|
const value = seedingJson[key as keyof ISeedingJson]
|
||||||
|
const newValue = value.map((item) => {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
[selected.property]: selected.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
x = [...x, ...newValue as ISeedDataType[]]
|
||||||
|
} else {
|
||||||
|
const value = seedingJson[key as keyof ISeedingJson]
|
||||||
|
x = [...x, ...value as ISeedDataType[]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return x as ISeedDataType[]
|
||||||
|
}
|
||||||
193
src/server/utils/pagination.util.ts
Normal file
193
src/server/utils/pagination.util.ts
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
// import { count, SQL } from "drizzle-orm";
|
||||||
|
// import { PgDatabase, PgTable, PgTableWithColumns } from "drizzle-orm/pg-core";
|
||||||
|
// // import { AnyTable } from 'drizzle-orm';
|
||||||
|
|
||||||
|
// // Adjust if you use SQLite or another database
|
||||||
|
// type DrizzleDB = PgDatabase<any, any>;
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Generic pagination utility for Drizzle ORM.
|
||||||
|
// *
|
||||||
|
// * @template T - The Drizzle ORM schema table type.
|
||||||
|
// * @param {DrizzleDB} db - The Drizzle ORM database instance.
|
||||||
|
// * @param {T} table - The Drizzle ORM schema table to paginate.
|
||||||
|
// * @param {number} page - The current page number (1-indexed).
|
||||||
|
// * @param {number} limit - The number of items per page.
|
||||||
|
// * @param {SQL | undefined} whereClause - Optional Drizzle SQL condition for filtering.
|
||||||
|
// * @returns {Promise<{ data: T[], totalRecords: number, totalPages: number, currentPage: number, pageSize: number }>}
|
||||||
|
// */
|
||||||
|
// export async function paginate<T extends PgTable>({ // Adjusted for PgTable
|
||||||
|
// db,
|
||||||
|
// table,
|
||||||
|
// page = 1,
|
||||||
|
// limit = 10,
|
||||||
|
// whereClause,
|
||||||
|
// }: {
|
||||||
|
// db: DrizzleDB;
|
||||||
|
// table: PgTableWithColumns<any>;
|
||||||
|
// page: number;
|
||||||
|
// limit: number;
|
||||||
|
// whereClause?: SQL | undefined; // Add optional where clause for filtering
|
||||||
|
// }): Promise<{
|
||||||
|
// data: (typeof table.$inferSelect)[]; // Use $inferSelect for proper type inference
|
||||||
|
// totalRecords: number;
|
||||||
|
// totalPages: number;
|
||||||
|
// currentPage: number;
|
||||||
|
// pageSize: number;
|
||||||
|
// }> {
|
||||||
|
// if (page < 1) page = 1;
|
||||||
|
// if (limit < 1) limit = 10;
|
||||||
|
|
||||||
|
// const offset = (page - 1) * limit;
|
||||||
|
|
||||||
|
// // Build the query
|
||||||
|
// let query = db.select().from(table).$dynamic(); // Use $dynamic to conditionally add clauses
|
||||||
|
|
||||||
|
// if (whereClause) {
|
||||||
|
// query = query.where(whereClause);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const dataPromise = query.limit(limit).offset(offset).execute();
|
||||||
|
|
||||||
|
// // Get the total count, potentially with the same where clause
|
||||||
|
// let countQuery = db
|
||||||
|
// .select({
|
||||||
|
// count: count(),
|
||||||
|
// })
|
||||||
|
// .from(table)
|
||||||
|
// .$dynamic();
|
||||||
|
|
||||||
|
// if (whereClause) {
|
||||||
|
// countQuery = countQuery.where(whereClause);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const countPromise = countQuery.execute();
|
||||||
|
|
||||||
|
// const [data, [countResult]] = await Promise.all([dataPromise, countPromise]);
|
||||||
|
|
||||||
|
// const totalRecords = countResult?.count || 0;
|
||||||
|
// const totalPages = Math.ceil(totalRecords / limit);
|
||||||
|
|
||||||
|
// return {
|
||||||
|
// data: data,
|
||||||
|
// totalRecords,
|
||||||
|
// totalPages,
|
||||||
|
// currentPage: page,
|
||||||
|
// pageSize: limit,
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import { and, count, SQL } from "drizzle-orm";
|
||||||
|
import { PgDatabase, PgTable, PgTableWithColumns } from "drizzle-orm/pg-core";
|
||||||
|
import { FilterCondition } from "../../lib/types";
|
||||||
|
|
||||||
|
type DrizzleDB = PgDatabase<any, any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic pagination utility for Drizzle ORM.
|
||||||
|
*
|
||||||
|
* @template T - The Drizzle ORM schema table type.
|
||||||
|
* @param {DrizzleDB} db - The Drizzle ORM database instance.
|
||||||
|
* @param {T} table - The Drizzle ORM schema table to paginate.
|
||||||
|
* @param {number} page - The current page number (1-indexed).
|
||||||
|
* @param {number} limit - The number of items per page.
|
||||||
|
* @param {FilterCondition[] | undefined} filters - An array of filter conditions, which can be single SQL expressions or nested 'and'/'or' groups.
|
||||||
|
* @returns {Promise<{ data: T[], totalRecords: number, totalPages: number, currentPage: number, pageSize: number }>}
|
||||||
|
*/
|
||||||
|
export async function paginate<T extends PgTable>({
|
||||||
|
db,
|
||||||
|
table,
|
||||||
|
page = 1,
|
||||||
|
limit = 10,
|
||||||
|
filters, // Now using FilterCondition[]
|
||||||
|
}: {
|
||||||
|
db: DrizzleDB;
|
||||||
|
table: PgTableWithColumns<any>;
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
filters?: FilterCondition[]; // Accepts the new structured filters
|
||||||
|
}): Promise<{
|
||||||
|
data: (typeof table.$inferSelect)[];
|
||||||
|
totalRecords: number;
|
||||||
|
totalPages: number;
|
||||||
|
currentPage: number;
|
||||||
|
pageSize: number;
|
||||||
|
}> {
|
||||||
|
if (page < 1) page = 1;
|
||||||
|
if (limit < 1) limit = 10;
|
||||||
|
|
||||||
|
const offset = (page - 1) * limit;
|
||||||
|
|
||||||
|
// Helper function to process the nested filter conditions
|
||||||
|
const buildWhereClause = (conditions?: FilterCondition[]): SQL | undefined => {
|
||||||
|
if (!conditions || conditions.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const builtConditions: SQL[] = [];
|
||||||
|
for (const condition of conditions) {
|
||||||
|
if (condition instanceof SQL) {
|
||||||
|
builtConditions.push(condition);
|
||||||
|
} else if ('and' in condition && Array.isArray(condition.and)) {
|
||||||
|
const nestedAnd = buildWhereClause(condition.and);
|
||||||
|
if (nestedAnd) {
|
||||||
|
builtConditions.push(nestedAnd);
|
||||||
|
}
|
||||||
|
} else if ('or' in condition && Array.isArray(condition.or)) {
|
||||||
|
const nestedOr = buildWhereClause(condition.or);
|
||||||
|
if (nestedOr) {
|
||||||
|
builtConditions.push(nestedOr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// By default, if multiple top-level conditions are provided without an explicit operator,
|
||||||
|
// we'll combine them with 'AND'. If you want a different default, adjust here.
|
||||||
|
if (builtConditions.length === 1) {
|
||||||
|
return builtConditions[0];
|
||||||
|
} else if (builtConditions.length > 1) {
|
||||||
|
return and(...builtConditions);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const finalWhereClause = buildWhereClause(filters);
|
||||||
|
|
||||||
|
// Build the query
|
||||||
|
let query = db.select().from(table).$dynamic();
|
||||||
|
|
||||||
|
if (finalWhereClause) {
|
||||||
|
query = query.where(finalWhereClause);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataPromise = query.limit(limit).offset(offset).execute();
|
||||||
|
|
||||||
|
// Get the total count, potentially with the same where clause
|
||||||
|
let countQuery = db
|
||||||
|
.select({
|
||||||
|
count: count(),
|
||||||
|
})
|
||||||
|
.from(table)
|
||||||
|
.$dynamic();
|
||||||
|
|
||||||
|
if (finalWhereClause) {
|
||||||
|
countQuery = countQuery.where(finalWhereClause);
|
||||||
|
}
|
||||||
|
|
||||||
|
const countPromise = countQuery.execute();
|
||||||
|
|
||||||
|
const [data, [countResult]] = await Promise.all([dataPromise, countPromise]);
|
||||||
|
|
||||||
|
const totalRecords = countResult?.count || 0;
|
||||||
|
const totalPages = Math.ceil(totalRecords / limit);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: data.reverse(),
|
||||||
|
totalRecords,
|
||||||
|
totalPages,
|
||||||
|
currentPage: page,
|
||||||
|
pageSize: limit,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -1,14 +0,0 @@
|
|||||||
import { SessionProvider, useSession } from '@hono/auth-js/dist/react'
|
|
||||||
|
|
||||||
export default function App() {
|
|
||||||
return (
|
|
||||||
<SessionProvider>
|
|
||||||
<Children />
|
|
||||||
</SessionProvider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Children() {
|
|
||||||
const { data: session, status } = useSession()
|
|
||||||
return <div>I am {session?.user}</div>
|
|
||||||
}
|
|
||||||
67
src/web/components/layout/Footer.tsx
Normal file
67
src/web/components/layout/Footer.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
|
||||||
|
export function Footer() {
|
||||||
|
return (
|
||||||
|
<footer className="bg-gray-50 border-t">
|
||||||
|
<div className="max-w-7xl mx-auto py-12 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
|
||||||
|
<div className="col-span-1 md:col-span-2">
|
||||||
|
<h3 className="text-sm font-semibold text-gray-400 tracking-wider uppercase">
|
||||||
|
About SubscriptionHub
|
||||||
|
</h3>
|
||||||
|
<p className="mt-4 text-base text-gray-500">
|
||||||
|
Powerful subscription management for microfinance, educational institutions,
|
||||||
|
and businesses. Manage clients, agents, and operations with ease.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold text-gray-400 tracking-wider uppercase">
|
||||||
|
Support
|
||||||
|
</h3>
|
||||||
|
<ul className="mt-4 space-y-4">
|
||||||
|
<li>
|
||||||
|
<a href="#" className="text-base text-gray-500 hover:text-gray-900">
|
||||||
|
Documentation
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" className="text-base text-gray-500 hover:text-gray-900">
|
||||||
|
Help Center
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" className="text-base text-gray-500 hover:text-gray-900">
|
||||||
|
Contact Us
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold text-gray-400 tracking-wider uppercase">
|
||||||
|
Legal
|
||||||
|
</h3>
|
||||||
|
<ul className="mt-4 space-y-4">
|
||||||
|
<li>
|
||||||
|
<a href="#" className="text-base text-gray-500 hover:text-gray-900">
|
||||||
|
Privacy Policy
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" className="text-base text-gray-500 hover:text-gray-900">
|
||||||
|
Terms of Service
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8 border-t border-gray-200 pt-8">
|
||||||
|
<p className="text-base text-gray-400 text-center">
|
||||||
|
© 2024 SubscriptionHub. All rights reserved.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
79
src/web/components/layout/Header.tsx
Normal file
79
src/web/components/layout/Header.tsx
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import { LogOut, User } from "lucide-react";
|
||||||
|
interface HeaderProps {
|
||||||
|
user?: { email: string; name: string } | null;
|
||||||
|
onLogout?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Header({ user, onLogout }: HeaderProps) {
|
||||||
|
return (
|
||||||
|
<header className="bg-white shadow-sm border-b">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="flex justify-between items-center h-16">
|
||||||
|
<div className="flex items-center">
|
||||||
|
{/* <Package className="h-8 w-8 text-primary-600" /> */}
|
||||||
|
<span className="ml-2 text-xl font-bold text-gray-900">
|
||||||
|
SubscriptionHub
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav className="hidden md:flex space-x-8">
|
||||||
|
<a
|
||||||
|
href="/"
|
||||||
|
className="text-gray-500 hover:text-gray-900 px-3 py-2 text-sm font-medium"
|
||||||
|
>
|
||||||
|
Home
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="/packages"
|
||||||
|
className="text-gray-500 hover:text-gray-900 px-3 py-2 text-sm font-medium"
|
||||||
|
>
|
||||||
|
Packages
|
||||||
|
</a>
|
||||||
|
{user && (
|
||||||
|
<a
|
||||||
|
href="/dashboard"
|
||||||
|
className="text-gray-500 hover:text-gray-900 px-3 py-2 text-sm font-medium"
|
||||||
|
>
|
||||||
|
Dashboard
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
{user ? (
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<User className="h-5 w-5 text-gray-400" />
|
||||||
|
<span className="text-sm text-gray-700">{user.email}</span>
|
||||||
|
{user.name && (
|
||||||
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-primary-100 text-primary-800">
|
||||||
|
{user.name}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href="/api/auth/signout"
|
||||||
|
className="flex items-center space-x-1 text-gray-500 hover:text-gray-700"
|
||||||
|
>
|
||||||
|
<LogOut className="h-4 w-4" />
|
||||||
|
<span className="text-sm">Logout</span>
|
||||||
|
</a>
|
||||||
|
{/* <button
|
||||||
|
onClick={onLogout}
|
||||||
|
className="flex items-center space-x-1 text-gray-500 hover:text-gray-700"
|
||||||
|
>
|
||||||
|
<LogOut className="h-4 w-4" />
|
||||||
|
<span className="text-sm">Logout</span>
|
||||||
|
</button> */}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button className="bg-primary-600 text-white px-4 py-2 rounded-md text-sm font-medium hover:bg-primary-700">
|
||||||
|
Sign In
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
109
src/web/components/packages/Packagecard.tsx
Normal file
109
src/web/components/packages/Packagecard.tsx
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import { Package } from '../../../database/schema';
|
||||||
|
import { formatCurrency } from '../../../lib/utils';
|
||||||
|
|
||||||
|
interface PackageCardProps {
|
||||||
|
package: Package;
|
||||||
|
onSelect?: (pkg: Package) => void;
|
||||||
|
isPopular?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PackageCard({ package: pkg, onSelect, isPopular }: PackageCardProps) {
|
||||||
|
const getSupportIcon = (type: string) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'basic': return '📧';
|
||||||
|
case 'premium': return '💬';
|
||||||
|
case 'enterprise': return '📞';
|
||||||
|
default: return '📧';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`relative bg-white rounded-lg shadow-lg border-2 transition-all duration-200 hover:shadow-xl ${
|
||||||
|
isPopular ? 'border-primary-500 ring-2 ring-primary-200' : 'border-gray-200 hover:border-primary-300'
|
||||||
|
}`}>
|
||||||
|
{isPopular && (
|
||||||
|
<div className="absolute -top-3 left-1/2 transform -translate-x-1/2">
|
||||||
|
<span className="bg-primary-500 text-white px-4 py-1 rounded-full text-sm font-medium">
|
||||||
|
Most Popular
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="text-2xl font-bold text-gray-900">{pkg.name}</h3>
|
||||||
|
<div className="mt-4">
|
||||||
|
<span className="text-4xl font-bold text-gray-900">
|
||||||
|
{formatCurrency(pkg.price / 100)}
|
||||||
|
</span>
|
||||||
|
<span className="text-gray-500">/month</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* <div className="mt-6 space-y-4">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Users />
|
||||||
|
<span className="ml-3 text-gray-700">
|
||||||
|
{pkg.clientsCount.toLocaleString()} Clients
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Building className="h-5 w-5 text-primary-500" />
|
||||||
|
<span className="ml-3 text-gray-700">
|
||||||
|
{pkg.agentsCount} Agents • {pkg.adminsCount} Admins
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<MessageSquare className="h-5 w-5 text-primary-500" />
|
||||||
|
<span className="ml-3 text-gray-700">
|
||||||
|
{pkg.smsCount.toLocaleString()} SMS/month
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Shield className="h-5 w-5 text-primary-500" />
|
||||||
|
<span className="ml-3 text-gray-700">
|
||||||
|
{getSupportIcon(pkg.supportType)} {pkg.supportType.charAt(0).toUpperCase() + pkg.supportType.slice(1)} Support
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 space-y-2">
|
||||||
|
{pkg.agentLocalization && (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Check className="h-4 w-4 text-green-500" />
|
||||||
|
<span className="ml-2 text-sm text-gray-600">Agent Localization</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{pkg.enableCommission && (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Check className="h-4 w-4 text-green-500" />
|
||||||
|
<span className="ml-2 text-sm text-gray-600">Commission System</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Check className="h-4 w-4 text-green-500" />
|
||||||
|
<span className="ml-2 text-sm text-gray-600">
|
||||||
|
{pkg.supervisorsCount} Supervisors • {pkg.cashiersCount} Cashiers
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div> */}
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => onSelect?.(pkg)}
|
||||||
|
className={`mt-8 w-full py-3 px-4 rounded-md font-medium transition-colors ${
|
||||||
|
isPopular
|
||||||
|
? 'bg-primary-600 text-white hover:bg-primary-700'
|
||||||
|
: 'bg-gray-100 text-gray-900 hover:bg-gray-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Choose Plan
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
302
src/web/pages/DashboardPage.tsx
Normal file
302
src/web/pages/DashboardPage.tsx
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
import { AlertCircle } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Subscription } from "../../database/schema";
|
||||||
|
import { formatDate } from "../../lib/utils";
|
||||||
|
import { SubscriptionService } from "../../server/services/subscriptions";
|
||||||
|
import { Header } from "../components/layout/Header";
|
||||||
|
|
||||||
|
export default function DashboardPage({
|
||||||
|
user,
|
||||||
|
}: {
|
||||||
|
user?: { email: string; name: string } | null;
|
||||||
|
}) {
|
||||||
|
const [subscriptions, setSubscriptions] = useState<Subscription[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
const getSubscriptions = async () => {
|
||||||
|
try {
|
||||||
|
console.log("Fetching subscriptions...");
|
||||||
|
const response = await SubscriptionService.getSubscriptions(0, 1);
|
||||||
|
console.log({ response });
|
||||||
|
if (response) {
|
||||||
|
setSubscriptions(response);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch subscriptions:", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getSubscriptions();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
<Header user={user} />
|
||||||
|
<div className="max-w-7xl mx-auto py-8 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="mb-8">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900">Dashboard</h1>
|
||||||
|
<p className="mt-2 text-gray-600">
|
||||||
|
Monitor your subscription status and usage metrics
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-center min-h-[60vh]">
|
||||||
|
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-primary-600"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeSubscription = subscriptions.find(
|
||||||
|
(sub) => sub.status === "active"
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
<Header user={user} />
|
||||||
|
|
||||||
|
<div className="max-w-7xl mx-auto py-8 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="mb-8">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900">Dashboard</h1>
|
||||||
|
<p className="mt-2 text-gray-600">
|
||||||
|
Monitor your subscription status and usage metrics
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!activeSubscription ? (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<AlertCircle className="mx-auto h-12 w-12 text-gray-400" />
|
||||||
|
<h3 className="mt-2 text-sm font-medium text-gray-900">
|
||||||
|
No active subscription
|
||||||
|
</h3>
|
||||||
|
<p className="mt-1 text-sm text-gray-500">
|
||||||
|
Get started by choosing a subscription package.
|
||||||
|
</p>
|
||||||
|
<div className="mt-6">
|
||||||
|
<a
|
||||||
|
href="/packages"
|
||||||
|
className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700"
|
||||||
|
>
|
||||||
|
View Packages
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* Subscription Overview */}
|
||||||
|
{/* <div className="bg-white overflow-hidden shadow rounded-lg mb-8">
|
||||||
|
<div className="px-4 py-5 sm:p-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg leading-6 font-medium text-gray-900">
|
||||||
|
Current Subscription
|
||||||
|
</h3>
|
||||||
|
<p className="mt-1 text-sm text-gray-500">
|
||||||
|
{activeSubscription.packageSnapshot.name} Plan
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||||||
|
activeSubscription.status === 'active'
|
||||||
|
? 'bg-green-100 text-green-800'
|
||||||
|
: 'bg-red-100 text-red-800'
|
||||||
|
}`}>
|
||||||
|
{activeSubscription.status.charAt(0).toUpperCase() + activeSubscription.status.slice(1)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
|
<div className="bg-gray-50 overflow-hidden rounded-lg p-4">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Calendar className="h-8 w-8 text-primary-600" />
|
||||||
|
<div className="ml-4">
|
||||||
|
<p className="text-sm font-medium text-gray-500">Expires</p>
|
||||||
|
<p className="text-lg font-semibold text-gray-900">
|
||||||
|
{formatDate(activeSubscription.endDate)}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
{getDaysUntilExpiry(activeSubscription.endDate)} days left
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-gray-50 overflow-hidden rounded-lg p-4">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Users className="h-8 w-8 text-primary-600" />
|
||||||
|
<div className="ml-4">
|
||||||
|
<p className="text-sm font-medium text-gray-500">Clients</p>
|
||||||
|
<p className="text-lg font-semibold text-gray-900">
|
||||||
|
{activeSubscription.packageSnapshot.clientsCount.toLocaleString()}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-500">Available</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-gray-50 overflow-hidden rounded-lg p-4">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<MessageSquare className="h-8 w-8 text-primary-600" />
|
||||||
|
<div className="ml-4">
|
||||||
|
<p className="text-sm font-medium text-gray-500">SMS Usage</p>
|
||||||
|
<p className="text-lg font-semibold text-gray-900">
|
||||||
|
{activeSubscription.sentSmsCount.toLocaleString()} / {activeSubscription.packageSnapshot.smsCount.toLocaleString()}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
{Math.round((activeSubscription.sentSmsCount / activeSubscription.packageSnapshot.smsCount) * 100)}% used
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-gray-50 overflow-hidden rounded-lg p-4">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<TrendingUp className="h-8 w-8 text-primary-600" />
|
||||||
|
<div className="ml-4">
|
||||||
|
<p className="text-sm font-medium text-gray-500">Monthly Cost</p>
|
||||||
|
<p className="text-lg font-semibold text-gray-900">
|
||||||
|
{formatCurrency(activeSubscription.packageSnapshot.price / 100)}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-500">Per month</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div> */}
|
||||||
|
|
||||||
|
{/* Usage Details */}
|
||||||
|
{/* <div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||||
|
<div className="bg-white overflow-hidden shadow rounded-lg">
|
||||||
|
<div className="px-4 py-5 sm:p-6">
|
||||||
|
<h3 className="text-lg leading-6 font-medium text-gray-900 mb-4">
|
||||||
|
Resource Limits
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span>Agents</span>
|
||||||
|
<span>{activeSubscription.packageSnapshot.agentsCount}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span>Admins</span>
|
||||||
|
<span>{activeSubscription.packageSnapshot.adminsCount}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span>Agencies</span>
|
||||||
|
<span>{activeSubscription.packageSnapshot.agenciesCount}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span>Supervisors</span>
|
||||||
|
<span>{activeSubscription.packageSnapshot.supervisorsCount}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span>Cashiers</span>
|
||||||
|
<span>{activeSubscription.packageSnapshot.cashiersCount}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white overflow-hidden shadow rounded-lg">
|
||||||
|
<div className="px-4 py-5 sm:p-6">
|
||||||
|
<h3 className="text-lg leading-6 font-medium text-gray-900 mb-4">
|
||||||
|
Features
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className={`h-2 w-2 rounded-full mr-3 ${
|
||||||
|
activeSubscription.packageSnapshot.agentLocalization ? 'bg-green-400' : 'bg-gray-300'
|
||||||
|
}`}></div>
|
||||||
|
<span className="text-sm text-gray-700">Agent Localization</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className={`h-2 w-2 rounded-full mr-3 ${
|
||||||
|
activeSubscription.packageSnapshot.enableCommission ? 'bg-green-400' : 'bg-gray-300'
|
||||||
|
}`}></div>
|
||||||
|
<span className="text-sm text-gray-700">Commission System</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="h-2 w-2 rounded-full mr-3 bg-green-400"></div>
|
||||||
|
<span className="text-sm text-gray-700">
|
||||||
|
{activeSubscription.packageSnapshot.supportType.charAt(0).toUpperCase() +
|
||||||
|
activeSubscription.packageSnapshot.supportType.slice(1)} Support
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div> */}
|
||||||
|
|
||||||
|
{/* Subscription Details */}
|
||||||
|
<div className="mt-8 bg-white overflow-hidden shadow rounded-lg">
|
||||||
|
<div className="px-4 py-5 sm:p-6">
|
||||||
|
<h3 className="text-lg leading-6 font-medium text-gray-900 mb-4">
|
||||||
|
Subscription Details
|
||||||
|
</h3>
|
||||||
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm font-medium text-gray-500">
|
||||||
|
Organization
|
||||||
|
</dt>
|
||||||
|
<dd className="mt-1 text-sm text-gray-900">
|
||||||
|
{activeSubscription.subscriber.name}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm font-medium text-gray-500">Type</dt>
|
||||||
|
<dd className="mt-1 text-sm text-gray-900 capitalize">
|
||||||
|
{activeSubscription.subscriber.type}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm font-medium text-gray-500">Email</dt>
|
||||||
|
<dd className="mt-1 text-sm text-gray-900">
|
||||||
|
{activeSubscription.subscriber.email}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm font-medium text-gray-500">Phone</dt>
|
||||||
|
<dd className="mt-1 text-sm text-gray-900">
|
||||||
|
{activeSubscription.subscriber.phone}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm font-medium text-gray-500">
|
||||||
|
Protect Key
|
||||||
|
</dt>
|
||||||
|
<dd className="mt-1 text-sm text-gray-900 font-mono">
|
||||||
|
{activeSubscription.protectKey}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm font-medium text-gray-500">
|
||||||
|
Started
|
||||||
|
</dt>
|
||||||
|
<dd className="mt-1 text-sm text-gray-900">
|
||||||
|
{formatDate(activeSubscription.startDate)}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
140
src/web/pages/HomePage.tsx
Normal file
140
src/web/pages/HomePage.tsx
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
|
||||||
|
import { BarChart3, Globe, Package, Shield, Users, Zap } from 'lucide-react';
|
||||||
|
import { Footer } from '../components/layout/Footer';
|
||||||
|
import { Header } from '../components/layout/Header';
|
||||||
|
|
||||||
|
export default function HomePage({ user }: { user?: { email: string; name: string } | null }) {
|
||||||
|
const features = [
|
||||||
|
{
|
||||||
|
icon: Users,
|
||||||
|
title: 'Client Management',
|
||||||
|
description: 'Manage thousands of clients with advanced filtering and organization tools.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: BarChart3,
|
||||||
|
title: 'Analytics Dashboard',
|
||||||
|
description: 'Real-time insights into your operations with comprehensive reporting.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Shield,
|
||||||
|
title: 'Secure & Reliable',
|
||||||
|
description: 'Enterprise-grade security with 99.9% uptime guarantee.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Zap,
|
||||||
|
title: 'Fast Performance',
|
||||||
|
description: 'Lightning-fast operations designed for high-volume transactions.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Globe,
|
||||||
|
title: 'Multi-Location',
|
||||||
|
description: 'Support for multiple branches and geographical locations.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Package,
|
||||||
|
title: 'Flexible Plans',
|
||||||
|
description: 'Choose from various packages tailored to your business needs.'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
<Header user={user} />
|
||||||
|
|
||||||
|
{/* Hero Section */}
|
||||||
|
<div className="relative bg-white overflow-hidden">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<div className="relative z-10 pb-8 bg-white sm:pb-16 md:pb-20 lg:max-w-2xl lg:w-full lg:pb-28 xl:pb-32">
|
||||||
|
<main className="mt-10 mx-auto max-w-7xl px-4 sm:mt-12 sm:px-6 md:mt-16 lg:mt-20 lg:px-8 xl:mt-28">
|
||||||
|
<div className="sm:text-center lg:text-left">
|
||||||
|
<h1 className="text-4xl tracking-tight font-extrabold text-gray-900 sm:text-5xl md:text-6xl">
|
||||||
|
<span className="block xl:inline">Powerful</span>{' '}
|
||||||
|
<span className="block text-primary-600 xl:inline">subscription management</span>
|
||||||
|
</h1>
|
||||||
|
<p className="mt-3 text-base text-gray-500 sm:mt-5 sm:text-lg sm:max-w-xl sm:mx-auto md:mt-5 md:text-xl lg:mx-0">
|
||||||
|
Streamline your microfinance, educational, or business operations with our comprehensive
|
||||||
|
subscription platform. Manage clients, agents, and operations with ease.
|
||||||
|
</p>
|
||||||
|
<div className="mt-5 sm:mt-8 sm:flex sm:justify-center lg:justify-start">
|
||||||
|
<div className="rounded-md shadow">
|
||||||
|
<a
|
||||||
|
href="/packages"
|
||||||
|
className="w-full flex items-center justify-center px-8 py-3 border border-transparent text-base font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 md:py-4 md:text-lg md:px-10"
|
||||||
|
>
|
||||||
|
View Packages
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 sm:mt-0 sm:ml-3">
|
||||||
|
<a
|
||||||
|
href="#features"
|
||||||
|
className="w-full flex items-center justify-center px-8 py-3 border border-transparent text-base font-medium rounded-md text-primary-700 bg-primary-100 hover:bg-primary-200 md:py-4 md:text-lg md:px-10"
|
||||||
|
>
|
||||||
|
Learn More
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="lg:absolute lg:inset-y-0 lg:right-0 lg:w-1/2">
|
||||||
|
<img
|
||||||
|
className="h-56 w-full object-cover sm:h-72 md:h-96 lg:w-full lg:h-full"
|
||||||
|
src="https://images.pexels.com/photos/3184360/pexels-photo-3184360.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2"
|
||||||
|
alt="Business team working"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Features Section */}
|
||||||
|
<div id="features" className="py-12 bg-white">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="lg:text-center">
|
||||||
|
<h2 className="text-base text-primary-600 font-semibold tracking-wide uppercase">Features</h2>
|
||||||
|
<p className="mt-2 text-3xl leading-8 font-extrabold tracking-tight text-gray-900 sm:text-4xl">
|
||||||
|
Everything you need to succeed
|
||||||
|
</p>
|
||||||
|
<p className="mt-4 max-w-2xl text-xl text-gray-500 lg:mx-auto">
|
||||||
|
Our platform provides all the tools you need to manage your subscription-based business effectively.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-10">
|
||||||
|
<div className="space-y-10 md:space-y-0 md:grid md:grid-cols-2 md:gap-x-8 md:gap-y-10 lg:grid-cols-3">
|
||||||
|
{features.map((feature) => (
|
||||||
|
<div key={feature.title} className="relative">
|
||||||
|
<div className="absolute flex items-center justify-center h-12 w-12 rounded-md bg-primary-500 text-white">
|
||||||
|
<feature.icon className="h-6 w-6" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
<p className="ml-16 text-lg leading-6 font-medium text-gray-900">{feature.title}</p>
|
||||||
|
<dd className="mt-2 ml-16 text-base text-gray-500">{feature.description}</dd>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* CTA Section */}
|
||||||
|
<div className="bg-primary-700">
|
||||||
|
<div className="max-w-2xl mx-auto text-center py-16 px-4 sm:py-20 sm:px-6 lg:px-8">
|
||||||
|
<h2 className="text-3xl font-extrabold text-white sm:text-4xl">
|
||||||
|
<span className="block">Ready to get started?</span>
|
||||||
|
<span className="block">Choose your perfect plan today.</span>
|
||||||
|
</h2>
|
||||||
|
<p className="mt-4 text-lg leading-6 text-primary-200">
|
||||||
|
Join thousands of businesses already using our platform to manage their operations.
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href="/packages"
|
||||||
|
className="mt-8 w-full inline-flex items-center justify-center px-5 py-3 border border-transparent text-base font-medium rounded-md text-primary-600 bg-white hover:bg-primary-50 sm:w-auto"
|
||||||
|
>
|
||||||
|
View Packages
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
134
src/web/pages/PackagesPage.tsx
Normal file
134
src/web/pages/PackagesPage.tsx
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Package } from "../../database/schema";
|
||||||
|
import { Header } from "../components/layout/Header";
|
||||||
|
import { PackageCard } from "../components/packages/Packagecard";
|
||||||
|
|
||||||
|
export default function PackagesPage({
|
||||||
|
user,
|
||||||
|
}: {
|
||||||
|
user?: { email: string; name: string; token: string } | null;
|
||||||
|
}) {
|
||||||
|
const [packages, setPackages] = useState<Package[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchPackages();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchPackages = async (limit = 10, page = 1) => {
|
||||||
|
console.log("Fetching packages...");
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/packages?limit=${limit}&page=${page}`,
|
||||||
|
);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
setPackages(data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch packages:", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectPackage = (pkg: Package) => {
|
||||||
|
// In a real app, this would open a subscription modal or redirect to checkout
|
||||||
|
alert(`Selected package: ${pkg.name}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
<Header user={user} />
|
||||||
|
<div className="flex items-center justify-center min-h-[60vh]">
|
||||||
|
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-primary-600"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
<Header user={user} />
|
||||||
|
|
||||||
|
<div className="max-w-7xl mx-auto py-16 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="text-center">
|
||||||
|
<h1 className="text-4xl font-extrabold text-gray-900 sm:text-5xl">
|
||||||
|
Choose Your Perfect Plan
|
||||||
|
</h1>
|
||||||
|
<p className="mt-4 text-xl text-gray-600 max-w-2xl mx-auto">
|
||||||
|
Select the subscription package that best fits your business needs.
|
||||||
|
All plans include our core features with varying limits and support
|
||||||
|
levels.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{packages.length === 0 ? (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<p className="text-gray-500 text-lg">
|
||||||
|
No packages available at the moment.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="mt-16 grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{packages.map((pkg, index) => (
|
||||||
|
<PackageCard
|
||||||
|
key={pkg.id}
|
||||||
|
package={pkg}
|
||||||
|
onSelect={handleSelectPackage}
|
||||||
|
isPopular={index === 1} // Make the second package popular
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* FAQ Section */}
|
||||||
|
<div className="mt-20">
|
||||||
|
<h2 className="text-3xl font-extrabold text-gray-900 text-center mb-12">
|
||||||
|
Frequently Asked Questions
|
||||||
|
</h2>
|
||||||
|
<div className="grid grid-cols-1 gap-8 lg:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||||
|
Can I change my plan later?
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
Yes, you can upgrade or downgrade your plan at any time. Changes
|
||||||
|
will be reflected in your next billing cycle.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||||
|
What happens if I exceed my limits?
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
We'll notify you when you're approaching your limits. You can
|
||||||
|
upgrade your plan or purchase additional resources.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||||
|
Is there a free trial?
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
Yes, all plans come with a 14-day free trial. No credit card
|
||||||
|
required to get started.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||||
|
What support is included?
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
All plans include email support. Premium and Enterprise plans
|
||||||
|
include priority support and phone assistance.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -2,6 +2,6 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"jsxImportSource": "hono/jsx"
|
"jsxImportSource": "react"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
49
yarn.lock
49
yarn.lock
@ -350,11 +350,21 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@hono/node-server/-/node-server-1.19.3.tgz#f0483de5471b415dce0799a34171c05bfe30a6fd"
|
resolved "https://registry.yarnpkg.com/@hono/node-server/-/node-server-1.19.3.tgz#f0483de5471b415dce0799a34171c05bfe30a6fd"
|
||||||
integrity sha512-Fjyxfux0rMPXMSob79OmddfpK5ArJa2xLkLCV+zamHkbeXQtSNKOi0keiBKyHZ/hCRKjigjmKGp4AJnDFq8PUw==
|
integrity sha512-Fjyxfux0rMPXMSob79OmddfpK5ArJa2xLkLCV+zamHkbeXQtSNKOi0keiBKyHZ/hCRKjigjmKGp4AJnDFq8PUw==
|
||||||
|
|
||||||
"@hono/react-renderer@^1.0.0":
|
"@hono/react-renderer@^1.0.1":
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@hono/react-renderer/-/react-renderer-1.0.1.tgz#c7fa54dbfd0e910800f7f82bc8201dce72a8d4d0"
|
resolved "https://registry.yarnpkg.com/@hono/react-renderer/-/react-renderer-1.0.1.tgz#c7fa54dbfd0e910800f7f82bc8201dce72a8d4d0"
|
||||||
integrity sha512-vjQ/70hVrbgsi2O44N7w5sO0v51lRcuXau/4caVzw0A1hje+U2zAnuhiBC3JhX56gGfhGT4kO5B0di4SROx0Lg==
|
integrity sha512-vjQ/70hVrbgsi2O44N7w5sO0v51lRcuXau/4caVzw0A1hje+U2zAnuhiBC3JhX56gGfhGT4kO5B0di4SROx0Lg==
|
||||||
|
|
||||||
|
"@hono/standard-validator@^0.1.5":
|
||||||
|
version "0.1.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@hono/standard-validator/-/standard-validator-0.1.5.tgz#f9a8dbaef9d858e2b75014ad2290630b1aeb4edf"
|
||||||
|
integrity sha512-EIyZPPwkyLn6XKwFj5NBEWHXhXbgmnVh2ceIFo5GO7gKI9WmzTjPDKnppQB0KrqKeAkq3kpoW4SIbu5X1dgx3w==
|
||||||
|
|
||||||
|
"@hono/swagger-ui@^0.5.2":
|
||||||
|
version "0.5.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@hono/swagger-ui/-/swagger-ui-0.5.2.tgz#37de7765d3e729a241def1a2f75c69f236d677fb"
|
||||||
|
integrity sha512-7wxLKdb8h7JTdZ+K8DJNE3KXQMIpJejkBTQjrYlUWF28Z1PGOKw6kUykARe5NTfueIN37jbyG/sBYsbzXzG53A==
|
||||||
|
|
||||||
"@humanfs/core@^0.19.1":
|
"@humanfs/core@^0.19.1":
|
||||||
version "0.19.1"
|
version "0.19.1"
|
||||||
resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.1.tgz#17c55ca7d426733fe3c561906b8173c336b40a77"
|
resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.1.tgz#17c55ca7d426733fe3c561906b8173c336b40a77"
|
||||||
@ -452,22 +462,16 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
undici-types "~7.12.0"
|
undici-types "~7.12.0"
|
||||||
|
|
||||||
"@types/prop-types@*":
|
"@types/react-dom@^19.1.9":
|
||||||
version "15.7.15"
|
version "19.1.9"
|
||||||
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.15.tgz#e6e5a86d602beaca71ce5163fadf5f95d70931c7"
|
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.1.9.tgz#5ab695fce1e804184767932365ae6569c11b4b4b"
|
||||||
integrity sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==
|
integrity sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==
|
||||||
|
|
||||||
"@types/react-dom@^18.3.0":
|
"@types/react@^19.1.13":
|
||||||
version "18.3.7"
|
version "19.1.13"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.7.tgz#b89ddf2cd83b4feafcc4e2ea41afdfb95a0d194f"
|
resolved "https://registry.yarnpkg.com/@types/react/-/react-19.1.13.tgz#fc650ffa680d739a25a530f5d7ebe00cdd771883"
|
||||||
integrity sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==
|
integrity sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==
|
||||||
|
|
||||||
"@types/react@^18.3.5":
|
|
||||||
version "18.3.24"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.24.tgz#f6a5a4c613242dfe3af0dcee2b4ec47b92d9b6bd"
|
|
||||||
integrity sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==
|
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/prop-types" "*"
|
|
||||||
csstype "^3.0.2"
|
csstype "^3.0.2"
|
||||||
|
|
||||||
acorn-jsx@^5.3.2:
|
acorn-jsx@^5.3.2:
|
||||||
@ -879,6 +883,11 @@ has-flag@^4.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
|
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
|
||||||
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
|
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
|
||||||
|
|
||||||
|
hono-openapi@^1.0.8:
|
||||||
|
version "1.0.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/hono-openapi/-/hono-openapi-1.0.8.tgz#1d0b03cf31891eb5fd040b2f635cc6e22ca98162"
|
||||||
|
integrity sha512-JjSdT4sNUgxQGgwO90boRLfnrVYp3ge+Y/vHqPMJrAZuaIhKekAVipoeJ8AgpTyK+ZaxPzqdcmDBA9L+Ce3X9Q==
|
||||||
|
|
||||||
hono@^4.9.8:
|
hono@^4.9.8:
|
||||||
version "4.9.8"
|
version "4.9.8"
|
||||||
resolved "https://registry.yarnpkg.com/hono/-/hono-4.9.8.tgz#1710981135ec775fe26fab5ea6535b403e92bcc3"
|
resolved "https://registry.yarnpkg.com/hono/-/hono-4.9.8.tgz#1710981135ec775fe26fab5ea6535b403e92bcc3"
|
||||||
@ -1010,6 +1019,11 @@ loose-envify@^1.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
js-tokens "^3.0.0 || ^4.0.0"
|
js-tokens "^3.0.0 || ^4.0.0"
|
||||||
|
|
||||||
|
lucide-react@^0.544.0:
|
||||||
|
version "0.544.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.544.0.tgz#4719953c10fd53a64dd8343bb0ed16ec79f3eeef"
|
||||||
|
integrity sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw==
|
||||||
|
|
||||||
luxon@^3.2.1:
|
luxon@^3.2.1:
|
||||||
version "3.7.2"
|
version "3.7.2"
|
||||||
resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.7.2.tgz#d697e48f478553cca187a0f8436aff468e3ba0ba"
|
resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.7.2.tgz#d697e48f478553cca187a0f8436aff468e3ba0ba"
|
||||||
@ -1387,3 +1401,8 @@ yocto-queue@^0.1.0:
|
|||||||
version "0.1.0"
|
version "0.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
||||||
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
||||||
|
|
||||||
|
zod@^4.1.11:
|
||||||
|
version "4.1.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/zod/-/zod-4.1.11.tgz#4aab62f76cfd45e6c6166519ba31b2ea019f75f5"
|
||||||
|
integrity sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user