feat: server
This commit is contained in:
parent
01c42bf9fc
commit
21f459e695
23
package.json
23
package.json
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "subscriptions-service",
|
||||
"scripts": {
|
||||
"dev": "bun run --hot src/index.ts",
|
||||
"dev": "bun run --hot src/index.tsx",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:migrate": "drizzle-kit migrate",
|
||||
"db:seed": "bun run src/server/seeds/index.ts"
|
||||
@ -10,31 +10,36 @@
|
||||
"@auth/core": "^0.40.0",
|
||||
"@hono/auth-js": "^1.1.0",
|
||||
"@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",
|
||||
"clsx": "^2.1.1",
|
||||
"drizzle-kit": "^0.31.4",
|
||||
"drizzle-orm": "^0.44.5",
|
||||
"hono": "^4.9.8",
|
||||
"hono-openapi": "^1.0.8",
|
||||
"ioredis": "^5.7.0",
|
||||
"lucide-react": "^0.544.0",
|
||||
"node-cron": "^4.2.1",
|
||||
"pg": "^8.16.3",
|
||||
"postgres": "^3.4.4",
|
||||
"@hono/react-renderer": "^1.0.0",
|
||||
"react": "^18.3.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"bcryptjs": "^2.4.3"
|
||||
"zod": "^4.1.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/bun": "latest",
|
||||
"@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",
|
||||
"prettier": "^3.6.2",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,6 +13,15 @@ CREATE TABLE "packages" (
|
||||
CONSTRAINT "packages_name_unique" UNIQUE("name")
|
||||
);
|
||||
--> 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" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"protect_key" varchar(255) NOT NULL,
|
||||
@ -28,15 +37,5 @@ CREATE TABLE "subscriptions" (
|
||||
CONSTRAINT "subscriptions_protect_key_unique" UNIQUE("protect_key")
|
||||
);
|
||||
--> 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 "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",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
@ -91,6 +91,60 @@
|
||||
"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": 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": {
|
||||
"name": "subscriptions",
|
||||
"schema": "",
|
||||
@ -198,66 +252,6 @@
|
||||
"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": 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": {
|
||||
|
||||
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,
|
||||
"version": "7",
|
||||
"when": 1758304299810,
|
||||
"tag": "0000_ordinary_micromax",
|
||||
"when": 1758426395659,
|
||||
"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
|
||||
}
|
||||
]
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
export * from './schema/packages';
|
||||
export * from './schema/products';
|
||||
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', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
name: varchar('name', { length: 255 }).notNull().unique(),
|
||||
description: varchar('description', { length: 255 }).notNull(),
|
||||
features: jsonb('features').notNull(),
|
||||
description: varchar('description', { length: 255 }).default(''),
|
||||
createdAt: timestamp('created_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 type { Subscriber } from '../../../lib/types';
|
||||
import type { ISubscriber } from '../../../lib/types';
|
||||
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 subscriptions = pgTable('subscriptions', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
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),
|
||||
product: uuid('product_id').notNull().references(() => products.id),
|
||||
startDate: timestamp('start_date').notNull().defaultNow(),
|
||||
durationInMonths: integer('duration_in_months').notNull(),
|
||||
endDate: timestamp('end_date').notNull(),
|
||||
@ -17,6 +19,7 @@ export const subscriptions = pgTable('subscriptions', {
|
||||
status: subscriptionStatusEnum('status').notNull().default('active'),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at').notNull().defaultNow(),
|
||||
usedFeatures: jsonb('used_features').notNull(),
|
||||
});
|
||||
|
||||
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 type SupportType = 'standard' | '24/7' | 'premium';
|
||||
export type SubscriptionStatus = 'active' | 'expired' | 'canceled';
|
||||
export * from "./packages";
|
||||
export * from "./products";
|
||||
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;
|
||||
name: string;
|
||||
email: string;
|
||||
@ -13,34 +25,19 @@ export interface Subscriber {
|
||||
country: string;
|
||||
}
|
||||
|
||||
|
||||
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 {
|
||||
export interface ICreateSubscriptionRequest {
|
||||
packageId: string;
|
||||
durationInMonths: number;
|
||||
subscriber: Subscriber;
|
||||
subscriber: ISubscriber;
|
||||
}
|
||||
|
||||
export interface ApiResponse<T = any> {
|
||||
export interface IApiResponse<T = any> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
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 { ProductFeaturesSchemas } from '../types';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return clsx(inputs);
|
||||
@ -37,4 +38,11 @@ export function getDaysUntilExpiry(endDate: Date): number {
|
||||
const now = new Date();
|
||||
const diffTime = endDate.getTime() - now.getTime();
|
||||
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 { Package } from '../../database/schema';
|
||||
import type { ApiResponse } from '../../lib/types';
|
||||
import { PackageService } from '../services/packages';
|
||||
import { eq, gte, ilike, lte } from "drizzle-orm";
|
||||
import { Hono } from "hono";
|
||||
import { db } from "../../database/connexion";
|
||||
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();
|
||||
|
||||
// get packages
|
||||
packageRoutes.get('/', async (c) => {
|
||||
packageRoutes.get("/", async (c) => {
|
||||
getSeedingData("packages");
|
||||
try {
|
||||
const packages = await PackageService.getPackages();
|
||||
const response: ApiResponse<Package[] | null> = {
|
||||
const page = Number(c.req.query("page")) || 1;
|
||||
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,
|
||||
data: packages
|
||||
...pkgs,
|
||||
data: pkgs.data as any,
|
||||
};
|
||||
return c.json(response);
|
||||
} catch (error) {
|
||||
const response: ApiResponse = {
|
||||
console.warn(error);
|
||||
const response: IApiResponse = {
|
||||
success: false,
|
||||
error: 'Failed to fetch packages'
|
||||
data: error,
|
||||
error: "Failed to fetch packages",
|
||||
};
|
||||
return c.json(response, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// get package by id
|
||||
packageRoutes.get('/:id', async (c) => {
|
||||
packageRoutes.get("/:id", async (c) => {
|
||||
try {
|
||||
const id = c.req.param('id');
|
||||
|
||||
const id = c.req.param("id");
|
||||
|
||||
const pkg = await PackageService.getPackageById(id);
|
||||
|
||||
|
||||
if (!pkg) {
|
||||
const response: ApiResponse = {
|
||||
const response: IApiResponse = {
|
||||
success: false,
|
||||
error: 'Package not found'
|
||||
error: "Package not found",
|
||||
};
|
||||
return c.json(response, 404);
|
||||
}
|
||||
|
||||
const response: ApiResponse<Package> = {
|
||||
|
||||
const response: IApiResponse<Package> = {
|
||||
success: true,
|
||||
data: pkg
|
||||
data: pkg,
|
||||
};
|
||||
return c.json(response);
|
||||
} catch (error) {
|
||||
const response: ApiResponse = {
|
||||
const response: IApiResponse = {
|
||||
success: false,
|
||||
error: 'Failed to fetch package'
|
||||
error: "Failed to fetch package",
|
||||
};
|
||||
return c.json(response, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// get package by name
|
||||
packageRoutes.get('/name/:name', async (c) => {
|
||||
packageRoutes.get("/name/:name", async (c) => {
|
||||
try {
|
||||
const name = c.req.param('name');
|
||||
|
||||
const name = c.req.param("name");
|
||||
|
||||
const pkg = await PackageService.getPackageByName(name);
|
||||
|
||||
|
||||
if (!pkg) {
|
||||
const response: ApiResponse = {
|
||||
const response: IApiResponse = {
|
||||
success: false,
|
||||
error: 'Package not found'
|
||||
error: "Package not found",
|
||||
};
|
||||
return c.json(response, 404);
|
||||
}
|
||||
|
||||
const response: ApiResponse<Package> = {
|
||||
|
||||
const response: IApiResponse<Package> = {
|
||||
success: true,
|
||||
data: pkg
|
||||
data: pkg,
|
||||
};
|
||||
return c.json(response);
|
||||
} catch (error) {
|
||||
const response: ApiResponse = {
|
||||
const response: IApiResponse = {
|
||||
success: false,
|
||||
error: 'Failed to fetch package'
|
||||
error: "Failed to fetch package",
|
||||
};
|
||||
return c.json(response, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// create package
|
||||
packageRoutes.post('/', async (c) => {
|
||||
packageRoutes.post("/", async (c) => {
|
||||
try {
|
||||
const packageData: Package = await c.req.json();
|
||||
|
||||
|
||||
const newPackage = await PackageService.createPackage(packageData);
|
||||
|
||||
const response: ApiResponse<Package> = {
|
||||
|
||||
const response: IApiResponse<Package> = {
|
||||
success: true,
|
||||
data: newPackage
|
||||
data: newPackage,
|
||||
};
|
||||
return c.json(response, 201);
|
||||
} catch (error) {
|
||||
const response: ApiResponse = {
|
||||
const response: IApiResponse = {
|
||||
success: false,
|
||||
error: 'Failed to create package'
|
||||
error: "Failed to create package",
|
||||
};
|
||||
return c.json(response, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// update package
|
||||
packageRoutes.put('/:id', async (c) => {
|
||||
packageRoutes.put("/:id", async (c) => {
|
||||
try {
|
||||
const id = c.req.param('id');
|
||||
const id = c.req.param("id");
|
||||
const packageData: Package = await c.req.json();
|
||||
|
||||
|
||||
const updatedPackage = await PackageService.updatePackage(id, packageData);
|
||||
|
||||
const response: ApiResponse<Package | null> = {
|
||||
|
||||
const response: IApiResponse<Package | null> = {
|
||||
success: true,
|
||||
data: updatedPackage
|
||||
data: updatedPackage,
|
||||
};
|
||||
return c.json(response);
|
||||
} catch (error) {
|
||||
const response: ApiResponse = {
|
||||
const response: IApiResponse = {
|
||||
success: false,
|
||||
error: 'Failed to update package'
|
||||
error: "Failed to update package",
|
||||
};
|
||||
return c.json(response, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// delete package
|
||||
packageRoutes.delete('/:id', async (c) => {
|
||||
packageRoutes.delete("/:id", async (c) => {
|
||||
try {
|
||||
const id = c.req.param('id');
|
||||
|
||||
const id = c.req.param("id");
|
||||
|
||||
const deletedPackage = await PackageService.deletePackage(id);
|
||||
|
||||
const response: ApiResponse<Package | null> = {
|
||||
|
||||
const response: IApiResponse<Package | null> = {
|
||||
success: true,
|
||||
data: deletedPackage
|
||||
data: deletedPackage,
|
||||
};
|
||||
return c.json(response);
|
||||
} catch (error) {
|
||||
const response: ApiResponse = {
|
||||
const response: IApiResponse = {
|
||||
success: false,
|
||||
error: 'Failed to delete package'
|
||||
error: "Failed to delete package",
|
||||
};
|
||||
return c.json(response, 500);
|
||||
}
|
||||
|
||||
@ -1,21 +1,32 @@
|
||||
import { Hono } from 'hono';
|
||||
import { Product } from '../../database/schema/schema/products';
|
||||
import type { ApiResponse } from '../../lib/types';
|
||||
import { ProductService } from '../services/prducts';
|
||||
import { db } from '../../database/connexion';
|
||||
import { Product, products } from '../../database/schema/schema/products';
|
||||
import type { IApiResponse } from '../../lib/types';
|
||||
import { ProductService } from '../services/products';
|
||||
import { paginate } from '../utils/pagination.util';
|
||||
|
||||
const productRoutes = new Hono();
|
||||
|
||||
// get products
|
||||
productRoutes.get('/', async (c) => {
|
||||
try {
|
||||
const pts = await ProductService.getProducts();
|
||||
const response: ApiResponse<Product[] | null> = {
|
||||
const page = Number(c.req.query('page')) || 1
|
||||
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,
|
||||
data: pts
|
||||
...pts,
|
||||
data: pts.data as any
|
||||
};
|
||||
return c.json(response);
|
||||
} catch (error) {
|
||||
const response: ApiResponse = {
|
||||
const response: IApiResponse = {
|
||||
success: false,
|
||||
error: 'Failed to fetch products'
|
||||
};
|
||||
@ -28,13 +39,13 @@ productRoutes.get('/:id', async (c) => {
|
||||
try {
|
||||
const id = c.req.param('id');
|
||||
const pt = await ProductService.getProductById(id);
|
||||
const response: ApiResponse<Product | null> = {
|
||||
const response: IApiResponse<Product | null> = {
|
||||
success: true,
|
||||
data: pt
|
||||
};
|
||||
return c.json(response);
|
||||
} catch (error) {
|
||||
const response: ApiResponse = {
|
||||
const response: IApiResponse = {
|
||||
success: false,
|
||||
error: 'Failed to fetch product'
|
||||
};
|
||||
@ -47,13 +58,13 @@ productRoutes.get('/name/:name', async (c) => {
|
||||
try {
|
||||
const name = c.req.param('name');
|
||||
const pt = await ProductService.getProductByName(name);
|
||||
const response: ApiResponse<Product | null> = {
|
||||
const response: IApiResponse<Product | null> = {
|
||||
success: true,
|
||||
data: pt
|
||||
};
|
||||
return c.json(response);
|
||||
} catch (error) {
|
||||
const response: ApiResponse = {
|
||||
const response: IApiResponse = {
|
||||
success: false,
|
||||
error: 'Failed to fetch product'
|
||||
};
|
||||
@ -66,13 +77,13 @@ productRoutes.post('/', async (c) => {
|
||||
try {
|
||||
const ptData: Product = await c.req.json();
|
||||
const newPt = await ProductService.createProduct(ptData);
|
||||
const response: ApiResponse<Product> = {
|
||||
const response: IApiResponse<Product> = {
|
||||
success: true,
|
||||
data: newPt
|
||||
};
|
||||
return c.json(response, 201);
|
||||
} catch (error) {
|
||||
const response: ApiResponse = {
|
||||
const response: IApiResponse = {
|
||||
success: false,
|
||||
error: 'Failed to create product'
|
||||
};
|
||||
@ -86,13 +97,13 @@ productRoutes.put('/:id', async (c) => {
|
||||
const id = c.req.param('id');
|
||||
const ptData: Product = await c.req.json();
|
||||
const updatedPt = await ProductService.updateProduct(id, ptData);
|
||||
const response: ApiResponse<Product | null> = {
|
||||
const response: IApiResponse<Product | null> = {
|
||||
success: true,
|
||||
data: updatedPt
|
||||
};
|
||||
return c.json(response);
|
||||
} catch (error) {
|
||||
const response: ApiResponse = {
|
||||
const response: IApiResponse = {
|
||||
success: false,
|
||||
error: 'Failed to update product'
|
||||
};
|
||||
@ -105,13 +116,13 @@ productRoutes.delete('/:id', async (c) => {
|
||||
try {
|
||||
const id = c.req.param('id');
|
||||
const deletedPt = await ProductService.deleteProduct(id);
|
||||
const response: ApiResponse<Product | null> = {
|
||||
const response: IApiResponse<Product | null> = {
|
||||
success: true,
|
||||
data: deletedPt
|
||||
};
|
||||
return c.json(response);
|
||||
} catch (error) {
|
||||
const response: ApiResponse = {
|
||||
const response: IApiResponse = {
|
||||
success: false,
|
||||
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,184 +1,294 @@
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { Hono } from 'hono';
|
||||
import { db } from '../../database/connexion';
|
||||
import { packages } from '../../database/schema';
|
||||
import type { ApiResponse, CreateSubscriptionRequest, Subscription } from '../../lib/types';
|
||||
import { SubscriptionService } from '../../server/services/subscriptions';
|
||||
import { eq, gte, ilike, lte, sql } from "drizzle-orm";
|
||||
import { Hono } from "hono";
|
||||
import { db } from "../../database/connexion";
|
||||
import { packages, Subscription, subscriptions } from "../../database/schema";
|
||||
import type {
|
||||
FilterCondition,
|
||||
IApiResponse,
|
||||
ICreateSubscriptionRequest,
|
||||
ISupportType,
|
||||
} from "../../lib/types";
|
||||
import { SubscriptionService } from "../../server/services/subscriptions";
|
||||
import { paginate } from "../utils/pagination.util";
|
||||
|
||||
const subscriptionRoutes = new Hono();
|
||||
|
||||
// Create subscription
|
||||
subscriptionRoutes.post('/', async (c) => {
|
||||
subscriptionRoutes.post("/", async (c) => {
|
||||
try {
|
||||
const subscriptionData: CreateSubscriptionRequest = await c.req.json();
|
||||
|
||||
const subscriptionData: ICreateSubscriptionRequest = await c.req.json();
|
||||
|
||||
// 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) {
|
||||
const response: ApiResponse = {
|
||||
const response: IApiResponse = {
|
||||
success: false,
|
||||
error: 'Package not found'
|
||||
error: "Package not found",
|
||||
};
|
||||
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,
|
||||
data: subscription
|
||||
data: subscription,
|
||||
};
|
||||
return c.json(response, 201);
|
||||
} catch (error) {
|
||||
console.error('Create subscription error:', error);
|
||||
const response: ApiResponse = {
|
||||
console.error("Create subscription error:", error);
|
||||
const response: IApiResponse = {
|
||||
success: false,
|
||||
error: 'Failed to create subscription'
|
||||
error: "Failed to create subscription",
|
||||
};
|
||||
return c.json(response, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Get subscription by protect key
|
||||
subscriptionRoutes.get('/status/:protectKey', async (c) => {
|
||||
subscriptionRoutes.get("/status/:protectKey", async (c) => {
|
||||
try {
|
||||
const protectKey = c.req.param('protectKey');
|
||||
|
||||
const subscription = await SubscriptionService.getSubscriptionByProtectKey(protectKey);
|
||||
const protectKey = c.req.param("protectKey");
|
||||
const subscription =
|
||||
await SubscriptionService.getSubscriptionByProtectKey(protectKey);
|
||||
|
||||
if (!subscription) {
|
||||
const response: ApiResponse = {
|
||||
const response: IApiResponse = {
|
||||
success: false,
|
||||
error: 'Subscription not found'
|
||||
error: "Subscription not found",
|
||||
};
|
||||
return c.json(response, 404);
|
||||
}
|
||||
|
||||
const response: ApiResponse<Subscription> = {
|
||||
const response: IApiResponse<Subscription> = {
|
||||
success: true,
|
||||
data: subscription
|
||||
data: subscription,
|
||||
};
|
||||
return c.json(response);
|
||||
} catch (error) {
|
||||
const response: ApiResponse = {
|
||||
const response: IApiResponse = {
|
||||
success: false,
|
||||
error: 'Failed to fetch subscription'
|
||||
error: "Failed to fetch subscription",
|
||||
};
|
||||
return c.json(response, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// get subscriptions
|
||||
subscriptionRoutes.get('/', async (c) => {
|
||||
subscriptionRoutes.get("/", async (c) => {
|
||||
try {
|
||||
const subscriptions = await SubscriptionService.getSubscriptions();
|
||||
const response: ApiResponse<Subscription[]> = {
|
||||
const page = Number(c.req.query("page")) || 1;
|
||||
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,
|
||||
data: subscriptions
|
||||
...subs,
|
||||
data: subs.data as any,
|
||||
};
|
||||
return c.json(response);
|
||||
} catch (error) {
|
||||
const response: ApiResponse = {
|
||||
const response: IApiResponse = {
|
||||
success: false,
|
||||
error: 'Failed to fetch subscriptions'
|
||||
error: "Failed to fetch subscriptions",
|
||||
};
|
||||
return c.json(response, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// get expired subscriptions
|
||||
subscriptionRoutes.get('/expired', async (c) => {
|
||||
subscriptionRoutes.get("/expired", async (c) => {
|
||||
try {
|
||||
const subscriptions = await SubscriptionService.getExpiredSubscriptions({
|
||||
limit: 10,
|
||||
page: 1,
|
||||
});
|
||||
const response: ApiResponse<Partial<Subscription>[]> = {
|
||||
const response: IApiResponse<Partial<Subscription>[]> = {
|
||||
success: true,
|
||||
data: subscriptions
|
||||
data: subscriptions,
|
||||
};
|
||||
return c.json(response);
|
||||
} catch (error) {
|
||||
const response: ApiResponse = {
|
||||
const response: IApiResponse = {
|
||||
success: false,
|
||||
error: 'Failed to fetch expired subscriptions'
|
||||
error: "Failed to fetch expired subscriptions",
|
||||
};
|
||||
return c.json(response, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// increment sms count
|
||||
subscriptionRoutes.post('/increment-sms-count/:protectKey', async (c) => {
|
||||
subscriptionRoutes.post("/increment-sms-count/:protectKey", async (c) => {
|
||||
try {
|
||||
const protectKey = c.req.param('protectKey');
|
||||
|
||||
const protectKey = c.req.param("protectKey");
|
||||
|
||||
await SubscriptionService.incrementSmsCount(protectKey);
|
||||
|
||||
const response: ApiResponse = {
|
||||
|
||||
const response: IApiResponse = {
|
||||
success: true,
|
||||
};
|
||||
return c.json(response);
|
||||
} catch (error) {
|
||||
const response: ApiResponse = {
|
||||
const response: IApiResponse = {
|
||||
success: false,
|
||||
error: 'Failed to increment SMS count'
|
||||
error: "Failed to increment SMS count",
|
||||
};
|
||||
return c.json(response, 500);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// update subscription status
|
||||
subscriptionRoutes.patch('/status/:id', async (c) => {
|
||||
subscriptionRoutes.patch("/status/:id", async (c) => {
|
||||
try {
|
||||
const id = c.req.param('id');
|
||||
const status = (await c.req.json()).status;
|
||||
|
||||
const updatedSubscription = await SubscriptionService.updateSubscriptionStatus(id, status);
|
||||
|
||||
const response: ApiResponse<Subscription | null> = {
|
||||
const id = c.req.param("id");
|
||||
const status = (await c.req.json()).status;
|
||||
|
||||
const updatedSubscription =
|
||||
await SubscriptionService.updateSubscriptionStatus(id, status);
|
||||
|
||||
const response: IApiResponse<Subscription | null> = {
|
||||
success: true,
|
||||
data: updatedSubscription
|
||||
data: updatedSubscription,
|
||||
};
|
||||
return c.json(response);
|
||||
} catch (error) {
|
||||
const response: ApiResponse = {
|
||||
const response: IApiResponse = {
|
||||
success: false,
|
||||
error: 'Failed to update subscription status'
|
||||
error: "Failed to update subscription status",
|
||||
};
|
||||
return c.json(response, 500);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// get by id
|
||||
subscriptionRoutes.get('/:id', async (c) => {
|
||||
subscriptionRoutes.get("/:id", async (c) => {
|
||||
try {
|
||||
const id = c.req.param('id');
|
||||
|
||||
const id = c.req.param("id");
|
||||
|
||||
const subscription = await SubscriptionService.getSubscriptionById(id);
|
||||
|
||||
|
||||
if (!subscription) {
|
||||
const response: ApiResponse = {
|
||||
const response: IApiResponse = {
|
||||
success: false,
|
||||
error: 'Subscription not found'
|
||||
error: "Subscription not found",
|
||||
};
|
||||
return c.json(response, 404);
|
||||
}
|
||||
|
||||
const response: ApiResponse<Subscription> = {
|
||||
|
||||
const response: IApiResponse<Subscription> = {
|
||||
success: true,
|
||||
data: subscription
|
||||
data: subscription,
|
||||
};
|
||||
return c.json(response);
|
||||
} catch (error) {
|
||||
const response: ApiResponse = {
|
||||
const response: IApiResponse = {
|
||||
success: false,
|
||||
error: 'Failed to fetch subscription'
|
||||
error: "Failed to fetch subscription",
|
||||
};
|
||||
return c.json(response, 500);
|
||||
}
|
||||
});
|
||||
|
||||
export default subscriptionRoutes;
|
||||
export default subscriptionRoutes;
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
import { packagesSeed } from "./packages";
|
||||
import { productSeed } from "./products";
|
||||
|
||||
async function seed() {
|
||||
console.log('🌱 Seeding database...');
|
||||
await productSeed()
|
||||
await packagesSeed();
|
||||
console.log('✅ Database seeded successfully!');
|
||||
}
|
||||
|
||||
seed();
|
||||
@ -1,68 +1,37 @@
|
||||
import { count } from 'drizzle-orm';
|
||||
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() {
|
||||
console.log('🌱 Seeding database...');
|
||||
|
||||
try {
|
||||
// const allPackages = await db.select().from(packages);
|
||||
// if (allPackages.length > 0) {
|
||||
// console.log('Database already seeded!');
|
||||
// return;
|
||||
// }
|
||||
const productNames = await db.select({ name: products.name, id: products.id }).from(products);
|
||||
if (productNames.length === 0) {
|
||||
console.error('❌ Error: Products not found');
|
||||
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
|
||||
await db.delete(packages);
|
||||
|
||||
// get sample
|
||||
// Insert sample packages
|
||||
const samplePackages = [
|
||||
{
|
||||
"name": "Starter",
|
||||
"price": 50000,
|
||||
"clientsCount": 1000,
|
||||
"agentsCount": 5,
|
||||
"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",
|
||||
}
|
||||
];
|
||||
const addPropertyPerProduct = productNames.map((item) => ({
|
||||
productName: item.name,
|
||||
property: 'product',
|
||||
value: item.id
|
||||
}))
|
||||
const samplePackages = getSeedingData('packages', addPropertyPerProduct)
|
||||
|
||||
// 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) {
|
||||
console.error('❌ Error seeding database:', error);
|
||||
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 { db } from '../../database/connexion';
|
||||
import { subscriptions } from '../../database/schema';
|
||||
import type { CreateSubscriptionRequest, Subscription } from '../../lib/types';
|
||||
import { addMonths, generateId } from '../../lib/utils';
|
||||
import { eq, lt, sql } from "drizzle-orm";
|
||||
import { db } from "../../database/connexion";
|
||||
import { products, Subscription, subscriptions } from "../../database/schema";
|
||||
import { addMonths, generateId } from "../../lib/utils";
|
||||
|
||||
export class SubscriptionService {
|
||||
static async createSubscription(data: CreateSubscriptionRequest) {
|
||||
const now = new Date();
|
||||
const newSub = await db.insert(subscriptions).values({
|
||||
protectKey: 'USSK_' + generateId(6),
|
||||
subscriber: data.subscriber,
|
||||
package: data.packageId,
|
||||
startDate: now,
|
||||
durationInMonths: data.durationInMonths,
|
||||
endDate: addMonths(now, data.durationInMonths),
|
||||
status: 'active',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}).returning();
|
||||
static async createSubscription(data: Omit<Subscription, 'protectKey' | 'id' | 'endDate'>) {
|
||||
const now = new Date();
|
||||
const newSub = await db
|
||||
.insert(subscriptions)
|
||||
.values({
|
||||
...data,
|
||||
protectKey: "USSK_" + generateId(6),
|
||||
startDate: data.startDate || now,
|
||||
endDate: addMonths(data.startDate || now, data.durationInMonths),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
status: data.status || "active",
|
||||
})
|
||||
.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];
|
||||
}
|
||||
|
||||
static async getSubscriptions(limit = 10, page = 1): Promise<Subscription[]> {
|
||||
const data= await db.select()
|
||||
const data = await db
|
||||
.select()
|
||||
.from(subscriptions)
|
||||
.limit(limit)
|
||||
.offset((page - 1) * limit);
|
||||
@ -33,12 +45,13 @@ export class SubscriptionService {
|
||||
|
||||
static async updateSubscriptionStatus(
|
||||
subscriptionId: string,
|
||||
status: 'active' | 'expired' | 'canceled'
|
||||
status: "active" | "expired" | "canceled"
|
||||
): Promise<Subscription | null> {
|
||||
const [updatedSubscription] = await db.update(subscriptions)
|
||||
const [updatedSubscription] = await db
|
||||
.update(subscriptions)
|
||||
.set({
|
||||
status,
|
||||
updatedAt: new Date()
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(subscriptions.id, subscriptionId))
|
||||
.returning();
|
||||
@ -46,18 +59,42 @@ export class SubscriptionService {
|
||||
return updatedSubscription || null;
|
||||
}
|
||||
|
||||
static async getSubscriptionByProtectKey(protectKey: string): Promise<Subscription | null> {
|
||||
const [subscription] = await db.select()
|
||||
static async getSubscriptionByProtectKey(
|
||||
protectKey: string
|
||||
): Promise<Subscription | null> {
|
||||
try {
|
||||
const [subscription] = await db
|
||||
.select()
|
||||
.from(subscriptions)
|
||||
.where(eq(subscriptions.protectKey, protectKey))
|
||||
.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> {
|
||||
const [subscription] = await db.select()
|
||||
const [subscription] = await db
|
||||
.select()
|
||||
.from(subscriptions)
|
||||
.$dynamic()
|
||||
.where(eq(subscriptions.id, id))
|
||||
.limit(1);
|
||||
|
||||
@ -69,8 +106,9 @@ export class SubscriptionService {
|
||||
page = 1,
|
||||
}): Promise<Partial<Subscription>[]> {
|
||||
const now = new Date();
|
||||
|
||||
const expiredSubscriptions = await db.select({
|
||||
|
||||
const expiredSubscriptions = await db
|
||||
.select({
|
||||
id: subscriptions.id,
|
||||
protectKey: subscriptions.protectKey,
|
||||
subscriber: subscriptions.subscriber,
|
||||
@ -79,7 +117,7 @@ export class SubscriptionService {
|
||||
endDate: subscriptions.endDate,
|
||||
sentSmsCount: subscriptions.sentSmsCount,
|
||||
status: subscriptions.status,
|
||||
})
|
||||
})
|
||||
.from(subscriptions)
|
||||
.where(lt(subscriptions.endDate, now))
|
||||
.limit(limit)
|
||||
@ -89,11 +127,12 @@ export class SubscriptionService {
|
||||
}
|
||||
|
||||
static async incrementSmsCount(protectKey: string): Promise<void> {
|
||||
await db.update(subscriptions)
|
||||
await db
|
||||
.update(subscriptions)
|
||||
.set({
|
||||
sentSmsCount: sql`${subscriptions.sentSmsCount} + 1`,
|
||||
updatedAt: new Date()
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.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": {
|
||||
"strict": true,
|
||||
"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"
|
||||
integrity sha512-Fjyxfux0rMPXMSob79OmddfpK5ArJa2xLkLCV+zamHkbeXQtSNKOi0keiBKyHZ/hCRKjigjmKGp4AJnDFq8PUw==
|
||||
|
||||
"@hono/react-renderer@^1.0.0":
|
||||
"@hono/react-renderer@^1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@hono/react-renderer/-/react-renderer-1.0.1.tgz#c7fa54dbfd0e910800f7f82bc8201dce72a8d4d0"
|
||||
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":
|
||||
version "0.19.1"
|
||||
resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.1.tgz#17c55ca7d426733fe3c561906b8173c336b40a77"
|
||||
@ -452,22 +462,16 @@
|
||||
dependencies:
|
||||
undici-types "~7.12.0"
|
||||
|
||||
"@types/prop-types@*":
|
||||
version "15.7.15"
|
||||
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.15.tgz#e6e5a86d602beaca71ce5163fadf5f95d70931c7"
|
||||
integrity sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==
|
||||
"@types/react-dom@^19.1.9":
|
||||
version "19.1.9"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.1.9.tgz#5ab695fce1e804184767932365ae6569c11b4b4b"
|
||||
integrity sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==
|
||||
|
||||
"@types/react-dom@^18.3.0":
|
||||
version "18.3.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.7.tgz#b89ddf2cd83b4feafcc4e2ea41afdfb95a0d194f"
|
||||
integrity sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==
|
||||
|
||||
"@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==
|
||||
"@types/react@^19.1.13":
|
||||
version "19.1.13"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-19.1.13.tgz#fc650ffa680d739a25a530f5d7ebe00cdd771883"
|
||||
integrity sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==
|
||||
dependencies:
|
||||
"@types/prop-types" "*"
|
||||
csstype "^3.0.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"
|
||||
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:
|
||||
version "4.9.8"
|
||||
resolved "https://registry.yarnpkg.com/hono/-/hono-4.9.8.tgz#1710981135ec775fe26fab5ea6535b403e92bcc3"
|
||||
@ -1010,6 +1019,11 @@ loose-envify@^1.1.0:
|
||||
dependencies:
|
||||
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:
|
||||
version "3.7.2"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
||||
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