From 7ccf822a5cec4a8d26f316edaf385e8ed55bace3 Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Tue, 17 Feb 2026 16:04:49 +0330 Subject: [PATCH] add: migration --- database/migrations/.snapshot-dzone-db.json | 507 ++++++++++++++++++ ...igration20260217121029_meterageREquired.ts | 13 - ...igration20260217122512_meterageREquired.ts | 28 + database/mikro-orm.config.ts | 2 + database/seeders/README-TEST-COMPANIES.md | 38 ++ .../seeders/cleanup-test-companies.seeder.ts | 44 ++ database/seeders/test-companies.seeder.ts | 120 +++++ package.json | 2 +- src/modules/bills/bills.controller.ts | 6 +- src/modules/bills/bills.module.ts | 16 +- src/modules/bills/bills.service.ts | 24 +- src/modules/bills/entities/bill.entity.ts | 1 - .../bills/repositories/bill.repository.ts | 55 +- .../companies/DTO/create-company.dto.ts | 2 +- src/modules/companies/companies.module.ts | 2 +- ...st.enitiy.ts => company-request.entity.ts} | 0 .../company-request.repository.ts | 2 +- .../companies/services/companies.service.ts | 2 +- 18 files changed, 790 insertions(+), 74 deletions(-) delete mode 100644 database/migrations/Migration20260217121029_meterageREquired.ts create mode 100644 database/migrations/Migration20260217122512_meterageREquired.ts create mode 100644 database/seeders/README-TEST-COMPANIES.md create mode 100644 database/seeders/cleanup-test-companies.seeder.ts create mode 100644 database/seeders/test-companies.seeder.ts rename src/modules/companies/entities/{company-request.enitiy.ts => company-request.entity.ts} (100%) diff --git a/database/migrations/.snapshot-dzone-db.json b/database/migrations/.snapshot-dzone-db.json index 7c6ace6..af2c0be 100644 --- a/database/migrations/.snapshot-dzone-db.json +++ b/database/migrations/.snapshot-dzone-db.json @@ -239,6 +239,15 @@ "OVERDUE" ] }, + "company_request_status": { + "name": "company_request_status", + "schema": "public", + "items": [ + "PENDING", + "APPROVED", + "REJECTED" + ] + }, "chat_session_status": { "name": "chat_session_status", "schema": "public", @@ -441,6 +450,15 @@ "OVERDUE" ] }, + "company_request_status": { + "name": "company_request_status", + "schema": "public", + "items": [ + "PENDING", + "APPROVED", + "REJECTED" + ] + }, "chat_session_status": { "name": "chat_session_status", "schema": "public", @@ -691,6 +709,15 @@ "OVERDUE" ] }, + "company_request_status": { + "name": "company_request_status", + "schema": "public", + "items": [ + "PENDING", + "APPROVED", + "REJECTED" + ] + }, "chat_session_status": { "name": "chat_session_status", "schema": "public", @@ -919,6 +946,15 @@ "OVERDUE" ] }, + "company_request_status": { + "name": "company_request_status", + "schema": "public", + "items": [ + "PENDING", + "APPROVED", + "REJECTED" + ] + }, "chat_session_status": { "name": "chat_session_status", "schema": "public", @@ -1148,6 +1184,15 @@ "OVERDUE" ] }, + "company_request_status": { + "name": "company_request_status", + "schema": "public", + "items": [ + "PENDING", + "APPROVED", + "REJECTED" + ] + }, "chat_session_status": { "name": "chat_session_status", "schema": "public", @@ -1375,6 +1420,15 @@ "OVERDUE" ] }, + "company_request_status": { + "name": "company_request_status", + "schema": "public", + "items": [ + "PENDING", + "APPROVED", + "REJECTED" + ] + }, "chat_session_status": { "name": "chat_session_status", "schema": "public", @@ -1556,6 +1610,15 @@ "OVERDUE" ] }, + "company_request_status": { + "name": "company_request_status", + "schema": "public", + "items": [ + "PENDING", + "APPROVED", + "REJECTED" + ] + }, "chat_session_status": { "name": "chat_session_status", "schema": "public", @@ -1805,6 +1868,15 @@ "OVERDUE" ] }, + "company_request_status": { + "name": "company_request_status", + "schema": "public", + "items": [ + "PENDING", + "APPROVED", + "REJECTED" + ] + }, "chat_session_status": { "name": "chat_session_status", "schema": "public", @@ -2033,6 +2105,15 @@ "OVERDUE" ] }, + "company_request_status": { + "name": "company_request_status", + "schema": "public", + "items": [ + "PENDING", + "APPROVED", + "REJECTED" + ] + }, "chat_session_status": { "name": "chat_session_status", "schema": "public", @@ -2357,6 +2438,15 @@ "OVERDUE" ] }, + "company_request_status": { + "name": "company_request_status", + "schema": "public", + "items": [ + "PENDING", + "APPROVED", + "REJECTED" + ] + }, "chat_session_status": { "name": "chat_session_status", "schema": "public", @@ -2566,6 +2656,15 @@ "OVERDUE" ] }, + "company_request_status": { + "name": "company_request_status", + "schema": "public", + "items": [ + "PENDING", + "APPROVED", + "REJECTED" + ] + }, "chat_session_status": { "name": "chat_session_status", "schema": "public", @@ -2818,6 +2917,15 @@ "OVERDUE" ] }, + "company_request_status": { + "name": "company_request_status", + "schema": "public", + "items": [ + "PENDING", + "APPROVED", + "REJECTED" + ] + }, "chat_session_status": { "name": "chat_session_status", "schema": "public", @@ -3077,6 +3185,15 @@ "OVERDUE" ] }, + "company_request_status": { + "name": "company_request_status", + "schema": "public", + "items": [ + "PENDING", + "APPROVED", + "REJECTED" + ] + }, "chat_session_status": { "name": "chat_session_status", "schema": "public", @@ -3276,6 +3393,15 @@ "OVERDUE" ] }, + "company_request_status": { + "name": "company_request_status", + "schema": "public", + "items": [ + "PENDING", + "APPROVED", + "REJECTED" + ] + }, "chat_session_status": { "name": "chat_session_status", "schema": "public", @@ -3710,6 +3836,15 @@ "OVERDUE" ] }, + "company_request_status": { + "name": "company_request_status", + "schema": "public", + "items": [ + "PENDING", + "APPROVED", + "REJECTED" + ] + }, "chat_session_status": { "name": "chat_session_status", "schema": "public", @@ -4037,6 +4172,15 @@ "OVERDUE" ] }, + "company_request_status": { + "name": "company_request_status", + "schema": "public", + "items": [ + "PENDING", + "APPROVED", + "REJECTED" + ] + }, "chat_session_status": { "name": "chat_session_status", "schema": "public", @@ -4279,6 +4423,15 @@ "OVERDUE" ] }, + "company_request_status": { + "name": "company_request_status", + "schema": "public", + "items": [ + "PENDING", + "APPROVED", + "REJECTED" + ] + }, "chat_session_status": { "name": "chat_session_status", "schema": "public", @@ -4478,6 +4631,15 @@ "OVERDUE" ] }, + "company_request_status": { + "name": "company_request_status", + "schema": "public", + "items": [ + "PENDING", + "APPROVED", + "REJECTED" + ] + }, "chat_session_status": { "name": "chat_session_status", "schema": "public", @@ -4842,6 +5004,15 @@ "OVERDUE" ] }, + "company_request_status": { + "name": "company_request_status", + "schema": "public", + "items": [ + "PENDING", + "APPROVED", + "REJECTED" + ] + }, "chat_session_status": { "name": "chat_session_status", "schema": "public", @@ -5144,6 +5315,15 @@ "OVERDUE" ] }, + "company_request_status": { + "name": "company_request_status", + "schema": "public", + "items": [ + "PENDING", + "APPROVED", + "REJECTED" + ] + }, "chat_session_status": { "name": "chat_session_status", "schema": "public", @@ -5385,6 +5565,15 @@ "OVERDUE" ] }, + "company_request_status": { + "name": "company_request_status", + "schema": "public", + "items": [ + "PENDING", + "APPROVED", + "REJECTED" + ] + }, "chat_session_status": { "name": "chat_session_status", "schema": "public", @@ -5594,6 +5783,279 @@ "OVERDUE" ] }, + "company_request_status": { + "name": "company_request_status", + "schema": "public", + "items": [ + "PENDING", + "APPROVED", + "REJECTED" + ] + }, + "chat_session_status": { + "name": "chat_session_status", + "schema": "public", + "items": [ + "active", + "closed", + "archived" + ] + }, + "message_type": { + "name": "message_type", + "schema": "public", + "items": [ + "user", + "bot", + "system" + ] + }, + "message_status": { + "name": "message_status", + "schema": "public", + "items": [ + "sent", + "delivered", + "read", + "failed" + ] + } + } + }, + { + "columns": { + "id": { + "name": "id", + "type": "uuid", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "uuid" + }, + "created_at": { + "name": "created_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "current_timestamp", + "mappedType": "datetime" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "current_timestamp", + "mappedType": "datetime" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" + }, + "reject_reason": { + "name": "reject_reason", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "request_status": { + "name": "request_status", + "type": "company_request_status", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "nativeEnumName": "company_request_status", + "default": "'PENDING'", + "enumItems": [ + "PENDING", + "APPROVED", + "REJECTED" + ], + "mappedType": "enum" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "uuid" + }, + "business_id": { + "name": "business_id", + "type": "uuid", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "uuid" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "uuid" + } + }, + "name": "company_request", + "schema": "public", + "indexes": [ + { + "keyName": "company_request_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "constraint": true, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": { + "company_request_company_id_foreign": { + "constraintName": "company_request_company_id_foreign", + "columnNames": [ + "company_id" + ], + "localTableName": "public.company_request", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.company", + "updateRule": "cascade" + }, + "company_request_business_id_foreign": { + "constraintName": "company_request_business_id_foreign", + "columnNames": [ + "business_id" + ], + "localTableName": "public.company_request", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.business", + "updateRule": "cascade" + }, + "company_request_user_id_foreign": { + "constraintName": "company_request_user_id_foreign", + "columnNames": [ + "user_id" + ], + "localTableName": "public.company_request", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.user", + "updateRule": "cascade" + } + }, + "nativeEnums": { + "role_enum": { + "name": "role_enum", + "schema": "public", + "items": [ + "ADMIN", + "USER" + ] + }, + "notif_type": { + "name": "notif_type", + "schema": "public", + "items": [ + "OTP", + "USER_LOGIN", + "ANNOUNCEMENT", + "WALLET_CHARGE", + "WALLET_DEDUCTION", + "BILL_INVOICE_REMINDER", + "BILL_INVOICE", + "APPROVE_INVOICE", + "CREATE_INVOICE", + "INVOICE_OVERDUE", + "CREATE_SERVICE", + "UNBLOCK_SERVICE", + "BLOCK_SERVICE", + "ANSWER_TICKET", + "CREATE_TICKET", + "ASSIGN_TICKET", + "RECURRING_INVOICE", + "PAYMENT_REMINDER", + "PAYMENT_CANCELLATION", + "NEW_CUSTOMER", + "NEW_TICKET", + "NEW_CRITICISM" + ] + }, + "company_status": { + "name": "company_status", + "schema": "public", + "items": [ + "APPROVED", + "PENDING", + "REJECTED" + ] + }, + "ticket_status": { + "name": "ticket_status", + "schema": "public", + "items": [ + "PENDING", + "ANSWERED", + "CLOSED" + ] + }, + "ticket_priority": { + "name": "ticket_priority", + "schema": "public", + "items": [ + "LOW", + "MEDIUM", + "HIGH" + ] + }, + "invoice_status": { + "name": "invoice_status", + "schema": "public", + "items": [ + "DRAFT", + "PENDING", + "WAIT_PAYMENT", + "PAID", + "ARCHIVED", + "OVERDUE" + ] + }, + "company_request_status": { + "name": "company_request_status", + "schema": "public", + "items": [ + "PENDING", + "APPROVED", + "REJECTED" + ] + }, "chat_session_status": { "name": "chat_session_status", "schema": "public", @@ -5803,6 +6265,15 @@ "OVERDUE" ] }, + "company_request_status": { + "name": "company_request_status", + "schema": "public", + "items": [ + "PENDING", + "APPROVED", + "REJECTED" + ] + }, "chat_session_status": { "name": "chat_session_status", "schema": "public", @@ -6036,6 +6507,15 @@ "OVERDUE" ] }, + "company_request_status": { + "name": "company_request_status", + "schema": "public", + "items": [ + "PENDING", + "APPROVED", + "REJECTED" + ] + }, "chat_session_status": { "name": "chat_session_status", "schema": "public", @@ -6316,6 +6796,15 @@ "OVERDUE" ] }, + "company_request_status": { + "name": "company_request_status", + "schema": "public", + "items": [ + "PENDING", + "APPROVED", + "REJECTED" + ] + }, "chat_session_status": { "name": "chat_session_status", "schema": "public", @@ -6537,6 +7026,15 @@ "OVERDUE" ] }, + "company_request_status": { + "name": "company_request_status", + "schema": "public", + "items": [ + "PENDING", + "APPROVED", + "REJECTED" + ] + }, "chat_session_status": { "name": "chat_session_status", "schema": "public", @@ -6644,6 +7142,15 @@ "OVERDUE" ] }, + "company_request_status": { + "name": "company_request_status", + "schema": "public", + "items": [ + "PENDING", + "APPROVED", + "REJECTED" + ] + }, "chat_session_status": { "name": "chat_session_status", "schema": "public", diff --git a/database/migrations/Migration20260217121029_meterageREquired.ts b/database/migrations/Migration20260217121029_meterageREquired.ts deleted file mode 100644 index af355a9..0000000 --- a/database/migrations/Migration20260217121029_meterageREquired.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Migration } from '@mikro-orm/migrations'; - -export class Migration20260217121029_meterageREquired extends Migration { - - override async up(): Promise { - this.addSql(`select 1`); - } - - override async down(): Promise { - this.addSql(`select 1`); - } - -} diff --git a/database/migrations/Migration20260217122512_meterageREquired.ts b/database/migrations/Migration20260217122512_meterageREquired.ts new file mode 100644 index 0000000..8bc128d --- /dev/null +++ b/database/migrations/Migration20260217122512_meterageREquired.ts @@ -0,0 +1,28 @@ +import { Migration } from "@mikro-orm/migrations"; + +export class Migration20260217122512_meterageREquired extends Migration { + override async up(): Promise { + this.addSql(`alter table "company" alter column "metrage" type int using ("metrage"::int);`); + this.addSql(`update "company" set "metrage" = 0 where "metrage" is null;`); + this.addSql(`alter table "company" alter column "metrage" set not null;`); + + this.addSql(`alter table "invoice" alter column "total_price" type numeric using ("total_price"::numeric);`); + this.addSql(`alter table "invoice" alter column "original_price" type numeric using ("original_price"::numeric);`); + this.addSql(`alter table "invoice" alter column "tax" type numeric using ("tax"::numeric);`); + this.addSql(`alter table "invoice" alter column "late_fee" type numeric using ("late_fee"::numeric);`); + + this.addSql(`alter table "payment" alter column "amount" type numeric using ("amount"::numeric);`); + } + + override async down(): Promise { + this.addSql(`alter table "company" alter column "metrage" type int4 using ("metrage"::int4);`); + this.addSql(`alter table "company" alter column "metrage" drop not null;`); + + this.addSql(`alter table "invoice" alter column "total_price" type numeric using ("total_price"::numeric);`); + this.addSql(`alter table "invoice" alter column "original_price" type numeric using ("original_price"::numeric);`); + this.addSql(`alter table "invoice" alter column "tax" type numeric using ("tax"::numeric);`); + this.addSql(`alter table "invoice" alter column "late_fee" type numeric using ("late_fee"::numeric);`); + + this.addSql(`alter table "payment" alter column "amount" type numeric using ("amount"::numeric);`); + } +} diff --git a/database/mikro-orm.config.ts b/database/mikro-orm.config.ts index c8cbc36..8d23523 100644 --- a/database/mikro-orm.config.ts +++ b/database/mikro-orm.config.ts @@ -1,4 +1,5 @@ import { Options } from "@mikro-orm/core"; +import { Migrator } from "@mikro-orm/migrations"; import { PostgreSqlDriver } from "@mikro-orm/postgresql"; import dotenv from "dotenv"; @@ -6,6 +7,7 @@ dotenv.config(); const config: Options = { driver: PostgreSqlDriver, + extensions: [Migrator], dbName: process.env.DB_NAME, host: process.env.DB_HOST, port: Number(process.env.DB_PORT), diff --git a/database/seeders/README-TEST-COMPANIES.md b/database/seeders/README-TEST-COMPANIES.md new file mode 100644 index 0000000..8f6fff3 --- /dev/null +++ b/database/seeders/README-TEST-COMPANIES.md @@ -0,0 +1,38 @@ +# Test companies seed (production-safe) + +Add about **1000 test companies** for a specific business, then remove them in one go. + +- **Business ID:** `e5e760b9-46a5-4194-bb3d-00a46c24d72a` +- All test rows use the prefix **`TEST_SEED_`** (company name, emails, etc.) so they are easy to identify and delete without touching real data. + +## 1. Seed test companies + +From project root: + +```bash +pnpm run build +pnpm run seed:test-companies +``` + +Requirements: + +- Business `e5e760b9-46a5-4194-bb3d-00a46c24d72a` must exist. +- At least one **Industry** must exist for that business (create one in the app if needed). +- **Role** `USER` must exist (run `pnpm run seed:run` once if you haven’t). + +## 2. After testing: remove test data + +Same DB, same business: + +```bash +pnpm run seed:cleanup-test-companies +``` + +This **soft-deletes** all companies whose name starts with `TEST_SEED_` and their linked users for that business. No schema changes, no hard deletes. + +## Changing business ID or count + +Edit the constants at the top of: + +- `database/seeders/test-companies.seeder.ts` — `TEST_BUSINESS_ID`, `TEST_COMPANIES_COUNT` +- `database/seeders/cleanup-test-companies.seeder.ts` — `TEST_BUSINESS_ID` (must match). diff --git a/database/seeders/cleanup-test-companies.seeder.ts b/database/seeders/cleanup-test-companies.seeder.ts new file mode 100644 index 0000000..e56b020 --- /dev/null +++ b/database/seeders/cleanup-test-companies.seeder.ts @@ -0,0 +1,44 @@ +import { EntityManager } from "@mikro-orm/postgresql"; +import { Seeder } from "@mikro-orm/seeder"; +import { Logger } from "@nestjs/common"; +import dayjs from "dayjs"; + +import { TEST_SEED_PREFIX } from "./test-companies.seeder"; +import { Company } from "../../src/modules/companies/entities/company.entity"; + +/** Must match the business ID used in TestCompaniesSeeder. */ +const TEST_BUSINESS_ID = "e5e760b9-46a5-4194-bb3d-00a46c24d72a"; + +export class CleanupTestCompaniesSeeder extends Seeder { + private readonly logger = new Logger(CleanupTestCompaniesSeeder.name); + + async run(em: EntityManager): Promise { + this.logger.log(`Cleaning up test companies (name like '${TEST_SEED_PREFIX}%') for business ${TEST_BUSINESS_ID}...`); + + const companies = await em.find( + Company, + { + name: { $like: `${TEST_SEED_PREFIX}%` }, + business: { id: TEST_BUSINESS_ID }, + deletedAt: null, + }, + { populate: ["user"] }, + ); + + if (companies.length === 0) { + this.logger.log("No test companies found. Nothing to delete."); + return; + } + + const now = dayjs().toDate(); + for (const company of companies) { + company.deletedAt = now; + if (company.user?.deletedAt == null) { + company.user.deletedAt = now; + } + } + + await em.flush(); + this.logger.log(`Soft-deleted ${companies.length} test companies and their users.`); + } +} diff --git a/database/seeders/test-companies.seeder.ts b/database/seeders/test-companies.seeder.ts new file mode 100644 index 0000000..4232ac6 --- /dev/null +++ b/database/seeders/test-companies.seeder.ts @@ -0,0 +1,120 @@ +import { EntityManager } from "@mikro-orm/postgresql"; +import { Seeder } from "@mikro-orm/seeder"; +import { Logger } from "@nestjs/common"; +import { genSalt, hash } from "bcrypt"; + +import { Business } from "../../src/modules/businesses/entities/business.entity"; +import { CompanyProduct } from "../../src/modules/companies/entities/company-product.entity"; +import { CompanyService } from "../../src/modules/companies/entities/company-service.entity"; +import { Company } from "../../src/modules/companies/entities/company.entity"; +import { CompanyStatus } from "../../src/modules/companies/enums/company-status.enum"; +import { Industry } from "../../src/modules/industries/entities/industry.entity"; +import { Role } from "../../src/modules/users/entities/role.entity"; +import { User } from "../../src/modules/users/entities/user.entity"; +import { RoleEnum } from "../../src/modules/users/enums/role.enum"; + +/** Business ID to seed test companies for. Change or use env if needed. */ +const TEST_BUSINESS_ID = "e5e760b9-46a5-4194-bb3d-00a46c24d72a"; + +/** All test companies/users use this prefix so they can be safely deleted later. */ +export const TEST_SEED_PREFIX = "TEST_SEED_"; + +const TEST_COMPANIES_COUNT = 1000; +const BATCH_SIZE = 50; +const TEST_PASSWORD = "TestPassword1!"; + +export class TestCompaniesSeeder extends Seeder { + private readonly logger = new Logger(TestCompaniesSeeder.name); + + async run(em: EntityManager): Promise { + const business = await em.findOne(Business, { id: TEST_BUSINESS_ID, deletedAt: null }); + if (!business) { + this.logger.error(`Business not found: ${TEST_BUSINESS_ID}. Aborting.`); + throw new Error(`Business not found: ${TEST_BUSINESS_ID}`); + } + + const industry = await em.findOne(Industry, { business: { id: TEST_BUSINESS_ID }, deletedAt: null }); + if (!industry) { + this.logger.error(`No industry found for business ${TEST_BUSINESS_ID}. Create an industry first.`); + throw new Error("No industry found for business"); + } + + const role = await em.findOne(Role, { name: RoleEnum.USER }); + if (!role) { + this.logger.error("USER role not found. Run default seed first (RoleSeeder)."); + throw new Error("USER role not found"); + } + + const hashedPassword = await hash(TEST_PASSWORD, await genSalt(10)); + const baseDate = "2020-01-01"; + const baseUrl = "https://example.com"; + + this.logger.log(`Seeding ${TEST_COMPANIES_COUNT} test companies for business ${TEST_BUSINESS_ID}...`); + + for (let i = 1; i <= TEST_COMPANIES_COUNT; i++) { + const pad4 = String(i).padStart(4, "0"); + const pad5 = String(i).padStart(5, "0"); + const pad7 = String(i).padStart(7, "0"); + const phone = `0912${pad7}`; + const email = `test.seed.${i}@test.local`; + const nationalCode = `0000${pad5}`; + const userName = `testseed${i}`; + + const user = em.create(User, { + firstName: "Test", + lastName: `User${i}`, + email, + phone, + userName, + nationalCode, + password: hashedPassword, + role, + business, + }); + await em.persist(user); + + const company = em.create(Company, { + name: `${TEST_SEED_PREFIX}Company_${pad4}`, + chiefExecutiveOfficer: `Test CEO ${i}`, + email, + phone, + identificationNumber: `TESTSEEDID${pad5}`, + dateOfEstablishment: new Date(baseDate), + address: `${TEST_SEED_PREFIX} Address ${i}`, + mapAddressLink: `${baseUrl}/map`, + description: `${TEST_SEED_PREFIX} Description ${i}`, + websiteUrl: `${baseUrl}`, + profileImageUrl: `${baseUrl}/profile.png`, + coverImageUrl: `${baseUrl}/cover.png`, + metrage: 100, + isActive: true, + status: CompanyStatus.APPROVED, + industry, + business, + user, + }); + const product = em.create(CompanyProduct, { + title: `${TEST_SEED_PREFIX}Product_${i}`, + imageUrl: `${baseUrl}/product.png`, + company, + }); + const service = em.create(CompanyService, { + title: `${TEST_SEED_PREFIX}Service_${i}`, + imageUrl: `${baseUrl}/service.png`, + company, + }); + company.products.add(product); + company.services.add(service); + await em.persist(company); + + if (i % BATCH_SIZE === 0) { + await em.flush(); + this.logger.log(` ... ${i}/${TEST_COMPANIES_COUNT}`); + } + } + + await em.flush(); + this.logger.log(`Done. Created ${TEST_COMPANIES_COUNT} test companies (name like '${TEST_SEED_PREFIX}%').`); + this.logger.warn(`To remove them later, run: pnpm run seed:cleanup-test-companies`); + } +} diff --git a/package.json b/package.json index 3baca36..8a1ff56 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "orm:create": "mikro-orm migration:create --config=database/mikro-orm.config.ts", "orm:up": "mikro-orm migration:up --config=database/mikro-orm.config.ts", "orm:down": "mikro-orm migration:down --config=database/mikro-orm.config.ts", - "orm:generate": "mikro-orm migration:create --blank --config=database/mikro-orm.config.ts", + "orm:generate": "mikro-orm migration:create --config=database/mikro-orm.config.ts", "orm:update": "mikro-orm schema:update --run --config=database/mikro-orm.config.ts", "orm:drop": "mikro-orm schema:drop --run --drop-migrations-table --drop-db --config=database/mikro-orm.config.ts", "orm:seed": "mikro-orm seeder:create --config=database/mikro-orm.config.ts", diff --git a/src/modules/bills/bills.controller.ts b/src/modules/bills/bills.controller.ts index 17bde7d..fcb096c 100644 --- a/src/modules/bills/bills.controller.ts +++ b/src/modules/bills/bills.controller.ts @@ -32,11 +32,7 @@ export class BillsController { @UseInterceptors(FileInterceptor("file")) @ApiBody({ type: CreateWaterBillFromExcelDto }) @Post("water/excel") - createWaterBillByExcel( - @UploadedFile() file: IFile, - @Body() dto: CreateWaterBillFromExcelDto, - @BusinessDec() business: Business, - ) { + createWaterBillByExcel(@UploadedFile() file: IFile, @Body() dto: CreateWaterBillFromExcelDto, @BusinessDec() business: Business) { return this.billsService.createWaterBillByExcel(file, dto, business); } diff --git a/src/modules/bills/bills.module.ts b/src/modules/bills/bills.module.ts index 151e992..e2a59cf 100644 --- a/src/modules/bills/bills.module.ts +++ b/src/modules/bills/bills.module.ts @@ -1,4 +1,5 @@ import { MikroOrmModule } from "@mikro-orm/nestjs"; +import { BullModule } from "@nestjs/bullmq"; import { Module } from "@nestjs/common"; import { BillsController } from "./bills.controller"; @@ -6,17 +7,20 @@ import { BillsService } from "./bills.service"; import { Bill } from "./entities/bill.entity"; import { BusinessesModule } from "../businesses/businesses.module"; import { CompaniesModule } from "../companies/companies.module"; -import { NotificationModule } from "../notifications/notifications.module"; -import { InvoicesModule } from "../invoices/invoices.module"; -import { BullModule } from "@nestjs/bullmq"; import { INVOICE } from "../invoices/constants"; +import { InvoicesModule } from "../invoices/invoices.module"; +import { NotificationModule } from "../notifications/notifications.module"; @Module({ imports: [ BullModule.registerQueue({ name: INVOICE.QUEUE_NAME }), - MikroOrmModule.forFeature([Bill]), CompaniesModule, BusinessesModule - , NotificationModule, InvoicesModule], + MikroOrmModule.forFeature([Bill]), + CompaniesModule, + BusinessesModule, + NotificationModule, + InvoicesModule, + ], controllers: [BillsController], providers: [BillsService], }) -export class BillsModule { } +export class BillsModule {} diff --git a/src/modules/bills/bills.service.ts b/src/modules/bills/bills.service.ts index 98692aa..b41f56e 100644 --- a/src/modules/bills/bills.service.ts +++ b/src/modules/bills/bills.service.ts @@ -1,11 +1,11 @@ import { EntityManager } from "@mikro-orm/postgresql"; +import { InjectQueue } from "@nestjs/bullmq"; import { BadRequestException, Injectable } from "@nestjs/common"; +import { Queue } from "bullmq"; import dayjs from "dayjs"; import Decimal from "decimal.js"; import { Workbook } from "exceljs"; -import { IFile } from "../utils/interfaces/IFile"; - import { BillListQueryDto } from "./dto/bill-list-query.dto"; import { CreateChargeBillDto } from "./dto/create-charge-bill.dto"; import { CreateWaterBillDto, CreateWaterBillFromExcelDto } from "./dto/create-water-bill.dto"; @@ -16,13 +16,11 @@ import { CompanyMessage } from "../../common/enums/message.enum"; import { Business } from "../businesses/entities/business.entity"; import { Company } from "../companies/entities/company.entity"; import { CompaniesService } from "../companies/services/companies.service"; +import { INVOICE } from "../invoices/constants"; import { InvoiceItem } from "../invoices/entities/invoice-item.entity"; import { Invoice } from "../invoices/entities/invoice.entity"; import { NotificationQueue } from "../notifications/queue/notification.queue"; -import { InjectQueue } from "@nestjs/bullmq"; -import { INVOICE } from "../invoices/constants"; -import { Queue } from "bullmq"; - +import { IFile } from "../utils/interfaces/IFile"; @Injectable() export class BillsService { @@ -33,8 +31,7 @@ export class BillsService { @InjectQueue(INVOICE.QUEUE_NAME) private readonly invoiceQueue: Queue, private readonly notificationQueue: NotificationQueue, - - ) { } + ) {} async createWaterBill(dto: CreateWaterBillDto, business: Business) { const { values, waterRate, dueDays } = dto; @@ -95,8 +92,8 @@ export class BillsService { } } - private async addInvoiceReminderJobs(billId: string) { - const invoices = await this.em.find(Invoice, { bill: { id: billId } }, { populate: ["items", "company","company.user"] }); + private async addInvoiceReminderJobs(billId: string) { + const invoices = await this.em.find(Invoice, { bill: { id: billId } }, { populate: ["items", "company", "company.user"] }); for (const invoice of invoices) { await this.notificationQueue.addInvoiceCreationNotification(invoice.company.user.id, { @@ -196,7 +193,7 @@ export class BillsService { await em.flush(); await em.commit(); - await this.addInvoiceReminderJobs(bill.id) + await this.addInvoiceReminderJobs(bill.id); return bill; } catch (error) { @@ -223,10 +220,7 @@ export class BillsService { } async remove(id: string, businessId: string) { - const bill = await this.billRepository.findOne( - { id, business: { id: businessId } }, - { populate: ["invoices"] }, - ); + const bill = await this.billRepository.findOne({ id, business: { id: businessId } }, { populate: ["invoices"] }); if (!bill) { throw new BadRequestException("Bill not found"); } diff --git a/src/modules/bills/entities/bill.entity.ts b/src/modules/bills/entities/bill.entity.ts index be045aa..6784a27 100644 --- a/src/modules/bills/entities/bill.entity.ts +++ b/src/modules/bills/entities/bill.entity.ts @@ -21,6 +21,5 @@ export class Bill extends BaseEntity { @Property({ persist: false }) invoiceCount?: number; - [EntityRepositoryType]?: BillsRepository; } diff --git a/src/modules/bills/repositories/bill.repository.ts b/src/modules/bills/repositories/bill.repository.ts index 8655060..dd5ba93 100755 --- a/src/modules/bills/repositories/bill.repository.ts +++ b/src/modules/bills/repositories/bill.repository.ts @@ -5,65 +5,62 @@ import { BillListQueryDto } from "../dto/bill-list-query.dto"; import { Bill } from "../entities/bill.entity"; export class BillsRepository extends EntityRepository { - async getBillsList(queryDto: BillListQueryDto, businessId: string) { const { limit, skip } = PaginationUtils(queryDto); - + const knex = this.em.getKnex(); const query = knex - .select('b.*') - .select(knex.raw('COUNT(i.id) as invoice_count')) - .from('bill as b') - .leftJoin('invoice as i', 'i.bill_id', 'b.id') - .where('b.business_id', businessId) - .groupBy('b.id') - .orderBy('b.created_at', 'desc') + .select("b.*") + .select(knex.raw("COUNT(i.id) as invoice_count")) + .from("bill as b") + .leftJoin("invoice as i", "i.bill_id", "b.id") + .where("b.business_id", businessId) + .groupBy("b.id") + .orderBy("b.created_at", "desc") .limit(limit) .offset(skip); - + // Add filters if (queryDto.type) { - query.andWhere('b.type', queryDto.type); + query.andWhere("b.type", queryDto.type); } - + if (queryDto.since) { - query.andWhere('b.created_at', '>=', new Date(queryDto.since)); + query.andWhere("b.created_at", ">=", new Date(queryDto.since)); } - + if (queryDto.to) { - query.andWhere('b.created_at', '<=', new Date(queryDto.to)); + query.andWhere("b.created_at", "<=", new Date(queryDto.to)); } - + // Execute the query const results = await query; - + // Get total count for pagination - const countQuery = knex('bill') - .count('* as total') - .where('business_id', businessId); - + const countQuery = knex("bill").count("* as total").where("business_id", businessId); + if (queryDto.type) { - countQuery.andWhere('type', queryDto.type); + countQuery.andWhere("type", queryDto.type); } - + if (queryDto.since) { - countQuery.andWhere('created_at', '>=', new Date(queryDto.since)); + countQuery.andWhere("created_at", ">=", new Date(queryDto.since)); } - + if (queryDto.to) { - countQuery.andWhere('created_at', '<=', new Date(queryDto.to)); + countQuery.andWhere("created_at", "<=", new Date(queryDto.to)); } - + const totalResult = await countQuery; const total = Number(totalResult[0].total); - + // Map results back to Bill entities (invoice_count from knex may be string/bigint) const bills = results.map((row) => { const bill = this.em.map(Bill, row); bill.invoiceCount = Number(row.invoice_count ?? 0); return bill; }); - + return [bills, total]; } } diff --git a/src/modules/companies/DTO/create-company.dto.ts b/src/modules/companies/DTO/create-company.dto.ts index 1aa3fba..776d42a 100644 --- a/src/modules/companies/DTO/create-company.dto.ts +++ b/src/modules/companies/DTO/create-company.dto.ts @@ -125,7 +125,7 @@ export class CreateCompanyRequestDto extends PickType(CreateCompanyDto, [ "industryId", "products", "services", - "metrage" + "metrage", ]) { @IsNotEmpty({ message: CompanyMessage.CHIEF_EXECUTIVE_REQUIRED }) @IsString({ message: CompanyMessage.CHIEF_EXECUTIVE_MUST_BE_STRING }) diff --git a/src/modules/companies/companies.module.ts b/src/modules/companies/companies.module.ts index 3c6e783..6553194 100644 --- a/src/modules/companies/companies.module.ts +++ b/src/modules/companies/companies.module.ts @@ -3,7 +3,7 @@ import { Module } from "@nestjs/common"; import { CompaniesController } from "./companies.controller"; import { CompanyProduct } from "./entities/company-product.entity"; -import { CompanyRequest } from "./entities/company-request.enitiy"; +import { CompanyRequest } from "./entities/company-request.entity"; import { CompanyService } from "./entities/company-service.entity"; import { Company } from "./entities/company.entity"; import { CompaniesService } from "./services/companies.service"; diff --git a/src/modules/companies/entities/company-request.enitiy.ts b/src/modules/companies/entities/company-request.entity.ts similarity index 100% rename from src/modules/companies/entities/company-request.enitiy.ts rename to src/modules/companies/entities/company-request.entity.ts diff --git a/src/modules/companies/repositories/company-request.repository.ts b/src/modules/companies/repositories/company-request.repository.ts index 5be9d81..721f467 100644 --- a/src/modules/companies/repositories/company-request.repository.ts +++ b/src/modules/companies/repositories/company-request.repository.ts @@ -1,5 +1,5 @@ import { EntityRepository } from "@mikro-orm/postgresql"; -import { CompanyRequest } from "../entities/company-request.enitiy"; +import { CompanyRequest } from "../entities/company-request.entity"; export class CompanyRequestRepository extends EntityRepository {} diff --git a/src/modules/companies/services/companies.service.ts b/src/modules/companies/services/companies.service.ts index 7691034..e021ffc 100644 --- a/src/modules/companies/services/companies.service.ts +++ b/src/modules/companies/services/companies.service.ts @@ -14,7 +14,7 @@ import { CompanyListPublicQueryDto, CompanyListQueryDto } from "../DTO/company-l import { ChangeCompanyRequestStatusDto, CreateCompanyDto, CreateCompanyRequestDto } from "../DTO/create-company.dto"; import { UpdateCompanyDto, UpdateUserCompanyDto } from "../DTO/update-company.dto"; import { CompanyProduct } from "../entities/company-product.entity"; -import { CompanyRequest } from "../entities/company-request.enitiy"; +import { CompanyRequest } from "../entities/company-request.entity"; import { CompanyService } from "../entities/company-service.entity"; import { Company } from "../entities/company.entity"; import { CompanyRequestStatus } from "../enums/company-request-status.enum";