diff --git a/database/migrations/.snapshot-negareh.json b/database/migrations/.snapshot-negareh.json index c2596e0..29e673a 100644 --- a/database/migrations/.snapshot-negareh.json +++ b/database/migrations/.snapshot-negareh.json @@ -823,25 +823,29 @@ "create_invoice", "update_invoice", "delete_invoice", - "view_all_invoiced_orders", - "view_my_assigned_invoiced_requests", "view_orders", "view_assigned_orders", "create_order", "update_order", "assign_designer", "delete_order", + "create_user", + "update_user", + "delete_user", + "view_users", "manage_admins", "manage_roles", - "manage_users", "manage_payments", "manage_settings", - "manage_form_builder", "create_print", "view_print", "manage_learnings", "manage_criticisms", - "manage_announcements" + "manage_announcements", + "manage_tickets", + "new_request_notification", + "new_payment_notification", + "new_ticket_notification" ], "mappedType": "enum" }, @@ -1367,101 +1371,6 @@ }, "nativeEnums": {} }, - { - "columns": { - "id": { - "name": "id", - "type": "char(26)", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": false, - "length": 26, - "mappedType": "character" - }, - "created_at": { - "name": "created_at", - "type": "timestamptz", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": false, - "length": 6, - "default": "now()", - "mappedType": "datetime" - }, - "deleted_at": { - "name": "deleted_at", - "type": "timestamptz", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": true, - "length": 6, - "mappedType": "datetime" - }, - "admin_id": { - "name": "admin_id", - "type": "char(26)", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": true, - "length": 26, - "mappedType": "character" - }, - "events": { - "name": "events", - "type": "jsonb", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": true, - "mappedType": "json" - } - }, - "name": "notification_preferences", - "schema": "public", - "indexes": [ - { - "columnNames": [ - "admin_id" - ], - "composite": false, - "keyName": "notification_preferences_admin_id_unique", - "constraint": true, - "primary": false, - "unique": true - }, - { - "keyName": "notification_preferences_pkey", - "columnNames": [ - "id" - ], - "composite": false, - "constraint": true, - "primary": true, - "unique": true - } - ], - "checks": [], - "foreignKeys": { - "notification_preferences_admin_id_foreign": { - "constraintName": "notification_preferences_admin_id_foreign", - "columnNames": [ - "admin_id" - ], - "localTableName": "public.notification_preferences", - "referencedColumnNames": [ - "id" - ], - "referencedTableName": "public.admins", - "deleteRule": "cascade", - "updateRule": "cascade" - } - }, - "nativeEnums": {} - }, { "columns": { "role_id": { @@ -1723,74 +1632,6 @@ "length": 26, "mappedType": "character" }, - "deleted_at": { - "name": "deleted_at", - "type": "timestamptz", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": true, - "length": 6, - "mappedType": "datetime" - }, - "content": { - "name": "content", - "type": "text", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": false, - "mappedType": "text" - }, - "user_id": { - "name": "user_id", - "type": "char(26)", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": true, - "length": 26, - "mappedType": "character" - }, - "admin_id": { - "name": "admin_id", - "type": "char(26)", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": true, - "length": 26, - "mappedType": "character" - }, - "attachments": { - "name": "attachments", - "type": "jsonb", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": true, - "mappedType": "json" - }, - "ref_id": { - "name": "ref_id", - "type": "varchar(255)", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": false, - "length": 255, - "mappedType": "string" - }, - "parent_id": { - "name": "parent_id", - "type": "char(26)", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": true, - "length": 26, - "mappedType": "character" - }, "created_at": { "name": "created_at", "type": "timestamptz", @@ -1802,8 +1643,8 @@ "default": "now()", "mappedType": "datetime" }, - "read_at": { - "name": "read_at", + "deleted_at": { + "name": "deleted_at", "type": "timestamptz", "unsigned": false, "autoincrement": false, @@ -1811,13 +1652,87 @@ "nullable": true, "length": 6, "mappedType": "datetime" + }, + "parent_id": { + "name": "parent_id", + "type": "char(26)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 26, + "mappedType": "character" + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 255, + "mappedType": "string" + }, + "content": { + "name": "content", + "type": "varchar(255)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 255, + "mappedType": "string" + }, + "status": { + "name": "status", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "enumItems": [ + "open", + "closed", + "pending", + "in_progress" + ], + "mappedType": "enum" + }, + "attachments": { + "name": "attachments", + "type": "text[]", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "array" + }, + "admin_id": { + "name": "admin_id", + "type": "char(26)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 26, + "mappedType": "character" + }, + "user_id": { + "name": "user_id", + "type": "char(26)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 26, + "mappedType": "character" } }, - "name": "ticket", + "name": "tickets", "schema": "public", "indexes": [ { - "keyName": "ticket_pkey", + "keyName": "tickets_pkey", "columnNames": [ "id" ], @@ -1829,25 +1744,25 @@ ], "checks": [], "foreignKeys": { - "ticket_user_id_foreign": { - "constraintName": "ticket_user_id_foreign", + "tickets_parent_id_foreign": { + "constraintName": "tickets_parent_id_foreign", "columnNames": [ - "user_id" + "parent_id" ], - "localTableName": "public.ticket", + "localTableName": "public.tickets", "referencedColumnNames": [ "id" ], - "referencedTableName": "public.users", + "referencedTableName": "public.tickets", "deleteRule": "set null", "updateRule": "cascade" }, - "ticket_admin_id_foreign": { - "constraintName": "ticket_admin_id_foreign", + "tickets_admin_id_foreign": { + "constraintName": "tickets_admin_id_foreign", "columnNames": [ "admin_id" ], - "localTableName": "public.ticket", + "localTableName": "public.tickets", "referencedColumnNames": [ "id" ], @@ -1855,16 +1770,16 @@ "deleteRule": "set null", "updateRule": "cascade" }, - "ticket_parent_id_foreign": { - "constraintName": "ticket_parent_id_foreign", + "tickets_user_id_foreign": { + "constraintName": "tickets_user_id_foreign", "columnNames": [ - "parent_id" + "user_id" ], - "localTableName": "public.ticket", + "localTableName": "public.tickets", "referencedColumnNames": [ "id" ], - "referencedTableName": "public.ticket", + "referencedTableName": "public.users", "deleteRule": "set null", "updateRule": "cascade" } @@ -2174,11 +2089,13 @@ "nullable": false, "enumItems": [ "newRequest", - "invoiced", + "newInvoice", + "newPayment", "newOrder", - "designerAssign", - "paymentSuccess", - "newCriticism" + "newCriticism", + "newTicket", + "newOrderMessage", + "newRequestMessage" ], "mappedType": "enum" }, @@ -2411,9 +2328,9 @@ }, "invoice_number": { "name": "invoice_number", - "type": "serial", - "unsigned": true, - "autoincrement": true, + "type": "int", + "unsigned": false, + "autoincrement": false, "primary": false, "nullable": false, "mappedType": "integer" @@ -2424,7 +2341,7 @@ "unsigned": false, "autoincrement": false, "primary": false, - "nullable": true, + "nullable": false, "mappedType": "integer" }, "sub_total": { @@ -2433,7 +2350,7 @@ "unsigned": false, "autoincrement": false, "primary": false, - "nullable": true, + "nullable": false, "mappedType": "integer" }, "tax_amount": { @@ -2442,7 +2359,7 @@ "unsigned": false, "autoincrement": false, "primary": false, - "nullable": true, + "nullable": false, "mappedType": "integer" }, "total": { @@ -2451,7 +2368,7 @@ "unsigned": false, "autoincrement": false, "primary": false, - "nullable": true, + "nullable": false, "mappedType": "integer" }, "paid_amount": { @@ -2471,7 +2388,6 @@ "autoincrement": false, "primary": false, "nullable": false, - "default": "0", "mappedType": "integer" }, "enable_tax": { @@ -2526,6 +2442,16 @@ "name": "invoices", "schema": "public", "indexes": [ + { + "columnNames": [ + "invoice_number" + ], + "composite": false, + "keyName": "invoices_invoice_number_unique", + "constraint": true, + "primary": false, + "unique": true + }, { "keyName": "invoices_user_id_index", "columnNames": [ @@ -2886,15 +2812,6 @@ "nullable": false, "mappedType": "integer" }, - "sub_total": { - "name": "sub_total", - "type": "int", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": true, - "mappedType": "integer" - }, "discount": { "name": "discount", "type": "int", @@ -2904,15 +2821,6 @@ "nullable": true, "mappedType": "integer" }, - "total": { - "name": "total", - "type": "int", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": true, - "mappedType": "integer" - }, "description": { "name": "description", "type": "text", @@ -3588,6 +3496,166 @@ }, "nativeEnums": {} }, + { + "columns": { + "id": { + "name": "id", + "type": "char(26)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 26, + "mappedType": "character" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" + }, + "content": { + "name": "content", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "user_id": { + "name": "user_id", + "type": "char(26)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 26, + "mappedType": "character" + }, + "admin_id": { + "name": "admin_id", + "type": "char(26)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 26, + "mappedType": "character" + }, + "attachments": { + "name": "attachments", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "json" + }, + "ref_id": { + "name": "ref_id", + "type": "varchar(255)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 255, + "mappedType": "string" + }, + "parent_id": { + "name": "parent_id", + "type": "char(26)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 26, + "mappedType": "character" + }, + "created_at": { + "name": "created_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "read_at": { + "name": "read_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" + } + }, + "name": "chat", + "schema": "public", + "indexes": [ + { + "keyName": "chat_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "constraint": true, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": { + "chat_user_id_foreign": { + "constraintName": "chat_user_id_foreign", + "columnNames": [ + "user_id" + ], + "localTableName": "public.chat", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.users", + "deleteRule": "set null", + "updateRule": "cascade" + }, + "chat_admin_id_foreign": { + "constraintName": "chat_admin_id_foreign", + "columnNames": [ + "admin_id" + ], + "localTableName": "public.chat", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.admins", + "deleteRule": "set null", + "updateRule": "cascade" + }, + "chat_parent_id_foreign": { + "constraintName": "chat_parent_id_foreign", + "columnNames": [ + "parent_id" + ], + "localTableName": "public.chat", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.chat", + "deleteRule": "set null", + "updateRule": "cascade" + } + }, + "nativeEnums": {} + }, { "columns": { "id": { diff --git a/database/migrations/Migration20260222084735.ts b/database/migrations/Migration20260222084735.ts deleted file mode 100644 index 4833658..0000000 --- a/database/migrations/Migration20260222084735.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Migration } from '@mikro-orm/migrations'; - -export class Migration20260222084735 extends Migration { - - override async up(): Promise { - this.addSql(`create table "learning_categories" ("id" char(26) not null, "created_at" timestamptz not null default now(), "deleted_at" timestamptz null, "name" varchar(100) not null, constraint "learning_categories_pkey" primary key ("id"));`); - this.addSql(`alter table "learning_categories" add constraint "learning_categories_name_unique" unique ("name");`); - - this.addSql(`create table "learnings" ("id" char(26) not null, "created_at" timestamptz not null default now(), "deleted_at" timestamptz null, "title" varchar(200) not null, "description" text not null, "cover_url" text not null, "video_url" text null, "video_duration" varchar(200) null, "category_id" char(26) not null, constraint "learnings_pkey" primary key ("id"));`); - - this.addSql(`create table "prints" ("id" char(26) not null, "created_at" timestamptz not null default now(), "deleted_at" timestamptz null, "fileds" jsonb null, constraint "prints_pkey" primary key ("id"));`); - - this.addSql(`create table "learning_progress" ("id" char(26) not null, "created_at" timestamptz not null default now(), "deleted_at" timestamptz null, "user_id" char(26) not null, "learning_id" char(26) not null, "watched_at" timestamptz not null default CURRENT_TIMESTAMP, constraint "learning_progress_pkey" primary key ("id"));`); - - this.addSql(`alter table "learnings" add constraint "learnings_category_id_foreign" foreign key ("category_id") references "learning_categories" ("id") on update cascade;`); - - this.addSql(`alter table "learning_progress" add constraint "learning_progress_user_id_foreign" foreign key ("user_id") references "users" ("id") on update cascade;`); - this.addSql(`alter table "learning_progress" add constraint "learning_progress_learning_id_foreign" foreign key ("learning_id") references "learnings" ("id") on update cascade;`); - - this.addSql(`alter table "permissions" drop constraint if exists "permissions_name_check";`); - - this.addSql(`alter table "permissions" add constraint "permissions_name_check" check("name" in ('manage_products', 'manage_categories', 'view_requests', 'delete_request', 'update_request', 'view_invoices', 'create_invoice', 'update_invoice', 'delete_invoice', 'view_all_invoiced_orders', 'view_my_assigned_invoiced_requests', 'view_orders', 'view_assigned_orders', 'create_order', 'update_order', 'assign_designer', 'delete_order', 'manage_admins', 'manage_roles', 'manage_users', 'manage_payments', 'manage_settings', 'manage_form_builder', 'create_print', 'view_print', 'manage_learnings'));`); - - this.addSql(`alter table "orders" add column "print_id" char(26) null;`); - this.addSql(`alter table "orders" add constraint "orders_print_id_foreign" foreign key ("print_id") references "prints" ("id") on update cascade on delete set null;`); - this.addSql(`alter table "orders" add constraint "orders_print_id_unique" unique ("print_id");`); - - this.addSql(`alter table "credit_transactions" rename column "order_id" to "invoice_id";`); - } - - override async down(): Promise { - this.addSql(`alter table "permissions" drop constraint if exists "permissions_name_check";`); - - this.addSql(`alter table "permissions" add constraint "permissions_name_check" check("name" in ('manage_products', 'manage_categories', 'view_requests', 'delete_request', 'update_request', 'view_invoices', 'create_invoice', 'update_invoice', 'delete_invoice', 'view_all_invoiced_orders', 'view_my_assigned_invoiced_requests', 'view_orders', 'view_assigned_orders', 'create_order', 'update_order', 'assign_designer', 'delete_order', 'manage_admins', 'manage_roles', 'manage_users', 'manage_payments', 'manage_settings', 'manage_form_builder', 'manage_print'));`); - - this.addSql(`alter table "orders" drop constraint "orders_print_id_unique";`); - - this.addSql(`alter table "orders" add column "attributes" jsonb null, add column "printery" varchar(255) null, add column "lithography" varchar(255) null;`); - - this.addSql(`alter table "credit_transactions" rename column "invoice_id" to "order_id";`); - } - -} diff --git a/database/migrations/Migration20260222092921.ts b/database/migrations/Migration20260222092921.ts deleted file mode 100644 index ab41a86..0000000 --- a/database/migrations/Migration20260222092921.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Migration } from '@mikro-orm/migrations'; - -export class Migration20260222092921 extends Migration { - - override async up(): Promise { - this.addSql(`create table "announcements" ("id" char(26) not null, "created_at" timestamptz not null default now(), "deleted_at" timestamptz null, "title" varchar(100) not null, "content" text not null, "publish_at" timestamptz null, "is_important" boolean not null default false, "is_public" boolean not null default false, constraint "announcements_pkey" primary key ("id"));`); - this.addSql(`create index "announcements_is_public_created_at_index" on "announcements" ("is_public", "created_at");`); - - this.addSql(`create table "user_announcements" ("id" char(26) not null, "created_at" timestamptz not null default now(), "deleted_at" timestamptz null, "user_id" char(26) not null, "announcement_id" char(26) not null, "is_read" boolean not null default false, constraint "user_announcements_pkey" primary key ("id"));`); - - this.addSql(`alter table "user_announcements" add constraint "user_announcements_user_id_foreign" foreign key ("user_id") references "users" ("id") on update cascade on delete cascade;`); - this.addSql(`alter table "user_announcements" add constraint "user_announcements_announcement_id_foreign" foreign key ("announcement_id") references "announcements" ("id") on update cascade on delete cascade;`); - - this.addSql(`alter table "permissions" drop constraint if exists "permissions_name_check";`); - - this.addSql(`alter table "permissions" add constraint "permissions_name_check" check("name" in ('manage_products', 'manage_categories', 'view_requests', 'delete_request', 'update_request', 'view_invoices', 'create_invoice', 'update_invoice', 'delete_invoice', 'view_all_invoiced_orders', 'view_my_assigned_invoiced_requests', 'view_orders', 'view_assigned_orders', 'create_order', 'update_order', 'assign_designer', 'delete_order', 'manage_admins', 'manage_roles', 'manage_users', 'manage_payments', 'manage_settings', 'manage_form_builder', 'create_print', 'view_print', 'manage_learnings', 'manage_criticisms', 'manage_announcements'));`); - } - - override async down(): Promise { - this.addSql(`alter table "permissions" drop constraint if exists "permissions_name_check";`); - this.addSql(`alter table "permissions" add constraint "permissions_name_check" check("name" in ('manage_products', 'manage_categories', 'view_requests', 'delete_request', 'update_request', 'view_invoices', 'create_invoice', 'update_invoice', 'delete_invoice', 'view_all_invoiced_orders', 'view_my_assigned_invoiced_requests', 'view_orders', 'view_assigned_orders', 'create_order', 'update_order', 'assign_designer', 'delete_order', 'manage_admins', 'manage_roles', 'manage_users', 'manage_payments', 'manage_settings', 'manage_form_builder', 'create_print', 'view_print', 'manage_learnings', 'manage_criticisms'));`); - - this.addSql(`drop table if exists "user_announcements";`); - this.addSql(`drop table if exists "announcements";`); - } - -} diff --git a/database/migrations/Migration20260404082727.ts b/database/migrations/Migration20260404082727.ts new file mode 100644 index 0000000..830c639 --- /dev/null +++ b/database/migrations/Migration20260404082727.ts @@ -0,0 +1,17 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20260404082727 extends Migration { + + override async up(): Promise { + this.addSql(`alter table "credit_transactions" drop constraint if exists "credit_transactions_type_check";`); + + this.addSql(`alter table "credit_transactions" add constraint "credit_transactions_type_check" check("type" in (''));`); + } + + override async down(): Promise { + this.addSql(`alter table "credit_transactions" drop constraint if exists "credit_transactions_type_check";`); + + this.addSql(`alter table "credit_transactions" add constraint "credit_transactions_type_check" check("type" in (''));`); + } + +} diff --git a/database/migrations/Migration20260404083020.ts b/database/migrations/Migration20260404083020.ts new file mode 100644 index 0000000..0850877 --- /dev/null +++ b/database/migrations/Migration20260404083020.ts @@ -0,0 +1,149 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20260404083020 extends Migration { + + override async up(): Promise { + //recalc_invoice_totals + this.addSql(` + CREATE OR REPLACE FUNCTION recalc_invoice_totals() + RETURNS TRIGGER AS $$ + DECLARE + v_invoice_id char(26); + v_subtotal int; + v_discount int; + v_tax int; + v_total int; + v_paid int; + v_enable_tax boolean; + BEGIN + + v_invoice_id := COALESCE(NEW.invoice_id, OLD.invoice_id); + + SELECT + COALESCE(SUM(sub_total),0), + COALESCE(SUM(discount),0) + INTO + v_subtotal, + v_discount + FROM invoice_items + WHERE invoice_id = v_invoice_id + AND deleted_at IS NULL; + + SELECT enable_tax + INTO v_enable_tax + FROM invoices + WHERE id = v_invoice_id; + + IF v_enable_tax THEN + v_tax := (v_subtotal - v_discount) * 0.10; + ELSE + v_tax := 0; + END IF; + + v_total := (v_subtotal - v_discount) + v_tax; + + SELECT COALESCE(SUM(amount),0) + INTO v_paid + FROM payments + WHERE invoice_id = v_invoice_id + AND status = 'SUCCESS'; + + UPDATE invoices + SET + sub_total = v_subtotal, + discount = v_discount, + tax_amount = v_tax, + total = v_total, + paid_amount = v_paid, + balance = v_total - v_paid + WHERE id = v_invoice_id; + + RETURN NULL; + END; + $$ LANGUAGE plpgsql; + `); + + // trigger + this.addSql(` + CREATE TRIGGER trg_invoice_items_recalc + AFTER INSERT OR UPDATE OR DELETE + ON invoice_items + FOR EACH ROW + EXECUTE FUNCTION recalc_invoice_totals(); + `); + + // recalc_invoice_payments + this.addSql(` + CREATE OR REPLACE FUNCTION recalc_invoice_payments() + RETURNS TRIGGER AS $$ + DECLARE + v_invoice_id char(26); + v_paid int; + v_total int; + BEGIN + + v_invoice_id := COALESCE(NEW.invoice_id, OLD.invoice_id); + + SELECT COALESCE(SUM(amount),0) + INTO v_paid + FROM payments + WHERE invoice_id = v_invoice_id + AND status = 'paid'; + + SELECT total + INTO v_total + FROM invoices + WHERE id = v_invoice_id; + + UPDATE invoices + SET + paid_amount = v_paid, + balance = v_total - v_paid + WHERE id = v_invoice_id; + + RETURN NULL; + END; + $$ LANGUAGE plpgsql; + `); + + // trigger + this.addSql(` + CREATE TRIGGER trg_payments_recalc + AFTER INSERT OR UPDATE OR DELETE + ON payments + FOR EACH ROW + EXECUTE FUNCTION recalc_invoice_payments(); + `); + + this.addSql(`CREATE SEQUENCE invoice_number_seq START 1;`); + + this.addSql(` + ALTER TABLE invoices + ALTER COLUMN invoice_number + SET DEFAULT nextval('invoice_number_seq'); + `); + + } + + override async down(): Promise { + + this.addSql(`DROP TRIGGER IF EXISTS trg_invoice_items_recalc ON invoice_items;`); + this.addSql(`DROP TRIGGER IF EXISTS trg_payments_recalc ON payments;`); + + this.addSql(`DROP FUNCTION IF EXISTS recalc_invoice_totals;`); + this.addSql(`DROP FUNCTION IF EXISTS recalc_invoice_payments;`); + + + // remove default from invoice_number + this.addSql(` + ALTER TABLE invoices + ALTER COLUMN invoice_number + DROP DEFAULT; + `); + + // drop sequence + this.addSql(`DROP SEQUENCE IF EXISTS invoice_number_seq;`); + + } + +} diff --git a/database/migrations/Migration20260404112450.ts b/database/migrations/Migration20260404112450.ts new file mode 100644 index 0000000..37cae86 --- /dev/null +++ b/database/migrations/Migration20260404112450.ts @@ -0,0 +1,40 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20260404112450 extends Migration { + + override async up(): Promise { + this.addSql(`alter table "invoices" alter column "invoice_number" type int using ("invoice_number"::int);`); + this.addSql(`alter table "invoices" alter column "discount" type int using ("discount"::int);`); + this.addSql(`alter table "invoices" alter column "discount" set not null;`); + this.addSql(`alter table "invoices" alter column "sub_total" type int using ("sub_total"::int);`); + this.addSql(`alter table "invoices" alter column "sub_total" set not null;`); + this.addSql(`alter table "invoices" alter column "tax_amount" type int using ("tax_amount"::int);`); + this.addSql(`alter table "invoices" alter column "tax_amount" set not null;`); + this.addSql(`alter table "invoices" alter column "total" type int using ("total"::int);`); + this.addSql(`alter table "invoices" alter column "total" set not null;`); + this.addSql(`alter table "invoices" alter column "balance" drop default;`); + this.addSql(`alter table "invoices" alter column "balance" type int using ("balance"::int);`); + this.addSql(`alter table "invoices" alter column "invoice_number" drop default;`); + this.addSql(`alter table "invoices" add constraint "invoices_invoice_number_unique" unique ("invoice_number");`); + } + + override async down(): Promise { + this.addSql(`alter table "invoices" drop constraint "invoices_invoice_number_unique";`); + + this.addSql(`alter table "invoices" alter column "invoice_number" type int using ("invoice_number"::int);`); + this.addSql(`alter table "invoices" alter column "discount" type int using ("discount"::int);`); + this.addSql(`alter table "invoices" alter column "discount" drop not null;`); + this.addSql(`alter table "invoices" alter column "sub_total" type int using ("sub_total"::int);`); + this.addSql(`alter table "invoices" alter column "sub_total" drop not null;`); + this.addSql(`alter table "invoices" alter column "tax_amount" type int using ("tax_amount"::int);`); + this.addSql(`alter table "invoices" alter column "tax_amount" drop not null;`); + this.addSql(`alter table "invoices" alter column "total" type int using ("total"::int);`); + this.addSql(`alter table "invoices" alter column "total" drop not null;`); + this.addSql(`alter table "invoices" alter column "balance" type int using ("balance"::int);`); + this.addSql(`alter table "invoices" alter column "balance" set default 0;`); + this.addSql(`create sequence if not exists "invoices_invoice_number_seq";`); + this.addSql(`select setval('invoices_invoice_number_seq', (select max("invoice_number") from "invoices"));`); + this.addSql(`alter table "invoices" alter column "invoice_number" set default nextval('invoices_invoice_number_seq');`); + } + +} diff --git a/src/modules/auth/guards/auth.guard.ts b/src/modules/auth/guards/auth.guard.ts index 75b2d96..af1bb51 100644 --- a/src/modules/auth/guards/auth.guard.ts +++ b/src/modules/auth/guards/auth.guard.ts @@ -54,6 +54,7 @@ export class AuthGuard implements CanActivate { throw new UnauthorizedException('Invalid or expired token'); } } + private extractTokenFromHeader(request: Request): string | undefined { const [type, token] = request.headers.authorization?.split(' ') ?? []; return type === 'Bearer' ? token : undefined; diff --git a/src/modules/chat/listeners/chat.listeners.ts b/src/modules/chat/listeners/chat.listeners.ts index 4db2f4a..7af6d4f 100644 --- a/src/modules/chat/listeners/chat.listeners.ts +++ b/src/modules/chat/listeners/chat.listeners.ts @@ -8,16 +8,21 @@ import { NotifChannelEnum, NotifTitle } from 'src/modules/notification/interface import { ChatCreatedByAdminEvent } from '../events/chat.events'; import { PermissionEnum } from 'src/common/enums/permission.enum'; import { AdminRepository } from 'src/modules/admin/repositories/admin.repository'; +import { ConfigService } from '@nestjs/config'; @Injectable() export class ChatListeners { private readonly logger = new Logger(ChatListeners.name); + private newChatSmsTemplateId: string; constructor( private readonly em: EntityManager, private readonly notificationService: NotificationService, private readonly adminRepository: AdminRepository, - ) { } + private readonly configService: ConfigService, + ) { + this.newChatSmsTemplateId = this.configService.get('SMS_PATTERN_NEW_CHAT') ?? 'new-chat'; + } @OnEvent(ChatCreatedByAdminEvent.name) async handleChatCreatedByAdmin(event: ChatCreatedByAdminEvent) { @@ -43,7 +48,7 @@ export class ChatListeners { title: NotifTitle.NEW_TICKET, content: body, sms: { - templateId: '', + templateId: this.newChatSmsTemplateId, parameters: {}, }, }, diff --git a/src/modules/invoice/entities/invoice-item.entity.ts b/src/modules/invoice/entities/invoice-item.entity.ts index 38c2553..a9cec54 100644 --- a/src/modules/invoice/entities/invoice-item.entity.ts +++ b/src/modules/invoice/entities/invoice-item.entity.ts @@ -1,4 +1,4 @@ -import { Collection, Entity, ManyToOne, OptionalProps, Property,OneToMany } from '@mikro-orm/core'; +import { Collection, Entity, ManyToOne, OptionalProps, Property, OneToMany, Opt } from '@mikro-orm/core'; import { Product } from 'src/modules/product/entities/product.entity'; import { BaseEntity } from 'src/common/entities/base.entity'; import { Invoice } from './invoice.entity'; @@ -7,7 +7,7 @@ import { Order } from 'src/modules/order/entities/order.entity'; @Entity({ tableName: 'invoice_items' }) export class InvoiceItem extends BaseEntity { - [OptionalProps]?: 'createdAt' | 'deletedAt' | 'subTotal' | 'total'; + [OptionalProps]?: 'createdAt' | 'deletedAt' | 'discount' | 'description' | 'confirmedAt'; @ManyToOne(() => Invoice) invoice!: Invoice; @@ -22,16 +22,15 @@ export class InvoiceItem extends BaseEntity { @Property({ type: 'int' }) quantity!: number; - @Property({ type: 'int' }) - unitPrice: number; + @Property({ type: 'int', fieldName: 'unit_price' }) + unitPrice!: number; @Property({ type: 'int', - // columnType: 'int GENERATED ALWAYS AS (COALESCE(unit_price, 0) * quantity) STORED', - // persist: false, - nullable: true + columnType: 'int GENERATED ALWAYS AS (COALESCE(unit_price, 0) * quantity) STORED', + persist: false, }) - subTotal!: number; + subTotal!: number & Opt; @Property({ type: 'int', @@ -41,11 +40,10 @@ export class InvoiceItem extends BaseEntity { @Property({ type: 'int', - nullable: true, - // columnType: 'int GENERATED ALWAYS AS ((COALESCE(unit_price, 0) * quantity) - COALESCE(discount, 0)) STORED', - // persist: false + columnType: 'int GENERATED ALWAYS AS ((COALESCE(unit_price, 0) * quantity) - COALESCE(discount, 0)) STORED', + persist: false }) - total!: number; + total!: number & Opt; @Property({ type: 'text', nullable: true }) description?: string; diff --git a/src/modules/invoice/entities/invoice.entity.ts b/src/modules/invoice/entities/invoice.entity.ts index fd05a30..37c92f3 100644 --- a/src/modules/invoice/entities/invoice.entity.ts +++ b/src/modules/invoice/entities/invoice.entity.ts @@ -9,6 +9,7 @@ import { Enum, PrimaryKey, OptionalProps, + Opt, } from '@mikro-orm/core'; import { User } from '../../user/entities/user.entity'; import { Payment } from 'src/modules/payment/entities/payment.entity'; @@ -23,7 +24,7 @@ import { IAttachment } from 'src/modules/order/interface/order.interface'; @Entity({ tableName: 'invoices' }) @Index({ properties: ['user'] }) export class Invoice extends BaseEntity { - [OptionalProps]?: 'createdAt' | 'deletedAt' | 'invoiceNumber' | 'request' + [OptionalProps]?: 'createdAt' | 'deletedAt' @PrimaryKey({ type: 'string', columnType: 'char(26)' }) id: string = ulid() @@ -48,34 +49,34 @@ export class Invoice extends BaseEntity { @Property({ - type: 'int', - autoincrement: true + type: 'int', + unique: true }) invoiceNumber: number; - @Property({ type: 'int', nullable: true }) - discount?: number; + @Property({ type: 'int' }) + discount!: number & Opt; - @Property({ type: 'int', nullable: true }) - subTotal?: number; + @Property({ type: 'int' }) + subTotal!: number & Opt; - @Property({ type: 'int', nullable: true }) - taxAmount?: number; + @Property({ type: 'int' }) + taxAmount!: number & Opt; - @Property({ type: 'int', nullable: true }) - total?: number; + @Property({ type: 'int' }) + total!: number & Opt; @Property({ type: 'int', default: 0 }) paidAmount: number = 0; - @Property({ type: 'int', default: 0 }) - balance: number = 0; + @Property({ type: 'int' }) + balance: number & Opt; // @Enum(() => InvoiceStatusEnum) diff --git a/src/modules/invoice/invoice.service.ts b/src/modules/invoice/invoice.service.ts index 305be23..c2ec260 100644 --- a/src/modules/invoice/invoice.service.ts +++ b/src/modules/invoice/invoice.service.ts @@ -61,7 +61,7 @@ export class InvoiceService { approvalDeadline, discount, paidAmount: 0, - balance: 0, + // balance: 0, attachments: [], paymentMethod: dto.paymentMethod, invoiceNumber: 20, @@ -69,14 +69,14 @@ export class InvoiceService { }); em.persist(invoice); - let subTotal = 0; + // let subTotal = 0; for (const item of dto.items) { const product = await this.productService.findOneOrFail(item.productId); const unitPrice = item.unitPrice; const itemDiscount = item.discount ?? 0; const itemTotal = Math.max(0, unitPrice * item.quantity - itemDiscount); - subTotal += itemTotal; + // subTotal += itemTotal; const invoiceItem = em.create(InvoiceItem, { invoice, @@ -89,16 +89,16 @@ export class InvoiceService { invoice.items.add(invoiceItem); } - const taxAmount = enableTax - ? Math.round((subTotal - discount) * TAX_RATE) - : 0; - const total = Math.max(0, subTotal - discount + taxAmount); + // const taxAmount = enableTax + // ? Math.round((subTotal - discount) * TAX_RATE) + // : 0; + // const total = Math.max(0, subTotal - discount + taxAmount); - invoice.subTotal = subTotal; + // invoice.subTotal = subTotal; invoice.attachments = dto.attachments; - invoice.taxAmount = taxAmount; - invoice.total = total; - invoice.balance = total - invoice.paidAmount; + // invoice.taxAmount = taxAmount; + // invoice.total = total; + // invoice.balance = total - invoice.paidAmount; if (dto.requestId) { const requestEntity = await em.findOne( diff --git a/src/modules/order/events/order.events.ts b/src/modules/order/events/order.events.ts index f937188..e2a746a 100644 --- a/src/modules/order/events/order.events.ts +++ b/src/modules/order/events/order.events.ts @@ -3,10 +3,9 @@ import type { OrderStatusEnum } from '../interface/order.interface'; export class OrderCreatedEvent { constructor( public readonly orderId: string, - public readonly: string, - public readonly orderNumber: string, - public readonly total: number, - ) { } + public readonly userId: string, + public readonly orderNumber: number, + ) {} } export class OrderStatusChangedEvent { diff --git a/src/modules/order/listeners/order.listeners.ts b/src/modules/order/listeners/order.listeners.ts index 2a6521e..8867702 100644 --- a/src/modules/order/listeners/order.listeners.ts +++ b/src/modules/order/listeners/order.listeners.ts @@ -1,212 +1,57 @@ import { Injectable, Logger } from '@nestjs/common'; import { OnEvent } from '@nestjs/event-emitter'; -import { AdminRepository } from 'src/modules/admin/repositories/admin.repository'; -import { OrderCreatedEvent, OrderStatusChangedEvent } from '../events/order.events'; -import { PermissionEnum } from 'src/common/enums/permission.enum'; +import { OrderCreatedEvent } from '../events/order.events'; import { NotifTitle } from 'src/modules/notification/interfaces/notification.interface'; import { NotificationService } from 'src/modules/notification/services/notification.service'; import { ConfigService } from '@nestjs/config'; -import { OrderRepository } from '../repositories/order.repository'; -// import { OrderStatusEnum } from '../interface/order.interface'; -import { PaymentMethodEnum } from 'src/modules/payment/interface/payment'; -import { UserService } from 'src/modules/user/providers/user.service'; +import { NotifChannelEnum } from 'src/modules/notification/interfaces/notification.interface'; @Injectable() export class OrderListeners { private readonly logger = new Logger(OrderListeners.name); private orderCreatedSmsTemplateId: string; - private orderStatusChangedSmsTemplateId: string; - // private orderCompletedSmsTemplateId: string; + constructor( - private readonly adminService: AdminRepository, - private readonly OrderRepository: OrderRepository, private readonly notificationService: NotificationService, private readonly configService: ConfigService, - private readonly userService: UserService, ) { - this.orderCreatedSmsTemplateId = this.configService.get('SMS_PATTERN_ORDER_CREATED') ?? '123'; - this.orderStatusChangedSmsTemplateId = this.configService.get('SMS_PATTERN_ORDER_STATUS_CHANGE') ?? '123'; - // this.orderCompletedSmsTemplateId = this.configService.get('SMS_PATTERN_ORDER_STATUS_COMPLETED') ?? '123'; + this.orderCreatedSmsTemplateId = + this.configService.get('SMS_PATTERN_ORDER_CREATED') ?? ''; } - // private getStatusFarsi(status: OrderStatus): string { - // const statusMap: Record = { - // [OrderStatus.PENDING_PAYMENT]: 'در انتظار پرداخت', - // [OrderStatus.PAID]: 'پرداخت شده', - // [OrderStatus.PREPARING]: 'در حال آماده‌سازی', - // [OrderStatus.DELIVERED_TO_RECEPTIONIST]: 'تحویل به پذیرش', - // [OrderStatus.DELIVERED_TO_WAITER]: 'تحویل به گارسون', - // [OrderStatus.SHIPPED]: 'ارسال شده', - // [OrderStatus.COMPLETED]: 'تکمیل شده', - // [OrderStatus.CANCELED]: 'لغو شده', - // }; - // return statusMap[status] || status; - // } + @OnEvent(OrderCreatedEvent.name) + async handleOrderCreated(event: OrderCreatedEvent) { + try { + this.logger.log( + `Order created event: orderId=${event.orderId}, orderNumber=${event.orderNumber}, userId=${event.userId}`, + ); - // @OnEvent(OrderCreatedEvent.name) - // async handleOrderCreated(event: OrderCreatedEvent) { - // try { - // this.logger.log( - // `Order created event received: ${event.orderId} for restaurant: ${event.} and order number: ${event.orderNumber}`, - // ); + const body = `سفارش شماره ${event.orderNumber} با موفقیت ایجاد شد`; - // const order = await this.OrderRepository.findOne(event.orderId); - // if (order?.paymentMethod.method === PaymentMethodEnum.Online) { - // return; - // } - - - // // get admnin os restuaraant that have order permissuins - // const admins = await this.adminService.findAdminsWithPermission(event., Permission.MANAGE_ORDERS); - // const recipients = admins.map(admin => ({ - // adminId: admin.id, - // })); - - // await this.notificationService.sendNotification({ - - // message: { - // title: NotifTitleEnum.ORDER_CREATED, - // content: `سفارش شماره ${event.orderNumber} با مبلغ ${event.total} تومان با موفقیت ایجاد شد`, - // sms: { - // templateId: this.orderCreatedSmsTemplateId, - // parameters: { - // orderNumber: event.orderNumber, - // total: event.total.toString(), - // }, - // }, - // pushNotif: { - // title: `سفارش جدید`, - // content: `سفارش شماره ${event.orderNumber} با مبلغ ${event.total} تومان با موفقیت ایجاد شد`, - // icon: `/`, - // action: { - // type: NotifTitleEnum.ORDER_CREATED, - // url: `/`, - // }, - // }, - // }, - // recipients, - // metadata: { - // priority: 1, - // }, - // }); - // } catch (error) { - // this.logger.error( - // `Failed to send notification for order created event: ${event.}`, - // error instanceof Error ? error.stack : String(error), - // ); - // } - // } - - // @OnEvent(OrderStatusChangedEvent.name) - // async handleOrderStatusChanged(event: OrderStatusChangedEvent) { - // try { - // this.logger.log( - // `Order status changed event received: ${event.orderId} for restaurant: ${event.} and order number: ${event.orderNumber}`, - // ); - // //TODO : REFACTOR to use queue or other way to handle this - // const recipients = [ - // { - // userId: event.userId, - // }, - // ]; - // if (event.newStatus === OrderStatus.COMPLETED) { - - // if (!event?.userId) { - // this.logger.log( - // `User not found for order: ${event.orderId} for restaurant: ${event.} and order number: ${event.orderNumber}`, - // ); - // } - - // // const restaurant = await this.RestaurantRepository.findOne(event.); - // // if (!restaurant) { - // // this.logger.log( - // // `Restaurant not found for order: ${event.orderId} for restaurant: ${event.} and order number: ${event.orderNumber}`, - // // ); - // // return; - // // } - // // const score = restaurant.score; - // // if (!score) { - // // this.logger.log( - // // `Score not found for restaurant: ${event.} and order number: ${event.orderNumber}`, - // // ); - // // return; - // // } - - // // increase score for user - // // const order = await this.OrderRepository.findOne(event.orderId); - // // if (!order) { - // // this.logger.log( - // // `Order not found for order: ${event.orderId} for restaurant: ${event.} and order number: ${event.orderNumber}`, - // // ); - // // return; - // // } - // // this.userService.createWalletTransaction(event.userId, event., { - // // amount: order.subTotal, - // // type: WalletTransactionType.CREDIT, - // // reason: WalletTransactionReason.ORDER_COMPLETED_DEPOSIT, - // // }); - - // await this.notificationService.sendNotification({ - // : event., - // message: { - // title: NotifTitleEnum.ORDER_STATUS_CHANGED, - // content: `لطفابرای ثبت نظر سفارش ${event.orderNumber} به اپ مراجعه کنید`, - // sms: { - // templateId: this.orderCreatedSmsTemplateId, - // parameters: { - // orderNumber: event.orderNumber, - // }, - // }, - // pushNotif: { - // title: `تغییر وضعیت سفارش`, - // content: `لطفا برای ثبت نظر سفارش ${event.orderNumber} به اپ مراجعه کنید`, - // icon: `/`, - // action: { - // type: NotifTitleEnum.ORDER_STATUS_CHANGED, - // url: ``, - // }, - // }, - // }, - // recipients, - // metadata: { - // priority: 1, - // }, - // }); - // } else { - // await this.notificationService.sendNotification({ - // : event., - // message: { - // title: NotifTitleEnum.ORDER_STATUS_CHANGED, - // content: `وضعیت سفارش شماره ${event.orderNumber} به ${this.getStatusFarsi(event.newStatus)} تغییر کرد`, - // sms: { - // templateId: this.orderStatusChangedSmsTemplateId, - // parameters: { - // orderNumber: event.orderNumber, - // status: this.getStatusFarsi(event.newStatus), - // }, - // }, - // pushNotif: { - // title: `تغییر وضعیت سفارش`, - // content: `وضعیت سفارش شماره ${event.orderNumber} به ${this.getStatusFarsi(event.newStatus)} تغییر کرد`, - // icon: `/`, - // action: { - // type: NotifTitleEnum.ORDER_STATUS_CHANGED, - // url: ``, - // }, - // }, - // }, - // recipients, - // metadata: { - // priority: 1, - // }, - // }); - // } - // } catch (error) { - // this.logger.error( - // `Failed to send notification for order status changed event: ${event.}`, - // error instanceof Error ? error.stack : String(error), - // ); - // } - // } + + await this.notificationService.sendNotification({ + channels: [NotifChannelEnum.SMS], + recipients: [{ userId: event.userId }], + message: { + title: NotifTitle.NEW_ORDER, + content: body, + sms: { + templateId: this.orderCreatedSmsTemplateId, + parameters: { + orderNumber: String(event.orderNumber), + }, + }, + }, + }); + this.logger.log( + `Notification sent to user ${event.userId} for order ${event.orderId}`, + ); + } catch (error) { + this.logger.error( + `Failed to send notification for order created: ${event.orderId}`, + error instanceof Error ? error.stack : String(error), + ); + } + } } diff --git a/src/modules/order/providers/order.service.ts b/src/modules/order/providers/order.service.ts index 9cf6d8b..e9f6258 100644 --- a/src/modules/order/providers/order.service.ts +++ b/src/modules/order/providers/order.service.ts @@ -8,30 +8,22 @@ import { } from '../dto/create-order.dto'; import { UserService } from 'src/modules/user/providers/user.service'; import { OrderStatusEnum } from '../interface/order.interface'; -import { ProductService } from 'src/modules/product/providers/product.service'; -import { ProductRepository } from 'src/modules/product/repositories/product.repository'; -import { ChattService } from 'src/modules/chat/providers/chat.service'; -import { ChatRepository } from 'src/modules/chat/repositories/chat.repository'; -import { AdminRepository } from 'src/modules/admin/repositories/admin.repository'; import { Order } from '../entities/order.entity'; -import { PaymentRepository } from 'src/modules/payment/repositories/payment.repository'; import { AdminService } from 'src/modules/admin/providers/admin.service'; import { UpdateOrderAsAdminDto } from '../dto/update-order.dto'; -import { FindOrderItemsDto } from '../dto/find-order-items.dto'; -import { PermissionService } from 'src/modules/roles/providers/permissions.service'; import { PermissionEnum } from 'src/common/enums/permission.enum'; import { User } from 'src/modules/user/entities/user.entity'; import { Product } from 'src/modules/product/entities/product.entity'; import { Category } from 'src/modules/product/entities/category.entity'; import { InvoiceItem } from 'src/modules/invoice/entities/invoice-item.entity'; import { PaginatedResult } from 'src/common/interfaces/pagination.interface'; -import { UpdateStatusAsDesignerDto } from '../dto/update-status-as-designer.dto'; import { UpdateStatusDto } from '../dto/update-status.dto'; import { CreateOrderPrintDto } from '../dto/create-order-print.dto'; import { OrderPrint } from '../entities/print.entity'; import { OrderPrintRepository } from '../repositories/order-print.repository'; import { Request } from 'src/modules/request/entities/request.entity'; import { Invoice } from 'src/modules/invoice/entities/invoice.entity'; +import { OrderCreatedEvent } from '../events/order.events'; @Injectable() export class OrderService { @@ -43,7 +35,8 @@ export class OrderService { private readonly userService: UserService, private readonly adminService: AdminService, private readonly orderPrintRepository: OrderPrintRepository, - ) { } + private readonly eventEmitter: EventEmitter2, + ) {} async createOrderAsAdmin(adminId: string, dto: CreateOrderAsAdminDto): Promise { let invoiceItem: null | InvoiceItem = null @@ -104,6 +97,12 @@ export class OrderService { }); await this.em.persistAndFlush(order); + + this.eventEmitter.emit( + OrderCreatedEvent.name, + new OrderCreatedEvent(order.id, user.id, order.orderNumber), + ); + return order; } diff --git a/src/modules/ticket/listeners/ticket.listeners.ts b/src/modules/ticket/listeners/ticket.listeners.ts index eea2dbc..a191e4c 100644 --- a/src/modules/ticket/listeners/ticket.listeners.ts +++ b/src/modules/ticket/listeners/ticket.listeners.ts @@ -66,4 +66,6 @@ export class TicketListeners { ); } } + + // TODO: add notification to user when ticket message is created by admin }