From aa0cc0a1908436f98de19be14be8d0e36eaf80f2 Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Sun, 7 Jun 2026 11:23:07 +0330 Subject: [PATCH] design request module --- database/migrations/.snapshot-dpage.json | 1055 ++++++++++------- ...igration20260607075148_addRequestDesign.ts | 15 + src/app.module.ts | 2 + .../design-request.controller.ts | 57 + .../design-request/design-request.module.ts | 14 + .../design-request/design-request.service.ts | 162 +++ .../dto/create-design-request.dto.ts | 30 + .../dto/find-design-requests.dto.ts | 27 + .../dto/finish-design-request.dto.ts | 15 + .../dto/mark-design-request-paid.dto.ts | 9 + .../dto/update-design-request.dto.ts | 4 + .../entities/design-request.entity.ts | 35 + .../repositories/design-request.repository.ts | 38 + 13 files changed, 1065 insertions(+), 398 deletions(-) create mode 100644 database/migrations/Migration20260607075148_addRequestDesign.ts create mode 100644 src/modules/design-request/design-request.controller.ts create mode 100644 src/modules/design-request/design-request.module.ts create mode 100644 src/modules/design-request/design-request.service.ts create mode 100644 src/modules/design-request/dto/create-design-request.dto.ts create mode 100644 src/modules/design-request/dto/find-design-requests.dto.ts create mode 100644 src/modules/design-request/dto/finish-design-request.dto.ts create mode 100644 src/modules/design-request/dto/mark-design-request-paid.dto.ts create mode 100644 src/modules/design-request/dto/update-design-request.dto.ts create mode 100644 src/modules/design-request/entities/design-request.entity.ts create mode 100644 src/modules/design-request/repositories/design-request.repository.ts diff --git a/database/migrations/.snapshot-dpage.json b/database/migrations/.snapshot-dpage.json index 32dc152..7d672cf 100644 --- a/database/migrations/.snapshot-dpage.json +++ b/database/migrations/.snapshot-dpage.json @@ -24,7 +24,7 @@ }, "created_at": { "name": "created_at", - "type": "timestamptz(6)", + "type": "timestamptz", "unsigned": false, "autoincrement": false, "primary": false, @@ -40,7 +40,7 @@ }, "updated_at": { "name": "updated_at", - "type": "timestamptz(6)", + "type": "timestamptz", "unsigned": false, "autoincrement": false, "primary": false, @@ -56,7 +56,266 @@ }, "deleted_at": { "name": "deleted_at", - "type": "timestamptz(6)", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "unique": false, + "length": 6, + "precision": null, + "scale": null, + "default": null, + "comment": null, + "enumItems": [], + "mappedType": "datetime" + }, + "danak_subscription_id": { + "name": "danak_subscription_id", + "type": "uuid", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "unique": false, + "length": null, + "precision": null, + "scale": null, + "default": null, + "comment": null, + "enumItems": [], + "mappedType": "uuid" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "unique": false, + "length": 255, + "precision": null, + "scale": null, + "default": null, + "comment": null, + "enumItems": [], + "mappedType": "string" + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "unique": false, + "length": 255, + "precision": null, + "scale": null, + "default": null, + "comment": null, + "enumItems": [], + "mappedType": "string" + }, + "domain": { + "name": "domain", + "type": "varchar(255)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "unique": false, + "length": 255, + "precision": null, + "scale": null, + "default": null, + "comment": null, + "enumItems": [], + "mappedType": "string" + }, + "is_domain_verified": { + "name": "is_domain_verified", + "type": "boolean", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "unique": false, + "length": null, + "precision": null, + "scale": null, + "default": "false", + "comment": null, + "enumItems": [], + "mappedType": "boolean" + }, + "domain_verification_token": { + "name": "domain_verification_token", + "type": "varchar(255)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "unique": false, + "length": 255, + "precision": null, + "scale": null, + "default": null, + "comment": null, + "enumItems": [], + "mappedType": "string" + }, + "domain_verified_at": { + "name": "domain_verified_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "unique": false, + "length": 6, + "precision": null, + "scale": null, + "default": null, + "comment": null, + "enumItems": [], + "mappedType": "datetime" + }, + "logo_url": { + "name": "logo_url", + "type": "varchar(255)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "unique": false, + "length": 255, + "precision": null, + "scale": null, + "default": null, + "comment": null, + "enumItems": [], + "mappedType": "string" + }, + "max_catalogues_count": { + "name": "max_catalogues_count", + "type": "numeric(10,0)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "unique": false, + "length": null, + "precision": 10, + "scale": 0, + "default": "0", + "comment": null, + "enumItems": [], + "mappedType": "decimal" + } + }, + "name": "business", + "schema": "public", + "indexes": [ + { + "columnNames": [ + "danak_subscription_id" + ], + "composite": false, + "keyName": "business_danak_subscription_id_unique", + "constraint": true, + "primary": false, + "unique": true + }, + { + "keyName": "business_created_at_index", + "columnNames": [ + "created_at" + ], + "composite": false, + "constraint": false, + "primary": false, + "unique": false + }, + { + "keyName": "business_deleted_at_index", + "columnNames": [ + "deleted_at" + ], + "composite": false, + "constraint": false, + "primary": false, + "unique": false + }, + { + "keyName": "business_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "constraint": true, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": {}, + "nativeEnums": {} + }, + { + "columns": { + "id": { + "name": "id", + "type": "char(26)", + "unsigned": false, + "autoincrement": false, + "primary": true, + "nullable": false, + "unique": false, + "length": 26, + "precision": null, + "scale": null, + "default": null, + "comment": null, + "enumItems": [], + "mappedType": "character" + }, + "created_at": { + "name": "created_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "unique": false, + "length": 6, + "precision": null, + "scale": null, + "default": "now()", + "comment": null, + "enumItems": [], + "mappedType": "datetime" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "unique": false, + "length": 6, + "precision": null, + "scale": null, + "default": "now()", + "comment": null, + "enumItems": [], + "mappedType": "datetime" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamptz", "unsigned": false, "autoincrement": false, "primary": false, @@ -125,7 +384,7 @@ "autoincrement": false, "primary": false, "nullable": false, - "unique": true, + "unique": false, "length": 26, "precision": null, "scale": null, @@ -143,60 +402,58 @@ "business_id" ], "composite": false, - "constraint": true, "keyName": "admins_business_id_unique", - "unique": true, - "primary": false + "constraint": true, + "primary": false, + "unique": true }, { + "keyName": "admins_created_at_index", "columnNames": [ "created_at" ], "composite": false, "constraint": false, - "keyName": "admins_created_at_index", - "unique": false, - "primary": false + "primary": false, + "unique": false }, { + "keyName": "admins_deleted_at_index", "columnNames": [ "deleted_at" ], "composite": false, "constraint": false, - "keyName": "admins_deleted_at_index", - "unique": false, - "primary": false + "primary": false, + "unique": false }, { + "keyName": "admins_pkey", "columnNames": [ "id" ], "composite": false, - "constraint": false, - "keyName": "admins_pkey", - "unique": true, - "primary": true + "constraint": true, + "primary": true, + "unique": true } ], "checks": [], "foreignKeys": { "admins_business_id_foreign": { + "constraintName": "admins_business_id_foreign", "columnNames": [ "business_id" ], - "constraintName": "admins_business_id_foreign", "localTableName": "public.admins", - "referencedTableName": "public.business", "referencedColumnNames": [ "id" ], - "updateRule": "cascade", - "deleteRule": "no action" + "referencedTableName": "public.business", + "updateRule": "cascade" } }, - "nativeEnums": {}, - "comment": null + "nativeEnums": {} }, { "columns": { @@ -218,7 +475,7 @@ }, "created_at": { "name": "created_at", - "type": "timestamptz(6)", + "type": "timestamptz", "unsigned": false, "autoincrement": false, "primary": false, @@ -234,7 +491,7 @@ }, "updated_at": { "name": "updated_at", - "type": "timestamptz(6)", + "type": "timestamptz", "unsigned": false, "autoincrement": false, "primary": false, @@ -250,7 +507,7 @@ }, "deleted_at": { "name": "deleted_at", - "type": "timestamptz(6)", + "type": "timestamptz", "unsigned": false, "autoincrement": false, "primary": false, @@ -264,22 +521,6 @@ "enumItems": [], "mappedType": "datetime" }, - "danak_subscription_id": { - "name": "danak_subscription_id", - "type": "uuid", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": false, - "unique": true, - "length": null, - "precision": null, - "scale": null, - "default": null, - "comment": null, - "enumItems": [], - "mappedType": "uuid" - }, "name": { "name": "name", "type": "varchar(255)", @@ -312,234 +553,6 @@ "enumItems": [], "mappedType": "string" }, - "domain": { - "name": "domain", - "type": "varchar(255)", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": true, - "unique": false, - "length": 255, - "precision": null, - "scale": null, - "default": null, - "comment": null, - "enumItems": [], - "mappedType": "string" - }, - "is_domain_verified": { - "name": "is_domain_verified", - "type": "bool", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": false, - "unique": false, - "length": null, - "precision": null, - "scale": null, - "default": "false", - "comment": null, - "enumItems": [], - "mappedType": "boolean" - }, - "domain_verification_token": { - "name": "domain_verification_token", - "type": "varchar(255)", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": true, - "unique": false, - "length": 255, - "precision": null, - "scale": null, - "default": null, - "comment": null, - "enumItems": [], - "mappedType": "string" - }, - "domain_verified_at": { - "name": "domain_verified_at", - "type": "timestamptz(6)", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": true, - "unique": false, - "length": 6, - "precision": null, - "scale": null, - "default": null, - "comment": null, - "enumItems": [], - "mappedType": "datetime" - }, - "logo_url": { - "name": "logo_url", - "type": "varchar(255)", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": true, - "unique": false, - "length": 255, - "precision": null, - "scale": null, - "default": null, - "comment": null, - "enumItems": [], - "mappedType": "string" - }, - "max_catalogues_count": { - "name": "max_catalogues_count", - "type": "numeric(10,0)", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": false, - "unique": false, - "length": null, - "precision": 10, - "scale": 0, - "default": "0", - "comment": null, - "enumItems": [], - "mappedType": "decimal" - } - }, - "name": "business", - "schema": "public", - "indexes": [ - { - "columnNames": [ - "created_at" - ], - "composite": false, - "constraint": false, - "keyName": "business_created_at_index", - "unique": false, - "primary": false - }, - { - "columnNames": [ - "danak_subscription_id" - ], - "composite": false, - "constraint": true, - "keyName": "business_danak_subscription_id_unique", - "unique": true, - "primary": false - }, - { - "columnNames": [ - "deleted_at" - ], - "composite": false, - "constraint": false, - "keyName": "business_deleted_at_index", - "unique": false, - "primary": false - }, - { - "columnNames": [ - "id" - ], - "composite": false, - "constraint": false, - "keyName": "business_pkey", - "unique": true, - "primary": true - } - ], - "checks": [], - "foreignKeys": {}, - "nativeEnums": {}, - "comment": null - }, - { - "columns": { - "id": { - "name": "id", - "type": "char(26)", - "unsigned": false, - "autoincrement": false, - "primary": true, - "nullable": false, - "unique": false, - "length": 26, - "precision": null, - "scale": null, - "default": null, - "comment": null, - "enumItems": [], - "mappedType": "character" - }, - "created_at": { - "name": "created_at", - "type": "timestamptz(6)", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": false, - "unique": false, - "length": 6, - "precision": null, - "scale": null, - "default": "now()", - "comment": null, - "enumItems": [], - "mappedType": "datetime" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamptz(6)", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": false, - "unique": false, - "length": 6, - "precision": null, - "scale": null, - "default": "now()", - "comment": null, - "enumItems": [], - "mappedType": "datetime" - }, - "deleted_at": { - "name": "deleted_at", - "type": "timestamptz(6)", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": true, - "unique": false, - "length": 6, - "precision": null, - "scale": null, - "default": null, - "comment": null, - "enumItems": [], - "mappedType": "datetime" - }, - "name": { - "name": "name", - "type": "varchar(255)", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": false, - "unique": false, - "length": 255, - "precision": null, - "scale": null, - "default": null, - "comment": null, - "enumItems": [], - "mappedType": "string" - }, "content": { "name": "content", "type": "jsonb", @@ -611,76 +624,59 @@ "comment": null, "enumItems": [], "mappedType": "character" - }, - "slug": { - "name": "slug", - "type": "varchar(255)", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": false, - "unique": false, - "length": 255, - "precision": null, - "scale": null, - "default": null, - "comment": null, - "enumItems": [], - "mappedType": "string" } }, "name": "catalogue", "schema": "public", "indexes": [ { + "keyName": "catalogue_created_at_index", "columnNames": [ "created_at" ], "composite": false, "constraint": false, - "keyName": "catalogue_created_at_index", - "unique": false, - "primary": false + "primary": false, + "unique": false }, { + "keyName": "catalogue_deleted_at_index", "columnNames": [ "deleted_at" ], "composite": false, "constraint": false, - "keyName": "catalogue_deleted_at_index", - "unique": false, - "primary": false + "primary": false, + "unique": false }, { + "keyName": "catalogue_pkey", "columnNames": [ "id" ], "composite": false, - "constraint": false, - "keyName": "catalogue_pkey", - "unique": true, - "primary": true + "constraint": true, + "primary": true, + "unique": true } ], "checks": [], "foreignKeys": { "catalogue_business_id_foreign": { + "constraintName": "catalogue_business_id_foreign", "columnNames": [ "business_id" ], - "constraintName": "catalogue_business_id_foreign", "localTableName": "public.catalogue", - "referencedTableName": "public.business", "referencedColumnNames": [ "id" ], - "updateRule": "cascade", - "deleteRule": "restrict" + "referencedTableName": "public.business", + "deleteRule": "restrict", + "updateRule": "cascade" } }, - "nativeEnums": {}, - "comment": null + "nativeEnums": {} }, { "columns": { @@ -702,7 +698,7 @@ }, "created_at": { "name": "created_at", - "type": "timestamptz(6)", + "type": "timestamptz", "unsigned": false, "autoincrement": false, "primary": false, @@ -718,7 +714,7 @@ }, "updated_at": { "name": "updated_at", - "type": "timestamptz(6)", + "type": "timestamptz", "unsigned": false, "autoincrement": false, "primary": false, @@ -734,7 +730,274 @@ }, "deleted_at": { "name": "deleted_at", - "type": "timestamptz(6)", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "unique": false, + "length": 6, + "precision": null, + "scale": null, + "default": null, + "comment": null, + "enumItems": [], + "mappedType": "datetime" + }, + "title": { + "name": "title", + "type": "varchar(255)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "unique": false, + "length": 255, + "precision": null, + "scale": null, + "default": null, + "comment": null, + "enumItems": [], + "mappedType": "string" + }, + "count": { + "name": "count", + "type": "int", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "unique": false, + "length": null, + "precision": null, + "scale": null, + "default": "1", + "comment": null, + "enumItems": [], + "mappedType": "integer" + }, + "desc": { + "name": "desc", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "unique": false, + "length": null, + "precision": null, + "scale": null, + "default": null, + "comment": null, + "enumItems": [], + "mappedType": "text" + }, + "expected_date": { + "name": "expected_date", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "unique": false, + "length": 6, + "precision": null, + "scale": null, + "default": null, + "comment": null, + "enumItems": [], + "mappedType": "datetime" + }, + "attachments": { + "name": "attachments", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "unique": false, + "length": null, + "precision": null, + "scale": null, + "default": "'[]'", + "comment": null, + "enumItems": [], + "mappedType": "json" + }, + "invoice_id": { + "name": "invoice_id", + "type": "uuid", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "unique": false, + "length": null, + "precision": null, + "scale": null, + "default": null, + "comment": null, + "enumItems": [], + "mappedType": "uuid" + }, + "paid_at": { + "name": "paid_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "unique": false, + "length": 6, + "precision": null, + "scale": null, + "default": null, + "comment": null, + "enumItems": [], + "mappedType": "datetime" + }, + "business_id": { + "name": "business_id", + "type": "char(26)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "unique": false, + "length": 26, + "precision": null, + "scale": null, + "default": null, + "comment": null, + "enumItems": [], + "mappedType": "character" + } + }, + "name": "design_requests", + "schema": "public", + "indexes": [ + { + "keyName": "design_requests_created_at_index", + "columnNames": [ + "created_at" + ], + "composite": false, + "constraint": false, + "primary": false, + "unique": false + }, + { + "keyName": "design_requests_deleted_at_index", + "columnNames": [ + "deleted_at" + ], + "composite": false, + "constraint": false, + "primary": false, + "unique": false + }, + { + "keyName": "design_requests_business_id_id_index", + "columnNames": [ + "business_id.id" + ], + "composite": false, + "constraint": false, + "primary": false, + "unique": false + }, + { + "keyName": "design_requests_invoice_id_index", + "columnNames": [ + "invoice_id" + ], + "composite": false, + "constraint": false, + "primary": false, + "unique": false + }, + { + "keyName": "design_requests_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "constraint": true, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": { + "design_requests_business_id_foreign": { + "constraintName": "design_requests_business_id_foreign", + "columnNames": [ + "business_id" + ], + "localTableName": "public.design_requests", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.business", + "deleteRule": "restrict", + "updateRule": "cascade" + } + }, + "nativeEnums": {} + }, + { + "columns": { + "id": { + "name": "id", + "type": "char(26)", + "unsigned": false, + "autoincrement": false, + "primary": true, + "nullable": false, + "unique": false, + "length": 26, + "precision": null, + "scale": null, + "default": null, + "comment": null, + "enumItems": [], + "mappedType": "character" + }, + "created_at": { + "name": "created_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "unique": false, + "length": 6, + "precision": null, + "scale": null, + "default": "now()", + "comment": null, + "enumItems": [], + "mappedType": "datetime" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "unique": false, + "length": 6, + "precision": null, + "scale": null, + "default": "now()", + "comment": null, + "enumItems": [], + "mappedType": "datetime" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamptz", "unsigned": false, "autoincrement": false, "primary": false, @@ -769,40 +1032,39 @@ "schema": "public", "indexes": [ { + "keyName": "icon_groups_created_at_index", "columnNames": [ "created_at" ], "composite": false, "constraint": false, - "keyName": "icon_groups_created_at_index", - "unique": false, - "primary": false + "primary": false, + "unique": false }, { + "keyName": "icon_groups_deleted_at_index", "columnNames": [ "deleted_at" ], "composite": false, "constraint": false, - "keyName": "icon_groups_deleted_at_index", - "unique": false, - "primary": false + "primary": false, + "unique": false }, { + "keyName": "icon_groups_pkey", "columnNames": [ "id" ], "composite": false, - "constraint": false, - "keyName": "icon_groups_pkey", - "unique": true, - "primary": true + "constraint": true, + "primary": true, + "unique": true } ], "checks": [], "foreignKeys": {}, - "nativeEnums": {}, - "comment": null + "nativeEnums": {} }, { "columns": { @@ -824,7 +1086,7 @@ }, "created_at": { "name": "created_at", - "type": "timestamptz(6)", + "type": "timestamptz", "unsigned": false, "autoincrement": false, "primary": false, @@ -840,7 +1102,7 @@ }, "updated_at": { "name": "updated_at", - "type": "timestamptz(6)", + "type": "timestamptz", "unsigned": false, "autoincrement": false, "primary": false, @@ -856,7 +1118,7 @@ }, "deleted_at": { "name": "deleted_at", - "type": "timestamptz(6)", + "type": "timestamptz", "unsigned": false, "autoincrement": false, "primary": false, @@ -907,64 +1169,62 @@ "schema": "public", "indexes": [ { + "keyName": "icons_created_at_index", "columnNames": [ "created_at" ], "composite": false, "constraint": false, - "keyName": "icons_created_at_index", - "unique": false, - "primary": false + "primary": false, + "unique": false }, { + "keyName": "icons_deleted_at_index", "columnNames": [ "deleted_at" ], "composite": false, "constraint": false, - "keyName": "icons_deleted_at_index", - "unique": false, - "primary": false - }, - { - "columnNames": [ - "id" - ], - "composite": false, - "constraint": false, - "keyName": "icons_pkey", - "unique": true, - "primary": true + "primary": false, + "unique": false }, { + "keyName": "icons_url_index", "columnNames": [ "url" ], "composite": false, "constraint": false, - "keyName": "icons_url_index", - "unique": false, - "primary": false + "primary": false, + "unique": false + }, + { + "keyName": "icons_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "constraint": true, + "primary": true, + "unique": true } ], "checks": [], "foreignKeys": { "icons_group_id_foreign": { + "constraintName": "icons_group_id_foreign", "columnNames": [ "group_id" ], - "constraintName": "icons_group_id_foreign", "localTableName": "public.icons", - "referencedTableName": "public.icon_groups", "referencedColumnNames": [ "id" ], - "updateRule": "cascade", - "deleteRule": "no action" + "referencedTableName": "public.icon_groups", + "updateRule": "cascade" } }, - "nativeEnums": {}, - "comment": null + "nativeEnums": {} }, { "columns": { @@ -986,7 +1246,7 @@ }, "created_at": { "name": "created_at", - "type": "timestamptz(6)", + "type": "timestamptz", "unsigned": false, "autoincrement": false, "primary": false, @@ -1002,7 +1262,7 @@ }, "updated_at": { "name": "updated_at", - "type": "timestamptz(6)", + "type": "timestamptz", "unsigned": false, "autoincrement": false, "primary": false, @@ -1018,7 +1278,7 @@ }, "deleted_at": { "name": "deleted_at", - "type": "timestamptz(6)", + "type": "timestamptz", "unsigned": false, "autoincrement": false, "primary": false, @@ -1082,7 +1342,7 @@ }, "expires_at": { "name": "expires_at", - "type": "timestamptz(6)", + "type": "timestamptz", "unsigned": false, "autoincrement": false, "primary": false, @@ -1101,71 +1361,70 @@ "schema": "public", "indexes": [ { + "keyName": "refreshtokens_created_at_index", + "columnNames": [ + "created_at" + ], + "composite": false, + "constraint": false, + "primary": false, + "unique": false + }, + { + "keyName": "refreshtokens_deleted_at_index", + "columnNames": [ + "deleted_at" + ], + "composite": false, + "constraint": false, + "primary": false, + "unique": false + }, + { + "keyName": "refreshtokens_expires_at_index", + "columnNames": [ + "expires_at" + ], + "composite": false, + "constraint": false, + "primary": false, + "unique": false + }, + { + "keyName": "refreshtokens_hashed_token_index", + "columnNames": [ + "hashed_token" + ], + "composite": false, + "constraint": false, + "primary": false, + "unique": false + }, + { + "keyName": "refreshtokens_admin_id_business_id_index", "columnNames": [ "admin_id", "business_id" ], "composite": true, "constraint": false, - "keyName": "refreshtokens_admin_id_business_id_index", - "unique": false, - "primary": false - }, - { - "columnNames": [ - "created_at" - ], - "composite": false, - "constraint": false, - "keyName": "refreshtokens_created_at_index", - "unique": false, - "primary": false - }, - { - "columnNames": [ - "deleted_at" - ], - "composite": false, - "constraint": false, - "keyName": "refreshtokens_deleted_at_index", - "unique": false, - "primary": false - }, - { - "columnNames": [ - "expires_at" - ], - "composite": false, - "constraint": false, - "keyName": "refreshtokens_expires_at_index", - "unique": false, - "primary": false - }, - { - "columnNames": [ - "hashed_token" - ], - "composite": false, - "constraint": false, - "keyName": "refreshtokens_hashed_token_index", - "unique": false, - "primary": false + "primary": false, + "unique": false }, { + "keyName": "refreshtokens_pkey", "columnNames": [ "id" ], "composite": false, - "constraint": false, - "keyName": "refreshtokens_pkey", - "unique": true, - "primary": true + "constraint": true, + "primary": true, + "unique": true } ], "checks": [], "foreignKeys": {}, - "nativeEnums": {}, - "comment": null + "nativeEnums": {} } ], "nativeEnums": {} diff --git a/database/migrations/Migration20260607075148_addRequestDesign.ts b/database/migrations/Migration20260607075148_addRequestDesign.ts new file mode 100644 index 0000000..2a580d1 --- /dev/null +++ b/database/migrations/Migration20260607075148_addRequestDesign.ts @@ -0,0 +1,15 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20260607075148_addRequestDesign extends Migration { + + override async up(): Promise { + this.addSql(`create table "design_requests" ("id" char(26) not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, "title" varchar(255) not null, "count" int not null default 1, "desc" text null, "expected_date" timestamptz null, "attachments" jsonb not null default '[]', "invoice_id" uuid not null, "paid_at" timestamptz null, "business_id" char(26) not null, constraint "design_requests_pkey" primary key ("id"));`); + this.addSql(`create index "design_requests_created_at_index" on "design_requests" ("created_at");`); + this.addSql(`create index "design_requests_deleted_at_index" on "design_requests" ("deleted_at");`); + this.addSql(`create index "design_requests_business_id_id_index" on "design_requests" (("business_id"->>'id'));`); + this.addSql(`create index "design_requests_invoice_id_index" on "design_requests" ("invoice_id");`); + + this.addSql(`alter table "design_requests" add constraint "design_requests_business_id_foreign" foreign key ("business_id") references "business" ("id") on update cascade on delete restrict;`); + } + +} diff --git a/src/app.module.ts b/src/app.module.ts index 4b556e6..f1a51d5 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -12,6 +12,7 @@ import { BusinessModule } from './modules/business/business.module'; import { CatalogueModule } from './modules/catalogue/catalogue.module'; import { HttpModule } from '@nestjs/axios'; import { IconsModule } from './modules/icons/icons.module'; +import { DesignRequestModule } from './modules/design-request/design-request.module'; @Module({ imports: [ @@ -32,6 +33,7 @@ import { IconsModule } from './modules/icons/icons.module'; CatalogueModule, HttpModule.register({ global: true, timeout: 10000, headers: { "Content-Type": "application/json" } }), IconsModule, + DesignRequestModule, ], controllers: [], providers: [], diff --git a/src/modules/design-request/design-request.controller.ts b/src/modules/design-request/design-request.controller.ts new file mode 100644 index 0000000..d7e5dc0 --- /dev/null +++ b/src/modules/design-request/design-request.controller.ts @@ -0,0 +1,57 @@ +import { Controller, Get, Post, Body, Patch, Param, Query, UseGuards } from '@nestjs/common'; +import { DesignRequestService } from './design-request.service'; +import { CreateDesignRequestDto } from './dto/create-design-request.dto'; +import { AdminAuthGuard } from '../auth/guards/adminAuth.guard'; +import { BusinessId } from 'src/common/decorators'; +import { SuperAdminAuthGuard } from '../auth/guards/superAdminAuth.guard'; +import { FinishDesignRequestDto } from './dto/finish-design-request.dto'; +import { FindDesignRequestsDto } from './dto/find-design-requests.dto'; +import { MarkDesignRequestPaidDto } from './dto/mark-design-request-paid.dto'; + +@Controller() +export class DesignRequestController { + constructor(private readonly designRequestService: DesignRequestService) { } + + + @Get('admin/design-request') + @UseGuards(AdminAuthGuard) + findAll(@Query() queryDto: FindDesignRequestsDto, @BusinessId() businessId: string) { + return this.designRequestService.findAll(businessId, queryDto); + } + + @Get('admin/design-request/:id') + @UseGuards(AdminAuthGuard) + findOne(@Param('id') id: string, @BusinessId() businessId: string) { + return this.designRequestService.findOne(id, businessId); + } + + + + @Post('admin/design-request/initiate') + @UseGuards(AdminAuthGuard) + createDesignRequest(@Body() dto: CreateDesignRequestDto, @BusinessId() businessId: string) { + return this.designRequestService.createDesignRequest(dto, businessId); + } + + // =============== super admin endpoints ============= + + @Get('super-admin/design-request/list') + @UseGuards(SuperAdminAuthGuard) + findAllAsSuperAdmin(@Query() queryDto: FindDesignRequestsDto) { + return this.designRequestService.findAllAsSuperAdmin(queryDto); + } + + @Post('super-admin/design-request/finish') + @UseGuards(SuperAdminAuthGuard) + finishDesignRequest(@Body() dto: FinishDesignRequestDto) { + return this.designRequestService.finishDesignRequest(dto); + } + + @Patch('super-admin/design-request/:id/paid') + @UseGuards(SuperAdminAuthGuard) + markAsPaid(@Param('id') id: string, @Body() dto: MarkDesignRequestPaidDto) { + return this.designRequestService.markAsPaid(id, dto); + } + + // =============== end ============= +} diff --git a/src/modules/design-request/design-request.module.ts b/src/modules/design-request/design-request.module.ts new file mode 100644 index 0000000..e86cf7b --- /dev/null +++ b/src/modules/design-request/design-request.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { MikroOrmModule } from '@mikro-orm/nestjs'; +import { DesignRequestService } from './design-request.service'; +import { DesignRequestController } from './design-request.controller'; +import { DesignRequest } from './entities/design-request.entity'; +import { BusinessModule } from '../business/business.module'; +import { JwtModule } from '@nestjs/jwt'; + +@Module({ + imports: [MikroOrmModule.forFeature([DesignRequest]), BusinessModule,JwtModule], + controllers: [DesignRequestController], + providers: [DesignRequestService], +}) +export class DesignRequestModule {} diff --git a/src/modules/design-request/design-request.service.ts b/src/modules/design-request/design-request.service.ts new file mode 100644 index 0000000..aaec524 --- /dev/null +++ b/src/modules/design-request/design-request.service.ts @@ -0,0 +1,162 @@ +import { Inject, Injectable, Logger, NotFoundException } from '@nestjs/common'; +import { CreateDesignRequestDto } from './dto/create-design-request.dto'; +import { UpdateDesignRequestDto } from './dto/update-design-request.dto'; +import { FinishDesignRequestDto } from './dto/finish-design-request.dto'; +import { MarkDesignRequestPaidDto } from './dto/mark-design-request-paid.dto'; +import { ConfigService } from '@nestjs/config'; +import { HttpService } from '@nestjs/axios'; +import { AxiosError } from 'axios'; +import { catchError, firstValueFrom, throwError } from 'rxjs'; +import { EntityManager } from '@mikro-orm/postgresql'; +import { BusinessService } from '../business/business.service'; +import { CreateExternalInvoiceDto } from '../catalogue/dto/create-invoice.dto'; +import { DesignRequest } from './entities/design-request.entity'; +import { DesignRequestRepository } from './repositories/design-request.repository'; +import { FindDesignRequestsDto } from './dto/find-design-requests.dto'; + +@Injectable() +export class DesignRequestService { + private readonly logger = new Logger(DesignRequestService.name); + + constructor( + private readonly em: EntityManager, + private readonly designRequestRepository: DesignRequestRepository, + private readonly businessService: BusinessService, + private readonly httpService: HttpService, + @Inject(ConfigService) + private readonly configService: ConfigService, + ) {} + + private getDesignUnitPrice(count: number): number { + if (count < 10) { + return +this.configService.getOrThrow('DESIGN_PRICE_UNDER_10'); + } + if (count <= 30) { + return +this.configService.getOrThrow('DESIGN_PRICE_10_TO_30'); + } + return +this.configService.getOrThrow('DESIGN_PRICE_ABOVE_30'); + } + + async createDesignRequest(dto: CreateDesignRequestDto, businessId: string) { + const { title, count, desc, expectedDate, attachments } = dto; + + const business = await this.businessService.findByIdOrFail(businessId); + + const danakApiUrl = this.configService.getOrThrow('DANAK_API_URL'); + const xApiKey = this.configService.getOrThrow('EXTERNAL_API_KEYS'); + const unitPrice = this.getDesignUnitPrice(count); + + const params: CreateExternalInvoiceDto = { + danakSubscriptionId: business.danakSubscriptionId, + businessId, + items: [ + { + name: 'purchase_design', + count, + unitPrice, + discount: 0, + }, + ], + }; + + let invoiceData: { id: string }; + + try { + const { data } = await firstValueFrom( + this.httpService + .post(`${danakApiUrl}/invoices/external/create`, params, { + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': xApiKey, + }, + }) + .pipe( + catchError((err: AxiosError) => { + this.logger.error(`Failed to create invoice: ${err.message}`, err.stack); + return throwError(() => err); + }), + ), + ); + invoiceData = data; + } catch (error: unknown) { + this.logger.error( + `Error create invoice: ${error instanceof Error ? error.message : 'Unknown error'}`, + ); + throw error; + } + + const designRequest = this.em.create(DesignRequest, { + title, + count, + desc, + expectedDate: expectedDate ? new Date(expectedDate) : undefined, + attachments: attachments ?? [], + invoiceId: invoiceData.id, + business, + }); + + await this.em.persistAndFlush(designRequest); + + return designRequest; + } + + async finishDesignRequest(dto: FinishDesignRequestDto) { + const { businessId, invoiceId } = dto; + + await this.businessService.findByIdOrFail(businessId); + + const designRequest = await this.em.findOne(DesignRequest, { invoiceId }); + if (!designRequest) { + throw new NotFoundException('design request not found'); + } + + designRequest.paidAt = new Date(); + await this.em.flush(); + + return designRequest; + } + + async markAsPaid(id: string, dto: MarkDesignRequestPaidDto = {}) { + const designRequest = await this.em.findOne(DesignRequest, { id }); + if (!designRequest) { + throw new NotFoundException('design request not found'); + } + + designRequest.paidAt = dto.paidAt ? new Date(dto.paidAt) : new Date(); + await this.em.flush(); + + return designRequest; + } + + async findAll(businessId: string, dto: FindDesignRequestsDto) { + await this.businessService.findByIdOrFail(businessId); + + return this.designRequestRepository.findAllPaginated(dto, businessId); + } + + findAllAsSuperAdmin(dto: FindDesignRequestsDto) { + return this.designRequestRepository.findAllPaginated(dto); + } + + async findOne(id: string, businessId: string) { + await this.businessService.findByIdOrFail(businessId); + + const designRequest = await this.em.findOne(DesignRequest, { + id, + business: { id: businessId }, + }); + if (!designRequest) { + throw new NotFoundException('design request not found'); + } + + return designRequest; + } + + update(id: number, updateDesignRequestDto: UpdateDesignRequestDto) { + return `This action updates a #${id} designRequest`; + } + + remove(id: number) { + return `This action removes a #${id} designRequest`; + } +} diff --git a/src/modules/design-request/dto/create-design-request.dto.ts b/src/modules/design-request/dto/create-design-request.dto.ts new file mode 100644 index 0000000..cc951f0 --- /dev/null +++ b/src/modules/design-request/dto/create-design-request.dto.ts @@ -0,0 +1,30 @@ +import { IsArray, IsDateString, IsInt, IsNotEmpty, IsOptional, IsString, Min } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class CreateDesignRequestDto { + @IsString() + @IsNotEmpty() + @ApiProperty() + title: string; + + @IsInt() + @Min(1) + @ApiProperty() + count: number; + + @IsOptional() + @IsString() + @ApiPropertyOptional() + desc?: string; + + @IsOptional() + @IsDateString() + @ApiPropertyOptional() + expectedDate?: string; + + @IsOptional() + @IsArray() + @IsString({ each: true }) + @ApiPropertyOptional({ type: [String] }) + attachments?: string[]; +} diff --git a/src/modules/design-request/dto/find-design-requests.dto.ts b/src/modules/design-request/dto/find-design-requests.dto.ts new file mode 100644 index 0000000..3cb051f --- /dev/null +++ b/src/modules/design-request/dto/find-design-requests.dto.ts @@ -0,0 +1,27 @@ +import { IsOptional, IsNumber, IsString, IsIn } from 'class-validator'; +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; + +export class FindDesignRequestsDto { + @IsOptional() + @IsNumber() + @Type(() => Number) + @ApiPropertyOptional({ example: 1 }) + page?: number; + + @IsOptional() + @IsNumber() + @Type(() => Number) + @ApiPropertyOptional({ example: 10 }) + limit?: number; + + @IsOptional() + @IsString() + @ApiPropertyOptional({ example: 'createdAt' }) + orderBy?: string; + + @IsOptional() + @IsIn(['asc', 'desc']) + @ApiPropertyOptional({ example: 'desc' }) + order?: 'asc' | 'desc'; +} diff --git a/src/modules/design-request/dto/finish-design-request.dto.ts b/src/modules/design-request/dto/finish-design-request.dto.ts new file mode 100644 index 0000000..ae18fc1 --- /dev/null +++ b/src/modules/design-request/dto/finish-design-request.dto.ts @@ -0,0 +1,15 @@ + +import { IsInt, IsNotEmpty, IsString } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class FinishDesignRequestDto { + @IsString() + @IsNotEmpty() + @ApiProperty() + businessId: string; + + @IsString() + @ApiProperty() + invoiceId: string; + +} diff --git a/src/modules/design-request/dto/mark-design-request-paid.dto.ts b/src/modules/design-request/dto/mark-design-request-paid.dto.ts new file mode 100644 index 0000000..a34ae9d --- /dev/null +++ b/src/modules/design-request/dto/mark-design-request-paid.dto.ts @@ -0,0 +1,9 @@ +import { IsDateString, IsOptional } from 'class-validator'; +import { ApiPropertyOptional } from '@nestjs/swagger'; + +export class MarkDesignRequestPaidDto { + @IsOptional() + @IsDateString() + @ApiPropertyOptional({ example: '2026-06-07T12:00:00.000Z' }) + paidAt?: string; +} diff --git a/src/modules/design-request/dto/update-design-request.dto.ts b/src/modules/design-request/dto/update-design-request.dto.ts new file mode 100644 index 0000000..73d95ea --- /dev/null +++ b/src/modules/design-request/dto/update-design-request.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateDesignRequestDto } from './create-design-request.dto'; + +export class UpdateDesignRequestDto extends PartialType(CreateDesignRequestDto) {} diff --git a/src/modules/design-request/entities/design-request.entity.ts b/src/modules/design-request/entities/design-request.entity.ts new file mode 100644 index 0000000..903cfdb --- /dev/null +++ b/src/modules/design-request/entities/design-request.entity.ts @@ -0,0 +1,35 @@ +import { Entity, EntityRepositoryType, Index, ManyToOne, Property } from '@mikro-orm/core'; +import { BaseEntity } from '../../../common/entities/base.entity'; +import { Business } from '../../business/entities/business.entity'; +import { DesignRequestRepository } from '../repositories/design-request.repository'; + +@Entity({ tableName: 'design_requests', repository: () => DesignRequestRepository }) +@Index({ properties: ['invoiceId'] }) +@Index({ properties: ['business.id'] }) +export class DesignRequest extends BaseEntity { + @Property({ type: 'varchar', length: 255, nullable: false }) + title!: string; + + @Property({ type: 'integer', nullable: false, default: 1 }) + count!: number; + + @Property({ type: 'text', nullable: true }) + desc?: string; + + @Property({ type: 'timestamptz', nullable: true }) + expectedDate?: Date; + + @Property({ type: 'json', default: [] }) + attachments: string[] = []; + + @Property({ type: 'uuid', nullable: false }) + invoiceId!: string; + + @Property({ type: 'timestamptz', nullable: true }) + paidAt?: Date; + + @ManyToOne(() => Business, { deleteRule: 'restrict' }) + business!: Business; + + [EntityRepositoryType]?: DesignRequestRepository; +} diff --git a/src/modules/design-request/repositories/design-request.repository.ts b/src/modules/design-request/repositories/design-request.repository.ts new file mode 100644 index 0000000..9511c33 --- /dev/null +++ b/src/modules/design-request/repositories/design-request.repository.ts @@ -0,0 +1,38 @@ +import { EntityRepository, FilterQuery } from '@mikro-orm/postgresql'; +import { PaginatedResult } from 'src/common/interfaces/pagination.interface'; +import { FindDesignRequestsDto } from '../dto/find-design-requests.dto'; +import { DesignRequest } from '../entities/design-request.entity'; + +export class DesignRequestRepository extends EntityRepository { + async findAllPaginated( + opts: FindDesignRequestsDto = {}, + businessId?: string, + ): Promise> { + const { page = 1, limit = 10, orderBy = 'createdAt', order = 'desc' } = opts; + + const offset = (page - 1) * limit; + + const where: FilterQuery = businessId + ? { business: { id: businessId } } + : {}; + + const [data, total] = await this.findAndCount(where, { + limit, + offset, + orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' }, + populate: businessId ? [] : ['business'], + }); + + const totalPages = Math.ceil(total / limit); + + return { + data, + meta: { + total, + page, + limit, + totalPages, + }, + }; + } +}