From 7a957a69c7a61bd8fab10129c1b8e6f6fd710923 Mon Sep 17 00:00:00 2001 From: Adrian Flores Cortes Date: Tue, 3 Feb 2026 04:40:16 -0600 Subject: [PATCH] refactor: Consolidate duplicate services and normalize module structure - Partners: Moved services to services/ directory, consolidated duplicates, kept singleton pattern version with ranking service - Products: Moved service to services/, removed duplicate class-based version, kept singleton with deletedAt filtering - Reports: Moved service to services/, kept raw SQL version for active controller - Warehouses: Moved service to services/, removed duplicate class-based version, kept singleton with proper tenant isolation All modules now follow consistent structure: - services/*.service.ts for business logic - services/index.ts for exports - Controllers import from ./services/index.js Co-Authored-By: Claude Opus 4.5 --- src/__tests__/core/countries.service.test.ts | 2 +- src/__tests__/core/currencies.service.test.ts | 2 +- src/__tests__/core/states.service.test.ts | 2 +- src/__tests__/core/uom.service.test.ts | 2 +- src/modules/auth/apiKeys.controller.ts | 2 +- src/modules/auth/auth.controller.ts | 2 +- src/modules/auth/index.ts | 4 +- .../auth/{ => services}/apiKeys.service.ts | 6 +- .../auth/{ => services}/auth.service.ts | 10 +- src/modules/companies/companies.controller.ts | 2 +- src/modules/companies/index.ts | 2 +- .../{ => services}/companies.service.ts | 8 +- src/modules/core/core.controller.ts | 16 +- src/modules/core/index.ts | 14 +- .../core/{ => services}/countries.service.ts | 8 +- .../core/{ => services}/currencies.service.ts | 8 +- .../{ => services}/currency-rates.service.ts | 10 +- .../{ => services}/discount-rules.service.ts | 8 +- .../{ => services}/payment-terms.service.ts | 8 +- .../product-categories.service.ts | 8 +- .../core/{ => services}/sequences.service.ts | 8 +- .../core/{ => services}/states.service.ts | 8 +- .../core/{ => services}/uom.service.ts | 10 +- src/modules/crm/crm.controller.ts | 6 +- src/modules/crm/index.ts | 10 +- .../crm/{ => services}/activities.service.ts | 6 +- .../crm/{ => services}/forecasting.service.ts | 4 +- .../crm/{ => services}/leads.service.ts | 4 +- .../{ => services}/opportunities.service.ts | 4 +- .../crm/{ => services}/stages.service.ts | 4 +- .../__tests__/accounts.service.spec.ts | 22 +- .../__tests__/payments.service.spec.ts | 2 +- src/modules/financial/financial.controller.ts | 12 +- src/modules/financial/index.ts | 14 +- .../{ => services}/accounts.service.ts | 8 +- .../{ => services}/fiscalPeriods.service.ts | 6 +- .../{ => services}/gl-posting.service.ts | 10 +- .../{ => services}/invoices.service.ts | 8 +- .../{ => services}/journal-entries.service.ts | 4 +- .../{ => services}/journals.service.ts | 4 +- .../{ => services}/payments.service.ts | 4 +- .../financial/{ => services}/taxes.service.ts | 4 +- src/modules/fiscal/fiscal.controller.ts | 2 +- src/modules/fiscal/index.ts | 2 +- .../{ => services}/fiscal-catalogs.service.ts | 8 +- src/modules/health/health.controller.ts | 2 +- src/modules/health/health.module.ts | 4 +- src/modules/health/index.ts | 2 +- .../health/{ => services}/health.service.ts | 0 src/modules/hr/hr.controller.ts | 8 +- src/modules/hr/index.ts | 5 +- .../hr/{ => services}/contracts.service.ts | 4 +- .../hr/{ => services}/departments.service.ts | 4 +- .../hr/{ => services}/employees.service.ts | 4 +- src/modules/hr/services/index.ts | 4 + .../hr/{ => services}/leaves.service.ts | 4 +- src/modules/inventory/inventory.controller.ts | 12 +- .../{ => services}/adjustments.service.ts | 6 +- src/modules/inventory/services/index.ts | 26 +- .../{ => services}/locations.service.ts | 4 +- .../inventory/{ => services}/lots.service.ts | 4 +- .../{ => services}/pickings.service.ts | 6 +- .../{ => services}/products.service.ts | 10 +- .../{ => services}/reorder-alerts.service.ts | 4 +- .../stock-reservation.service.ts | 6 +- .../{ => services}/valuation.service.ts | 6 +- .../{ => services}/warehouses.service.ts | 12 +- src/modules/inventory/valuation.controller.ts | 2 +- .../__tests__/partners.service.spec.ts | 2 +- .../__tests__/partners.service.test.ts | 2 +- src/modules/partners/partners.controller.ts | 3 +- src/modules/partners/partners.service.ts | 350 ------- src/modules/partners/ranking.controller.ts | 2 +- src/modules/partners/services/index.ts | 17 +- .../partners/services/partners.service.ts | 554 +++++----- .../{ => services}/ranking.service.ts | 8 +- .../__tests__/products.service.test.ts | 2 +- src/modules/products/products.controller.ts | 2 +- src/modules/products/products.service.ts | 300 ------ src/modules/products/services/index.ts | 10 +- .../products/services/products.service.ts | 270 +++-- src/modules/projects/index.ts | 10 +- src/modules/projects/projects.controller.ts | 6 +- .../{ => services}/billing.service.ts | 6 +- .../{ => services}/hr-integration.service.ts | 6 +- src/modules/projects/services/index.ts | 6 + .../{ => services}/projects.service.ts | 4 +- .../projects/{ => services}/tasks.service.ts | 4 +- .../{ => services}/timesheets.service.ts | 4 +- src/modules/purchases/purchases.controller.ts | 4 +- src/modules/purchases/services/index.ts | 4 + .../{ => services}/purchases.service.ts | 8 +- .../purchases/{ => services}/rfqs.service.ts | 4 +- src/modules/reports/reports.controller.ts | 2 +- src/modules/reports/reports.service.ts | 580 ----------- src/modules/reports/services/index.ts | 21 +- .../reports/services/reports.service.ts | 954 ++++++++++-------- src/modules/roles/index.ts | 8 +- src/modules/roles/permissions.controller.ts | 2 +- src/modules/roles/roles.controller.ts | 2 +- src/modules/roles/services/index.ts | 3 + .../{ => services}/permissions.service.ts | 8 +- .../roles/{ => services}/roles.service.ts | 8 +- src/modules/sales/sales.controller.ts | 10 +- .../{ => services}/customer-groups.service.ts | 4 +- src/modules/sales/services/index.ts | 9 +- .../sales/{ => services}/orders.service.ts | 12 +- .../{ => services}/pricelists.service.ts | 4 +- .../{ => services}/quotations.service.ts | 6 +- .../{ => services}/sales-teams.service.ts | 4 +- src/modules/system/index.ts | 6 +- .../{ => services}/activities.service.ts | 4 +- .../system/{ => services}/messages.service.ts | 4 +- .../{ => services}/notifications.service.ts | 4 +- src/modules/system/system.controller.ts | 6 +- src/modules/tenants/index.ts | 4 +- .../tenants/{ => services}/tenants.service.ts | 8 +- src/modules/tenants/tenants.controller.ts | 2 +- src/modules/users/index.ts | 2 +- .../users/{ => services}/users.service.ts | 10 +- src/modules/users/users.controller.ts | 2 +- .../__tests__/warehouses.service.test.ts | 2 +- src/modules/warehouses/services/index.ts | 8 +- .../warehouses/services/warehouses.service.ts | 354 +++---- .../warehouses/warehouses.controller.ts | 2 +- src/modules/warehouses/warehouses.service.ts | 270 ----- .../middleware/apiKeyAuth.middleware.ts | 2 +- 127 files changed, 1516 insertions(+), 2836 deletions(-) rename src/modules/auth/{ => services}/apiKeys.service.ts (98%) rename src/modules/auth/{ => services}/auth.service.ts (95%) rename src/modules/companies/{ => services}/companies.service.ts (98%) rename src/modules/core/{ => services}/countries.service.ts (79%) rename src/modules/core/{ => services}/currencies.service.ts (91%) rename src/modules/core/{ => services}/currency-rates.service.ts (95%) rename src/modules/core/{ => services}/discount-rules.service.ts (98%) rename src/modules/core/{ => services}/payment-terms.service.ts (98%) rename src/modules/core/{ => services}/product-categories.service.ts (95%) rename src/modules/core/{ => services}/sequences.service.ts (97%) rename src/modules/core/{ => services}/states.service.ts (94%) rename src/modules/core/{ => services}/uom.service.ts (95%) rename src/modules/crm/{ => services}/activities.service.ts (98%) rename src/modules/crm/{ => services}/forecasting.service.ts (99%) rename src/modules/crm/{ => services}/leads.service.ts (99%) rename src/modules/crm/{ => services}/opportunities.service.ts (99%) rename src/modules/crm/{ => services}/stages.service.ts (98%) rename src/modules/financial/{ => services}/accounts.service.ts (98%) rename src/modules/financial/{ => services}/fiscalPeriods.service.ts (98%) rename src/modules/financial/{ => services}/gl-posting.service.ts (98%) rename src/modules/financial/{ => services}/invoices.service.ts (98%) rename src/modules/financial/{ => services}/journal-entries.service.ts (99%) rename src/modules/financial/{ => services}/journals.service.ts (97%) rename src/modules/financial/{ => services}/payments.service.ts (98%) rename src/modules/financial/{ => services}/taxes.service.ts (98%) rename src/modules/fiscal/{ => services}/fiscal-catalogs.service.ts (98%) rename src/modules/health/{ => services}/health.service.ts (100%) rename src/modules/hr/{ => services}/contracts.service.ts (98%) rename src/modules/hr/{ => services}/departments.service.ts (98%) rename src/modules/hr/{ => services}/employees.service.ts (99%) create mode 100644 src/modules/hr/services/index.ts rename src/modules/hr/{ => services}/leaves.service.ts (99%) rename src/modules/inventory/{ => services}/adjustments.service.ts (99%) rename src/modules/inventory/{ => services}/locations.service.ts (97%) rename src/modules/inventory/{ => services}/lots.service.ts (98%) rename src/modules/inventory/{ => services}/pickings.service.ts (99%) rename src/modules/inventory/{ => services}/products.service.ts (97%) rename src/modules/inventory/{ => services}/reorder-alerts.service.ts (99%) rename src/modules/inventory/{ => services}/stock-reservation.service.ts (98%) rename src/modules/inventory/{ => services}/valuation.service.ts (98%) rename src/modules/inventory/{ => services}/warehouses.service.ts (96%) delete mode 100644 src/modules/partners/partners.service.ts rename src/modules/partners/{ => services}/ranking.service.ts (98%) delete mode 100644 src/modules/products/products.service.ts rename src/modules/projects/{ => services}/billing.service.ts (99%) rename src/modules/projects/{ => services}/hr-integration.service.ts (98%) create mode 100644 src/modules/projects/services/index.ts rename src/modules/projects/{ => services}/projects.service.ts (99%) rename src/modules/projects/{ => services}/tasks.service.ts (98%) rename src/modules/projects/{ => services}/timesheets.service.ts (98%) rename src/modules/purchases/{ => services}/purchases.service.ts (98%) rename src/modules/purchases/{ => services}/rfqs.service.ts (98%) delete mode 100644 src/modules/reports/reports.service.ts create mode 100644 src/modules/roles/services/index.ts rename src/modules/roles/{ => services}/permissions.service.ts (97%) rename src/modules/roles/{ => services}/roles.service.ts (97%) rename src/modules/sales/{ => services}/customer-groups.service.ts (97%) rename src/modules/sales/{ => services}/orders.service.ts (98%) rename src/modules/sales/{ => services}/pricelists.service.ts (98%) rename src/modules/sales/{ => services}/quotations.service.ts (99%) rename src/modules/sales/{ => services}/sales-teams.service.ts (97%) rename src/modules/system/{ => services}/activities.service.ts (98%) rename src/modules/system/{ => services}/messages.service.ts (98%) rename src/modules/system/{ => services}/notifications.service.ts (97%) rename src/modules/tenants/{ => services}/tenants.service.ts (98%) rename src/modules/users/{ => services}/users.service.ts (96%) delete mode 100644 src/modules/warehouses/warehouses.service.ts diff --git a/src/__tests__/core/countries.service.test.ts b/src/__tests__/core/countries.service.test.ts index 8d7a095..0ddd1c7 100644 --- a/src/__tests__/core/countries.service.test.ts +++ b/src/__tests__/core/countries.service.test.ts @@ -11,7 +11,7 @@ jest.mock('../../config/typeorm', () => ({ })); // Import after mocking -import { countriesService } from '../../modules/core/countries.service'; +import { countriesService } from '../../modules/core/services/countries.service'; describe('CountriesService', () => { beforeEach(() => { diff --git a/src/__tests__/core/currencies.service.test.ts b/src/__tests__/core/currencies.service.test.ts index 38bbed9..19c4f4a 100644 --- a/src/__tests__/core/currencies.service.test.ts +++ b/src/__tests__/core/currencies.service.test.ts @@ -13,7 +13,7 @@ jest.mock('../../config/typeorm', () => ({ })); // Import after mocking -import { currenciesService } from '../../modules/core/currencies.service'; +import { currenciesService } from '../../modules/core/services/currencies.service'; describe('CurrenciesService', () => { beforeEach(() => { diff --git a/src/__tests__/core/states.service.test.ts b/src/__tests__/core/states.service.test.ts index 7bde38d..dc708bf 100644 --- a/src/__tests__/core/states.service.test.ts +++ b/src/__tests__/core/states.service.test.ts @@ -16,7 +16,7 @@ jest.mock('../../config/typeorm', () => ({ }, })); -import { statesService } from '../../modules/core/states.service'; +import { statesService } from '../../modules/core/services/states.service'; describe('StatesService', () => { beforeEach(() => { diff --git a/src/__tests__/core/uom.service.test.ts b/src/__tests__/core/uom.service.test.ts index 307f90e..859a91b 100644 --- a/src/__tests__/core/uom.service.test.ts +++ b/src/__tests__/core/uom.service.test.ts @@ -18,7 +18,7 @@ jest.mock('../../config/typeorm', () => ({ }, })); -import { uomService } from '../../modules/core/uom.service'; +import { uomService } from '../../modules/core/services/uom.service'; describe('UomService', () => { beforeEach(() => { diff --git a/src/modules/auth/apiKeys.controller.ts b/src/modules/auth/apiKeys.controller.ts index a637387..cc7d648 100644 --- a/src/modules/auth/apiKeys.controller.ts +++ b/src/modules/auth/apiKeys.controller.ts @@ -1,6 +1,6 @@ import { Response, NextFunction } from 'express'; import { z } from 'zod'; -import { apiKeysService, CreateApiKeyDto, UpdateApiKeyDto, ApiKeyFilters } from './apiKeys.service.js'; +import { apiKeysService, CreateApiKeyDto, UpdateApiKeyDto, ApiKeyFilters } from './services/apiKeys.service.js'; import { AuthenticatedRequest, ValidationError, ApiResponse } from '../../shared/types/index.js'; // ============================================================================ diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index 5e6c5e0..0b5b27d 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from 'express'; import { z } from 'zod'; -import { authService } from './auth.service.js'; +import { authService } from './services/auth.service.js'; import { ApiResponse, AuthenticatedRequest, ValidationError } from '../../shared/types/index.js'; // Validation schemas diff --git a/src/modules/auth/index.ts b/src/modules/auth/index.ts index 2afcd75..9ad5e57 100644 --- a/src/modules/auth/index.ts +++ b/src/modules/auth/index.ts @@ -1,8 +1,8 @@ -export * from './auth.service.js'; +export * from './services/auth.service.js'; export * from './auth.controller.js'; export { default as authRoutes } from './auth.routes.js'; // API Keys -export * from './apiKeys.service.js'; +export * from './services/apiKeys.service.js'; export * from './apiKeys.controller.js'; export { default as apiKeysRoutes } from './apiKeys.routes.js'; diff --git a/src/modules/auth/apiKeys.service.ts b/src/modules/auth/services/apiKeys.service.ts similarity index 98% rename from src/modules/auth/apiKeys.service.ts rename to src/modules/auth/services/apiKeys.service.ts index 784640a..6c066ef 100644 --- a/src/modules/auth/apiKeys.service.ts +++ b/src/modules/auth/services/apiKeys.service.ts @@ -1,7 +1,7 @@ import crypto from 'crypto'; -import { query, queryOne } from '../../config/database.js'; -import { ValidationError, NotFoundError, UnauthorizedError } from '../../shared/types/index.js'; -import { logger } from '../../shared/utils/logger.js'; +import { query, queryOne } from '../../../config/database.js'; +import { ValidationError, NotFoundError, UnauthorizedError } from '../../../shared/types/index.js'; +import { logger } from '../../../shared/utils/logger.js'; // ============================================================================ // TYPES diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/services/auth.service.ts similarity index 95% rename from src/modules/auth/auth.service.ts rename to src/modules/auth/services/auth.service.ts index 43efe10..0981473 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/services/auth.service.ts @@ -1,10 +1,10 @@ import bcrypt from 'bcryptjs'; import { Repository } from 'typeorm'; -import { AppDataSource } from '../../config/typeorm.js'; -import { User, UserStatus, Role } from './entities/index.js'; -import { tokenService, TokenPair, RequestMetadata } from './services/token.service.js'; -import { UnauthorizedError, ValidationError, NotFoundError } from '../../shared/types/index.js'; -import { logger } from '../../shared/utils/logger.js'; +import { AppDataSource } from '../../../config/typeorm.js'; +import { User, UserStatus, Role } from '../entities/index.js'; +import { tokenService, TokenPair, RequestMetadata } from './token.service.js'; +import { UnauthorizedError, ValidationError, NotFoundError } from '../../../shared/types/index.js'; +import { logger } from '../../../shared/utils/logger.js'; export interface LoginDto { email: string; diff --git a/src/modules/companies/companies.controller.ts b/src/modules/companies/companies.controller.ts index e59bc40..39b426b 100644 --- a/src/modules/companies/companies.controller.ts +++ b/src/modules/companies/companies.controller.ts @@ -1,6 +1,6 @@ import { Response, NextFunction } from 'express'; import { z } from 'zod'; -import { companiesService, CreateCompanyDto, UpdateCompanyDto, CompanyFilters } from './companies.service.js'; +import { companiesService, CreateCompanyDto, UpdateCompanyDto, CompanyFilters } from './services/companies.service.js'; import { ApiResponse, AuthenticatedRequest, ValidationError } from '../../shared/types/index.js'; // Validation schemas (accept both snake_case and camelCase from frontend) diff --git a/src/modules/companies/index.ts b/src/modules/companies/index.ts index fbf5e5b..06935d4 100644 --- a/src/modules/companies/index.ts +++ b/src/modules/companies/index.ts @@ -1,3 +1,3 @@ -export * from './companies.service.js'; +export * from './services/companies.service.js'; export * from './companies.controller.js'; export { default as companiesRoutes } from './companies.routes.js'; diff --git a/src/modules/companies/companies.service.ts b/src/modules/companies/services/companies.service.ts similarity index 98% rename from src/modules/companies/companies.service.ts rename to src/modules/companies/services/companies.service.ts index f42e47e..f4589c6 100644 --- a/src/modules/companies/companies.service.ts +++ b/src/modules/companies/services/companies.service.ts @@ -1,8 +1,8 @@ import { Repository, IsNull } from 'typeorm'; -import { AppDataSource } from '../../config/typeorm.js'; -import { Company } from '../auth/entities/index.js'; -import { NotFoundError, ValidationError, ForbiddenError } from '../../shared/types/index.js'; -import { logger } from '../../shared/utils/logger.js'; +import { AppDataSource } from '../../../config/typeorm.js'; +import { Company } from '../../auth/entities/index.js'; +import { NotFoundError, ValidationError, ForbiddenError } from '../../../shared/types/index.js'; +import { logger } from '../../../shared/utils/logger.js'; // ===== Interfaces ===== diff --git a/src/modules/core/core.controller.ts b/src/modules/core/core.controller.ts index 165848c..3decac2 100644 --- a/src/modules/core/core.controller.ts +++ b/src/modules/core/core.controller.ts @@ -1,13 +1,13 @@ import { Response, NextFunction } from 'express'; import { z } from 'zod'; -import { currenciesService, CreateCurrencyDto, UpdateCurrencyDto } from './currencies.service.js'; -import { countriesService } from './countries.service.js'; -import { statesService, CreateStateDto, UpdateStateDto } from './states.service.js'; -import { currencyRatesService, CreateCurrencyRateDto, ConvertCurrencyDto } from './currency-rates.service.js'; -import { uomService, CreateUomDto, UpdateUomDto } from './uom.service.js'; -import { productCategoriesService, CreateProductCategoryDto, UpdateProductCategoryDto } from './product-categories.service.js'; -import { paymentTermsService, CreatePaymentTermDto, UpdatePaymentTermDto } from './payment-terms.service.js'; -import { discountRulesService, CreateDiscountRuleDto, UpdateDiscountRuleDto, ApplyDiscountContext } from './discount-rules.service.js'; +import { currenciesService, CreateCurrencyDto, UpdateCurrencyDto } from './services/currencies.service.js'; +import { countriesService } from './services/countries.service.js'; +import { statesService, CreateStateDto, UpdateStateDto } from './services/states.service.js'; +import { currencyRatesService, CreateCurrencyRateDto, ConvertCurrencyDto } from './services/currency-rates.service.js'; +import { uomService, CreateUomDto, UpdateUomDto } from './services/uom.service.js'; +import { productCategoriesService, CreateProductCategoryDto, UpdateProductCategoryDto } from './services/product-categories.service.js'; +import { paymentTermsService, CreatePaymentTermDto, UpdatePaymentTermDto } from './services/payment-terms.service.js'; +import { discountRulesService, CreateDiscountRuleDto, UpdateDiscountRuleDto, ApplyDiscountContext } from './services/discount-rules.service.js'; import { PaymentTermLineType } from './entities/payment-term.entity.js'; import { DiscountType, DiscountAppliesTo, DiscountCondition } from './entities/discount-rule.entity.js'; import { AuthenticatedRequest } from '../../shared/middleware/auth.middleware.js'; diff --git a/src/modules/core/index.ts b/src/modules/core/index.ts index 01167f3..ad4c57b 100644 --- a/src/modules/core/index.ts +++ b/src/modules/core/index.ts @@ -1,10 +1,10 @@ -export * from './currencies.service.js'; -export * from './countries.service.js'; -export * from './uom.service.js'; -export * from './product-categories.service.js'; -export * from './sequences.service.js'; -export * from './payment-terms.service.js'; -export * from './discount-rules.service.js'; +export * from './services/currencies.service.js'; +export * from './services/countries.service.js'; +export * from './services/uom.service.js'; +export * from './services/product-categories.service.js'; +export * from './services/sequences.service.js'; +export * from './services/payment-terms.service.js'; +export * from './services/discount-rules.service.js'; export * from './entities/index.js'; export * from './core.controller.js'; export { default as coreRoutes } from './core.routes.js'; diff --git a/src/modules/core/countries.service.ts b/src/modules/core/services/countries.service.ts similarity index 79% rename from src/modules/core/countries.service.ts rename to src/modules/core/services/countries.service.ts index 943a37c..2e21210 100644 --- a/src/modules/core/countries.service.ts +++ b/src/modules/core/services/countries.service.ts @@ -1,8 +1,8 @@ import { Repository } from 'typeorm'; -import { AppDataSource } from '../../config/typeorm.js'; -import { Country } from './entities/country.entity.js'; -import { NotFoundError } from '../../shared/errors/index.js'; -import { logger } from '../../shared/utils/logger.js'; +import { AppDataSource } from '../../../config/typeorm.js'; +import { Country } from '../entities/country.entity.js'; +import { NotFoundError } from '../../../shared/errors/index.js'; +import { logger } from '../../../shared/utils/logger.js'; class CountriesService { private repository: Repository; diff --git a/src/modules/core/currencies.service.ts b/src/modules/core/services/currencies.service.ts similarity index 91% rename from src/modules/core/currencies.service.ts rename to src/modules/core/services/currencies.service.ts index 2d0e988..f2dd3be 100644 --- a/src/modules/core/currencies.service.ts +++ b/src/modules/core/services/currencies.service.ts @@ -1,8 +1,8 @@ import { Repository } from 'typeorm'; -import { AppDataSource } from '../../config/typeorm.js'; -import { Currency } from './entities/currency.entity.js'; -import { NotFoundError, ConflictError } from '../../shared/errors/index.js'; -import { logger } from '../../shared/utils/logger.js'; +import { AppDataSource } from '../../../config/typeorm.js'; +import { Currency } from '../entities/currency.entity.js'; +import { NotFoundError, ConflictError } from '../../../shared/errors/index.js'; +import { logger } from '../../../shared/utils/logger.js'; export interface CreateCurrencyDto { code: string; diff --git a/src/modules/core/currency-rates.service.ts b/src/modules/core/services/currency-rates.service.ts similarity index 95% rename from src/modules/core/currency-rates.service.ts rename to src/modules/core/services/currency-rates.service.ts index 694b8c1..d6698c5 100644 --- a/src/modules/core/currency-rates.service.ts +++ b/src/modules/core/services/currency-rates.service.ts @@ -1,9 +1,9 @@ import { Repository, LessThanOrEqual } from 'typeorm'; -import { AppDataSource } from '../../config/typeorm.js'; -import { CurrencyRate, RateSource } from './entities/currency-rate.entity.js'; -import { Currency } from './entities/currency.entity.js'; -import { NotFoundError } from '../../shared/errors/index.js'; -import { logger } from '../../shared/utils/logger.js'; +import { AppDataSource } from '../../../config/typeorm.js'; +import { CurrencyRate, RateSource } from '../entities/currency-rate.entity.js'; +import { Currency } from '../entities/currency.entity.js'; +import { NotFoundError } from '../../../shared/errors/index.js'; +import { logger } from '../../../shared/utils/logger.js'; export interface CreateCurrencyRateDto { tenantId?: string; diff --git a/src/modules/core/discount-rules.service.ts b/src/modules/core/services/discount-rules.service.ts similarity index 98% rename from src/modules/core/discount-rules.service.ts rename to src/modules/core/services/discount-rules.service.ts index 9a0bb11..776ad0a 100644 --- a/src/modules/core/discount-rules.service.ts +++ b/src/modules/core/services/discount-rules.service.ts @@ -1,13 +1,13 @@ import { Repository, LessThanOrEqual, MoreThanOrEqual, IsNull, Or } from 'typeorm'; -import { AppDataSource } from '../../config/typeorm.js'; +import { AppDataSource } from '../../../config/typeorm.js'; import { DiscountRule, DiscountType, DiscountAppliesTo, DiscountCondition, -} from './entities/discount-rule.entity.js'; -import { NotFoundError, ValidationError, ConflictError } from '../../shared/errors/index.js'; -import { logger } from '../../shared/utils/logger.js'; +} from '../entities/discount-rule.entity.js'; +import { NotFoundError, ValidationError, ConflictError } from '../../../shared/errors/index.js'; +import { logger } from '../../../shared/utils/logger.js'; // ============================================================================ // TYPES diff --git a/src/modules/core/payment-terms.service.ts b/src/modules/core/services/payment-terms.service.ts similarity index 98% rename from src/modules/core/payment-terms.service.ts rename to src/modules/core/services/payment-terms.service.ts index 1d22b46..d12cfaf 100644 --- a/src/modules/core/payment-terms.service.ts +++ b/src/modules/core/services/payment-terms.service.ts @@ -1,12 +1,12 @@ import { Repository } from 'typeorm'; -import { AppDataSource } from '../../config/typeorm.js'; +import { AppDataSource } from '../../../config/typeorm.js'; import { PaymentTerm, PaymentTermLine, PaymentTermLineType, -} from './entities/payment-term.entity.js'; -import { NotFoundError, ValidationError, ConflictError } from '../../shared/errors/index.js'; -import { logger } from '../../shared/utils/logger.js'; +} from '../entities/payment-term.entity.js'; +import { NotFoundError, ValidationError, ConflictError } from '../../../shared/errors/index.js'; +import { logger } from '../../../shared/utils/logger.js'; // ============================================================================ // TYPES diff --git a/src/modules/core/product-categories.service.ts b/src/modules/core/services/product-categories.service.ts similarity index 95% rename from src/modules/core/product-categories.service.ts rename to src/modules/core/services/product-categories.service.ts index 8401c99..ccad970 100644 --- a/src/modules/core/product-categories.service.ts +++ b/src/modules/core/services/product-categories.service.ts @@ -1,8 +1,8 @@ import { Repository, IsNull } from 'typeorm'; -import { AppDataSource } from '../../config/typeorm.js'; -import { ProductCategory } from './entities/product-category.entity.js'; -import { NotFoundError, ConflictError } from '../../shared/errors/index.js'; -import { logger } from '../../shared/utils/logger.js'; +import { AppDataSource } from '../../../config/typeorm.js'; +import { ProductCategory } from '../entities/product-category.entity.js'; +import { NotFoundError, ConflictError } from '../../../shared/errors/index.js'; +import { logger } from '../../../shared/utils/logger.js'; export interface CreateProductCategoryDto { name: string; diff --git a/src/modules/core/sequences.service.ts b/src/modules/core/services/sequences.service.ts similarity index 97% rename from src/modules/core/sequences.service.ts rename to src/modules/core/services/sequences.service.ts index 7c5982a..b83d31c 100644 --- a/src/modules/core/sequences.service.ts +++ b/src/modules/core/services/sequences.service.ts @@ -1,8 +1,8 @@ import { Repository, DataSource } from 'typeorm'; -import { AppDataSource } from '../../config/typeorm.js'; -import { Sequence, ResetPeriod } from './entities/sequence.entity.js'; -import { NotFoundError, ValidationError } from '../../shared/errors/index.js'; -import { logger } from '../../shared/utils/logger.js'; +import { AppDataSource } from '../../../config/typeorm.js'; +import { Sequence, ResetPeriod } from '../entities/sequence.entity.js'; +import { NotFoundError, ValidationError } from '../../../shared/errors/index.js'; +import { logger } from '../../../shared/utils/logger.js'; // ============================================================================ // TYPES diff --git a/src/modules/core/states.service.ts b/src/modules/core/services/states.service.ts similarity index 94% rename from src/modules/core/states.service.ts rename to src/modules/core/services/states.service.ts index c89a9a9..0e65ca3 100644 --- a/src/modules/core/states.service.ts +++ b/src/modules/core/services/states.service.ts @@ -1,8 +1,8 @@ import { Repository } from 'typeorm'; -import { AppDataSource } from '../../config/typeorm.js'; -import { State } from './entities/state.entity.js'; -import { NotFoundError } from '../../shared/errors/index.js'; -import { logger } from '../../shared/utils/logger.js'; +import { AppDataSource } from '../../../config/typeorm.js'; +import { State } from '../entities/state.entity.js'; +import { NotFoundError } from '../../../shared/errors/index.js'; +import { logger } from '../../../shared/utils/logger.js'; export interface CreateStateDto { countryId: string; diff --git a/src/modules/core/uom.service.ts b/src/modules/core/services/uom.service.ts similarity index 95% rename from src/modules/core/uom.service.ts rename to src/modules/core/services/uom.service.ts index 93c0ace..160c38d 100644 --- a/src/modules/core/uom.service.ts +++ b/src/modules/core/services/uom.service.ts @@ -1,9 +1,9 @@ import { Repository } from 'typeorm'; -import { AppDataSource } from '../../config/typeorm.js'; -import { Uom, UomType } from './entities/uom.entity.js'; -import { UomCategory } from './entities/uom-category.entity.js'; -import { NotFoundError, ConflictError } from '../../shared/errors/index.js'; -import { logger } from '../../shared/utils/logger.js'; +import { AppDataSource } from '../../../config/typeorm.js'; +import { Uom, UomType } from '../entities/uom.entity.js'; +import { UomCategory } from '../entities/uom-category.entity.js'; +import { NotFoundError, ConflictError } from '../../../shared/errors/index.js'; +import { logger } from '../../../shared/utils/logger.js'; export interface CreateUomDto { name: string; diff --git a/src/modules/crm/crm.controller.ts b/src/modules/crm/crm.controller.ts index d69bce6..ecead65 100644 --- a/src/modules/crm/crm.controller.ts +++ b/src/modules/crm/crm.controller.ts @@ -1,8 +1,8 @@ import { Response, NextFunction } from 'express'; import { z } from 'zod'; -import { leadsService, CreateLeadDto, UpdateLeadDto, LeadFilters } from './leads.service.js'; -import { opportunitiesService, CreateOpportunityDto, UpdateOpportunityDto, OpportunityFilters } from './opportunities.service.js'; -import { stagesService, CreateLeadStageDto, UpdateLeadStageDto, CreateOpportunityStageDto, UpdateOpportunityStageDto, CreateLostReasonDto, UpdateLostReasonDto } from './stages.service.js'; +import { leadsService, CreateLeadDto, UpdateLeadDto, LeadFilters } from './services/leads.service.js'; +import { opportunitiesService, CreateOpportunityDto, UpdateOpportunityDto, OpportunityFilters } from './services/opportunities.service.js'; +import { stagesService, CreateLeadStageDto, UpdateLeadStageDto, CreateOpportunityStageDto, UpdateOpportunityStageDto, CreateLostReasonDto, UpdateLostReasonDto } from './services/stages.service.js'; import { AuthenticatedRequest } from '../../shared/middleware/auth.middleware.js'; import { ValidationError } from '../../shared/errors/index.js'; diff --git a/src/modules/crm/index.ts b/src/modules/crm/index.ts index 1120038..bcf91b1 100644 --- a/src/modules/crm/index.ts +++ b/src/modules/crm/index.ts @@ -1,7 +1,7 @@ -export * from './leads.service.js'; -export * from './opportunities.service.js'; -export * from './stages.service.js'; -export * from './activities.service.js'; -export * from './forecasting.service.js'; +export * from './services/leads.service.js'; +export * from './services/opportunities.service.js'; +export * from './services/stages.service.js'; +export * from './services/activities.service.js'; +export * from './services/forecasting.service.js'; export * from './crm.controller.js'; export { default as crmRoutes } from './crm.routes.js'; diff --git a/src/modules/crm/activities.service.ts b/src/modules/crm/services/activities.service.ts similarity index 98% rename from src/modules/crm/activities.service.ts rename to src/modules/crm/services/activities.service.ts index 9798e9b..08d054d 100644 --- a/src/modules/crm/activities.service.ts +++ b/src/modules/crm/services/activities.service.ts @@ -1,6 +1,6 @@ -import { query, queryOne, getClient } from '../../config/database.js'; -import { NotFoundError, ValidationError } from '../../shared/errors/index.js'; -import { logger } from '../../shared/utils/logger.js'; +import { query, queryOne, getClient } from '../../../config/database.js'; +import { NotFoundError, ValidationError } from '../../../shared/errors/index.js'; +import { logger } from '../../../shared/utils/logger.js'; // ============================================================================ // TYPES diff --git a/src/modules/crm/forecasting.service.ts b/src/modules/crm/services/forecasting.service.ts similarity index 99% rename from src/modules/crm/forecasting.service.ts rename to src/modules/crm/services/forecasting.service.ts index bcfaeca..3e5ce33 100644 --- a/src/modules/crm/forecasting.service.ts +++ b/src/modules/crm/services/forecasting.service.ts @@ -1,5 +1,5 @@ -import { query, queryOne } from '../../config/database.js'; -import { logger } from '../../shared/utils/logger.js'; +import { query, queryOne } from '../../../config/database.js'; +import { logger } from '../../../shared/utils/logger.js'; // ============================================================================ // TYPES diff --git a/src/modules/crm/leads.service.ts b/src/modules/crm/services/leads.service.ts similarity index 99% rename from src/modules/crm/leads.service.ts rename to src/modules/crm/services/leads.service.ts index 4dfeadc..b209498 100644 --- a/src/modules/crm/leads.service.ts +++ b/src/modules/crm/services/leads.service.ts @@ -1,5 +1,5 @@ -import { query, queryOne, getClient } from '../../config/database.js'; -import { NotFoundError, ValidationError, ConflictError } from '../../shared/errors/index.js'; +import { query, queryOne, getClient } from '../../../config/database.js'; +import { NotFoundError, ValidationError, ConflictError } from '../../../shared/errors/index.js'; export type LeadStatus = 'new' | 'contacted' | 'qualified' | 'converted' | 'lost'; export type LeadSource = 'website' | 'phone' | 'email' | 'referral' | 'social_media' | 'advertising' | 'event' | 'other'; diff --git a/src/modules/crm/opportunities.service.ts b/src/modules/crm/services/opportunities.service.ts similarity index 99% rename from src/modules/crm/opportunities.service.ts rename to src/modules/crm/services/opportunities.service.ts index 7d051a7..c7fbe12 100644 --- a/src/modules/crm/opportunities.service.ts +++ b/src/modules/crm/services/opportunities.service.ts @@ -1,5 +1,5 @@ -import { query, queryOne, getClient } from '../../config/database.js'; -import { NotFoundError, ValidationError } from '../../shared/errors/index.js'; +import { query, queryOne, getClient } from '../../../config/database.js'; +import { NotFoundError, ValidationError } from '../../../shared/errors/index.js'; import { LeadSource } from './leads.service.js'; export type OpportunityStatus = 'open' | 'won' | 'lost'; diff --git a/src/modules/crm/stages.service.ts b/src/modules/crm/services/stages.service.ts similarity index 98% rename from src/modules/crm/stages.service.ts rename to src/modules/crm/services/stages.service.ts index 92f01f9..ce7689b 100644 --- a/src/modules/crm/stages.service.ts +++ b/src/modules/crm/services/stages.service.ts @@ -1,5 +1,5 @@ -import { query, queryOne } from '../../config/database.js'; -import { NotFoundError, ConflictError } from '../../shared/errors/index.js'; +import { query, queryOne } from '../../../config/database.js'; +import { NotFoundError, ConflictError } from '../../../shared/errors/index.js'; // ========== LEAD STAGES ========== diff --git a/src/modules/financial/__tests__/accounts.service.spec.ts b/src/modules/financial/__tests__/accounts.service.spec.ts index a1633aa..b7b1aa9 100644 --- a/src/modules/financial/__tests__/accounts.service.spec.ts +++ b/src/modules/financial/__tests__/accounts.service.spec.ts @@ -108,7 +108,7 @@ describe('AccountsService', () => { describe('AccountTypes Operations', () => { it('should return all account types', async () => { // Import dynamically to get fresh instance with mocks - const { accountsService } = await import('../accounts.service.js'); + const { accountsService } = await import('../services/accounts.service.js'); const result = await accountsService.findAllAccountTypes(); @@ -119,7 +119,7 @@ describe('AccountsService', () => { }); it('should return account type by ID', async () => { - const { accountsService } = await import('../accounts.service.js'); + const { accountsService } = await import('../services/accounts.service.js'); const result = await accountsService.findAccountTypeById(mockAccountTypeId); @@ -132,7 +132,7 @@ describe('AccountsService', () => { it('should throw NotFoundError when account type not found', async () => { mockAccountTypeRepository.findOne = jest.fn().mockResolvedValue(null); - const { accountsService } = await import('../accounts.service.js'); + const { accountsService } = await import('../services/accounts.service.js'); await expect( accountsService.findAccountTypeById('non-existent-id') @@ -142,7 +142,7 @@ describe('AccountsService', () => { describe('Account CRUD Operations', () => { it('should find all accounts with filters', async () => { - const { accountsService } = await import('../accounts.service.js'); + const { accountsService } = await import('../services/accounts.service.js'); const result = await accountsService.findAll(mockTenantId, { companyId: mockCompanyId, @@ -164,7 +164,7 @@ describe('AccountsService', () => { isReconcilable: true, }; - const { accountsService } = await import('../accounts.service.js'); + const { accountsService } = await import('../services/accounts.service.js'); // Service signature: create(dto, tenantId, userId) const result = await accountsService.create(createDto, mockTenantId, 'mock-user-id'); @@ -175,7 +175,7 @@ describe('AccountsService', () => { }); it('should find account by ID', async () => { - const { accountsService } = await import('../accounts.service.js'); + const { accountsService } = await import('../services/accounts.service.js'); // Service signature: findById(id, tenantId) const result = await accountsService.findById( @@ -190,7 +190,7 @@ describe('AccountsService', () => { it('should throw NotFoundError when account not found', async () => { mockQueryBuilder.getOne = jest.fn().mockResolvedValue(null); - const { accountsService } = await import('../accounts.service.js'); + const { accountsService } = await import('../services/accounts.service.js'); await expect( accountsService.findById('non-existent-id', mockTenantId) @@ -202,7 +202,7 @@ describe('AccountsService', () => { name: 'Updated Bank Account', }; - const { accountsService } = await import('../accounts.service.js'); + const { accountsService } = await import('../services/accounts.service.js'); // Service signature: update(id, dto, tenantId, userId) const result = await accountsService.update( @@ -217,7 +217,7 @@ describe('AccountsService', () => { }); it('should soft delete an account', async () => { - const { accountsService } = await import('../accounts.service.js'); + const { accountsService } = await import('../services/accounts.service.js'); // Service signature: delete(id, tenantId, userId) await accountsService.delete(mockAccount.id as string, mockTenantId, 'mock-user-id'); @@ -241,7 +241,7 @@ describe('AccountsService', () => { accountTypeId: mockAccountTypeId, }; - const { accountsService } = await import('../accounts.service.js'); + const { accountsService } = await import('../services/accounts.service.js'); // This should handle duplicate validation // Exact behavior depends on service implementation @@ -258,7 +258,7 @@ describe('AccountsService', () => { // // mockAccountRepository.find = jest.fn().mockResolvedValue([mockAccount]); // - // const { accountsService } = await import('../accounts.service.js'); + // const { accountsService } = await import('../services/accounts.service.js'); // // const result = await accountsService.getChartOfAccounts( // mockTenantId, diff --git a/src/modules/financial/__tests__/payments.service.spec.ts b/src/modules/financial/__tests__/payments.service.spec.ts index df68f2d..25b7e5f 100644 --- a/src/modules/financial/__tests__/payments.service.spec.ts +++ b/src/modules/financial/__tests__/payments.service.spec.ts @@ -1,7 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { Repository } from 'typeorm'; import { getRepositoryToken } from '@nestjs/typeorm'; -import { PaymentsService } from '../payments.service'; +import { PaymentsService } from '../services/payments.service'; import { Payment, PaymentMethod, PaymentStatus } from '../entities'; import { CreatePaymentDto, UpdatePaymentDto } from '../dto'; diff --git a/src/modules/financial/financial.controller.ts b/src/modules/financial/financial.controller.ts index f9b06d7..0a0e8be 100644 --- a/src/modules/financial/financial.controller.ts +++ b/src/modules/financial/financial.controller.ts @@ -1,11 +1,11 @@ import { Response, NextFunction } from 'express'; import { z } from 'zod'; -import { accountsService, CreateAccountDto, UpdateAccountDto, AccountFilters } from './accounts.service.js'; -import { journalsService, CreateJournalDto, UpdateJournalDto, JournalFilters } from './journals.service.js'; -import { journalEntriesService, CreateJournalEntryDto, UpdateJournalEntryDto, JournalEntryFilters } from './journal-entries.service.js'; -import { invoicesService, CreateInvoiceDto, UpdateInvoiceDto, CreateInvoiceLineDto, UpdateInvoiceLineDto, InvoiceFilters } from './invoices.service.js'; -import { paymentsService, CreatePaymentDto, UpdatePaymentDto, ReconcileDto, PaymentFilters } from './payments.service.js'; -import { taxesService, CreateTaxDto, UpdateTaxDto, TaxFilters } from './taxes.service.js'; +import { accountsService, CreateAccountDto, UpdateAccountDto, AccountFilters } from './services/accounts.service.js'; +import { journalsService, CreateJournalDto, UpdateJournalDto, JournalFilters } from './services/journals.service.js'; +import { journalEntriesService, CreateJournalEntryDto, UpdateJournalEntryDto, JournalEntryFilters } from './services/journal-entries.service.js'; +import { invoicesService, CreateInvoiceDto, UpdateInvoiceDto, CreateInvoiceLineDto, UpdateInvoiceLineDto, InvoiceFilters } from './services/invoices.service.js'; +import { paymentsService, CreatePaymentDto, UpdatePaymentDto, ReconcileDto, PaymentFilters } from './services/payments.service.js'; +import { taxesService, CreateTaxDto, UpdateTaxDto, TaxFilters } from './services/taxes.service.js'; import { AuthenticatedRequest } from '../../shared/middleware/auth.middleware.js'; import { ValidationError } from '../../shared/errors/index.js'; diff --git a/src/modules/financial/index.ts b/src/modules/financial/index.ts index ddef377..2478e15 100644 --- a/src/modules/financial/index.ts +++ b/src/modules/financial/index.ts @@ -1,9 +1,9 @@ -export * from './accounts.service.js'; -export * from './journals.service.js'; -export * from './journal-entries.service.js'; -export * from './invoices.service.js'; -export * from './payments.service.js'; -export * from './taxes.service.js'; -export * from './gl-posting.service.js'; +export * from './services/accounts.service.js'; +export * from './services/journals.service.js'; +export * from './services/journal-entries.service.js'; +export * from './services/invoices.service.js'; +export * from './services/payments.service.js'; +export * from './services/taxes.service.js'; +export * from './services/gl-posting.service.js'; export * from './financial.controller.js'; export { default as financialRoutes } from './financial.routes.js'; diff --git a/src/modules/financial/accounts.service.ts b/src/modules/financial/services/accounts.service.ts similarity index 98% rename from src/modules/financial/accounts.service.ts rename to src/modules/financial/services/accounts.service.ts index 612c295..c01dc8f 100644 --- a/src/modules/financial/accounts.service.ts +++ b/src/modules/financial/services/accounts.service.ts @@ -1,8 +1,8 @@ import { Repository, IsNull } from 'typeorm'; -import { AppDataSource } from '../../config/typeorm.js'; -import { Account, AccountType } from './entities/index.js'; -import { NotFoundError, ValidationError, ForbiddenError } from '../../shared/types/index.js'; -import { logger } from '../../shared/utils/logger.js'; +import { AppDataSource } from '../../../config/typeorm.js'; +import { Account, AccountType } from '../entities/index.js'; +import { NotFoundError, ValidationError, ForbiddenError } from '../../../shared/types/index.js'; +import { logger } from '../../../shared/utils/logger.js'; // ===== Interfaces ===== diff --git a/src/modules/financial/fiscalPeriods.service.ts b/src/modules/financial/services/fiscalPeriods.service.ts similarity index 98% rename from src/modules/financial/fiscalPeriods.service.ts rename to src/modules/financial/services/fiscalPeriods.service.ts index f286cba..174305c 100644 --- a/src/modules/financial/fiscalPeriods.service.ts +++ b/src/modules/financial/services/fiscalPeriods.service.ts @@ -1,6 +1,6 @@ -import { query, queryOne } from '../../config/database.js'; -import { NotFoundError, ValidationError, ConflictError } from '../../shared/errors/index.js'; -import { logger } from '../../shared/utils/logger.js'; +import { query, queryOne } from '../../../config/database.js'; +import { NotFoundError, ValidationError, ConflictError } from '../../../shared/errors/index.js'; +import { logger } from '../../../shared/utils/logger.js'; // ============================================================================ // TYPES diff --git a/src/modules/financial/gl-posting.service.ts b/src/modules/financial/services/gl-posting.service.ts similarity index 98% rename from src/modules/financial/gl-posting.service.ts rename to src/modules/financial/services/gl-posting.service.ts index cab3171..42e87a9 100644 --- a/src/modules/financial/gl-posting.service.ts +++ b/src/modules/financial/services/gl-posting.service.ts @@ -1,8 +1,8 @@ -import { query, queryOne, getClient, PoolClient } from '../../config/database.js'; -import { NotFoundError, ValidationError } from '../../shared/errors/index.js'; -import { AccountMappingType } from './entities/account-mapping.entity.js'; -import { sequencesService, SEQUENCE_CODES } from '../core/sequences.service.js'; -import { logger } from '../../shared/utils/logger.js'; +import { query, queryOne, getClient, PoolClient } from '../../../config/database.js'; +import { NotFoundError, ValidationError } from '../../../shared/errors/index.js'; +import { AccountMappingType } from '../entities/account-mapping.entity.js'; +import { sequencesService, SEQUENCE_CODES } from '../../core/services/sequences.service.js'; +import { logger } from '../../../shared/utils/logger.js'; // ============================================================================ // TYPES diff --git a/src/modules/financial/invoices.service.ts b/src/modules/financial/services/invoices.service.ts similarity index 98% rename from src/modules/financial/invoices.service.ts rename to src/modules/financial/services/invoices.service.ts index f1f2351..92f285d 100644 --- a/src/modules/financial/invoices.service.ts +++ b/src/modules/financial/services/invoices.service.ts @@ -1,9 +1,9 @@ -import { query, queryOne, getClient } from '../../config/database.js'; -import { NotFoundError, ValidationError } from '../../shared/errors/index.js'; +import { query, queryOne, getClient } from '../../../config/database.js'; +import { NotFoundError, ValidationError } from '../../../shared/errors/index.js'; import { taxesService } from './taxes.service.js'; import { glPostingService, InvoiceForPosting } from './gl-posting.service.js'; -import { sequencesService, SEQUENCE_CODES } from '../core/sequences.service.js'; -import { logger } from '../../shared/utils/logger.js'; +import { sequencesService, SEQUENCE_CODES } from '../../core/services/sequences.service.js'; +import { logger } from '../../../shared/utils/logger.js'; export interface InvoiceLine { id: string; diff --git a/src/modules/financial/journal-entries.service.ts b/src/modules/financial/services/journal-entries.service.ts similarity index 99% rename from src/modules/financial/journal-entries.service.ts rename to src/modules/financial/services/journal-entries.service.ts index 1469e05..695def9 100644 --- a/src/modules/financial/journal-entries.service.ts +++ b/src/modules/financial/services/journal-entries.service.ts @@ -1,5 +1,5 @@ -import { query, queryOne, getClient } from '../../config/database.js'; -import { NotFoundError, ConflictError, ValidationError } from '../../shared/errors/index.js'; +import { query, queryOne, getClient } from '../../../config/database.js'; +import { NotFoundError, ConflictError, ValidationError } from '../../../shared/errors/index.js'; export type EntryStatus = 'draft' | 'posted' | 'cancelled'; diff --git a/src/modules/financial/journals.service.ts b/src/modules/financial/services/journals.service.ts similarity index 97% rename from src/modules/financial/journals.service.ts rename to src/modules/financial/services/journals.service.ts index 8061b68..1a1b619 100644 --- a/src/modules/financial/journals.service.ts +++ b/src/modules/financial/services/journals.service.ts @@ -1,5 +1,5 @@ -import { query, queryOne } from '../../config/database.js'; -import { NotFoundError, ConflictError } from '../../shared/errors/index.js'; +import { query, queryOne } from '../../../config/database.js'; +import { NotFoundError, ConflictError } from '../../../shared/errors/index.js'; export type JournalType = 'sale' | 'purchase' | 'cash' | 'bank' | 'general'; diff --git a/src/modules/financial/payments.service.ts b/src/modules/financial/services/payments.service.ts similarity index 98% rename from src/modules/financial/payments.service.ts rename to src/modules/financial/services/payments.service.ts index 531103c..f66cd16 100644 --- a/src/modules/financial/payments.service.ts +++ b/src/modules/financial/services/payments.service.ts @@ -1,5 +1,5 @@ -import { query, queryOne, getClient } from '../../config/database.js'; -import { NotFoundError, ValidationError } from '../../shared/errors/index.js'; +import { query, queryOne, getClient } from '../../../config/database.js'; +import { NotFoundError, ValidationError } from '../../../shared/errors/index.js'; export interface PaymentInvoice { invoice_id: string; diff --git a/src/modules/financial/taxes.service.ts b/src/modules/financial/services/taxes.service.ts similarity index 98% rename from src/modules/financial/taxes.service.ts rename to src/modules/financial/services/taxes.service.ts index d856ca3..bd02057 100644 --- a/src/modules/financial/taxes.service.ts +++ b/src/modules/financial/services/taxes.service.ts @@ -1,5 +1,5 @@ -import { query, queryOne } from '../../config/database.js'; -import { NotFoundError, ConflictError } from '../../shared/errors/index.js'; +import { query, queryOne } from '../../../config/database.js'; +import { NotFoundError, ConflictError } from '../../../shared/errors/index.js'; export interface Tax { id: string; diff --git a/src/modules/fiscal/fiscal.controller.ts b/src/modules/fiscal/fiscal.controller.ts index a03d6ff..1288b7c 100644 --- a/src/modules/fiscal/fiscal.controller.ts +++ b/src/modules/fiscal/fiscal.controller.ts @@ -6,7 +6,7 @@ import { paymentMethodsService, paymentTypesService, withholdingTypesService, -} from './fiscal-catalogs.service.js'; +} from './services/fiscal-catalogs.service.js'; import { PersonType } from './entities/fiscal-regime.entity.js'; import { AuthenticatedRequest } from '../../shared/middleware/auth.middleware.js'; diff --git a/src/modules/fiscal/index.ts b/src/modules/fiscal/index.ts index ef65dc9..b8c7d96 100644 --- a/src/modules/fiscal/index.ts +++ b/src/modules/fiscal/index.ts @@ -1,4 +1,4 @@ export * from './entities/index.js'; -export * from './fiscal-catalogs.service.js'; +export * from './services/fiscal-catalogs.service.js'; export { fiscalController } from './fiscal.controller.js'; export { default as fiscalRoutes } from './fiscal.routes.js'; diff --git a/src/modules/fiscal/fiscal-catalogs.service.ts b/src/modules/fiscal/services/fiscal-catalogs.service.ts similarity index 98% rename from src/modules/fiscal/fiscal-catalogs.service.ts rename to src/modules/fiscal/services/fiscal-catalogs.service.ts index cc37e4a..3edd3ba 100644 --- a/src/modules/fiscal/fiscal-catalogs.service.ts +++ b/src/modules/fiscal/services/fiscal-catalogs.service.ts @@ -1,5 +1,5 @@ import { Repository } from 'typeorm'; -import { AppDataSource } from '../../config/typeorm.js'; +import { AppDataSource } from '../../../config/typeorm.js'; import { TaxCategory, FiscalRegime, @@ -8,9 +8,9 @@ import { PaymentType, WithholdingType, PersonType, -} from './entities/index.js'; -import { NotFoundError } from '../../shared/errors/index.js'; -import { logger } from '../../shared/utils/logger.js'; +} from '../entities/index.js'; +import { NotFoundError } from '../../../shared/errors/index.js'; +import { logger } from '../../../shared/utils/logger.js'; // ========================================== // TAX CATEGORIES SERVICE diff --git a/src/modules/health/health.controller.ts b/src/modules/health/health.controller.ts index 6c4dad4..bb9d3f7 100644 --- a/src/modules/health/health.controller.ts +++ b/src/modules/health/health.controller.ts @@ -1,5 +1,5 @@ import { Router, Request, Response } from 'express'; -import { HealthService } from './health.service'; +import { HealthService } from './services/health.service'; export class HealthController { public router: Router; diff --git a/src/modules/health/health.module.ts b/src/modules/health/health.module.ts index 7bb0228..edb75f4 100644 --- a/src/modules/health/health.module.ts +++ b/src/modules/health/health.module.ts @@ -1,6 +1,6 @@ import { Router } from 'express'; import { DataSource } from 'typeorm'; -import { HealthService } from './health.service'; +import { HealthService } from './services/health.service'; import { HealthController } from './health.controller'; export interface HealthModuleOptions { @@ -30,5 +30,5 @@ export class HealthModule { } } -export { HealthService } from './health.service'; +export { HealthService } from './services/health.service'; export { HealthController } from './health.controller'; diff --git a/src/modules/health/index.ts b/src/modules/health/index.ts index 958cab2..b7d1db6 100644 --- a/src/modules/health/index.ts +++ b/src/modules/health/index.ts @@ -1,3 +1,3 @@ export { HealthModule, HealthModuleOptions } from './health.module'; -export { HealthService, HealthStatus, HealthCheck } from './health.service'; +export { HealthService, HealthStatus, HealthCheck } from './services/health.service'; export { HealthController } from './health.controller'; diff --git a/src/modules/health/health.service.ts b/src/modules/health/services/health.service.ts similarity index 100% rename from src/modules/health/health.service.ts rename to src/modules/health/services/health.service.ts diff --git a/src/modules/hr/hr.controller.ts b/src/modules/hr/hr.controller.ts index 382c30d..f3def8b 100644 --- a/src/modules/hr/hr.controller.ts +++ b/src/modules/hr/hr.controller.ts @@ -1,9 +1,9 @@ import { Response, NextFunction } from 'express'; import { z } from 'zod'; -import { employeesService, CreateEmployeeDto, UpdateEmployeeDto, EmployeeFilters } from './employees.service.js'; -import { departmentsService, CreateDepartmentDto, UpdateDepartmentDto, DepartmentFilters, CreateJobPositionDto, UpdateJobPositionDto } from './departments.service.js'; -import { contractsService, CreateContractDto, UpdateContractDto, ContractFilters } from './contracts.service.js'; -import { leavesService, CreateLeaveDto, UpdateLeaveDto, LeaveFilters, CreateLeaveTypeDto, UpdateLeaveTypeDto } from './leaves.service.js'; +import { employeesService, CreateEmployeeDto, UpdateEmployeeDto, EmployeeFilters } from './services/employees.service.js'; +import { departmentsService, CreateDepartmentDto, UpdateDepartmentDto, DepartmentFilters, CreateJobPositionDto, UpdateJobPositionDto } from './services/departments.service.js'; +import { contractsService, CreateContractDto, UpdateContractDto, ContractFilters } from './services/contracts.service.js'; +import { leavesService, CreateLeaveDto, UpdateLeaveDto, LeaveFilters, CreateLeaveTypeDto, UpdateLeaveTypeDto } from './services/leaves.service.js'; import { AuthenticatedRequest } from '../../shared/middleware/auth.middleware.js'; import { ValidationError } from '../../shared/errors/index.js'; diff --git a/src/modules/hr/index.ts b/src/modules/hr/index.ts index 1a5223b..4d5b2f7 100644 --- a/src/modules/hr/index.ts +++ b/src/modules/hr/index.ts @@ -1,6 +1,3 @@ -export * from './employees.service.js'; -export * from './departments.service.js'; -export * from './contracts.service.js'; -export * from './leaves.service.js'; +export * from './services/index.js'; export * from './hr.controller.js'; export { default as hrRoutes } from './hr.routes.js'; diff --git a/src/modules/hr/contracts.service.ts b/src/modules/hr/services/contracts.service.ts similarity index 98% rename from src/modules/hr/contracts.service.ts rename to src/modules/hr/services/contracts.service.ts index 1ea40b5..a308596 100644 --- a/src/modules/hr/contracts.service.ts +++ b/src/modules/hr/services/contracts.service.ts @@ -1,5 +1,5 @@ -import { query, queryOne } from '../../config/database.js'; -import { NotFoundError, ValidationError } from '../../shared/errors/index.js'; +import { query, queryOne } from '../../../config/database.js'; +import { NotFoundError, ValidationError } from '../../../shared/errors/index.js'; export type ContractStatus = 'draft' | 'active' | 'expired' | 'terminated' | 'cancelled'; export type ContractType = 'permanent' | 'temporary' | 'contractor' | 'internship' | 'part_time'; diff --git a/src/modules/hr/departments.service.ts b/src/modules/hr/services/departments.service.ts similarity index 98% rename from src/modules/hr/departments.service.ts rename to src/modules/hr/services/departments.service.ts index 5d676e8..770906c 100644 --- a/src/modules/hr/departments.service.ts +++ b/src/modules/hr/services/departments.service.ts @@ -1,5 +1,5 @@ -import { query, queryOne } from '../../config/database.js'; -import { NotFoundError, ConflictError } from '../../shared/errors/index.js'; +import { query, queryOne } from '../../../config/database.js'; +import { NotFoundError, ConflictError } from '../../../shared/errors/index.js'; export interface Department { id: string; diff --git a/src/modules/hr/employees.service.ts b/src/modules/hr/services/employees.service.ts similarity index 99% rename from src/modules/hr/employees.service.ts rename to src/modules/hr/services/employees.service.ts index 7138b94..595156e 100644 --- a/src/modules/hr/employees.service.ts +++ b/src/modules/hr/services/employees.service.ts @@ -1,5 +1,5 @@ -import { query, queryOne } from '../../config/database.js'; -import { NotFoundError, ConflictError, ValidationError } from '../../shared/errors/index.js'; +import { query, queryOne } from '../../../config/database.js'; +import { NotFoundError, ConflictError, ValidationError } from '../../../shared/errors/index.js'; export type EmployeeStatus = 'active' | 'inactive' | 'on_leave' | 'terminated'; diff --git a/src/modules/hr/services/index.ts b/src/modules/hr/services/index.ts new file mode 100644 index 0000000..89739eb --- /dev/null +++ b/src/modules/hr/services/index.ts @@ -0,0 +1,4 @@ +export * from './employees.service.js'; +export * from './departments.service.js'; +export * from './contracts.service.js'; +export * from './leaves.service.js'; diff --git a/src/modules/hr/leaves.service.ts b/src/modules/hr/services/leaves.service.ts similarity index 99% rename from src/modules/hr/leaves.service.ts rename to src/modules/hr/services/leaves.service.ts index 957dd24..765bc27 100644 --- a/src/modules/hr/leaves.service.ts +++ b/src/modules/hr/services/leaves.service.ts @@ -1,5 +1,5 @@ -import { query, queryOne } from '../../config/database.js'; -import { NotFoundError, ValidationError, ConflictError } from '../../shared/errors/index.js'; +import { query, queryOne } from '../../../config/database.js'; +import { NotFoundError, ValidationError, ConflictError } from '../../../shared/errors/index.js'; export type LeaveStatus = 'draft' | 'submitted' | 'approved' | 'rejected' | 'cancelled'; export type LeaveType = 'vacation' | 'sick' | 'personal' | 'maternity' | 'paternity' | 'bereavement' | 'unpaid' | 'other'; diff --git a/src/modules/inventory/inventory.controller.ts b/src/modules/inventory/inventory.controller.ts index 96d3223..d058644 100644 --- a/src/modules/inventory/inventory.controller.ts +++ b/src/modules/inventory/inventory.controller.ts @@ -1,11 +1,11 @@ import { Response, NextFunction } from 'express'; import { z } from 'zod'; -import { productsService, CreateProductDto, UpdateProductDto, ProductFilters } from './products.service.js'; -import { warehousesService, CreateWarehouseDto, UpdateWarehouseDto, WarehouseFilters } from './warehouses.service.js'; -import { locationsService, CreateLocationDto, UpdateLocationDto, LocationFilters } from './locations.service.js'; -import { pickingsService, CreatePickingDto, PickingFilters } from './pickings.service.js'; -import { lotsService, CreateLotDto, UpdateLotDto, LotFilters } from './lots.service.js'; -import { adjustmentsService, CreateAdjustmentDto, UpdateAdjustmentDto, CreateAdjustmentLineDto, UpdateAdjustmentLineDto, AdjustmentFilters } from './adjustments.service.js'; +import { productsService, CreateProductDto, UpdateProductDto, ProductFilters } from './services/products.service.js'; +import { warehousesService, CreateWarehouseDto, UpdateWarehouseDto, WarehouseFilters } from './services/warehouses.service.js'; +import { locationsService, CreateLocationDto, UpdateLocationDto, LocationFilters } from './services/locations.service.js'; +import { pickingsService, CreatePickingDto, PickingFilters } from './services/pickings.service.js'; +import { lotsService, CreateLotDto, UpdateLotDto, LotFilters } from './services/lots.service.js'; +import { adjustmentsService, CreateAdjustmentDto, UpdateAdjustmentDto, CreateAdjustmentLineDto, UpdateAdjustmentLineDto, AdjustmentFilters } from './services/adjustments.service.js'; import { AuthenticatedRequest } from '../../shared/middleware/auth.middleware.js'; import { ValidationError } from '../../shared/errors/index.js'; diff --git a/src/modules/inventory/adjustments.service.ts b/src/modules/inventory/services/adjustments.service.ts similarity index 99% rename from src/modules/inventory/adjustments.service.ts rename to src/modules/inventory/services/adjustments.service.ts index 967450f..9b0ac1f 100644 --- a/src/modules/inventory/adjustments.service.ts +++ b/src/modules/inventory/services/adjustments.service.ts @@ -1,7 +1,7 @@ -import { query, queryOne, getClient } from '../../config/database.js'; -import { NotFoundError, ValidationError, ConflictError } from '../../shared/errors/index.js'; +import { query, queryOne, getClient } from '../../../config/database.js'; +import { NotFoundError, ValidationError, ConflictError } from '../../../shared/errors/index.js'; import { valuationService } from './valuation.service.js'; -import { logger } from '../../shared/utils/logger.js'; +import { logger } from '../../../shared/utils/logger.js'; export type AdjustmentStatus = 'draft' | 'confirmed' | 'done' | 'cancelled'; diff --git a/src/modules/inventory/services/index.ts b/src/modules/inventory/services/index.ts index 30d2f49..449580c 100644 --- a/src/modules/inventory/services/index.ts +++ b/src/modules/inventory/services/index.ts @@ -2,7 +2,25 @@ export { InventoryService, StockSearchParams, MovementSearchParams, -} from './inventory.service'; +} from './inventory.service.js'; + +// Products service +export * from './products.service.js'; + +// Warehouses service +export * from './warehouses.service.js'; + +// Locations service +export * from './locations.service.js'; + +// Lots service +export * from './lots.service.js'; + +// Pickings service +export * from './pickings.service.js'; + +// Adjustments service +export * from './adjustments.service.js'; // Stock reservation service for sales orders and transfers export { @@ -11,7 +29,7 @@ export { ReservationResult, ReservationLineResult, StockAvailability, -} from '../stock-reservation.service.js'; +} from './stock-reservation.service.js'; // Valuation service for FIFO/Average costing export { @@ -22,7 +40,7 @@ export { ValuationSummary, FifoConsumptionResult, ProductCostResult, -} from '../valuation.service.js'; +} from './valuation.service.js'; // Reorder alerts service for stock level monitoring export { @@ -32,4 +50,4 @@ export { StockSummary, ReorderAlertFilters, StockLevelFilters, -} from '../reorder-alerts.service.js'; +} from './reorder-alerts.service.js'; diff --git a/src/modules/inventory/locations.service.ts b/src/modules/inventory/services/locations.service.ts similarity index 97% rename from src/modules/inventory/locations.service.ts rename to src/modules/inventory/services/locations.service.ts index c55aba4..38aab6d 100644 --- a/src/modules/inventory/locations.service.ts +++ b/src/modules/inventory/services/locations.service.ts @@ -1,5 +1,5 @@ -import { query, queryOne } from '../../config/database.js'; -import { NotFoundError, ConflictError } from '../../shared/errors/index.js'; +import { query, queryOne } from '../../../config/database.js'; +import { NotFoundError, ConflictError } from '../../../shared/errors/index.js'; export type LocationType = 'internal' | 'supplier' | 'customer' | 'inventory' | 'production' | 'transit'; diff --git a/src/modules/inventory/lots.service.ts b/src/modules/inventory/services/lots.service.ts similarity index 98% rename from src/modules/inventory/lots.service.ts rename to src/modules/inventory/services/lots.service.ts index 2a9d5e8..701dbfc 100644 --- a/src/modules/inventory/lots.service.ts +++ b/src/modules/inventory/services/lots.service.ts @@ -1,5 +1,5 @@ -import { query, queryOne } from '../../config/database.js'; -import { NotFoundError, ConflictError } from '../../shared/errors/index.js'; +import { query, queryOne } from '../../../config/database.js'; +import { NotFoundError, ConflictError } from '../../../shared/errors/index.js'; export interface Lot { id: string; diff --git a/src/modules/inventory/pickings.service.ts b/src/modules/inventory/services/pickings.service.ts similarity index 99% rename from src/modules/inventory/pickings.service.ts rename to src/modules/inventory/services/pickings.service.ts index 27d6678..9b000b8 100644 --- a/src/modules/inventory/pickings.service.ts +++ b/src/modules/inventory/services/pickings.service.ts @@ -1,8 +1,8 @@ -import { query, queryOne, getClient } from '../../config/database.js'; -import { NotFoundError, ConflictError, ValidationError } from '../../shared/errors/index.js'; +import { query, queryOne, getClient } from '../../../config/database.js'; +import { NotFoundError, ConflictError, ValidationError } from '../../../shared/errors/index.js'; import { stockReservationService, ReservationLine } from './stock-reservation.service.js'; import { valuationService } from './valuation.service.js'; -import { logger } from '../../shared/utils/logger.js'; +import { logger } from '../../../shared/utils/logger.js'; export type PickingType = 'incoming' | 'outgoing' | 'internal'; export type MoveStatus = 'draft' | 'waiting' | 'confirmed' | 'assigned' | 'done' | 'cancelled'; diff --git a/src/modules/inventory/products.service.ts b/src/modules/inventory/services/products.service.ts similarity index 97% rename from src/modules/inventory/products.service.ts rename to src/modules/inventory/services/products.service.ts index 29334c3..c73484b 100644 --- a/src/modules/inventory/products.service.ts +++ b/src/modules/inventory/services/products.service.ts @@ -1,9 +1,9 @@ import { Repository, IsNull, ILike } from 'typeorm'; -import { AppDataSource } from '../../config/typeorm.js'; -import { Product, ProductType, TrackingType, ValuationMethod } from './entities/product.entity.js'; -import { StockQuant } from './entities/stock-quant.entity.js'; -import { NotFoundError, ValidationError, ConflictError } from '../../shared/types/index.js'; -import { logger } from '../../shared/utils/logger.js'; +import { AppDataSource } from '../../../config/typeorm.js'; +import { Product, ProductType, TrackingType, ValuationMethod } from '../entities/product.entity.js'; +import { StockQuant } from '../entities/stock-quant.entity.js'; +import { NotFoundError, ValidationError, ConflictError } from '../../../shared/types/index.js'; +import { logger } from '../../../shared/utils/logger.js'; // ===== Interfaces ===== diff --git a/src/modules/inventory/reorder-alerts.service.ts b/src/modules/inventory/services/reorder-alerts.service.ts similarity index 99% rename from src/modules/inventory/reorder-alerts.service.ts rename to src/modules/inventory/services/reorder-alerts.service.ts index a206669..ee323a3 100644 --- a/src/modules/inventory/reorder-alerts.service.ts +++ b/src/modules/inventory/services/reorder-alerts.service.ts @@ -1,5 +1,5 @@ -import { query, queryOne } from '../../config/database.js'; -import { logger } from '../../shared/utils/logger.js'; +import { query, queryOne } from '../../../config/database.js'; +import { logger } from '../../../shared/utils/logger.js'; // ============================================================================ // TYPES diff --git a/src/modules/inventory/stock-reservation.service.ts b/src/modules/inventory/services/stock-reservation.service.ts similarity index 98% rename from src/modules/inventory/stock-reservation.service.ts rename to src/modules/inventory/services/stock-reservation.service.ts index 4be2f87..2157e10 100644 --- a/src/modules/inventory/stock-reservation.service.ts +++ b/src/modules/inventory/services/stock-reservation.service.ts @@ -1,6 +1,6 @@ -import { query, queryOne, getClient, PoolClient } from '../../config/database.js'; -import { ValidationError } from '../../shared/errors/index.js'; -import { logger } from '../../shared/utils/logger.js'; +import { query, queryOne, getClient, PoolClient } from '../../../config/database.js'; +import { ValidationError } from '../../../shared/errors/index.js'; +import { logger } from '../../../shared/utils/logger.js'; /** * Stock Reservation Service diff --git a/src/modules/inventory/valuation.service.ts b/src/modules/inventory/services/valuation.service.ts similarity index 98% rename from src/modules/inventory/valuation.service.ts rename to src/modules/inventory/services/valuation.service.ts index a4909a7..8336cc7 100644 --- a/src/modules/inventory/valuation.service.ts +++ b/src/modules/inventory/services/valuation.service.ts @@ -1,6 +1,6 @@ -import { query, queryOne, getClient, PoolClient } from '../../config/database.js'; -import { NotFoundError, ValidationError } from '../../shared/errors/index.js'; -import { logger } from '../../shared/utils/logger.js'; +import { query, queryOne, getClient, PoolClient } from '../../../config/database.js'; +import { NotFoundError, ValidationError } from '../../../shared/errors/index.js'; +import { logger } from '../../../shared/utils/logger.js'; // ============================================================================ // TYPES diff --git a/src/modules/inventory/warehouses.service.ts b/src/modules/inventory/services/warehouses.service.ts similarity index 96% rename from src/modules/inventory/warehouses.service.ts rename to src/modules/inventory/services/warehouses.service.ts index 73e0a2c..3c922b2 100644 --- a/src/modules/inventory/warehouses.service.ts +++ b/src/modules/inventory/services/warehouses.service.ts @@ -1,10 +1,10 @@ import { Repository, IsNull } from 'typeorm'; -import { AppDataSource } from '../../config/typeorm.js'; -import { Warehouse } from '../warehouses/entities/warehouse.entity.js'; -import { Location } from './entities/location.entity.js'; -import { StockQuant } from './entities/stock-quant.entity.js'; -import { NotFoundError, ValidationError, ConflictError } from '../../shared/types/index.js'; -import { logger } from '../../shared/utils/logger.js'; +import { AppDataSource } from '../../../config/typeorm.js'; +import { Warehouse } from '../../warehouses/entities/warehouse.entity.js'; +import { Location } from '../entities/location.entity.js'; +import { StockQuant } from '../entities/stock-quant.entity.js'; +import { NotFoundError, ValidationError, ConflictError } from '../../../shared/types/index.js'; +import { logger } from '../../../shared/utils/logger.js'; // ===== Interfaces ===== diff --git a/src/modules/inventory/valuation.controller.ts b/src/modules/inventory/valuation.controller.ts index b72a96e..0dc6681 100644 --- a/src/modules/inventory/valuation.controller.ts +++ b/src/modules/inventory/valuation.controller.ts @@ -1,6 +1,6 @@ import { Response, NextFunction } from 'express'; import { z } from 'zod'; -import { valuationService, CreateValuationLayerDto } from './valuation.service.js'; +import { valuationService, CreateValuationLayerDto } from './services/valuation.service.js'; import { AuthenticatedRequest, ValidationError, ApiResponse } from '../../shared/types/index.js'; // ============================================================================ diff --git a/src/modules/partners/__tests__/partners.service.spec.ts b/src/modules/partners/__tests__/partners.service.spec.ts index 8c41d76..77508c6 100644 --- a/src/modules/partners/__tests__/partners.service.spec.ts +++ b/src/modules/partners/__tests__/partners.service.spec.ts @@ -1,7 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { Repository } from 'typeorm'; import { getRepositoryToken } from '@nestjs/typeorm'; -import { PartnersService } from '../partners.service'; +import { partnersService } from '../services/partners.service.js'; import { Partner, PartnerStatus, PartnerType } from '../entities'; import { CreatePartnerDto, UpdatePartnerDto } from '../dto'; diff --git a/src/modules/partners/__tests__/partners.service.test.ts b/src/modules/partners/__tests__/partners.service.test.ts index e10f470..363ecec 100644 --- a/src/modules/partners/__tests__/partners.service.test.ts +++ b/src/modules/partners/__tests__/partners.service.test.ts @@ -21,7 +21,7 @@ jest.mock('../../../shared/utils/logger.js', () => ({ })); // Import after mocking -import { partnersService } from '../partners.service.js'; +import { partnersService } from '../services/partners.service.js'; import { NotFoundError, ValidationError } from '../../../shared/types/index.js'; describe('PartnersService', () => { diff --git a/src/modules/partners/partners.controller.ts b/src/modules/partners/partners.controller.ts index 891f150..4813ff3 100644 --- a/src/modules/partners/partners.controller.ts +++ b/src/modules/partners/partners.controller.ts @@ -1,6 +1,7 @@ import { Response, NextFunction } from 'express'; import { z } from 'zod'; -import { partnersService, CreatePartnerDto, UpdatePartnerDto, PartnerFilters, PartnerType } from './partners.service.js'; +import { partnersService, CreatePartnerDto, UpdatePartnerDto, PartnerFilters } from './services/index.js'; +import type { PartnerType } from './services/index.js'; import { ApiResponse, AuthenticatedRequest, ValidationError } from '../../shared/types/index.js'; // Validation schemas (accept both snake_case and camelCase from frontend) diff --git a/src/modules/partners/partners.service.ts b/src/modules/partners/partners.service.ts deleted file mode 100644 index 67b459e..0000000 --- a/src/modules/partners/partners.service.ts +++ /dev/null @@ -1,350 +0,0 @@ -import { Repository, IsNull, Like } from 'typeorm'; -import { AppDataSource } from '../../config/typeorm.js'; -import { Partner, PartnerType } from './entities/index.js'; -import { NotFoundError, ValidationError, ForbiddenError } from '../../shared/types/index.js'; -import { logger } from '../../shared/utils/logger.js'; - -// Re-export PartnerType for controller use -export type { PartnerType }; - -// ===== Interfaces ===== - -export interface CreatePartnerDto { - code: string; - displayName: string; - legalName?: string; - partnerType?: PartnerType; - email?: string; - phone?: string; - mobile?: string; - website?: string; - taxId?: string; - taxRegime?: string; - cfdiUse?: string; - paymentTermDays?: number; - creditLimit?: number; - priceListId?: string; - discountPercent?: number; - category?: string; - tags?: string[]; - notes?: string; - salesRepId?: string; -} - -export interface UpdatePartnerDto { - displayName?: string; - legalName?: string | null; - partnerType?: PartnerType; - email?: string | null; - phone?: string | null; - mobile?: string | null; - website?: string | null; - taxId?: string | null; - taxRegime?: string | null; - cfdiUse?: string | null; - paymentTermDays?: number; - creditLimit?: number; - priceListId?: string | null; - discountPercent?: number; - category?: string | null; - tags?: string[]; - notes?: string | null; - isActive?: boolean; - isVerified?: boolean; - salesRepId?: string | null; -} - -export interface PartnerFilters { - search?: string; - partnerType?: PartnerType; - category?: string; - isActive?: boolean; - isVerified?: boolean; - page?: number; - limit?: number; -} - -export interface PartnerWithRelations extends Partner { - // Add computed fields if needed -} - -// ===== PartnersService Class ===== - -class PartnersService { - private partnerRepository: Repository; - - constructor() { - this.partnerRepository = AppDataSource.getRepository(Partner); - } - - /** - * Get all partners for a tenant with filters and pagination - */ - async findAll( - tenantId: string, - filters: PartnerFilters = {} - ): Promise<{ data: Partner[]; total: number }> { - try { - const { search, partnerType, category, isActive, isVerified, page = 1, limit = 20 } = filters; - const skip = (page - 1) * limit; - - const queryBuilder = this.partnerRepository - .createQueryBuilder('partner') - .where('partner.tenantId = :tenantId', { tenantId }) - .andWhere('partner.deletedAt IS NULL'); - - // Apply search filter - if (search) { - queryBuilder.andWhere( - '(partner.displayName ILIKE :search OR partner.legalName ILIKE :search OR partner.email ILIKE :search OR partner.taxId ILIKE :search OR partner.code ILIKE :search)', - { search: `%${search}%` } - ); - } - - // Filter by partner type - if (partnerType !== undefined) { - queryBuilder.andWhere('partner.partnerType = :partnerType', { partnerType }); - } - - // Filter by category - if (category) { - queryBuilder.andWhere('partner.category = :category', { category }); - } - - // Filter by active status - if (isActive !== undefined) { - queryBuilder.andWhere('partner.isActive = :isActive', { isActive }); - } - - // Filter by verified status - if (isVerified !== undefined) { - queryBuilder.andWhere('partner.isVerified = :isVerified', { isVerified }); - } - - // Get total count - const total = await queryBuilder.getCount(); - - // Get paginated results - const data = await queryBuilder - .orderBy('partner.displayName', 'ASC') - .skip(skip) - .take(limit) - .getMany(); - - logger.debug('Partners retrieved', { tenantId, count: data.length, total }); - - return { data, total }; - } catch (error) { - logger.error('Error retrieving partners', { - error: (error as Error).message, - tenantId, - }); - throw error; - } - } - - /** - * Get partner by ID - */ - async findById(id: string, tenantId: string): Promise { - try { - const partner = await this.partnerRepository.findOne({ - where: { - id, - tenantId, - deletedAt: IsNull(), - }, - }); - - if (!partner) { - throw new NotFoundError('Contacto no encontrado'); - } - - return partner; - } catch (error) { - logger.error('Error finding partner', { - error: (error as Error).message, - id, - tenantId, - }); - throw error; - } - } - - /** - * Create a new partner - */ - async create( - dto: CreatePartnerDto, - tenantId: string, - userId: string - ): Promise { - try { - // Check if code already exists - const existing = await this.partnerRepository.findOne({ - where: { code: dto.code, tenantId }, - }); - - if (existing) { - throw new ValidationError('Ya existe un contacto con este código'); - } - - // Create partner - only include defined fields - const partnerData: Partial = { - tenantId, - code: dto.code, - displayName: dto.displayName, - partnerType: dto.partnerType || 'customer', - paymentTermDays: dto.paymentTermDays ?? 0, - creditLimit: dto.creditLimit ?? 0, - discountPercent: dto.discountPercent ?? 0, - tags: dto.tags || [], - isActive: true, - isVerified: false, - createdBy: userId, - }; - - // Add optional fields only if defined - if (dto.legalName) partnerData.legalName = dto.legalName; - if (dto.email) partnerData.email = dto.email.toLowerCase(); - if (dto.phone) partnerData.phone = dto.phone; - if (dto.mobile) partnerData.mobile = dto.mobile; - if (dto.website) partnerData.website = dto.website; - if (dto.taxId) partnerData.taxId = dto.taxId; - if (dto.taxRegime) partnerData.taxRegime = dto.taxRegime; - if (dto.cfdiUse) partnerData.cfdiUse = dto.cfdiUse; - if (dto.priceListId) partnerData.priceListId = dto.priceListId; - if (dto.category) partnerData.category = dto.category; - if (dto.notes) partnerData.notes = dto.notes; - if (dto.salesRepId) partnerData.salesRepId = dto.salesRepId; - - const partner = this.partnerRepository.create(partnerData); - - await this.partnerRepository.save(partner); - - logger.info('Partner created', { - partnerId: partner.id, - tenantId, - code: partner.code, - displayName: partner.displayName, - createdBy: userId, - }); - - return partner; - } catch (error) { - logger.error('Error creating partner', { - error: (error as Error).message, - tenantId, - dto, - }); - throw error; - } - } - - /** - * Update a partner - */ - async update( - id: string, - dto: UpdatePartnerDto, - tenantId: string, - userId: string - ): Promise { - try { - const existing = await this.findById(id, tenantId); - - // Update allowed fields - if (dto.displayName !== undefined) existing.displayName = dto.displayName; - if (dto.legalName !== undefined) existing.legalName = dto.legalName as string; - if (dto.partnerType !== undefined) existing.partnerType = dto.partnerType; - if (dto.email !== undefined) existing.email = dto.email?.toLowerCase() || null as any; - if (dto.phone !== undefined) existing.phone = dto.phone as string; - if (dto.mobile !== undefined) existing.mobile = dto.mobile as string; - if (dto.website !== undefined) existing.website = dto.website as string; - if (dto.taxId !== undefined) existing.taxId = dto.taxId as string; - if (dto.taxRegime !== undefined) existing.taxRegime = dto.taxRegime as string; - if (dto.cfdiUse !== undefined) existing.cfdiUse = dto.cfdiUse as string; - if (dto.paymentTermDays !== undefined) existing.paymentTermDays = dto.paymentTermDays; - if (dto.creditLimit !== undefined) existing.creditLimit = dto.creditLimit; - if (dto.priceListId !== undefined) existing.priceListId = dto.priceListId as string; - if (dto.discountPercent !== undefined) existing.discountPercent = dto.discountPercent; - if (dto.category !== undefined) existing.category = dto.category as string; - if (dto.tags !== undefined) existing.tags = dto.tags; - if (dto.notes !== undefined) existing.notes = dto.notes as string; - if (dto.isActive !== undefined) existing.isActive = dto.isActive; - if (dto.isVerified !== undefined) existing.isVerified = dto.isVerified; - if (dto.salesRepId !== undefined) existing.salesRepId = dto.salesRepId as string; - - existing.updatedBy = userId; - - await this.partnerRepository.save(existing); - - logger.info('Partner updated', { - partnerId: id, - tenantId, - updatedBy: userId, - }); - - return await this.findById(id, tenantId); - } catch (error) { - logger.error('Error updating partner', { - error: (error as Error).message, - id, - tenantId, - }); - throw error; - } - } - - /** - * Soft delete a partner - */ - async delete(id: string, tenantId: string, userId: string): Promise { - try { - const partner = await this.findById(id, tenantId); - - // Soft delete using the deletedAt column - partner.deletedAt = new Date(); - partner.isActive = false; - - await this.partnerRepository.save(partner); - - logger.info('Partner deleted', { - partnerId: id, - tenantId, - deletedBy: userId, - }); - } catch (error) { - logger.error('Error deleting partner', { - error: (error as Error).message, - id, - tenantId, - }); - throw error; - } - } - - /** - * Get customers only - */ - async findCustomers( - tenantId: string, - filters: Omit - ): Promise<{ data: Partner[]; total: number }> { - return this.findAll(tenantId, { ...filters, partnerType: 'customer' }); - } - - /** - * Get suppliers only - */ - async findSuppliers( - tenantId: string, - filters: Omit - ): Promise<{ data: Partner[]; total: number }> { - return this.findAll(tenantId, { ...filters, partnerType: 'supplier' }); - } -} - -// ===== Export Singleton Instance ===== - -export const partnersService = new PartnersService(); diff --git a/src/modules/partners/ranking.controller.ts b/src/modules/partners/ranking.controller.ts index 95e15c1..bf8fccb 100644 --- a/src/modules/partners/ranking.controller.ts +++ b/src/modules/partners/ranking.controller.ts @@ -1,7 +1,7 @@ import { Response, NextFunction } from 'express'; import { z } from 'zod'; import { AuthenticatedRequest } from '../../shared/types/index.js'; -import { rankingService, ABCClassification } from './ranking.service.js'; +import { rankingService, ABCClassification } from './services/index.js'; // ============================================================================ // VALIDATION SCHEMAS diff --git a/src/modules/partners/services/index.ts b/src/modules/partners/services/index.ts index bd0ac0d..fb23544 100644 --- a/src/modules/partners/services/index.ts +++ b/src/modules/partners/services/index.ts @@ -1 +1,16 @@ -export { PartnersService, PartnerSearchParams } from './partners.service'; +export { + partnersService, + CreatePartnerDto, + UpdatePartnerDto, + PartnerFilters, + PartnerWithRelations, +} from './partners.service.js'; +export type { PartnerType } from './partners.service.js'; +export { + rankingService, + ABCClassification, + PartnerRanking, + RankingCalculationResult, + RankingFilters, + TopPartner, +} from './ranking.service.js'; diff --git a/src/modules/partners/services/partners.service.ts b/src/modules/partners/services/partners.service.ts index cac026d..3fbbbf8 100644 --- a/src/modules/partners/services/partners.service.ts +++ b/src/modules/partners/services/partners.service.ts @@ -1,266 +1,350 @@ -import { Repository, FindOptionsWhere, ILike } from 'typeorm'; -import { Partner, PartnerAddress, PartnerContact, PartnerBankAccount } from '../entities'; -import { - CreatePartnerDto, - UpdatePartnerDto, - CreatePartnerAddressDto, - CreatePartnerContactDto, - CreatePartnerBankAccountDto, -} from '../dto'; +import { Repository, IsNull, Like } from 'typeorm'; +import { AppDataSource } from '../../../config/typeorm.js'; +import { Partner, PartnerType } from '../entities/index.js'; +import { NotFoundError, ValidationError, ForbiddenError } from '../../../shared/types/index.js'; +import { logger } from '../../../shared/utils/logger.js'; -export interface PartnerSearchParams { - tenantId: string; +// Re-export PartnerType for controller use +export type { PartnerType }; + +// ===== Interfaces ===== + +export interface CreatePartnerDto { + code: string; + displayName: string; + legalName?: string; + partnerType?: PartnerType; + email?: string; + phone?: string; + mobile?: string; + website?: string; + taxId?: string; + taxRegime?: string; + cfdiUse?: string; + paymentTermDays?: number; + creditLimit?: number; + priceListId?: string; + discountPercent?: number; + category?: string; + tags?: string[]; + notes?: string; + salesRepId?: string; +} + +export interface UpdatePartnerDto { + displayName?: string; + legalName?: string | null; + partnerType?: PartnerType; + email?: string | null; + phone?: string | null; + mobile?: string | null; + website?: string | null; + taxId?: string | null; + taxRegime?: string | null; + cfdiUse?: string | null; + paymentTermDays?: number; + creditLimit?: number; + priceListId?: string | null; + discountPercent?: number; + category?: string | null; + tags?: string[]; + notes?: string | null; + isActive?: boolean; + isVerified?: boolean; + salesRepId?: string | null; +} + +export interface PartnerFilters { search?: string; - partnerType?: 'customer' | 'supplier' | 'both'; + partnerType?: PartnerType; category?: string; isActive?: boolean; - salesRepId?: string; + isVerified?: boolean; + page?: number; limit?: number; - offset?: number; } -export class PartnersService { - constructor( - private readonly partnerRepository: Repository, - private readonly addressRepository: Repository, - private readonly contactRepository: Repository, - private readonly bankAccountRepository: Repository - ) {} +export interface PartnerWithRelations extends Partner { + // Add computed fields if needed +} - // ==================== Partners ==================== +// ===== PartnersService Class ===== - async findAll(params: PartnerSearchParams): Promise<{ data: Partner[]; total: number }> { - const { - tenantId, - search, - partnerType, - category, - isActive, - salesRepId, - limit = 50, - offset = 0, - } = params; +class PartnersService { + private partnerRepository: Repository; - const where: FindOptionsWhere[] = []; - const baseWhere: FindOptionsWhere = { tenantId }; - - if (partnerType) { - baseWhere.partnerType = partnerType; - } - - if (category) { - baseWhere.category = category; - } - - if (isActive !== undefined) { - baseWhere.isActive = isActive; - } - - if (salesRepId) { - baseWhere.salesRepId = salesRepId; - } - - if (search) { - where.push( - { ...baseWhere, displayName: ILike(`%${search}%`) }, - { ...baseWhere, legalName: ILike(`%${search}%`) }, - { ...baseWhere, code: ILike(`%${search}%`) }, - { ...baseWhere, taxId: ILike(`%${search}%`) }, - { ...baseWhere, email: ILike(`%${search}%`) } - ); - } else { - where.push(baseWhere); - } - - const [data, total] = await this.partnerRepository.findAndCount({ - where, - take: limit, - skip: offset, - order: { displayName: 'ASC' }, - }); - - return { data, total }; + constructor() { + this.partnerRepository = AppDataSource.getRepository(Partner); } - async findOne(id: string, tenantId: string): Promise { - return this.partnerRepository.findOne({ where: { id, tenantId } }); - } + /** + * Get all partners for a tenant with filters and pagination + */ + async findAll( + tenantId: string, + filters: PartnerFilters = {} + ): Promise<{ data: Partner[]; total: number }> { + try { + const { search, partnerType, category, isActive, isVerified, page = 1, limit = 20 } = filters; + const skip = (page - 1) * limit; - async findByCode(code: string, tenantId: string): Promise { - return this.partnerRepository.findOne({ where: { code, tenantId } }); - } + const queryBuilder = this.partnerRepository + .createQueryBuilder('partner') + .where('partner.tenantId = :tenantId', { tenantId }) + .andWhere('partner.deletedAt IS NULL'); - async findByTaxId(taxId: string, tenantId: string): Promise { - return this.partnerRepository.findOne({ where: { taxId, tenantId } }); - } - - async create(tenantId: string, dto: CreatePartnerDto, createdBy?: string): Promise { - // Check for existing code - const existingCode = await this.findByCode(dto.code, tenantId); - if (existingCode) { - throw new Error('A partner with this code already exists'); - } - - // Check for existing tax ID - if (dto.taxId) { - const existingTaxId = await this.findByTaxId(dto.taxId, tenantId); - if (existingTaxId) { - throw new Error('A partner with this tax ID already exists'); + // Apply search filter + if (search) { + queryBuilder.andWhere( + '(partner.displayName ILIKE :search OR partner.legalName ILIKE :search OR partner.email ILIKE :search OR partner.taxId ILIKE :search OR partner.code ILIKE :search)', + { search: `%${search}%` } + ); } + + // Filter by partner type + if (partnerType !== undefined) { + queryBuilder.andWhere('partner.partnerType = :partnerType', { partnerType }); + } + + // Filter by category + if (category) { + queryBuilder.andWhere('partner.category = :category', { category }); + } + + // Filter by active status + if (isActive !== undefined) { + queryBuilder.andWhere('partner.isActive = :isActive', { isActive }); + } + + // Filter by verified status + if (isVerified !== undefined) { + queryBuilder.andWhere('partner.isVerified = :isVerified', { isVerified }); + } + + // Get total count + const total = await queryBuilder.getCount(); + + // Get paginated results + const data = await queryBuilder + .orderBy('partner.displayName', 'ASC') + .skip(skip) + .take(limit) + .getMany(); + + logger.debug('Partners retrieved', { tenantId, count: data.length, total }); + + return { data, total }; + } catch (error) { + logger.error('Error retrieving partners', { + error: (error as Error).message, + tenantId, + }); + throw error; } - - const partner = this.partnerRepository.create({ - ...dto, - tenantId, - createdBy, - }); - - return this.partnerRepository.save(partner); } + /** + * Get partner by ID + */ + async findById(id: string, tenantId: string): Promise { + try { + const partner = await this.partnerRepository.findOne({ + where: { + id, + tenantId, + deletedAt: IsNull(), + }, + }); + + if (!partner) { + throw new NotFoundError('Contacto no encontrado'); + } + + return partner; + } catch (error) { + logger.error('Error finding partner', { + error: (error as Error).message, + id, + tenantId, + }); + throw error; + } + } + + /** + * Create a new partner + */ + async create( + dto: CreatePartnerDto, + tenantId: string, + userId: string + ): Promise { + try { + // Check if code already exists + const existing = await this.partnerRepository.findOne({ + where: { code: dto.code, tenantId }, + }); + + if (existing) { + throw new ValidationError('Ya existe un contacto con este código'); + } + + // Create partner - only include defined fields + const partnerData: Partial = { + tenantId, + code: dto.code, + displayName: dto.displayName, + partnerType: dto.partnerType || 'customer', + paymentTermDays: dto.paymentTermDays ?? 0, + creditLimit: dto.creditLimit ?? 0, + discountPercent: dto.discountPercent ?? 0, + tags: dto.tags || [], + isActive: true, + isVerified: false, + createdBy: userId, + }; + + // Add optional fields only if defined + if (dto.legalName) partnerData.legalName = dto.legalName; + if (dto.email) partnerData.email = dto.email.toLowerCase(); + if (dto.phone) partnerData.phone = dto.phone; + if (dto.mobile) partnerData.mobile = dto.mobile; + if (dto.website) partnerData.website = dto.website; + if (dto.taxId) partnerData.taxId = dto.taxId; + if (dto.taxRegime) partnerData.taxRegime = dto.taxRegime; + if (dto.cfdiUse) partnerData.cfdiUse = dto.cfdiUse; + if (dto.priceListId) partnerData.priceListId = dto.priceListId; + if (dto.category) partnerData.category = dto.category; + if (dto.notes) partnerData.notes = dto.notes; + if (dto.salesRepId) partnerData.salesRepId = dto.salesRepId; + + const partner = this.partnerRepository.create(partnerData); + + await this.partnerRepository.save(partner); + + logger.info('Partner created', { + partnerId: partner.id, + tenantId, + code: partner.code, + displayName: partner.displayName, + createdBy: userId, + }); + + return partner; + } catch (error) { + logger.error('Error creating partner', { + error: (error as Error).message, + tenantId, + dto, + }); + throw error; + } + } + + /** + * Update a partner + */ async update( id: string, - tenantId: string, dto: UpdatePartnerDto, - updatedBy?: string - ): Promise { - const partner = await this.findOne(id, tenantId); - if (!partner) return null; + tenantId: string, + userId: string + ): Promise { + try { + const existing = await this.findById(id, tenantId); - // If changing code, check for duplicates - if (dto.code && dto.code !== partner.code) { - const existing = await this.findByCode(dto.code, tenantId); - if (existing) { - throw new Error('A partner with this code already exists'); - } + // Update allowed fields + if (dto.displayName !== undefined) existing.displayName = dto.displayName; + if (dto.legalName !== undefined) existing.legalName = dto.legalName as string; + if (dto.partnerType !== undefined) existing.partnerType = dto.partnerType; + if (dto.email !== undefined) existing.email = dto.email?.toLowerCase() || null as any; + if (dto.phone !== undefined) existing.phone = dto.phone as string; + if (dto.mobile !== undefined) existing.mobile = dto.mobile as string; + if (dto.website !== undefined) existing.website = dto.website as string; + if (dto.taxId !== undefined) existing.taxId = dto.taxId as string; + if (dto.taxRegime !== undefined) existing.taxRegime = dto.taxRegime as string; + if (dto.cfdiUse !== undefined) existing.cfdiUse = dto.cfdiUse as string; + if (dto.paymentTermDays !== undefined) existing.paymentTermDays = dto.paymentTermDays; + if (dto.creditLimit !== undefined) existing.creditLimit = dto.creditLimit; + if (dto.priceListId !== undefined) existing.priceListId = dto.priceListId as string; + if (dto.discountPercent !== undefined) existing.discountPercent = dto.discountPercent; + if (dto.category !== undefined) existing.category = dto.category as string; + if (dto.tags !== undefined) existing.tags = dto.tags; + if (dto.notes !== undefined) existing.notes = dto.notes as string; + if (dto.isActive !== undefined) existing.isActive = dto.isActive; + if (dto.isVerified !== undefined) existing.isVerified = dto.isVerified; + if (dto.salesRepId !== undefined) existing.salesRepId = dto.salesRepId as string; + + existing.updatedBy = userId; + + await this.partnerRepository.save(existing); + + logger.info('Partner updated', { + partnerId: id, + tenantId, + updatedBy: userId, + }); + + return await this.findById(id, tenantId); + } catch (error) { + logger.error('Error updating partner', { + error: (error as Error).message, + id, + tenantId, + }); + throw error; } + } - // If changing tax ID, check for duplicates - if (dto.taxId && dto.taxId !== partner.taxId) { - const existing = await this.findByTaxId(dto.taxId, tenantId); - if (existing && existing.id !== id) { - throw new Error('A partner with this tax ID already exists'); - } + /** + * Soft delete a partner + */ + async delete(id: string, tenantId: string, userId: string): Promise { + try { + const partner = await this.findById(id, tenantId); + + // Soft delete using the deletedAt column + partner.deletedAt = new Date(); + partner.isActive = false; + + await this.partnerRepository.save(partner); + + logger.info('Partner deleted', { + partnerId: id, + tenantId, + deletedBy: userId, + }); + } catch (error) { + logger.error('Error deleting partner', { + error: (error as Error).message, + id, + tenantId, + }); + throw error; } - - Object.assign(partner, { - ...dto, - updatedBy, - }); - - return this.partnerRepository.save(partner); } - async delete(id: string, tenantId: string): Promise { - const partner = await this.findOne(id, tenantId); - if (!partner) return false; - - const result = await this.partnerRepository.softDelete(id); - return (result.affected ?? 0) > 0; + /** + * Get customers only + */ + async findCustomers( + tenantId: string, + filters: Omit + ): Promise<{ data: Partner[]; total: number }> { + return this.findAll(tenantId, { ...filters, partnerType: 'customer' }); } - async getCustomers(tenantId: string): Promise { - return this.partnerRepository.find({ - where: [ - { tenantId, partnerType: 'customer', isActive: true }, - { tenantId, partnerType: 'both', isActive: true }, - ], - order: { displayName: 'ASC' }, - }); - } - - async getSuppliers(tenantId: string): Promise { - return this.partnerRepository.find({ - where: [ - { tenantId, partnerType: 'supplier', isActive: true }, - { tenantId, partnerType: 'both', isActive: true }, - ], - order: { displayName: 'ASC' }, - }); - } - - // ==================== Addresses ==================== - - async getAddresses(partnerId: string): Promise { - return this.addressRepository.find({ - where: { partnerId }, - order: { isDefault: 'DESC', addressType: 'ASC' }, - }); - } - - async createAddress(dto: CreatePartnerAddressDto): Promise { - // If setting as default, unset other defaults of same type - if (dto.isDefault) { - await this.addressRepository.update( - { partnerId: dto.partnerId, addressType: dto.addressType }, - { isDefault: false } - ); - } - - const address = this.addressRepository.create(dto); - return this.addressRepository.save(address); - } - - async deleteAddress(id: string): Promise { - const result = await this.addressRepository.delete(id); - return (result.affected ?? 0) > 0; - } - - // ==================== Contacts ==================== - - async getContacts(partnerId: string): Promise { - return this.contactRepository.find({ - where: { partnerId }, - order: { isPrimary: 'DESC', fullName: 'ASC' }, - }); - } - - async createContact(dto: CreatePartnerContactDto): Promise { - // If setting as primary, unset other primaries - if (dto.isPrimary) { - await this.contactRepository.update({ partnerId: dto.partnerId }, { isPrimary: false }); - } - - const contact = this.contactRepository.create(dto); - return this.contactRepository.save(contact); - } - - async deleteContact(id: string): Promise { - const result = await this.contactRepository.delete(id); - return (result.affected ?? 0) > 0; - } - - // ==================== Bank Accounts ==================== - - async getBankAccounts(partnerId: string): Promise { - return this.bankAccountRepository.find({ - where: { partnerId }, - order: { isDefault: 'DESC', bankName: 'ASC' }, - }); - } - - async createBankAccount(dto: CreatePartnerBankAccountDto): Promise { - // If setting as default, unset other defaults - if (dto.isDefault) { - await this.bankAccountRepository.update({ partnerId: dto.partnerId }, { isDefault: false }); - } - - const bankAccount = this.bankAccountRepository.create(dto); - return this.bankAccountRepository.save(bankAccount); - } - - async deleteBankAccount(id: string): Promise { - const result = await this.bankAccountRepository.delete(id); - return (result.affected ?? 0) > 0; - } - - async verifyBankAccount(id: string): Promise { - const bankAccount = await this.bankAccountRepository.findOne({ where: { id } }); - if (!bankAccount) return null; - - bankAccount.isVerified = true; - bankAccount.verifiedAt = new Date(); - - return this.bankAccountRepository.save(bankAccount); + /** + * Get suppliers only + */ + async findSuppliers( + tenantId: string, + filters: Omit + ): Promise<{ data: Partner[]; total: number }> { + return this.findAll(tenantId, { ...filters, partnerType: 'supplier' }); } } + +// ===== Export Singleton Instance ===== + +export const partnersService = new PartnersService(); diff --git a/src/modules/partners/ranking.service.ts b/src/modules/partners/services/ranking.service.ts similarity index 98% rename from src/modules/partners/ranking.service.ts rename to src/modules/partners/services/ranking.service.ts index 2647315..96a91c8 100644 --- a/src/modules/partners/ranking.service.ts +++ b/src/modules/partners/services/ranking.service.ts @@ -1,8 +1,8 @@ import { Repository, IsNull } from 'typeorm'; -import { AppDataSource } from '../../config/typeorm.js'; -import { Partner } from './entities/index.js'; -import { NotFoundError } from '../../shared/types/index.js'; -import { logger } from '../../shared/utils/logger.js'; +import { AppDataSource } from '../../../config/typeorm.js'; +import { Partner } from '../entities/index.js'; +import { NotFoundError } from '../../../shared/types/index.js'; +import { logger } from '../../../shared/utils/logger.js'; // ============================================================================ // TYPES diff --git a/src/modules/products/__tests__/products.service.test.ts b/src/modules/products/__tests__/products.service.test.ts index 173ea22..17616fa 100644 --- a/src/modules/products/__tests__/products.service.test.ts +++ b/src/modules/products/__tests__/products.service.test.ts @@ -16,7 +16,7 @@ jest.mock('../../../config/typeorm.js', () => ({ })); // Import after mocking -import { productsService } from '../products.service.js'; +import { productsService } from '../services/products.service.js'; describe('ProductsService', () => { const tenantId = 'test-tenant-uuid'; diff --git a/src/modules/products/products.controller.ts b/src/modules/products/products.controller.ts index a5fd0d1..bdf9668 100644 --- a/src/modules/products/products.controller.ts +++ b/src/modules/products/products.controller.ts @@ -1,6 +1,6 @@ import { Response, NextFunction } from 'express'; import { z } from 'zod'; -import { productsService, CreateProductDto, UpdateProductDto, CreateCategoryDto, UpdateCategoryDto } from './products.service.js'; +import { productsService, CreateProductDto, UpdateProductDto, CreateCategoryDto, UpdateCategoryDto } from './services/index.js'; import { ApiResponse, AuthenticatedRequest, ValidationError } from '../../shared/types/index.js'; // Validation schemas diff --git a/src/modules/products/products.service.ts b/src/modules/products/products.service.ts deleted file mode 100644 index 9b98886..0000000 --- a/src/modules/products/products.service.ts +++ /dev/null @@ -1,300 +0,0 @@ -import { FindOptionsWhere, ILike, IsNull } from 'typeorm'; -import { AppDataSource } from '../../config/typeorm.js'; -import { Product } from './entities/product.entity.js'; -import { ProductCategory } from './entities/product-category.entity.js'; - -export interface ProductSearchParams { - tenantId: string; - search?: string; - categoryId?: string; - productType?: 'product' | 'service' | 'consumable' | 'kit'; - isActive?: boolean; - isSellable?: boolean; - isPurchasable?: boolean; - limit?: number; - offset?: number; -} - -export interface CategorySearchParams { - tenantId: string; - search?: string; - parentId?: string; - isActive?: boolean; - limit?: number; - offset?: number; -} - -export interface CreateProductDto { - sku: string; - name: string; - description?: string; - shortName?: string; - barcode?: string; - categoryId?: string; - productType?: 'product' | 'service' | 'consumable' | 'kit'; - salePrice?: number; - costPrice?: number; - currency?: string; - taxRate?: number; - isActive?: boolean; - isSellable?: boolean; - isPurchasable?: boolean; -} - -export interface UpdateProductDto { - sku?: string; - name?: string; - description?: string | null; - shortName?: string | null; - barcode?: string | null; - categoryId?: string | null; - productType?: 'product' | 'service' | 'consumable' | 'kit'; - salePrice?: number; - costPrice?: number; - currency?: string; - taxRate?: number; - isActive?: boolean; - isSellable?: boolean; - isPurchasable?: boolean; -} - -export interface CreateCategoryDto { - code: string; - name: string; - description?: string; - parentId?: string; - isActive?: boolean; -} - -export interface UpdateCategoryDto { - code?: string; - name?: string; - description?: string | null; - parentId?: string | null; - isActive?: boolean; -} - -class ProductsServiceClass { - private get productRepository() { - return AppDataSource.getRepository(Product); - } - - private get categoryRepository() { - return AppDataSource.getRepository(ProductCategory); - } - - // ==================== Products ==================== - - async findAll(params: ProductSearchParams): Promise<{ data: Product[]; total: number }> { - const { - tenantId, - search, - categoryId, - productType, - isActive, - isSellable, - isPurchasable, - limit = 50, - offset = 0, - } = params; - - const where: FindOptionsWhere[] = []; - const baseWhere: FindOptionsWhere = { tenantId, deletedAt: IsNull() }; - - if (categoryId) { - baseWhere.categoryId = categoryId; - } - - if (productType) { - baseWhere.productType = productType; - } - - if (isActive !== undefined) { - baseWhere.isActive = isActive; - } - - if (isSellable !== undefined) { - baseWhere.isSellable = isSellable; - } - - if (isPurchasable !== undefined) { - baseWhere.isPurchasable = isPurchasable; - } - - if (search) { - where.push( - { ...baseWhere, name: ILike(`%${search}%`) }, - { ...baseWhere, sku: ILike(`%${search}%`) }, - { ...baseWhere, barcode: ILike(`%${search}%`) } - ); - } else { - where.push(baseWhere); - } - - const [data, total] = await this.productRepository.findAndCount({ - where, - relations: ['category'], - take: limit, - skip: offset, - order: { name: 'ASC' }, - }); - - return { data, total }; - } - - async findOne(id: string, tenantId: string): Promise { - return this.productRepository.findOne({ - where: { id, tenantId, deletedAt: IsNull() }, - relations: ['category'], - }); - } - - async findBySku(sku: string, tenantId: string): Promise { - return this.productRepository.findOne({ - where: { sku, tenantId, deletedAt: IsNull() }, - relations: ['category'], - }); - } - - async findByBarcode(barcode: string, tenantId: string): Promise { - return this.productRepository.findOne({ - where: { barcode, tenantId, deletedAt: IsNull() }, - relations: ['category'], - }); - } - - async create(tenantId: string, dto: CreateProductDto, createdBy?: string): Promise { - // Validate unique SKU within tenant (RLS compliance) - const existingSku = await this.productRepository.findOne({ - where: { sku: dto.sku, tenantId, deletedAt: IsNull() }, - }); - if (existingSku) { - throw new Error(`Product with SKU '${dto.sku}' already exists`); - } - - // Validate unique barcode within tenant if provided (RLS compliance) - if (dto.barcode) { - const existingBarcode = await this.productRepository.findOne({ - where: { barcode: dto.barcode, tenantId, deletedAt: IsNull() }, - }); - if (existingBarcode) { - throw new Error(`Product with barcode '${dto.barcode}' already exists`); - } - } - - const product = this.productRepository.create({ - ...dto, - tenantId, - createdBy, - }); - return this.productRepository.save(product); - } - - async update(id: string, tenantId: string, dto: UpdateProductDto, updatedBy?: string): Promise { - const product = await this.findOne(id, tenantId); - if (!product) return null; - - // Validate unique SKU within tenant if changing (RLS compliance) - if (dto.sku && dto.sku !== product.sku) { - const existingSku = await this.productRepository.findOne({ - where: { sku: dto.sku, tenantId, deletedAt: IsNull() }, - }); - if (existingSku) { - throw new Error(`Product with SKU '${dto.sku}' already exists`); - } - } - - // Validate unique barcode within tenant if changing (RLS compliance) - if (dto.barcode && dto.barcode !== product.barcode) { - const existingBarcode = await this.productRepository.findOne({ - where: { barcode: dto.barcode, tenantId, deletedAt: IsNull() }, - }); - if (existingBarcode) { - throw new Error(`Product with barcode '${dto.barcode}' already exists`); - } - } - - Object.assign(product, { ...dto, updatedBy }); - return this.productRepository.save(product); - } - - async delete(id: string, tenantId: string): Promise { - const result = await this.productRepository.softDelete({ id, tenantId }); - return (result.affected ?? 0) > 0; - } - - async getSellableProducts(tenantId: string, limit = 50, offset = 0): Promise<{ data: Product[]; total: number }> { - return this.findAll({ tenantId, isSellable: true, isActive: true, limit, offset }); - } - - async getPurchasableProducts(tenantId: string, limit = 50, offset = 0): Promise<{ data: Product[]; total: number }> { - return this.findAll({ tenantId, isPurchasable: true, isActive: true, limit, offset }); - } - - // ==================== Categories ==================== - - async findAllCategories(params: CategorySearchParams): Promise<{ data: ProductCategory[]; total: number }> { - const { tenantId, search, parentId, isActive, limit = 50, offset = 0 } = params; - - const where: FindOptionsWhere = { tenantId, deletedAt: IsNull() }; - - if (parentId) { - where.parentId = parentId; - } - - if (isActive !== undefined) { - where.isActive = isActive; - } - - if (search) { - const [data, total] = await this.categoryRepository.findAndCount({ - where: [ - { ...where, name: ILike(`%${search}%`) }, - { ...where, code: ILike(`%${search}%`) }, - ], - take: limit, - skip: offset, - order: { name: 'ASC' }, - }); - return { data, total }; - } - - const [data, total] = await this.categoryRepository.findAndCount({ - where, - take: limit, - skip: offset, - order: { sortOrder: 'ASC', name: 'ASC' }, - }); - - return { data, total }; - } - - async findCategory(id: string, tenantId: string): Promise { - return this.categoryRepository.findOne({ - where: { id, tenantId, deletedAt: IsNull() }, - }); - } - - async createCategory(tenantId: string, dto: CreateCategoryDto, _createdBy?: string): Promise { - const category = this.categoryRepository.create({ - ...dto, - tenantId, - }); - return this.categoryRepository.save(category); - } - - async updateCategory(id: string, tenantId: string, dto: UpdateCategoryDto, _updatedBy?: string): Promise { - const category = await this.findCategory(id, tenantId); - if (!category) return null; - Object.assign(category, dto); - return this.categoryRepository.save(category); - } - - async deleteCategory(id: string, tenantId: string): Promise { - const result = await this.categoryRepository.softDelete({ id, tenantId }); - return (result.affected ?? 0) > 0; - } -} - -// Export singleton instance -export const productsService = new ProductsServiceClass(); diff --git a/src/modules/products/services/index.ts b/src/modules/products/services/index.ts index 33a92cf..37f9bc0 100644 --- a/src/modules/products/services/index.ts +++ b/src/modules/products/services/index.ts @@ -1 +1,9 @@ -export { ProductsService, ProductSearchParams, CategorySearchParams } from './products.service'; +export { + productsService, + ProductSearchParams, + CategorySearchParams, + CreateProductDto, + UpdateProductDto, + CreateCategoryDto, + UpdateCategoryDto, +} from './products.service.js'; diff --git a/src/modules/products/services/products.service.ts b/src/modules/products/services/products.service.ts index ee32e64..1bfc74a 100644 --- a/src/modules/products/services/products.service.ts +++ b/src/modules/products/services/products.service.ts @@ -1,11 +1,7 @@ -import { Repository, FindOptionsWhere, ILike } from 'typeorm'; -import { Product, ProductCategory } from '../entities'; -import { - CreateProductDto, - UpdateProductDto, - CreateProductCategoryDto, - UpdateProductCategoryDto, -} from '../dto'; +import { FindOptionsWhere, ILike, IsNull } from 'typeorm'; +import { AppDataSource } from '../../../config/typeorm.js'; +import { Product } from '../entities/product.entity.js'; +import { ProductCategory } from '../entities/product-category.entity.js'; export interface ProductSearchParams { tenantId: string; @@ -28,11 +24,64 @@ export interface CategorySearchParams { offset?: number; } -export class ProductsService { - constructor( - private readonly productRepository: Repository, - private readonly categoryRepository: Repository - ) {} +export interface CreateProductDto { + sku: string; + name: string; + description?: string; + shortName?: string; + barcode?: string; + categoryId?: string; + productType?: 'product' | 'service' | 'consumable' | 'kit'; + salePrice?: number; + costPrice?: number; + currency?: string; + taxRate?: number; + isActive?: boolean; + isSellable?: boolean; + isPurchasable?: boolean; +} + +export interface UpdateProductDto { + sku?: string; + name?: string; + description?: string | null; + shortName?: string | null; + barcode?: string | null; + categoryId?: string | null; + productType?: 'product' | 'service' | 'consumable' | 'kit'; + salePrice?: number; + costPrice?: number; + currency?: string; + taxRate?: number; + isActive?: boolean; + isSellable?: boolean; + isPurchasable?: boolean; +} + +export interface CreateCategoryDto { + code: string; + name: string; + description?: string; + parentId?: string; + isActive?: boolean; +} + +export interface UpdateCategoryDto { + code?: string; + name?: string; + description?: string | null; + parentId?: string | null; + isActive?: boolean; +} + +class ProductsServiceClass { + private get productRepository() { + return AppDataSource.getRepository(Product); + } + + private get categoryRepository() { + return AppDataSource.getRepository(ProductCategory); + } // ==================== Products ==================== @@ -50,7 +99,7 @@ export class ProductsService { } = params; const where: FindOptionsWhere[] = []; - const baseWhere: FindOptionsWhere = { tenantId }; + const baseWhere: FindOptionsWhere = { tenantId, deletedAt: IsNull() }; if (categoryId) { baseWhere.categoryId = categoryId; @@ -76,8 +125,7 @@ export class ProductsService { where.push( { ...baseWhere, name: ILike(`%${search}%`) }, { ...baseWhere, sku: ILike(`%${search}%`) }, - { ...baseWhere, barcode: ILike(`%${search}%`) }, - { ...baseWhere, description: ILike(`%${search}%`) } + { ...baseWhere, barcode: ILike(`%${search}%`) } ); } else { where.push(baseWhere); @@ -96,37 +144,41 @@ export class ProductsService { async findOne(id: string, tenantId: string): Promise { return this.productRepository.findOne({ - where: { id, tenantId }, + where: { id, tenantId, deletedAt: IsNull() }, relations: ['category'], }); } async findBySku(sku: string, tenantId: string): Promise { return this.productRepository.findOne({ - where: { sku, tenantId }, + where: { sku, tenantId, deletedAt: IsNull() }, relations: ['category'], }); } async findByBarcode(barcode: string, tenantId: string): Promise { return this.productRepository.findOne({ - where: { barcode, tenantId }, + where: { barcode, tenantId, deletedAt: IsNull() }, relations: ['category'], }); } async create(tenantId: string, dto: CreateProductDto, createdBy?: string): Promise { - // Check for existing SKU - const existingSku = await this.findBySku(dto.sku, tenantId); + // Validate unique SKU within tenant (RLS compliance) + const existingSku = await this.productRepository.findOne({ + where: { sku: dto.sku, tenantId, deletedAt: IsNull() }, + }); if (existingSku) { - throw new Error('A product with this SKU already exists'); + throw new Error(`Product with SKU '${dto.sku}' already exists`); } - // Check for existing barcode + // Validate unique barcode within tenant if provided (RLS compliance) if (dto.barcode) { - const existingBarcode = await this.findByBarcode(dto.barcode, tenantId); + const existingBarcode = await this.productRepository.findOne({ + where: { barcode: dto.barcode, tenantId, deletedAt: IsNull() }, + }); if (existingBarcode) { - throw new Error('A product with this barcode already exists'); + throw new Error(`Product with barcode '${dto.barcode}' already exists`); } } @@ -135,92 +187,76 @@ export class ProductsService { tenantId, createdBy, }); - return this.productRepository.save(product); } - async update( - id: string, - tenantId: string, - dto: UpdateProductDto, - updatedBy?: string - ): Promise { + async update(id: string, tenantId: string, dto: UpdateProductDto, updatedBy?: string): Promise { const product = await this.findOne(id, tenantId); if (!product) return null; - // If changing SKU, check for duplicates + // Validate unique SKU within tenant if changing (RLS compliance) if (dto.sku && dto.sku !== product.sku) { - const existing = await this.findBySku(dto.sku, tenantId); - if (existing) { - throw new Error('A product with this SKU already exists'); + const existingSku = await this.productRepository.findOne({ + where: { sku: dto.sku, tenantId, deletedAt: IsNull() }, + }); + if (existingSku) { + throw new Error(`Product with SKU '${dto.sku}' already exists`); } } - // If changing barcode, check for duplicates + // Validate unique barcode within tenant if changing (RLS compliance) if (dto.barcode && dto.barcode !== product.barcode) { - const existing = await this.findByBarcode(dto.barcode, tenantId); - if (existing && existing.id !== id) { - throw new Error('A product with this barcode already exists'); + const existingBarcode = await this.productRepository.findOne({ + where: { barcode: dto.barcode, tenantId, deletedAt: IsNull() }, + }); + if (existingBarcode) { + throw new Error(`Product with barcode '${dto.barcode}' already exists`); } } - Object.assign(product, { - ...dto, - updatedBy, - }); - + Object.assign(product, { ...dto, updatedBy }); return this.productRepository.save(product); } async delete(id: string, tenantId: string): Promise { - const product = await this.findOne(id, tenantId); - if (!product) return false; - - const result = await this.productRepository.softDelete(id); + const result = await this.productRepository.softDelete({ id, tenantId }); return (result.affected ?? 0) > 0; } - async getSellableProducts(tenantId: string): Promise { - return this.productRepository.find({ - where: { tenantId, isActive: true, isSellable: true }, - relations: ['category'], - order: { name: 'ASC' }, - }); + async getSellableProducts(tenantId: string, limit = 50, offset = 0): Promise<{ data: Product[]; total: number }> { + return this.findAll({ tenantId, isSellable: true, isActive: true, limit, offset }); } - async getPurchasableProducts(tenantId: string): Promise { - return this.productRepository.find({ - where: { tenantId, isActive: true, isPurchasable: true }, - relations: ['category'], - order: { name: 'ASC' }, - }); + async getPurchasableProducts(tenantId: string, limit = 50, offset = 0): Promise<{ data: Product[]; total: number }> { + return this.findAll({ tenantId, isPurchasable: true, isActive: true, limit, offset }); } // ==================== Categories ==================== - async findAllCategories( - params: CategorySearchParams - ): Promise<{ data: ProductCategory[]; total: number }> { - const { tenantId, search, parentId, isActive, limit = 100, offset = 0 } = params; + async findAllCategories(params: CategorySearchParams): Promise<{ data: ProductCategory[]; total: number }> { + const { tenantId, search, parentId, isActive, limit = 50, offset = 0 } = params; - const where: FindOptionsWhere[] = []; - const baseWhere: FindOptionsWhere = { tenantId }; + const where: FindOptionsWhere = { tenantId, deletedAt: IsNull() }; - if (parentId !== undefined) { - baseWhere.parentId = parentId || undefined; + if (parentId) { + where.parentId = parentId; } if (isActive !== undefined) { - baseWhere.isActive = isActive; + where.isActive = isActive; } if (search) { - where.push( - { ...baseWhere, name: ILike(`%${search}%`) }, - { ...baseWhere, code: ILike(`%${search}%`) } - ); - } else { - where.push(baseWhere); + const [data, total] = await this.categoryRepository.findAndCount({ + where: [ + { ...where, name: ILike(`%${search}%`) }, + { ...where, code: ILike(`%${search}%`) }, + ], + take: limit, + skip: offset, + order: { name: 'ASC' }, + }); + return { data, total }; } const [data, total] = await this.categoryRepository.findAndCount({ @@ -234,95 +270,31 @@ export class ProductsService { } async findCategory(id: string, tenantId: string): Promise { - return this.categoryRepository.findOne({ where: { id, tenantId } }); + return this.categoryRepository.findOne({ + where: { id, tenantId, deletedAt: IsNull() }, + }); } - async findCategoryByCode(code: string, tenantId: string): Promise { - return this.categoryRepository.findOne({ where: { code, tenantId } }); - } - - async createCategory( - tenantId: string, - dto: CreateProductCategoryDto - ): Promise { - // Check for existing code - const existingCode = await this.findCategoryByCode(dto.code, tenantId); - if (existingCode) { - throw new Error('A category with this code already exists'); - } - - // Calculate hierarchy if parent exists - let hierarchyPath = `/${dto.code}`; - let hierarchyLevel = 0; - - if (dto.parentId) { - const parent = await this.findCategory(dto.parentId, tenantId); - if (parent) { - hierarchyPath = `${parent.hierarchyPath}/${dto.code}`; - hierarchyLevel = parent.hierarchyLevel + 1; - } - } - + async createCategory(tenantId: string, dto: CreateCategoryDto, _createdBy?: string): Promise { const category = this.categoryRepository.create({ ...dto, tenantId, - hierarchyPath, - hierarchyLevel, }); - return this.categoryRepository.save(category); } - async updateCategory( - id: string, - tenantId: string, - dto: UpdateProductCategoryDto - ): Promise { + async updateCategory(id: string, tenantId: string, dto: UpdateCategoryDto, _updatedBy?: string): Promise { const category = await this.findCategory(id, tenantId); if (!category) return null; - - // If changing code, check for duplicates - if (dto.code && dto.code !== category.code) { - const existing = await this.findCategoryByCode(dto.code, tenantId); - if (existing) { - throw new Error('A category with this code already exists'); - } - } - Object.assign(category, dto); return this.categoryRepository.save(category); } async deleteCategory(id: string, tenantId: string): Promise { - const category = await this.findCategory(id, tenantId); - if (!category) return false; - - // Check if category has children - const children = await this.categoryRepository.findOne({ - where: { parentId: id, tenantId }, - }); - if (children) { - throw new Error('Cannot delete category with children'); - } - - // Check if category has products - const products = await this.productRepository.findOne({ - where: { categoryId: id, tenantId }, - }); - if (products) { - throw new Error('Cannot delete category with products'); - } - - const result = await this.categoryRepository.softDelete(id); + const result = await this.categoryRepository.softDelete({ id, tenantId }); return (result.affected ?? 0) > 0; } - - async getCategoryTree(tenantId: string): Promise { - const categories = await this.categoryRepository.find({ - where: { tenantId, isActive: true }, - order: { hierarchyPath: 'ASC', sortOrder: 'ASC' }, - }); - - return categories; - } } + +// Export singleton instance +export const productsService = new ProductsServiceClass(); diff --git a/src/modules/projects/index.ts b/src/modules/projects/index.ts index c5a5215..a1cb2c8 100644 --- a/src/modules/projects/index.ts +++ b/src/modules/projects/index.ts @@ -1,8 +1,8 @@ -export * from './projects.service.js'; -export * from './tasks.service.js'; -export * from './timesheets.service.js'; -export * from './billing.service.js'; -export * from './hr-integration.service.js'; +export * from './services/projects.service.js'; +export * from './services/tasks.service.js'; +export * from './services/timesheets.service.js'; +export * from './services/billing.service.js'; +export * from './services/hr-integration.service.js'; export * from './projects.controller.js'; export { default as projectsRoutes } from './projects.routes.js'; diff --git a/src/modules/projects/projects.controller.ts b/src/modules/projects/projects.controller.ts index 403ee8d..0f949e1 100644 --- a/src/modules/projects/projects.controller.ts +++ b/src/modules/projects/projects.controller.ts @@ -1,8 +1,8 @@ import { Response, NextFunction } from 'express'; import { z } from 'zod'; -import { projectsService, CreateProjectDto, UpdateProjectDto, ProjectFilters } from './projects.service.js'; -import { tasksService, CreateTaskDto, UpdateTaskDto, TaskFilters } from './tasks.service.js'; -import { timesheetsService, CreateTimesheetDto, UpdateTimesheetDto, TimesheetFilters } from './timesheets.service.js'; +import { projectsService, CreateProjectDto, UpdateProjectDto, ProjectFilters } from './services/projects.service.js'; +import { tasksService, CreateTaskDto, UpdateTaskDto, TaskFilters } from './services/tasks.service.js'; +import { timesheetsService, CreateTimesheetDto, UpdateTimesheetDto, TimesheetFilters } from './services/timesheets.service.js'; import { AuthenticatedRequest } from '../../shared/middleware/auth.middleware.js'; import { ValidationError } from '../../shared/errors/index.js'; diff --git a/src/modules/projects/billing.service.ts b/src/modules/projects/services/billing.service.ts similarity index 99% rename from src/modules/projects/billing.service.ts rename to src/modules/projects/services/billing.service.ts index 855f016..ff2d98c 100644 --- a/src/modules/projects/billing.service.ts +++ b/src/modules/projects/services/billing.service.ts @@ -1,6 +1,6 @@ -import { query, queryOne, getClient } from '../../config/database.js'; -import { NotFoundError, ValidationError } from '../../shared/errors/index.js'; -import { logger } from '../../shared/utils/logger.js'; +import { query, queryOne, getClient } from '../../../config/database.js'; +import { NotFoundError, ValidationError } from '../../../shared/errors/index.js'; +import { logger } from '../../../shared/utils/logger.js'; // ============================================================================ // TYPES diff --git a/src/modules/projects/hr-integration.service.ts b/src/modules/projects/services/hr-integration.service.ts similarity index 98% rename from src/modules/projects/hr-integration.service.ts rename to src/modules/projects/services/hr-integration.service.ts index bf59bca..9b8c98b 100644 --- a/src/modules/projects/hr-integration.service.ts +++ b/src/modules/projects/services/hr-integration.service.ts @@ -1,6 +1,6 @@ -import { query, queryOne } from '../../config/database.js'; -import { NotFoundError, ValidationError } from '../../shared/errors/index.js'; -import { logger } from '../../shared/utils/logger.js'; +import { query, queryOne } from '../../../config/database.js'; +import { NotFoundError, ValidationError } from '../../../shared/errors/index.js'; +import { logger } from '../../../shared/utils/logger.js'; // ============================================================================ // TYPES diff --git a/src/modules/projects/services/index.ts b/src/modules/projects/services/index.ts new file mode 100644 index 0000000..1ba1565 --- /dev/null +++ b/src/modules/projects/services/index.ts @@ -0,0 +1,6 @@ +// Projects services barrel export +export * from './projects.service.js'; +export * from './tasks.service.js'; +export * from './timesheets.service.js'; +export * from './billing.service.js'; +export * from './hr-integration.service.js'; diff --git a/src/modules/projects/projects.service.ts b/src/modules/projects/services/projects.service.ts similarity index 99% rename from src/modules/projects/projects.service.ts rename to src/modules/projects/services/projects.service.ts index 136c8c0..3697ec1 100644 --- a/src/modules/projects/projects.service.ts +++ b/src/modules/projects/services/projects.service.ts @@ -1,5 +1,5 @@ -import { query, queryOne } from '../../config/database.js'; -import { NotFoundError, ConflictError, ValidationError } from '../../shared/errors/index.js'; +import { query, queryOne } from '../../../config/database.js'; +import { NotFoundError, ConflictError, ValidationError } from '../../../shared/errors/index.js'; export interface Project { id: string; diff --git a/src/modules/projects/tasks.service.ts b/src/modules/projects/services/tasks.service.ts similarity index 98% rename from src/modules/projects/tasks.service.ts rename to src/modules/projects/services/tasks.service.ts index fc47bed..33708c7 100644 --- a/src/modules/projects/tasks.service.ts +++ b/src/modules/projects/services/tasks.service.ts @@ -1,5 +1,5 @@ -import { query, queryOne } from '../../config/database.js'; -import { NotFoundError, ValidationError } from '../../shared/errors/index.js'; +import { query, queryOne } from '../../../config/database.js'; +import { NotFoundError, ValidationError } from '../../../shared/errors/index.js'; export interface Task { id: string; diff --git a/src/modules/projects/timesheets.service.ts b/src/modules/projects/services/timesheets.service.ts similarity index 98% rename from src/modules/projects/timesheets.service.ts rename to src/modules/projects/services/timesheets.service.ts index 7a7fe9a..a628fc7 100644 --- a/src/modules/projects/timesheets.service.ts +++ b/src/modules/projects/services/timesheets.service.ts @@ -1,5 +1,5 @@ -import { query, queryOne } from '../../config/database.js'; -import { NotFoundError, ValidationError } from '../../shared/errors/index.js'; +import { query, queryOne } from '../../../config/database.js'; +import { NotFoundError, ValidationError } from '../../../shared/errors/index.js'; export interface Timesheet { id: string; diff --git a/src/modules/purchases/purchases.controller.ts b/src/modules/purchases/purchases.controller.ts index ff3283c..8a72298 100644 --- a/src/modules/purchases/purchases.controller.ts +++ b/src/modules/purchases/purchases.controller.ts @@ -1,7 +1,7 @@ import { Response, NextFunction } from 'express'; import { z } from 'zod'; -import { purchasesService, CreatePurchaseOrderDto, UpdatePurchaseOrderDto, PurchaseOrderFilters } from './purchases.service.js'; -import { rfqsService, CreateRfqDto, UpdateRfqDto, CreateRfqLineDto, UpdateRfqLineDto, RfqFilters } from './rfqs.service.js'; +import { purchasesService, CreatePurchaseOrderDto, UpdatePurchaseOrderDto, PurchaseOrderFilters } from './services/purchases.service.js'; +import { rfqsService, CreateRfqDto, UpdateRfqDto, CreateRfqLineDto, UpdateRfqLineDto, RfqFilters } from './services/rfqs.service.js'; import { AuthenticatedRequest } from '../../shared/middleware/auth.middleware.js'; import { ValidationError } from '../../shared/errors/index.js'; diff --git a/src/modules/purchases/services/index.ts b/src/modules/purchases/services/index.ts index 6ff21d2..fce4c3f 100644 --- a/src/modules/purchases/services/index.ts +++ b/src/modules/purchases/services/index.ts @@ -5,6 +5,10 @@ import { CreatePurchaseOrderDto, UpdatePurchaseOrderDto } from '../dto'; // Export 3-Way Matching Service export { ThreeWayMatchingService, MatchingSearchParams, InvoiceData, InvoiceLineData } from './three-way-matching.service'; +// Export SQL-based services (used by controllers) +export { purchasesService, CreatePurchaseOrderDto, UpdatePurchaseOrderDto, PurchaseOrderFilters } from './purchases.service.js'; +export { rfqsService, CreateRfqDto, UpdateRfqDto, CreateRfqLineDto, UpdateRfqLineDto, RfqFilters } from './rfqs.service.js'; + export interface PurchaseSearchParams { tenantId: string; supplierId?: string; diff --git a/src/modules/purchases/purchases.service.ts b/src/modules/purchases/services/purchases.service.ts similarity index 98% rename from src/modules/purchases/purchases.service.ts rename to src/modules/purchases/services/purchases.service.ts index 7630fe2..5bfa304 100644 --- a/src/modules/purchases/purchases.service.ts +++ b/src/modules/purchases/services/purchases.service.ts @@ -1,7 +1,7 @@ -import { query, queryOne, getClient } from '../../config/database.js'; -import { NotFoundError, ConflictError, ValidationError } from '../../shared/errors/index.js'; -import { sequencesService, SEQUENCE_CODES } from '../core/sequences.service.js'; -import { logger } from '../../shared/utils/logger.js'; +import { query, queryOne, getClient } from '../../../config/database.js'; +import { NotFoundError, ConflictError, ValidationError } from '../../../shared/errors/index.js'; +import { sequencesService, SEQUENCE_CODES } from '../../core/services/sequences.service.js'; +import { logger } from '../../../shared/utils/logger.js'; export type OrderStatus = 'draft' | 'sent' | 'confirmed' | 'done' | 'cancelled'; diff --git a/src/modules/purchases/rfqs.service.ts b/src/modules/purchases/services/rfqs.service.ts similarity index 98% rename from src/modules/purchases/rfqs.service.ts rename to src/modules/purchases/services/rfqs.service.ts index 8c2e72d..df4b86a 100644 --- a/src/modules/purchases/rfqs.service.ts +++ b/src/modules/purchases/services/rfqs.service.ts @@ -1,5 +1,5 @@ -import { query, queryOne, getClient } from '../../config/database.js'; -import { NotFoundError, ValidationError } from '../../shared/errors/index.js'; +import { query, queryOne, getClient } from '../../../config/database.js'; +import { NotFoundError, ValidationError } from '../../../shared/errors/index.js'; export type RfqStatus = 'draft' | 'sent' | 'responded' | 'accepted' | 'rejected' | 'cancelled'; diff --git a/src/modules/reports/reports.controller.ts b/src/modules/reports/reports.controller.ts index 42e0286..b60abcb 100644 --- a/src/modules/reports/reports.controller.ts +++ b/src/modules/reports/reports.controller.ts @@ -1,7 +1,7 @@ import { Response, NextFunction } from 'express'; import { z } from 'zod'; import { AuthenticatedRequest } from '../../shared/types/index.js'; -import { reportsService } from './reports.service.js'; +import { reportsService } from './services/index.js'; // ============================================================================ // VALIDATION SCHEMAS diff --git a/src/modules/reports/reports.service.ts b/src/modules/reports/reports.service.ts deleted file mode 100644 index 717af87..0000000 --- a/src/modules/reports/reports.service.ts +++ /dev/null @@ -1,580 +0,0 @@ -import { query, queryOne, getClient } from '../../config/database.js'; -import { NotFoundError, ValidationError } from '../../shared/errors/index.js'; -import { logger } from '../../shared/utils/logger.js'; - -// ============================================================================ -// TYPES -// ============================================================================ - -export type ReportType = 'financial' | 'accounting' | 'tax' | 'management' | 'custom'; -export type ExecutionStatus = 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'; -export type DeliveryMethod = 'none' | 'email' | 'storage' | 'webhook'; - -export interface ReportDefinition { - id: string; - tenant_id: string; - code: string; - name: string; - description: string | null; - report_type: ReportType; - category: string | null; - base_query: string | null; - query_function: string | null; - parameters_schema: Record; - columns_config: any[]; - grouping_options: string[]; - totals_config: Record; - export_formats: string[]; - pdf_template: string | null; - xlsx_template: string | null; - is_system: boolean; - is_active: boolean; - required_permissions: string[]; - version: number; - created_at: Date; -} - -export interface ReportExecution { - id: string; - tenant_id: string; - definition_id: string; - definition_name?: string; - definition_code?: string; - parameters: Record; - status: ExecutionStatus; - started_at: Date | null; - completed_at: Date | null; - execution_time_ms: number | null; - row_count: number | null; - result_data: any; - result_summary: Record | null; - output_files: any[]; - error_message: string | null; - error_details: Record | null; - requested_by: string; - requested_by_name?: string; - created_at: Date; -} - -export interface ReportSchedule { - id: string; - tenant_id: string; - definition_id: string; - definition_name?: string; - company_id: string | null; - name: string; - default_parameters: Record; - cron_expression: string; - timezone: string; - is_active: boolean; - last_execution_id: string | null; - last_run_at: Date | null; - next_run_at: Date | null; - delivery_method: DeliveryMethod; - delivery_config: Record; - created_at: Date; -} - -export interface CreateReportDefinitionDto { - code: string; - name: string; - description?: string; - report_type?: ReportType; - category?: string; - base_query?: string; - query_function?: string; - parameters_schema?: Record; - columns_config?: any[]; - export_formats?: string[]; - required_permissions?: string[]; -} - -export interface ExecuteReportDto { - definition_id: string; - parameters: Record; -} - -export interface ReportFilters { - report_type?: ReportType; - category?: string; - is_system?: boolean; - search?: string; - page?: number; - limit?: number; -} - -// ============================================================================ -// SERVICE -// ============================================================================ - -class ReportsService { - // ==================== DEFINITIONS ==================== - - async findAllDefinitions( - tenantId: string, - filters: ReportFilters = {} - ): Promise<{ data: ReportDefinition[]; total: number }> { - const { report_type, category, is_system, search, page = 1, limit = 20 } = filters; - const conditions: string[] = ['tenant_id = $1', 'is_active = true']; - const params: any[] = [tenantId]; - let idx = 2; - - if (report_type) { - conditions.push(`report_type = $${idx++}`); - params.push(report_type); - } - - if (category) { - conditions.push(`category = $${idx++}`); - params.push(category); - } - - if (is_system !== undefined) { - conditions.push(`is_system = $${idx++}`); - params.push(is_system); - } - - if (search) { - conditions.push(`(name ILIKE $${idx} OR code ILIKE $${idx} OR description ILIKE $${idx})`); - params.push(`%${search}%`); - idx++; - } - - const whereClause = conditions.join(' AND '); - - const countResult = await queryOne<{ count: string }>( - `SELECT COUNT(*) as count FROM reports.report_definitions WHERE ${whereClause}`, - params - ); - - const offset = (page - 1) * limit; - params.push(limit, offset); - - const data = await query( - `SELECT * FROM reports.report_definitions - WHERE ${whereClause} - ORDER BY is_system DESC, name ASC - LIMIT $${idx} OFFSET $${idx + 1}`, - params - ); - - return { - data, - total: parseInt(countResult?.count || '0', 10), - }; - } - - async findDefinitionById(id: string, tenantId: string): Promise { - const definition = await queryOne( - `SELECT * FROM reports.report_definitions WHERE id = $1 AND tenant_id = $2`, - [id, tenantId] - ); - - if (!definition) { - throw new NotFoundError('Definición de reporte no encontrada'); - } - - return definition; - } - - async findDefinitionByCode(code: string, tenantId: string): Promise { - return queryOne( - `SELECT * FROM reports.report_definitions WHERE code = $1 AND tenant_id = $2`, - [code, tenantId] - ); - } - - async createDefinition( - dto: CreateReportDefinitionDto, - tenantId: string, - userId: string - ): Promise { - const definition = await queryOne( - `INSERT INTO reports.report_definitions ( - tenant_id, code, name, description, report_type, category, - base_query, query_function, parameters_schema, columns_config, - export_formats, required_permissions, created_by - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) - RETURNING *`, - [ - tenantId, - dto.code, - dto.name, - dto.description || null, - dto.report_type || 'custom', - dto.category || null, - dto.base_query || null, - dto.query_function || null, - JSON.stringify(dto.parameters_schema || {}), - JSON.stringify(dto.columns_config || []), - JSON.stringify(dto.export_formats || ['pdf', 'xlsx', 'csv']), - JSON.stringify(dto.required_permissions || []), - userId, - ] - ); - - logger.info('Report definition created', { definitionId: definition?.id, code: dto.code }); - - return definition!; - } - - // ==================== EXECUTIONS ==================== - - async executeReport( - dto: ExecuteReportDto, - tenantId: string, - userId: string - ): Promise { - const definition = await this.findDefinitionById(dto.definition_id, tenantId); - - // Validar parámetros contra el schema - this.validateParameters(dto.parameters, definition.parameters_schema); - - // Crear registro de ejecución - const execution = await queryOne( - `INSERT INTO reports.report_executions ( - tenant_id, definition_id, parameters, status, requested_by - ) VALUES ($1, $2, $3, 'pending', $4) - RETURNING *`, - [tenantId, dto.definition_id, JSON.stringify(dto.parameters), userId] - ); - - // Ejecutar el reporte de forma asíncrona - this.runReportExecution(execution!.id, definition, dto.parameters, tenantId) - .catch(err => logger.error('Report execution failed', { executionId: execution!.id, error: err })); - - return execution!; - } - - private async runReportExecution( - executionId: string, - definition: ReportDefinition, - parameters: Record, - tenantId: string - ): Promise { - const startTime = Date.now(); - - try { - // Marcar como ejecutando - await query( - `UPDATE reports.report_executions SET status = 'running', started_at = NOW() WHERE id = $1`, - [executionId] - ); - - let resultData: any; - let rowCount = 0; - - if (definition.query_function) { - // Ejecutar función PostgreSQL - const funcParams = this.buildFunctionParams(definition.query_function, parameters, tenantId); - resultData = await query( - `SELECT * FROM ${definition.query_function}(${funcParams.placeholders})`, - funcParams.values - ); - rowCount = resultData.length; - } else if (definition.base_query) { - // Ejecutar query base con parámetros sustituidos - // IMPORTANTE: Sanitizar los parámetros para evitar SQL injection - const sanitizedQuery = this.buildSafeQuery(definition.base_query, parameters, tenantId); - resultData = await query(sanitizedQuery.sql, sanitizedQuery.values); - rowCount = resultData.length; - } else { - throw new Error('La definición del reporte no tiene query ni función definida'); - } - - const executionTime = Date.now() - startTime; - - // Calcular resumen si hay config de totales - const resultSummary = this.calculateSummary(resultData, definition.totals_config); - - // Actualizar con resultados - await query( - `UPDATE reports.report_executions - SET status = 'completed', - completed_at = NOW(), - execution_time_ms = $2, - row_count = $3, - result_data = $4, - result_summary = $5 - WHERE id = $1`, - [executionId, executionTime, rowCount, JSON.stringify(resultData), JSON.stringify(resultSummary)] - ); - - logger.info('Report execution completed', { executionId, rowCount, executionTime }); - - } catch (error: any) { - const executionTime = Date.now() - startTime; - - await query( - `UPDATE reports.report_executions - SET status = 'failed', - completed_at = NOW(), - execution_time_ms = $2, - error_message = $3, - error_details = $4 - WHERE id = $1`, - [ - executionId, - executionTime, - error.message, - JSON.stringify({ stack: error.stack }), - ] - ); - - logger.error('Report execution failed', { executionId, error: error.message }); - } - } - - private buildFunctionParams( - functionName: string, - parameters: Record, - tenantId: string - ): { placeholders: string; values: any[] } { - // Construir parámetros para funciones conocidas - const values: any[] = [tenantId]; - let idx = 2; - - if (functionName.includes('trial_balance')) { - values.push( - parameters.company_id || null, - parameters.date_from, - parameters.date_to, - parameters.include_zero || false - ); - return { placeholders: '$1, $2, $3, $4, $5', values }; - } - - if (functionName.includes('general_ledger')) { - values.push( - parameters.company_id || null, - parameters.account_id, - parameters.date_from, - parameters.date_to - ); - return { placeholders: '$1, $2, $3, $4, $5', values }; - } - - // Default: solo tenant_id - return { placeholders: '$1', values }; - } - - private buildSafeQuery( - baseQuery: string, - parameters: Record, - tenantId: string - ): { sql: string; values: any[] } { - // Reemplazar placeholders de forma segura - let sql = baseQuery; - const values: any[] = [tenantId]; - let idx = 2; - - // Reemplazar {{tenant_id}} con $1 - sql = sql.replace(/\{\{tenant_id\}\}/g, '$1'); - - // Reemplazar otros parámetros - for (const [key, value] of Object.entries(parameters)) { - const placeholder = `{{${key}}}`; - if (sql.includes(placeholder)) { - sql = sql.replace(new RegExp(placeholder, 'g'), `$${idx}`); - values.push(value); - idx++; - } - } - - return { sql, values }; - } - - private calculateSummary(data: any[], totalsConfig: Record): Record { - if (!totalsConfig.show_totals || !totalsConfig.total_columns) { - return {}; - } - - const summary: Record = {}; - - for (const column of totalsConfig.total_columns) { - summary[column] = data.reduce((sum, row) => sum + (parseFloat(row[column]) || 0), 0); - } - - return summary; - } - - private validateParameters(params: Record, schema: Record): void { - for (const [key, config] of Object.entries(schema)) { - const paramConfig = config as { required?: boolean; type?: string }; - - if (paramConfig.required && (params[key] === undefined || params[key] === null)) { - throw new ValidationError(`Parámetro requerido: ${key}`); - } - } - } - - async findExecutionById(id: string, tenantId: string): Promise { - const execution = await queryOne( - `SELECT re.*, - rd.name as definition_name, - rd.code as definition_code, - u.full_name as requested_by_name - FROM reports.report_executions re - JOIN reports.report_definitions rd ON re.definition_id = rd.id - JOIN auth.users u ON re.requested_by = u.id - WHERE re.id = $1 AND re.tenant_id = $2`, - [id, tenantId] - ); - - if (!execution) { - throw new NotFoundError('Ejecución de reporte no encontrada'); - } - - return execution; - } - - async findRecentExecutions( - tenantId: string, - definitionId?: string, - limit: number = 20 - ): Promise { - let sql = ` - SELECT re.*, - rd.name as definition_name, - rd.code as definition_code, - u.full_name as requested_by_name - FROM reports.report_executions re - JOIN reports.report_definitions rd ON re.definition_id = rd.id - JOIN auth.users u ON re.requested_by = u.id - WHERE re.tenant_id = $1 - `; - const params: any[] = [tenantId]; - - if (definitionId) { - sql += ` AND re.definition_id = $2`; - params.push(definitionId); - } - - sql += ` ORDER BY re.created_at DESC LIMIT $${params.length + 1}`; - params.push(limit); - - return query(sql, params); - } - - // ==================== SCHEDULES ==================== - - async findAllSchedules(tenantId: string): Promise { - return query( - `SELECT rs.*, - rd.name as definition_name - FROM reports.report_schedules rs - JOIN reports.report_definitions rd ON rs.definition_id = rd.id - WHERE rs.tenant_id = $1 - ORDER BY rs.name`, - [tenantId] - ); - } - - async createSchedule( - data: { - definition_id: string; - name: string; - cron_expression: string; - default_parameters?: Record; - company_id?: string; - timezone?: string; - delivery_method?: DeliveryMethod; - delivery_config?: Record; - }, - tenantId: string, - userId: string - ): Promise { - // Verificar que la definición existe - await this.findDefinitionById(data.definition_id, tenantId); - - const schedule = await queryOne( - `INSERT INTO reports.report_schedules ( - tenant_id, definition_id, name, cron_expression, - default_parameters, company_id, timezone, - delivery_method, delivery_config, created_by - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) - RETURNING *`, - [ - tenantId, - data.definition_id, - data.name, - data.cron_expression, - JSON.stringify(data.default_parameters || {}), - data.company_id || null, - data.timezone || 'America/Mexico_City', - data.delivery_method || 'none', - JSON.stringify(data.delivery_config || {}), - userId, - ] - ); - - logger.info('Report schedule created', { scheduleId: schedule?.id, name: data.name }); - - return schedule!; - } - - async toggleSchedule(id: string, tenantId: string, isActive: boolean): Promise { - const schedule = await queryOne( - `UPDATE reports.report_schedules - SET is_active = $3, updated_at = NOW() - WHERE id = $1 AND tenant_id = $2 - RETURNING *`, - [id, tenantId, isActive] - ); - - if (!schedule) { - throw new NotFoundError('Programación no encontrada'); - } - - return schedule; - } - - async deleteSchedule(id: string, tenantId: string): Promise { - const result = await query( - `DELETE FROM reports.report_schedules WHERE id = $1 AND tenant_id = $2`, - [id, tenantId] - ); - - // Check if any row was deleted - if (!result || result.length === 0) { - // Try to verify it existed - const exists = await queryOne<{ id: string }>( - `SELECT id FROM reports.report_schedules WHERE id = $1`, - [id] - ); - if (!exists) { - throw new NotFoundError('Programación no encontrada'); - } - } - } - - // ==================== QUICK REPORTS ==================== - - async generateTrialBalance( - tenantId: string, - companyId: string | null, - dateFrom: string, - dateTo: string, - includeZero: boolean = false - ): Promise { - return query( - `SELECT * FROM reports.generate_trial_balance($1, $2, $3, $4, $5)`, - [tenantId, companyId, dateFrom, dateTo, includeZero] - ); - } - - async generateGeneralLedger( - tenantId: string, - companyId: string | null, - accountId: string, - dateFrom: string, - dateTo: string - ): Promise { - return query( - `SELECT * FROM reports.generate_general_ledger($1, $2, $3, $4, $5)`, - [tenantId, companyId, accountId, dateFrom, dateTo] - ); - } -} - -export const reportsService = new ReportsService(); diff --git a/src/modules/reports/services/index.ts b/src/modules/reports/services/index.ts index d875321..cbd6549 100644 --- a/src/modules/reports/services/index.ts +++ b/src/modules/reports/services/index.ts @@ -1,8 +1,19 @@ -// Re-export new services -export { ReportsService } from './reports.service'; -export { ReportExecutionService } from './report-execution.service'; -export { ReportSchedulerService } from './report-scheduler.service'; -export { DashboardsService } from './dashboards.service'; +// Re-export services +export { + reportsService, + ReportType, + ExecutionStatus, + DeliveryMethod, + ReportDefinition, + ReportExecution, + ReportSchedule, + CreateReportDefinitionDto, + ExecuteReportDto, + ReportFilters, +} from './reports.service.js'; +export { ReportExecutionService } from './report-execution.service.js'; +export { ReportSchedulerService } from './report-scheduler.service.js'; +export { DashboardsService } from './dashboards.service.js'; // Legacy types and service (kept for backwards compatibility) import { DataSource } from 'typeorm'; diff --git a/src/modules/reports/services/reports.service.ts b/src/modules/reports/services/reports.service.ts index 839ac8c..928bf4d 100644 --- a/src/modules/reports/services/reports.service.ts +++ b/src/modules/reports/services/reports.service.ts @@ -1,510 +1,580 @@ -import { Repository, ILike, FindOptionsWhere, In } from 'typeorm'; -import { - ReportDefinition, - ReportExecution, - ReportSchedule, - ReportRecipient, - CustomReport, - ExecutionStatus, - ReportType, -} from '../entities'; -import { - CreateReportDefinitionDto, - UpdateReportDefinitionDto, - CreateReportScheduleDto, - UpdateReportScheduleDto, - CreateReportRecipientDto, - UpdateReportRecipientDto, - CreateCustomReportDto, - UpdateCustomReportDto, - ReportDefinitionFiltersDto, - ReportScheduleFiltersDto, - CustomReportFiltersDto, -} from '../dto'; -import { NotFoundError, ValidationError, ConflictError } from '../../../shared/errors/index.js'; +import { query, queryOne, getClient } from '../../../config/database.js'; +import { NotFoundError, ValidationError } from '../../../shared/errors/index.js'; import { logger } from '../../../shared/utils/logger.js'; -/** - * ReportsService - * - * Manages report definitions, schedules, recipients, and custom reports. - * Provides CRUD operations with multi-tenant isolation. - */ -export class ReportsService { - constructor( - private readonly definitionRepository: Repository, - private readonly scheduleRepository: Repository, - private readonly recipientRepository: Repository, - private readonly customReportRepository: Repository - ) {} +// ============================================================================ +// TYPES +// ============================================================================ - // ==================== REPORT DEFINITIONS ==================== +export type ReportType = 'financial' | 'accounting' | 'tax' | 'management' | 'custom'; +export type ExecutionStatus = 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'; +export type DeliveryMethod = 'none' | 'email' | 'storage' | 'webhook'; + +export interface ReportDefinition { + id: string; + tenant_id: string; + code: string; + name: string; + description: string | null; + report_type: ReportType; + category: string | null; + base_query: string | null; + query_function: string | null; + parameters_schema: Record; + columns_config: any[]; + grouping_options: string[]; + totals_config: Record; + export_formats: string[]; + pdf_template: string | null; + xlsx_template: string | null; + is_system: boolean; + is_active: boolean; + required_permissions: string[]; + version: number; + created_at: Date; +} + +export interface ReportExecution { + id: string; + tenant_id: string; + definition_id: string; + definition_name?: string; + definition_code?: string; + parameters: Record; + status: ExecutionStatus; + started_at: Date | null; + completed_at: Date | null; + execution_time_ms: number | null; + row_count: number | null; + result_data: any; + result_summary: Record | null; + output_files: any[]; + error_message: string | null; + error_details: Record | null; + requested_by: string; + requested_by_name?: string; + created_at: Date; +} + +export interface ReportSchedule { + id: string; + tenant_id: string; + definition_id: string; + definition_name?: string; + company_id: string | null; + name: string; + default_parameters: Record; + cron_expression: string; + timezone: string; + is_active: boolean; + last_execution_id: string | null; + last_run_at: Date | null; + next_run_at: Date | null; + delivery_method: DeliveryMethod; + delivery_config: Record; + created_at: Date; +} + +export interface CreateReportDefinitionDto { + code: string; + name: string; + description?: string; + report_type?: ReportType; + category?: string; + base_query?: string; + query_function?: string; + parameters_schema?: Record; + columns_config?: any[]; + export_formats?: string[]; + required_permissions?: string[]; +} + +export interface ExecuteReportDto { + definition_id: string; + parameters: Record; +} + +export interface ReportFilters { + report_type?: ReportType; + category?: string; + is_system?: boolean; + search?: string; + page?: number; + limit?: number; +} + +// ============================================================================ +// SERVICE +// ============================================================================ + +class ReportsService { + // ==================== DEFINITIONS ==================== - /** - * Get all report definitions with optional filtering - */ async findAllDefinitions( tenantId: string, - filters: ReportDefinitionFiltersDto = {} + filters: ReportFilters = {} ): Promise<{ data: ReportDefinition[]; total: number }> { - const { reportType, category, isPublic, isActive, search, page = 1, limit = 20 } = filters; + const { report_type, category, is_system, search, page = 1, limit = 20 } = filters; + const conditions: string[] = ['tenant_id = $1', 'is_active = true']; + const params: any[] = [tenantId]; + let idx = 2; - const where: FindOptionsWhere[] = []; - const baseWhere: FindOptionsWhere = { tenantId }; - - if (reportType) { - baseWhere.reportType = reportType; + if (report_type) { + conditions.push(`report_type = $${idx++}`); + params.push(report_type); } if (category) { - baseWhere.category = category; + conditions.push(`category = $${idx++}`); + params.push(category); } - if (isPublic !== undefined) { - baseWhere.isPublic = isPublic; - } - - if (isActive !== undefined) { - baseWhere.isActive = isActive; + if (is_system !== undefined) { + conditions.push(`is_system = $${idx++}`); + params.push(is_system); } if (search) { - where.push( - { ...baseWhere, name: ILike(`%${search}%`) }, - { ...baseWhere, code: ILike(`%${search}%`) }, - { ...baseWhere, description: ILike(`%${search}%`) } - ); - } else { - where.push(baseWhere); + conditions.push(`(name ILIKE $${idx} OR code ILIKE $${idx} OR description ILIKE $${idx})`); + params.push(`%${search}%`); + idx++; } - const [data, total] = await this.definitionRepository.findAndCount({ - where: where.length > 0 ? where : undefined, - order: { category: 'ASC', name: 'ASC' }, - skip: (page - 1) * limit, - take: limit, - }); + const whereClause = conditions.join(' AND '); - return { data, total }; + const countResult = await queryOne<{ count: string }>( + `SELECT COUNT(*) as count FROM reports.report_definitions WHERE ${whereClause}`, + params + ); + + const offset = (page - 1) * limit; + params.push(limit, offset); + + const data = await query( + `SELECT * FROM reports.report_definitions + WHERE ${whereClause} + ORDER BY is_system DESC, name ASC + LIMIT $${idx} OFFSET $${idx + 1}`, + params + ); + + return { + data, + total: parseInt(countResult?.count || '0', 10), + }; } - /** - * Get a report definition by ID - */ async findDefinitionById(id: string, tenantId: string): Promise { - const definition = await this.definitionRepository.findOne({ - where: { id, tenantId }, - }); + const definition = await queryOne( + `SELECT * FROM reports.report_definitions WHERE id = $1 AND tenant_id = $2`, + [id, tenantId] + ); if (!definition) { - throw new NotFoundError('Report definition not found'); + throw new NotFoundError('Definición de reporte no encontrada'); } return definition; } - /** - * Get a report definition by code - */ async findDefinitionByCode(code: string, tenantId: string): Promise { - return this.definitionRepository.findOne({ - where: { code, tenantId }, - }); + return queryOne( + `SELECT * FROM reports.report_definitions WHERE code = $1 AND tenant_id = $2`, + [code, tenantId] + ); } - /** - * Create a new report definition - */ async createDefinition( dto: CreateReportDefinitionDto, tenantId: string, - createdBy: string + userId: string ): Promise { - const existing = await this.findDefinitionByCode(dto.code, tenantId); - if (existing) { - throw new ConflictError(`Report definition with code '${dto.code}' already exists`); - } + const definition = await queryOne( + `INSERT INTO reports.report_definitions ( + tenant_id, code, name, description, report_type, category, + base_query, query_function, parameters_schema, columns_config, + export_formats, required_permissions, created_by + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) + RETURNING *`, + [ + tenantId, + dto.code, + dto.name, + dto.description || null, + dto.report_type || 'custom', + dto.category || null, + dto.base_query || null, + dto.query_function || null, + JSON.stringify(dto.parameters_schema || {}), + JSON.stringify(dto.columns_config || []), + JSON.stringify(dto.export_formats || ['pdf', 'xlsx', 'csv']), + JSON.stringify(dto.required_permissions || []), + userId, + ] + ); - const definition = this.definitionRepository.create({ - ...dto, - tenantId, - createdBy, - }); + logger.info('Report definition created', { definitionId: definition?.id, code: dto.code }); - const saved = await this.definitionRepository.save(definition); - logger.info('Report definition created', { id: saved.id, code: dto.code, tenantId }); - - return saved; + return definition!; } - /** - * Update a report definition - */ - async updateDefinition( - id: string, - dto: UpdateReportDefinitionDto, - tenantId: string - ): Promise { - const definition = await this.findDefinitionById(id, tenantId); + // ==================== EXECUTIONS ==================== - Object.assign(definition, dto); - - const updated = await this.definitionRepository.save(definition); - logger.info('Report definition updated', { id, tenantId }); - - return updated; - } - - /** - * Delete a report definition (soft delete by setting isActive = false) - */ - async deleteDefinition(id: string, tenantId: string): Promise { - const definition = await this.findDefinitionById(id, tenantId); - definition.isActive = false; - await this.definitionRepository.save(definition); - logger.info('Report definition deactivated', { id, tenantId }); - } - - /** - * Get all unique categories - */ - async getCategories(tenantId: string): Promise { - const result = await this.definitionRepository - .createQueryBuilder('def') - .select('DISTINCT def.category', 'category') - .where('def.tenant_id = :tenantId', { tenantId }) - .andWhere('def.category IS NOT NULL') - .andWhere('def.is_active = true') - .orderBy('def.category', 'ASC') - .getRawMany(); - - return result.map((r: { category: string }) => r.category); - } - - // ==================== REPORT SCHEDULES ==================== - - /** - * Get all report schedules with optional filtering - */ - async findAllSchedules( + async executeReport( + dto: ExecuteReportDto, tenantId: string, - filters: ReportScheduleFiltersDto = {} - ): Promise<{ data: ReportSchedule[]; total: number }> { - const { reportDefinitionId, isActive, search, page = 1, limit = 20 } = filters; + userId: string + ): Promise { + const definition = await this.findDefinitionById(dto.definition_id, tenantId); - const where: FindOptionsWhere[] = []; - const baseWhere: FindOptionsWhere = { tenantId }; + // Validar parámetros contra el schema + this.validateParameters(dto.parameters, definition.parameters_schema); - if (reportDefinitionId) { - baseWhere.reportDefinitionId = reportDefinitionId; - } + // Crear registro de ejecución + const execution = await queryOne( + `INSERT INTO reports.report_executions ( + tenant_id, definition_id, parameters, status, requested_by + ) VALUES ($1, $2, $3, 'pending', $4) + RETURNING *`, + [tenantId, dto.definition_id, JSON.stringify(dto.parameters), userId] + ); - if (isActive !== undefined) { - baseWhere.isActive = isActive; - } + // Ejecutar el reporte de forma asíncrona + this.runReportExecution(execution!.id, definition, dto.parameters, tenantId) + .catch(err => logger.error('Report execution failed', { executionId: execution!.id, error: err })); - if (search) { - where.push({ ...baseWhere, name: ILike(`%${search}%`) }); - } else { - where.push(baseWhere); - } - - const [data, total] = await this.scheduleRepository.findAndCount({ - where: where.length > 0 ? where : undefined, - relations: ['reportDefinition'], - order: { name: 'ASC' }, - skip: (page - 1) * limit, - take: limit, - }); - - return { data, total }; + return execution!; } - /** - * Get a report schedule by ID - */ - async findScheduleById(id: string, tenantId: string): Promise { - const schedule = await this.scheduleRepository.findOne({ - where: { id, tenantId }, - relations: ['reportDefinition', 'recipients'], - }); + private async runReportExecution( + executionId: string, + definition: ReportDefinition, + parameters: Record, + tenantId: string + ): Promise { + const startTime = Date.now(); + + try { + // Marcar como ejecutando + await query( + `UPDATE reports.report_executions SET status = 'running', started_at = NOW() WHERE id = $1`, + [executionId] + ); + + let resultData: any; + let rowCount = 0; + + if (definition.query_function) { + // Ejecutar función PostgreSQL + const funcParams = this.buildFunctionParams(definition.query_function, parameters, tenantId); + resultData = await query( + `SELECT * FROM ${definition.query_function}(${funcParams.placeholders})`, + funcParams.values + ); + rowCount = resultData.length; + } else if (definition.base_query) { + // Ejecutar query base con parámetros sustituidos + // IMPORTANTE: Sanitizar los parámetros para evitar SQL injection + const sanitizedQuery = this.buildSafeQuery(definition.base_query, parameters, tenantId); + resultData = await query(sanitizedQuery.sql, sanitizedQuery.values); + rowCount = resultData.length; + } else { + throw new Error('La definición del reporte no tiene query ni función definida'); + } + + const executionTime = Date.now() - startTime; + + // Calcular resumen si hay config de totales + const resultSummary = this.calculateSummary(resultData, definition.totals_config); + + // Actualizar con resultados + await query( + `UPDATE reports.report_executions + SET status = 'completed', + completed_at = NOW(), + execution_time_ms = $2, + row_count = $3, + result_data = $4, + result_summary = $5 + WHERE id = $1`, + [executionId, executionTime, rowCount, JSON.stringify(resultData), JSON.stringify(resultSummary)] + ); + + logger.info('Report execution completed', { executionId, rowCount, executionTime }); + + } catch (error: any) { + const executionTime = Date.now() - startTime; + + await query( + `UPDATE reports.report_executions + SET status = 'failed', + completed_at = NOW(), + execution_time_ms = $2, + error_message = $3, + error_details = $4 + WHERE id = $1`, + [ + executionId, + executionTime, + error.message, + JSON.stringify({ stack: error.stack }), + ] + ); + + logger.error('Report execution failed', { executionId, error: error.message }); + } + } + + private buildFunctionParams( + functionName: string, + parameters: Record, + tenantId: string + ): { placeholders: string; values: any[] } { + // Construir parámetros para funciones conocidas + const values: any[] = [tenantId]; + let idx = 2; + + if (functionName.includes('trial_balance')) { + values.push( + parameters.company_id || null, + parameters.date_from, + parameters.date_to, + parameters.include_zero || false + ); + return { placeholders: '$1, $2, $3, $4, $5', values }; + } + + if (functionName.includes('general_ledger')) { + values.push( + parameters.company_id || null, + parameters.account_id, + parameters.date_from, + parameters.date_to + ); + return { placeholders: '$1, $2, $3, $4, $5', values }; + } + + // Default: solo tenant_id + return { placeholders: '$1', values }; + } + + private buildSafeQuery( + baseQuery: string, + parameters: Record, + tenantId: string + ): { sql: string; values: any[] } { + // Reemplazar placeholders de forma segura + let sql = baseQuery; + const values: any[] = [tenantId]; + let idx = 2; + + // Reemplazar {{tenant_id}} con $1 + sql = sql.replace(/\{\{tenant_id\}\}/g, '$1'); + + // Reemplazar otros parámetros + for (const [key, value] of Object.entries(parameters)) { + const placeholder = `{{${key}}}`; + if (sql.includes(placeholder)) { + sql = sql.replace(new RegExp(placeholder, 'g'), `$${idx}`); + values.push(value); + idx++; + } + } + + return { sql, values }; + } + + private calculateSummary(data: any[], totalsConfig: Record): Record { + if (!totalsConfig.show_totals || !totalsConfig.total_columns) { + return {}; + } + + const summary: Record = {}; + + for (const column of totalsConfig.total_columns) { + summary[column] = data.reduce((sum, row) => sum + (parseFloat(row[column]) || 0), 0); + } + + return summary; + } + + private validateParameters(params: Record, schema: Record): void { + for (const [key, config] of Object.entries(schema)) { + const paramConfig = config as { required?: boolean; type?: string }; + + if (paramConfig.required && (params[key] === undefined || params[key] === null)) { + throw new ValidationError(`Parámetro requerido: ${key}`); + } + } + } + + async findExecutionById(id: string, tenantId: string): Promise { + const execution = await queryOne( + `SELECT re.*, + rd.name as definition_name, + rd.code as definition_code, + u.full_name as requested_by_name + FROM reports.report_executions re + JOIN reports.report_definitions rd ON re.definition_id = rd.id + JOIN auth.users u ON re.requested_by = u.id + WHERE re.id = $1 AND re.tenant_id = $2`, + [id, tenantId] + ); + + if (!execution) { + throw new NotFoundError('Ejecución de reporte no encontrada'); + } + + return execution; + } + + async findRecentExecutions( + tenantId: string, + definitionId?: string, + limit: number = 20 + ): Promise { + let sql = ` + SELECT re.*, + rd.name as definition_name, + rd.code as definition_code, + u.full_name as requested_by_name + FROM reports.report_executions re + JOIN reports.report_definitions rd ON re.definition_id = rd.id + JOIN auth.users u ON re.requested_by = u.id + WHERE re.tenant_id = $1 + `; + const params: any[] = [tenantId]; + + if (definitionId) { + sql += ` AND re.definition_id = $2`; + params.push(definitionId); + } + + sql += ` ORDER BY re.created_at DESC LIMIT $${params.length + 1}`; + params.push(limit); + + return query(sql, params); + } + + // ==================== SCHEDULES ==================== + + async findAllSchedules(tenantId: string): Promise { + return query( + `SELECT rs.*, + rd.name as definition_name + FROM reports.report_schedules rs + JOIN reports.report_definitions rd ON rs.definition_id = rd.id + WHERE rs.tenant_id = $1 + ORDER BY rs.name`, + [tenantId] + ); + } + + async createSchedule( + data: { + definition_id: string; + name: string; + cron_expression: string; + default_parameters?: Record; + company_id?: string; + timezone?: string; + delivery_method?: DeliveryMethod; + delivery_config?: Record; + }, + tenantId: string, + userId: string + ): Promise { + // Verificar que la definición existe + await this.findDefinitionById(data.definition_id, tenantId); + + const schedule = await queryOne( + `INSERT INTO reports.report_schedules ( + tenant_id, definition_id, name, cron_expression, + default_parameters, company_id, timezone, + delivery_method, delivery_config, created_by + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) + RETURNING *`, + [ + tenantId, + data.definition_id, + data.name, + data.cron_expression, + JSON.stringify(data.default_parameters || {}), + data.company_id || null, + data.timezone || 'America/Mexico_City', + data.delivery_method || 'none', + JSON.stringify(data.delivery_config || {}), + userId, + ] + ); + + logger.info('Report schedule created', { scheduleId: schedule?.id, name: data.name }); + + return schedule!; + } + + async toggleSchedule(id: string, tenantId: string, isActive: boolean): Promise { + const schedule = await queryOne( + `UPDATE reports.report_schedules + SET is_active = $3, updated_at = NOW() + WHERE id = $1 AND tenant_id = $2 + RETURNING *`, + [id, tenantId, isActive] + ); if (!schedule) { - throw new NotFoundError('Report schedule not found'); + throw new NotFoundError('Programación no encontrada'); } return schedule; } - /** - * Create a new report schedule - */ - async createSchedule( - dto: CreateReportScheduleDto, - tenantId: string, - createdBy: string - ): Promise { - // Verify definition exists - await this.findDefinitionById(dto.reportDefinitionId, tenantId); - - const schedule = this.scheduleRepository.create({ - ...dto, - tenantId, - createdBy, - }); - - const saved = await this.scheduleRepository.save(schedule); - logger.info('Report schedule created', { id: saved.id, name: dto.name, tenantId }); - - return saved; - } - - /** - * Update a report schedule - */ - async updateSchedule( - id: string, - dto: UpdateReportScheduleDto, - tenantId: string - ): Promise { - const schedule = await this.findScheduleById(id, tenantId); - - Object.assign(schedule, dto); - - const updated = await this.scheduleRepository.save(schedule); - logger.info('Report schedule updated', { id, tenantId }); - - return updated; - } - - /** - * Toggle schedule active status - */ - async toggleSchedule(id: string, tenantId: string, isActive: boolean): Promise { - const schedule = await this.findScheduleById(id, tenantId); - schedule.isActive = isActive; - - const updated = await this.scheduleRepository.save(schedule); - logger.info('Report schedule toggled', { id, isActive, tenantId }); - - return updated; - } - - /** - * Delete a report schedule - */ async deleteSchedule(id: string, tenantId: string): Promise { - const schedule = await this.findScheduleById(id, tenantId); - await this.scheduleRepository.remove(schedule); - logger.info('Report schedule deleted', { id, tenantId }); - } + const result = await query( + `DELETE FROM reports.report_schedules WHERE id = $1 AND tenant_id = $2`, + [id, tenantId] + ); - /** - * Get schedules due for execution - */ - async getDueSchedules(): Promise { - return this.scheduleRepository - .createQueryBuilder('schedule') - .where('schedule.is_active = true') - .andWhere('schedule.next_run_at <= NOW()') - .leftJoinAndSelect('schedule.reportDefinition', 'definition') - .leftJoinAndSelect('schedule.recipients', 'recipients') - .getMany(); - } - - // ==================== REPORT RECIPIENTS ==================== - - /** - * Get recipients for a schedule - */ - async findRecipientsBySchedule(scheduleId: string): Promise { - return this.recipientRepository.find({ - where: { scheduleId }, - order: { createdAt: 'ASC' }, - }); - } - - /** - * Add a recipient to a schedule - */ - async addRecipient( - dto: CreateReportRecipientDto, - tenantId: string - ): Promise { - // Verify schedule exists and belongs to tenant - await this.findScheduleById(dto.scheduleId, tenantId); - - if (!dto.userId && !dto.email) { - throw new ValidationError('Either userId or email is required'); - } - - const recipient = this.recipientRepository.create(dto); - const saved = await this.recipientRepository.save(recipient); - logger.info('Report recipient added', { id: saved.id, scheduleId: dto.scheduleId }); - - return saved; - } - - /** - * Update a recipient - */ - async updateRecipient( - id: string, - dto: UpdateReportRecipientDto - ): Promise { - const recipient = await this.recipientRepository.findOne({ where: { id } }); - - if (!recipient) { - throw new NotFoundError('Report recipient not found'); - } - - Object.assign(recipient, dto); - - const updated = await this.recipientRepository.save(recipient); - logger.info('Report recipient updated', { id }); - - return updated; - } - - /** - * Remove a recipient - */ - async removeRecipient(id: string): Promise { - const recipient = await this.recipientRepository.findOne({ where: { id } }); - - if (!recipient) { - throw new NotFoundError('Report recipient not found'); - } - - await this.recipientRepository.remove(recipient); - logger.info('Report recipient removed', { id }); - } - - // ==================== CUSTOM REPORTS ==================== - - /** - * Get all custom reports for a user - */ - async findAllCustomReports( - tenantId: string, - ownerId: string, - filters: CustomReportFiltersDto = {} - ): Promise<{ data: CustomReport[]; total: number }> { - const { baseDefinitionId, isFavorite, search, page = 1, limit = 20 } = filters; - - const where: FindOptionsWhere[] = []; - const baseWhere: FindOptionsWhere = { tenantId, ownerId }; - - if (baseDefinitionId) { - baseWhere.baseDefinitionId = baseDefinitionId; - } - - if (isFavorite !== undefined) { - baseWhere.isFavorite = isFavorite; - } - - if (search) { - where.push( - { ...baseWhere, name: ILike(`%${search}%`) }, - { ...baseWhere, description: ILike(`%${search}%`) } + // Check if any row was deleted + if (!result || result.length === 0) { + // Try to verify it existed + const exists = await queryOne<{ id: string }>( + `SELECT id FROM reports.report_schedules WHERE id = $1`, + [id] ); - } else { - where.push(baseWhere); + if (!exists) { + throw new NotFoundError('Programación no encontrada'); + } } - - const [data, total] = await this.customReportRepository.findAndCount({ - where: where.length > 0 ? where : undefined, - relations: ['baseDefinition'], - order: { isFavorite: 'DESC', name: 'ASC' }, - skip: (page - 1) * limit, - take: limit, - }); - - return { data, total }; } - /** - * Get a custom report by ID - */ - async findCustomReportById(id: string, tenantId: string, ownerId: string): Promise { - const customReport = await this.customReportRepository.findOne({ - where: { id, tenantId, ownerId }, - relations: ['baseDefinition'], - }); + // ==================== QUICK REPORTS ==================== - if (!customReport) { - throw new NotFoundError('Custom report not found'); - } - - return customReport; - } - - /** - * Create a custom report - */ - async createCustomReport( - dto: CreateCustomReportDto, + async generateTrialBalance( tenantId: string, - ownerId: string - ): Promise { - if (dto.baseDefinitionId) { - await this.findDefinitionById(dto.baseDefinitionId, tenantId); - } - - const customReport = this.customReportRepository.create({ - ...dto, - tenantId, - ownerId, - }); - - const saved = await this.customReportRepository.save(customReport); - logger.info('Custom report created', { id: saved.id, name: dto.name, tenantId, ownerId }); - - return saved; + companyId: string | null, + dateFrom: string, + dateTo: string, + includeZero: boolean = false + ): Promise { + return query( + `SELECT * FROM reports.generate_trial_balance($1, $2, $3, $4, $5)`, + [tenantId, companyId, dateFrom, dateTo, includeZero] + ); } - /** - * Update a custom report - */ - async updateCustomReport( - id: string, - dto: UpdateCustomReportDto, + async generateGeneralLedger( tenantId: string, - ownerId: string - ): Promise { - const customReport = await this.findCustomReportById(id, tenantId, ownerId); - - if (dto.baseDefinitionId) { - await this.findDefinitionById(dto.baseDefinitionId, tenantId); - } - - Object.assign(customReport, dto); - - const updated = await this.customReportRepository.save(customReport); - logger.info('Custom report updated', { id, tenantId, ownerId }); - - return updated; - } - - /** - * Toggle favorite status - */ - async toggleFavorite(id: string, tenantId: string, ownerId: string): Promise { - const customReport = await this.findCustomReportById(id, tenantId, ownerId); - customReport.isFavorite = !customReport.isFavorite; - - const updated = await this.customReportRepository.save(customReport); - logger.info('Custom report favorite toggled', { id, isFavorite: customReport.isFavorite }); - - return updated; - } - - /** - * Delete a custom report - */ - async deleteCustomReport(id: string, tenantId: string, ownerId: string): Promise { - const customReport = await this.findCustomReportById(id, tenantId, ownerId); - await this.customReportRepository.remove(customReport); - logger.info('Custom report deleted', { id, tenantId, ownerId }); + companyId: string | null, + accountId: string, + dateFrom: string, + dateTo: string + ): Promise { + return query( + `SELECT * FROM reports.generate_general_ledger($1, $2, $3, $4, $5)`, + [tenantId, companyId, accountId, dateFrom, dateTo] + ); } } + +export const reportsService = new ReportsService(); diff --git a/src/modules/roles/index.ts b/src/modules/roles/index.ts index 1bf9c73..e665cb5 100644 --- a/src/modules/roles/index.ts +++ b/src/modules/roles/index.ts @@ -1,6 +1,6 @@ // Roles module exports -export { rolesService } from './roles.service.js'; -export { permissionsService } from './permissions.service.js'; +export { rolesService } from './services/roles.service.js'; +export { permissionsService } from './services/permissions.service.js'; export { rolesController } from './roles.controller.js'; export { permissionsController } from './permissions.controller.js'; @@ -9,5 +9,5 @@ export { default as rolesRoutes } from './roles.routes.js'; export { default as permissionsRoutes } from './permissions.routes.js'; // Types -export type { CreateRoleDto, UpdateRoleDto, RoleWithPermissions } from './roles.service.js'; -export type { PermissionFilter, EffectivePermission } from './permissions.service.js'; +export type { CreateRoleDto, UpdateRoleDto, RoleWithPermissions } from './services/roles.service.js'; +export type { PermissionFilter, EffectivePermission } from './services/permissions.service.js'; diff --git a/src/modules/roles/permissions.controller.ts b/src/modules/roles/permissions.controller.ts index b91c808..71ad860 100644 --- a/src/modules/roles/permissions.controller.ts +++ b/src/modules/roles/permissions.controller.ts @@ -1,6 +1,6 @@ import { Response, NextFunction } from 'express'; import { z } from 'zod'; -import { permissionsService } from './permissions.service.js'; +import { permissionsService } from './services/permissions.service.js'; import { PermissionAction } from '../auth/entities/index.js'; import { ApiResponse, AuthenticatedRequest, ValidationError, PaginationParams } from '../../shared/types/index.js'; diff --git a/src/modules/roles/roles.controller.ts b/src/modules/roles/roles.controller.ts index 578ce5c..31cb83c 100644 --- a/src/modules/roles/roles.controller.ts +++ b/src/modules/roles/roles.controller.ts @@ -1,6 +1,6 @@ import { Response, NextFunction } from 'express'; import { z } from 'zod'; -import { rolesService } from './roles.service.js'; +import { rolesService } from './services/roles.service.js'; import { ApiResponse, AuthenticatedRequest, ValidationError, PaginationParams } from '../../shared/types/index.js'; // Validation schemas diff --git a/src/modules/roles/services/index.ts b/src/modules/roles/services/index.ts new file mode 100644 index 0000000..ca96654 --- /dev/null +++ b/src/modules/roles/services/index.ts @@ -0,0 +1,3 @@ +// Roles services barrel export +export { rolesService, CreateRoleDto, UpdateRoleDto, RoleWithPermissions } from './roles.service.js'; +export { permissionsService, PermissionFilter, EffectivePermission } from './permissions.service.js'; diff --git a/src/modules/roles/permissions.service.ts b/src/modules/roles/services/permissions.service.ts similarity index 97% rename from src/modules/roles/permissions.service.ts rename to src/modules/roles/services/permissions.service.ts index 5d5a314..f94754d 100644 --- a/src/modules/roles/permissions.service.ts +++ b/src/modules/roles/services/permissions.service.ts @@ -1,8 +1,8 @@ import { Repository, In } from 'typeorm'; -import { AppDataSource } from '../../config/typeorm.js'; -import { Permission, PermissionAction, Role, User } from '../auth/entities/index.js'; -import { PaginationParams } from '../../shared/types/index.js'; -import { logger } from '../../shared/utils/logger.js'; +import { AppDataSource } from '../../../config/typeorm.js'; +import { Permission, PermissionAction, Role, User } from '../../auth/entities/index.js'; +import { PaginationParams } from '../../../shared/types/index.js'; +import { logger } from '../../../shared/utils/logger.js'; // ===== Interfaces ===== diff --git a/src/modules/roles/roles.service.ts b/src/modules/roles/services/roles.service.ts similarity index 97% rename from src/modules/roles/roles.service.ts rename to src/modules/roles/services/roles.service.ts index 5d24572..58ed2f8 100644 --- a/src/modules/roles/roles.service.ts +++ b/src/modules/roles/services/roles.service.ts @@ -1,8 +1,8 @@ import { Repository, In } from 'typeorm'; -import { AppDataSource } from '../../config/typeorm.js'; -import { Role, Permission } from '../auth/entities/index.js'; -import { PaginationParams, NotFoundError, ValidationError, ForbiddenError } from '../../shared/types/index.js'; -import { logger } from '../../shared/utils/logger.js'; +import { AppDataSource } from '../../../config/typeorm.js'; +import { Role, Permission } from '../../auth/entities/index.js'; +import { PaginationParams, NotFoundError, ValidationError, ForbiddenError } from '../../../shared/types/index.js'; +import { logger } from '../../../shared/utils/logger.js'; // ===== Interfaces ===== diff --git a/src/modules/sales/sales.controller.ts b/src/modules/sales/sales.controller.ts index efd8a83..e95b46c 100644 --- a/src/modules/sales/sales.controller.ts +++ b/src/modules/sales/sales.controller.ts @@ -1,10 +1,10 @@ import { Response, NextFunction } from 'express'; import { z } from 'zod'; -import { pricelistsService, CreatePricelistDto, UpdatePricelistDto, CreatePricelistItemDto, PricelistFilters } from './pricelists.service.js'; -import { salesTeamsService, CreateSalesTeamDto, UpdateSalesTeamDto, SalesTeamFilters } from './sales-teams.service.js'; -import { customerGroupsService, CreateCustomerGroupDto, UpdateCustomerGroupDto, CustomerGroupFilters } from './customer-groups.service.js'; -import { quotationsService, CreateQuotationDto, UpdateQuotationDto, CreateQuotationLineDto, UpdateQuotationLineDto, QuotationFilters } from './quotations.service.js'; -import { ordersService, CreateSalesOrderDto, UpdateSalesOrderDto, CreateSalesOrderLineDto, UpdateSalesOrderLineDto, SalesOrderFilters } from './orders.service.js'; +import { pricelistsService, CreatePricelistDto, UpdatePricelistDto, CreatePricelistItemDto, PricelistFilters } from './services/pricelists.service.js'; +import { salesTeamsService, CreateSalesTeamDto, UpdateSalesTeamDto, SalesTeamFilters } from './services/sales-teams.service.js'; +import { customerGroupsService, CreateCustomerGroupDto, UpdateCustomerGroupDto, CustomerGroupFilters } from './services/customer-groups.service.js'; +import { quotationsService, CreateQuotationDto, UpdateQuotationDto, CreateQuotationLineDto, UpdateQuotationLineDto, QuotationFilters } from './services/quotations.service.js'; +import { ordersService, CreateSalesOrderDto, UpdateSalesOrderDto, CreateSalesOrderLineDto, UpdateSalesOrderLineDto, SalesOrderFilters } from './services/orders.service.js'; import { AuthenticatedRequest } from '../../shared/middleware/auth.middleware.js'; import { ValidationError } from '../../shared/errors/index.js'; diff --git a/src/modules/sales/customer-groups.service.ts b/src/modules/sales/services/customer-groups.service.ts similarity index 97% rename from src/modules/sales/customer-groups.service.ts rename to src/modules/sales/services/customer-groups.service.ts index 5a16503..6775933 100644 --- a/src/modules/sales/customer-groups.service.ts +++ b/src/modules/sales/services/customer-groups.service.ts @@ -1,5 +1,5 @@ -import { query, queryOne } from '../../config/database.js'; -import { NotFoundError, ConflictError } from '../../shared/errors/index.js'; +import { query, queryOne } from '../../../config/database.js'; +import { NotFoundError, ConflictError } from '../../../shared/errors/index.js'; export interface CustomerGroupMember { id: string; diff --git a/src/modules/sales/services/index.ts b/src/modules/sales/services/index.ts index 29d721c..1d5cdce 100644 --- a/src/modules/sales/services/index.ts +++ b/src/modules/sales/services/index.ts @@ -2,8 +2,15 @@ import { Repository, FindOptionsWhere, ILike } from 'typeorm'; import { Quotation, SalesOrder } from '../entities/index.js'; import { CreateQuotationDto, UpdateQuotationDto, CreateSalesOrderDto, UpdateSalesOrderDto } from '../dto/index.js'; +// Export SQL-based services (used by controllers) +export { pricelistsService, CreatePricelistDto, UpdatePricelistDto, CreatePricelistItemDto, PricelistFilters } from './pricelists.service.js'; +export { salesTeamsService, CreateSalesTeamDto, UpdateSalesTeamDto, SalesTeamFilters } from './sales-teams.service.js'; +export { customerGroupsService, CreateCustomerGroupDto, UpdateCustomerGroupDto, CustomerGroupFilters } from './customer-groups.service.js'; +export { quotationsService, CreateQuotationDto as SQLCreateQuotationDto, UpdateQuotationDto as SQLUpdateQuotationDto, CreateQuotationLineDto, UpdateQuotationLineDto, QuotationFilters } from './quotations.service.js'; +export { ordersService, CreateSalesOrderDto as SQLCreateSalesOrderDto, UpdateSalesOrderDto as SQLUpdateSalesOrderDto, CreateSalesOrderLineDto, UpdateSalesOrderLineDto, SalesOrderFilters } from './orders.service.js'; + /** - * @deprecated Use ordersService from '../orders.service.js' for full Order-to-Cash flow + * @deprecated Use ordersService from './orders.service.js' for full Order-to-Cash flow * This TypeORM-based service provides basic CRUD operations. * For advanced features (stock reservation, auto-picking, delivery tracking), * use the SQL-based ordersService instead. diff --git a/src/modules/sales/orders.service.ts b/src/modules/sales/services/orders.service.ts similarity index 98% rename from src/modules/sales/orders.service.ts rename to src/modules/sales/services/orders.service.ts index 1caeb92..679c865 100644 --- a/src/modules/sales/orders.service.ts +++ b/src/modules/sales/services/orders.service.ts @@ -1,9 +1,9 @@ -import { query, queryOne, getClient } from '../../config/database.js'; -import { NotFoundError, ConflictError, ValidationError } from '../../shared/errors/index.js'; -import { taxesService } from '../financial/taxes.service.js'; -import { sequencesService, SEQUENCE_CODES } from '../core/sequences.service.js'; -import { stockReservationService, ReservationLine } from '../inventory/stock-reservation.service.js'; -import { logger } from '../../shared/utils/logger.js'; +import { query, queryOne, getClient } from '../../../config/database.js'; +import { NotFoundError, ConflictError, ValidationError } from '../../../shared/errors/index.js'; +import { taxesService } from '../../financial/services/taxes.service.js'; +import { sequencesService, SEQUENCE_CODES } from '../../core/services/sequences.service.js'; +import { stockReservationService, ReservationLine } from '../../inventory/services/stock-reservation.service.js'; +import { logger } from '../../../shared/utils/logger.js'; export interface SalesOrderLine { id: string; diff --git a/src/modules/sales/pricelists.service.ts b/src/modules/sales/services/pricelists.service.ts similarity index 98% rename from src/modules/sales/pricelists.service.ts rename to src/modules/sales/services/pricelists.service.ts index edbe75f..d42e4a8 100644 --- a/src/modules/sales/pricelists.service.ts +++ b/src/modules/sales/services/pricelists.service.ts @@ -1,5 +1,5 @@ -import { query, queryOne, getClient } from '../../config/database.js'; -import { NotFoundError, ConflictError, ValidationError } from '../../shared/errors/index.js'; +import { query, queryOne, getClient } from '../../../config/database.js'; +import { NotFoundError, ConflictError, ValidationError } from '../../../shared/errors/index.js'; export interface PricelistItem { id: string; diff --git a/src/modules/sales/quotations.service.ts b/src/modules/sales/services/quotations.service.ts similarity index 99% rename from src/modules/sales/quotations.service.ts rename to src/modules/sales/services/quotations.service.ts index 9485e14..66e9ed7 100644 --- a/src/modules/sales/quotations.service.ts +++ b/src/modules/sales/services/quotations.service.ts @@ -1,6 +1,6 @@ -import { query, queryOne, getClient } from '../../config/database.js'; -import { NotFoundError, ConflictError, ValidationError } from '../../shared/errors/index.js'; -import { taxesService } from '../financial/taxes.service.js'; +import { query, queryOne, getClient } from '../../../config/database.js'; +import { NotFoundError, ConflictError, ValidationError } from '../../../shared/errors/index.js'; +import { taxesService } from '../../financial/services/taxes.service.js'; export interface QuotationLine { id: string; diff --git a/src/modules/sales/sales-teams.service.ts b/src/modules/sales/services/sales-teams.service.ts similarity index 97% rename from src/modules/sales/sales-teams.service.ts rename to src/modules/sales/services/sales-teams.service.ts index b9185b5..04967cb 100644 --- a/src/modules/sales/sales-teams.service.ts +++ b/src/modules/sales/services/sales-teams.service.ts @@ -1,5 +1,5 @@ -import { query, queryOne } from '../../config/database.js'; -import { NotFoundError, ConflictError } from '../../shared/errors/index.js'; +import { query, queryOne } from '../../../config/database.js'; +import { NotFoundError, ConflictError } from '../../../shared/errors/index.js'; export interface SalesTeamMember { id: string; diff --git a/src/modules/system/index.ts b/src/modules/system/index.ts index 7a4c7a1..48e0fb8 100644 --- a/src/modules/system/index.ts +++ b/src/modules/system/index.ts @@ -1,5 +1,5 @@ -export * from './messages.service.js'; -export * from './notifications.service.js'; -export * from './activities.service.js'; +export * from './services/messages.service.js'; +export * from './services/notifications.service.js'; +export * from './services/activities.service.js'; export * from './system.controller.js'; export { default as systemRoutes } from './system.routes.js'; diff --git a/src/modules/system/activities.service.ts b/src/modules/system/services/activities.service.ts similarity index 98% rename from src/modules/system/activities.service.ts rename to src/modules/system/services/activities.service.ts index abdce3e..101f201 100644 --- a/src/modules/system/activities.service.ts +++ b/src/modules/system/services/activities.service.ts @@ -1,5 +1,5 @@ -import { query, queryOne } from '../../config/database.js'; -import { NotFoundError, ValidationError } from '../../shared/errors/index.js'; +import { query, queryOne } from '../../../config/database.js'; +import { NotFoundError, ValidationError } from '../../../shared/errors/index.js'; export interface Activity { id: string; diff --git a/src/modules/system/messages.service.ts b/src/modules/system/services/messages.service.ts similarity index 98% rename from src/modules/system/messages.service.ts rename to src/modules/system/services/messages.service.ts index d0a64f3..ab17c37 100644 --- a/src/modules/system/messages.service.ts +++ b/src/modules/system/services/messages.service.ts @@ -1,5 +1,5 @@ -import { query, queryOne } from '../../config/database.js'; -import { NotFoundError } from '../../shared/errors/index.js'; +import { query, queryOne } from '../../../config/database.js'; +import { NotFoundError } from '../../../shared/errors/index.js'; export interface Message { id: string; diff --git a/src/modules/system/notifications.service.ts b/src/modules/system/services/notifications.service.ts similarity index 97% rename from src/modules/system/notifications.service.ts rename to src/modules/system/services/notifications.service.ts index 1b023e8..eabbd8f 100644 --- a/src/modules/system/notifications.service.ts +++ b/src/modules/system/services/notifications.service.ts @@ -1,5 +1,5 @@ -import { query, queryOne } from '../../config/database.js'; -import { NotFoundError } from '../../shared/errors/index.js'; +import { query, queryOne } from '../../../config/database.js'; +import { NotFoundError } from '../../../shared/errors/index.js'; export interface Notification { id: string; diff --git a/src/modules/system/system.controller.ts b/src/modules/system/system.controller.ts index 5ee4413..94b4f80 100644 --- a/src/modules/system/system.controller.ts +++ b/src/modules/system/system.controller.ts @@ -1,8 +1,8 @@ import { Response, NextFunction } from 'express'; import { z } from 'zod'; -import { messagesService, CreateMessageDto, MessageFilters, AddFollowerDto } from './messages.service.js'; -import { notificationsService, CreateNotificationDto, NotificationFilters } from './notifications.service.js'; -import { activitiesService, CreateActivityDto, UpdateActivityDto, ActivityFilters } from './activities.service.js'; +import { messagesService, CreateMessageDto, MessageFilters, AddFollowerDto } from './services/messages.service.js'; +import { notificationsService, CreateNotificationDto, NotificationFilters } from './services/notifications.service.js'; +import { activitiesService, CreateActivityDto, UpdateActivityDto, ActivityFilters } from './services/activities.service.js'; import { AuthenticatedRequest } from '../../shared/middleware/auth.middleware.js'; import { ValidationError } from '../../shared/errors/index.js'; diff --git a/src/modules/tenants/index.ts b/src/modules/tenants/index.ts index de1b03d..e11a800 100644 --- a/src/modules/tenants/index.ts +++ b/src/modules/tenants/index.ts @@ -1,7 +1,7 @@ // Tenants module exports -export { tenantsService } from './tenants.service.js'; +export { tenantsService } from './services/tenants.service.js'; export { tenantsController } from './tenants.controller.js'; export { default as tenantsRoutes } from './tenants.routes.js'; // Types -export type { CreateTenantDto, UpdateTenantDto, TenantStats, TenantWithStats } from './tenants.service.js'; +export type { CreateTenantDto, UpdateTenantDto, TenantStats, TenantWithStats } from './services/tenants.service.js'; diff --git a/src/modules/tenants/tenants.service.ts b/src/modules/tenants/services/tenants.service.ts similarity index 98% rename from src/modules/tenants/tenants.service.ts rename to src/modules/tenants/services/tenants.service.ts index ca2bbfa..a5a5687 100644 --- a/src/modules/tenants/tenants.service.ts +++ b/src/modules/tenants/services/tenants.service.ts @@ -1,8 +1,8 @@ import { Repository } from 'typeorm'; -import { AppDataSource } from '../../config/typeorm.js'; -import { Tenant, TenantStatus, User, UserStatus, Company, Role } from '../auth/entities/index.js'; -import { PaginationParams, NotFoundError, ValidationError, ForbiddenError } from '../../shared/types/index.js'; -import { logger } from '../../shared/utils/logger.js'; +import { AppDataSource } from '../../../config/typeorm.js'; +import { Tenant, TenantStatus, User, UserStatus, Company, Role } from '../../auth/entities/index.js'; +import { PaginationParams, NotFoundError, ValidationError, ForbiddenError } from '../../../shared/types/index.js'; +import { logger } from '../../../shared/utils/logger.js'; // ===== Interfaces ===== diff --git a/src/modules/tenants/tenants.controller.ts b/src/modules/tenants/tenants.controller.ts index 6f02fb0..a8fce8c 100644 --- a/src/modules/tenants/tenants.controller.ts +++ b/src/modules/tenants/tenants.controller.ts @@ -1,6 +1,6 @@ import { Response, NextFunction } from 'express'; import { z } from 'zod'; -import { tenantsService } from './tenants.service.js'; +import { tenantsService } from './services/tenants.service.js'; import { TenantStatus } from '../auth/entities/index.js'; import { ApiResponse, AuthenticatedRequest, ValidationError, PaginationParams } from '../../shared/types/index.js'; diff --git a/src/modules/users/index.ts b/src/modules/users/index.ts index e7fab79..343419c 100644 --- a/src/modules/users/index.ts +++ b/src/modules/users/index.ts @@ -1,3 +1,3 @@ -export * from './users.service.js'; +export * from './services/users.service.js'; export * from './users.controller.js'; export { default as usersRoutes } from './users.routes.js'; diff --git a/src/modules/users/users.service.ts b/src/modules/users/services/users.service.ts similarity index 96% rename from src/modules/users/users.service.ts rename to src/modules/users/services/users.service.ts index a2f63c9..bf70a1e 100644 --- a/src/modules/users/users.service.ts +++ b/src/modules/users/services/users.service.ts @@ -1,10 +1,10 @@ import bcrypt from 'bcryptjs'; import { Repository, IsNull } from 'typeorm'; -import { AppDataSource } from '../../config/typeorm.js'; -import { User, UserStatus, Role } from '../auth/entities/index.js'; -import { NotFoundError, ValidationError } from '../../shared/types/index.js'; -import { logger } from '../../shared/utils/logger.js'; -import { splitFullName, buildFullName } from '../auth/auth.service.js'; +import { AppDataSource } from '../../../config/typeorm.js'; +import { User, UserStatus, Role } from '../../auth/entities/index.js'; +import { NotFoundError, ValidationError } from '../../../shared/types/index.js'; +import { logger } from '../../../shared/utils/logger.js'; +import { splitFullName, buildFullName } from '../../auth/services/auth.service.js'; export interface CreateUserDto { tenant_id: string; diff --git a/src/modules/users/users.controller.ts b/src/modules/users/users.controller.ts index 6c45d84..ef00075 100644 --- a/src/modules/users/users.controller.ts +++ b/src/modules/users/users.controller.ts @@ -1,6 +1,6 @@ import { Response, NextFunction } from 'express'; import { z } from 'zod'; -import { usersService } from './users.service.js'; +import { usersService } from './services/users.service.js'; import { ApiResponse, AuthenticatedRequest, ValidationError, PaginationParams } from '../../shared/types/index.js'; const createUserSchema = z.object({ diff --git a/src/modules/warehouses/__tests__/warehouses.service.test.ts b/src/modules/warehouses/__tests__/warehouses.service.test.ts index 1daa93d..f96ad9e 100644 --- a/src/modules/warehouses/__tests__/warehouses.service.test.ts +++ b/src/modules/warehouses/__tests__/warehouses.service.test.ts @@ -17,7 +17,7 @@ jest.mock('../../../config/typeorm.js', () => ({ })); // Import after mocking -import { warehousesService } from '../warehouses.service.js'; +import { warehousesService } from '../services/warehouses.service.js'; describe('WarehousesService', () => { const tenantId = 'test-tenant-uuid'; diff --git a/src/modules/warehouses/services/index.ts b/src/modules/warehouses/services/index.ts index 94a9de4..8e00f12 100644 --- a/src/modules/warehouses/services/index.ts +++ b/src/modules/warehouses/services/index.ts @@ -1,5 +1,9 @@ export { - WarehousesService, + warehousesService, WarehouseSearchParams, LocationSearchParams, -} from './warehouses.service'; + CreateWarehouseDto, + UpdateWarehouseDto, + CreateLocationDto, + UpdateLocationDto, +} from './warehouses.service.js'; diff --git a/src/modules/warehouses/services/warehouses.service.ts b/src/modules/warehouses/services/warehouses.service.ts index 8e8f8f4..5ee3f41 100644 --- a/src/modules/warehouses/services/warehouses.service.ts +++ b/src/modules/warehouses/services/warehouses.service.ts @@ -1,294 +1,270 @@ -import { Repository, FindOptionsWhere, ILike } from 'typeorm'; -import { Warehouse, WarehouseLocation } from '../entities'; -import { - CreateWarehouseDto, - UpdateWarehouseDto, - CreateWarehouseLocationDto, - UpdateWarehouseLocationDto, -} from '../dto'; +import { FindOptionsWhere, ILike, IsNull } from 'typeorm'; +import { AppDataSource } from '../../../config/typeorm.js'; +import { Warehouse } from '../entities/warehouse.entity.js'; +import { WarehouseLocation } from '../entities/warehouse-location.entity.js'; export interface WarehouseSearchParams { tenantId: string; search?: string; - warehouseType?: 'standard' | 'transit' | 'returns' | 'quarantine' | 'virtual'; - branchId?: string; isActive?: boolean; limit?: number; offset?: number; } export interface LocationSearchParams { - warehouseId: string; - search?: string; - locationType?: 'zone' | 'aisle' | 'rack' | 'shelf' | 'bin'; + warehouseId?: string; parentId?: string; + locationType?: 'zone' | 'aisle' | 'rack' | 'shelf' | 'bin'; isActive?: boolean; limit?: number; offset?: number; } -export class WarehousesService { - constructor( - private readonly warehouseRepository: Repository, - private readonly locationRepository: Repository - ) {} +export interface CreateWarehouseDto { + code: string; + name: string; + address?: string; + city?: string; + state?: string; + country?: string; + postalCode?: string; + phone?: string; + email?: string; + isActive?: boolean; + isDefault?: boolean; +} + +export interface UpdateWarehouseDto { + code?: string; + name?: string; + address?: string | null; + city?: string | null; + state?: string | null; + country?: string | null; + postalCode?: string | null; + phone?: string | null; + email?: string | null; + isActive?: boolean; + isDefault?: boolean; +} + +export interface CreateLocationDto { + warehouseId: string; + code: string; + name: string; + parentId?: string; + locationType?: 'zone' | 'aisle' | 'rack' | 'shelf' | 'bin'; + barcode?: string; + isActive?: boolean; +} + +export interface UpdateLocationDto { + code?: string; + name?: string; + parentId?: string | null; + locationType?: 'zone' | 'aisle' | 'rack' | 'shelf' | 'bin'; + barcode?: string | null; + isActive?: boolean; +} + +class WarehousesServiceClass { + private get warehouseRepository() { + return AppDataSource.getRepository(Warehouse); + } + + private get locationRepository() { + return AppDataSource.getRepository(WarehouseLocation); + } // ==================== Warehouses ==================== async findAll(params: WarehouseSearchParams): Promise<{ data: Warehouse[]; total: number }> { - const { tenantId, search, warehouseType, branchId, isActive, limit = 50, offset = 0 } = params; + const { tenantId, search, isActive, limit = 50, offset = 0 } = params; - const where: FindOptionsWhere[] = []; - const baseWhere: FindOptionsWhere = { tenantId }; - - if (warehouseType) { - baseWhere.warehouseType = warehouseType; - } - - if (branchId) { - baseWhere.branchId = branchId; - } + const where: FindOptionsWhere = { tenantId, deletedAt: IsNull() }; if (isActive !== undefined) { - baseWhere.isActive = isActive; + where.isActive = isActive; } if (search) { - where.push( - { ...baseWhere, name: ILike(`%${search}%`) }, - { ...baseWhere, code: ILike(`%${search}%`) } - ); - } else { - where.push(baseWhere); + const [data, total] = await this.warehouseRepository.findAndCount({ + where: [ + { ...where, name: ILike(`%${search}%`) }, + { ...where, code: ILike(`%${search}%`) }, + ], + take: limit, + skip: offset, + order: { name: 'ASC' }, + }); + return { data, total }; } const [data, total] = await this.warehouseRepository.findAndCount({ where, take: limit, skip: offset, - order: { name: 'ASC' }, + order: { isDefault: 'DESC', name: 'ASC' }, }); return { data, total }; } async findOne(id: string, tenantId: string): Promise { - return this.warehouseRepository.findOne({ where: { id, tenantId } }); + return this.warehouseRepository.findOne({ + where: { id, tenantId, deletedAt: IsNull() }, + }); } async findByCode(code: string, tenantId: string): Promise { - return this.warehouseRepository.findOne({ where: { code, tenantId } }); + return this.warehouseRepository.findOne({ + where: { code, tenantId, deletedAt: IsNull() }, + }); } - async getDefaultWarehouse(tenantId: string): Promise { - return this.warehouseRepository.findOne({ where: { tenantId, isDefault: true, isActive: true } }); + async getDefault(tenantId: string): Promise { + return this.warehouseRepository.findOne({ + where: { tenantId, isDefault: true, deletedAt: IsNull() }, + }); } - async create(tenantId: string, dto: CreateWarehouseDto, createdBy?: string): Promise { - // Check for existing code - const existingCode = await this.findByCode(dto.code, tenantId); + async getActive(tenantId: string): Promise { + return this.warehouseRepository.find({ + where: { tenantId, isActive: true, deletedAt: IsNull() }, + order: { isDefault: 'DESC', name: 'ASC' }, + }); + } + + async create(tenantId: string, dto: CreateWarehouseDto, _createdBy?: string): Promise { + // Validate unique code within tenant (RLS compliance) + const existingCode = await this.warehouseRepository.findOne({ + where: { code: dto.code, tenantId, deletedAt: IsNull() }, + }); if (existingCode) { - throw new Error('A warehouse with this code already exists'); + throw new Error(`Warehouse with code '${dto.code}' already exists`); } - // If setting as default, unset other defaults + // If this is set as default, unset other defaults if (dto.isDefault) { - await this.warehouseRepository.update({ tenantId, isDefault: true }, { isDefault: false }); + await this.warehouseRepository.update( + { tenantId, isDefault: true }, + { isDefault: false } + ); } const warehouse = this.warehouseRepository.create({ ...dto, tenantId, - createdBy, }); - return this.warehouseRepository.save(warehouse); } - async update( - id: string, - tenantId: string, - dto: UpdateWarehouseDto, - updatedBy?: string - ): Promise { + async update(id: string, tenantId: string, dto: UpdateWarehouseDto, _updatedBy?: string): Promise { const warehouse = await this.findOne(id, tenantId); if (!warehouse) return null; - // If changing code, check for duplicates - if (dto.code && dto.code !== warehouse.code) { - const existing = await this.findByCode(dto.code, tenantId); - if (existing) { - throw new Error('A warehouse with this code already exists'); - } - } - // If setting as default, unset other defaults if (dto.isDefault && !warehouse.isDefault) { - await this.warehouseRepository.update({ tenantId, isDefault: true }, { isDefault: false }); + await this.warehouseRepository.update( + { tenantId, isDefault: true }, + { isDefault: false } + ); } - Object.assign(warehouse, { - ...dto, - updatedBy, - }); - + Object.assign(warehouse, dto); return this.warehouseRepository.save(warehouse); } async delete(id: string, tenantId: string): Promise { - const warehouse = await this.findOne(id, tenantId); - if (!warehouse) return false; - - // Check if warehouse has locations - const locations = await this.locationRepository.findOne({ where: { warehouseId: id } }); - if (locations) { - throw new Error('Cannot delete warehouse with locations'); - } - - const result = await this.warehouseRepository.softDelete(id); + const result = await this.warehouseRepository.softDelete({ id, tenantId }); return (result.affected ?? 0) > 0; } - async getActiveWarehouses(tenantId: string): Promise { - return this.warehouseRepository.find({ - where: { tenantId, isActive: true }, - order: { isDefault: 'DESC', name: 'ASC' }, - }); - } - // ==================== Locations ==================== + // Note: Locations don't have tenantId directly, they belong to a Warehouse which has tenantId - async findAllLocations( - params: LocationSearchParams - ): Promise<{ data: WarehouseLocation[]; total: number }> { - const { warehouseId, search, locationType, parentId, isActive, limit = 100, offset = 0 } = params; + async findAllLocations(params: LocationSearchParams & { tenantId: string }): Promise<{ data: WarehouseLocation[]; total: number }> { + const { tenantId, warehouseId, parentId, locationType, isActive, limit = 50, offset = 0 } = params; - const where: FindOptionsWhere[] = []; - const baseWhere: FindOptionsWhere = { warehouseId }; + // Build query to join with warehouse for tenant filtering + const queryBuilder = this.locationRepository + .createQueryBuilder('location') + .leftJoinAndSelect('location.warehouse', 'warehouse') + .where('warehouse.tenantId = :tenantId', { tenantId }) + .andWhere('location.deletedAt IS NULL'); - if (locationType) { - baseWhere.locationType = locationType; + if (warehouseId) { + queryBuilder.andWhere('location.warehouseId = :warehouseId', { warehouseId }); } - if (parentId !== undefined) { - baseWhere.parentId = parentId || undefined; + if (parentId) { + queryBuilder.andWhere('location.parentId = :parentId', { parentId }); + } + + if (locationType) { + queryBuilder.andWhere('location.locationType = :locationType', { locationType }); } if (isActive !== undefined) { - baseWhere.isActive = isActive; + queryBuilder.andWhere('location.isActive = :isActive', { isActive }); } - if (search) { - where.push( - { ...baseWhere, name: ILike(`%${search}%`) }, - { ...baseWhere, code: ILike(`%${search}%`) }, - { ...baseWhere, barcode: ILike(`%${search}%`) } - ); - } else { - where.push(baseWhere); - } - - const [data, total] = await this.locationRepository.findAndCount({ - where, - take: limit, - skip: offset, - order: { hierarchyPath: 'ASC', code: 'ASC' }, - }); + queryBuilder.orderBy('location.code', 'ASC'); + queryBuilder.skip(offset).take(limit); + const [data, total] = await queryBuilder.getManyAndCount(); return { data, total }; } - async findLocation(id: string): Promise { - return this.locationRepository.findOne({ where: { id }, relations: ['warehouse'] }); + async findLocation(id: string, tenantId: string): Promise { + return this.locationRepository + .createQueryBuilder('location') + .leftJoinAndSelect('location.warehouse', 'warehouse') + .where('location.id = :id', { id }) + .andWhere('warehouse.tenantId = :tenantId', { tenantId }) + .andWhere('location.deletedAt IS NULL') + .getOne(); } - async findLocationByCode(code: string, warehouseId: string): Promise { - return this.locationRepository.findOne({ where: { code, warehouseId } }); - } - - async findLocationByBarcode(barcode: string): Promise { - return this.locationRepository.findOne({ where: { barcode }, relations: ['warehouse'] }); - } - - async createLocation(dto: CreateWarehouseLocationDto): Promise { - // Check for existing code in warehouse - const existingCode = await this.findLocationByCode(dto.code, dto.warehouseId); - if (existingCode) { - throw new Error('A location with this code already exists in this warehouse'); - } - - // Calculate hierarchy if parent exists - let hierarchyPath = `/${dto.code}`; - let hierarchyLevel = 0; - - if (dto.parentId) { - const parent = await this.findLocation(dto.parentId); - if (parent) { - hierarchyPath = `${parent.hierarchyPath}/${dto.code}`; - hierarchyLevel = parent.hierarchyLevel + 1; - } - } - + async createLocation(_tenantId: string, dto: CreateLocationDto, _createdBy?: string): Promise { const location = this.locationRepository.create({ - ...dto, - hierarchyPath, - hierarchyLevel, + warehouseId: dto.warehouseId, + code: dto.code, + name: dto.name, + parentId: dto.parentId, + locationType: dto.locationType || 'shelf', + barcode: dto.barcode, + isActive: dto.isActive ?? true, }); - return this.locationRepository.save(location); } - async updateLocation( - id: string, - dto: UpdateWarehouseLocationDto - ): Promise { - const location = await this.findLocation(id); + async updateLocation(id: string, tenantId: string, dto: UpdateLocationDto, _updatedBy?: string): Promise { + const location = await this.findLocation(id, tenantId); if (!location) return null; - - // If changing code, check for duplicates - if (dto.code && dto.code !== location.code) { - const existing = await this.findLocationByCode(dto.code, location.warehouseId); - if (existing) { - throw new Error('A location with this code already exists in this warehouse'); - } - } - Object.assign(location, dto); return this.locationRepository.save(location); } - async deleteLocation(id: string): Promise { - const location = await this.findLocation(id); + async deleteLocation(id: string, tenantId: string): Promise { + const location = await this.findLocation(id, tenantId); if (!location) return false; - - // Check if location has children - const children = await this.locationRepository.findOne({ where: { parentId: id } }); - if (children) { - throw new Error('Cannot delete location with children'); - } - - const result = await this.locationRepository.softDelete(id); + const result = await this.locationRepository.softDelete({ id }); return (result.affected ?? 0) > 0; } - async getLocationTree(warehouseId: string): Promise { - return this.locationRepository.find({ - where: { warehouseId, isActive: true }, - order: { hierarchyPath: 'ASC' }, - }); - } - - async getPickableLocations(warehouseId: string): Promise { - return this.locationRepository.find({ - where: { warehouseId, isActive: true, isPickable: true }, - order: { code: 'ASC' }, - }); - } - - async getReceivableLocations(warehouseId: string): Promise { - return this.locationRepository.find({ - where: { warehouseId, isActive: true, isReceivable: true }, - order: { code: 'ASC' }, - }); + async getLocationsByWarehouse(warehouseId: string, tenantId: string): Promise { + return this.locationRepository + .createQueryBuilder('location') + .leftJoin('location.warehouse', 'warehouse') + .where('location.warehouseId = :warehouseId', { warehouseId }) + .andWhere('warehouse.tenantId = :tenantId', { tenantId }) + .andWhere('location.isActive = :isActive', { isActive: true }) + .andWhere('location.deletedAt IS NULL') + .orderBy('location.code', 'ASC') + .getMany(); } } + +// Export singleton instance +export const warehousesService = new WarehousesServiceClass(); diff --git a/src/modules/warehouses/warehouses.controller.ts b/src/modules/warehouses/warehouses.controller.ts index bbee5d9..1e013cc 100644 --- a/src/modules/warehouses/warehouses.controller.ts +++ b/src/modules/warehouses/warehouses.controller.ts @@ -1,6 +1,6 @@ import { Response, NextFunction } from 'express'; import { z } from 'zod'; -import { warehousesService, CreateWarehouseDto, UpdateWarehouseDto, CreateLocationDto, UpdateLocationDto } from './warehouses.service.js'; +import { warehousesService, CreateWarehouseDto, UpdateWarehouseDto, CreateLocationDto, UpdateLocationDto } from './services/index.js'; import { ApiResponse, AuthenticatedRequest, ValidationError } from '../../shared/types/index.js'; // Validation schemas diff --git a/src/modules/warehouses/warehouses.service.ts b/src/modules/warehouses/warehouses.service.ts deleted file mode 100644 index 9adc958..0000000 --- a/src/modules/warehouses/warehouses.service.ts +++ /dev/null @@ -1,270 +0,0 @@ -import { FindOptionsWhere, ILike, IsNull } from 'typeorm'; -import { AppDataSource } from '../../config/typeorm.js'; -import { Warehouse } from './entities/warehouse.entity.js'; -import { WarehouseLocation } from './entities/warehouse-location.entity.js'; - -export interface WarehouseSearchParams { - tenantId: string; - search?: string; - isActive?: boolean; - limit?: number; - offset?: number; -} - -export interface LocationSearchParams { - warehouseId?: string; - parentId?: string; - locationType?: 'zone' | 'aisle' | 'rack' | 'shelf' | 'bin'; - isActive?: boolean; - limit?: number; - offset?: number; -} - -export interface CreateWarehouseDto { - code: string; - name: string; - address?: string; - city?: string; - state?: string; - country?: string; - postalCode?: string; - phone?: string; - email?: string; - isActive?: boolean; - isDefault?: boolean; -} - -export interface UpdateWarehouseDto { - code?: string; - name?: string; - address?: string | null; - city?: string | null; - state?: string | null; - country?: string | null; - postalCode?: string | null; - phone?: string | null; - email?: string | null; - isActive?: boolean; - isDefault?: boolean; -} - -export interface CreateLocationDto { - warehouseId: string; - code: string; - name: string; - parentId?: string; - locationType?: 'zone' | 'aisle' | 'rack' | 'shelf' | 'bin'; - barcode?: string; - isActive?: boolean; -} - -export interface UpdateLocationDto { - code?: string; - name?: string; - parentId?: string | null; - locationType?: 'zone' | 'aisle' | 'rack' | 'shelf' | 'bin'; - barcode?: string | null; - isActive?: boolean; -} - -class WarehousesServiceClass { - private get warehouseRepository() { - return AppDataSource.getRepository(Warehouse); - } - - private get locationRepository() { - return AppDataSource.getRepository(WarehouseLocation); - } - - // ==================== Warehouses ==================== - - async findAll(params: WarehouseSearchParams): Promise<{ data: Warehouse[]; total: number }> { - const { tenantId, search, isActive, limit = 50, offset = 0 } = params; - - const where: FindOptionsWhere = { tenantId, deletedAt: IsNull() }; - - if (isActive !== undefined) { - where.isActive = isActive; - } - - if (search) { - const [data, total] = await this.warehouseRepository.findAndCount({ - where: [ - { ...where, name: ILike(`%${search}%`) }, - { ...where, code: ILike(`%${search}%`) }, - ], - take: limit, - skip: offset, - order: { name: 'ASC' }, - }); - return { data, total }; - } - - const [data, total] = await this.warehouseRepository.findAndCount({ - where, - take: limit, - skip: offset, - order: { isDefault: 'DESC', name: 'ASC' }, - }); - - return { data, total }; - } - - async findOne(id: string, tenantId: string): Promise { - return this.warehouseRepository.findOne({ - where: { id, tenantId, deletedAt: IsNull() }, - }); - } - - async findByCode(code: string, tenantId: string): Promise { - return this.warehouseRepository.findOne({ - where: { code, tenantId, deletedAt: IsNull() }, - }); - } - - async getDefault(tenantId: string): Promise { - return this.warehouseRepository.findOne({ - where: { tenantId, isDefault: true, deletedAt: IsNull() }, - }); - } - - async getActive(tenantId: string): Promise { - return this.warehouseRepository.find({ - where: { tenantId, isActive: true, deletedAt: IsNull() }, - order: { isDefault: 'DESC', name: 'ASC' }, - }); - } - - async create(tenantId: string, dto: CreateWarehouseDto, _createdBy?: string): Promise { - // Validate unique code within tenant (RLS compliance) - const existingCode = await this.warehouseRepository.findOne({ - where: { code: dto.code, tenantId, deletedAt: IsNull() }, - }); - if (existingCode) { - throw new Error(`Warehouse with code '${dto.code}' already exists`); - } - - // If this is set as default, unset other defaults - if (dto.isDefault) { - await this.warehouseRepository.update( - { tenantId, isDefault: true }, - { isDefault: false } - ); - } - - const warehouse = this.warehouseRepository.create({ - ...dto, - tenantId, - }); - return this.warehouseRepository.save(warehouse); - } - - async update(id: string, tenantId: string, dto: UpdateWarehouseDto, _updatedBy?: string): Promise { - const warehouse = await this.findOne(id, tenantId); - if (!warehouse) return null; - - // If setting as default, unset other defaults - if (dto.isDefault && !warehouse.isDefault) { - await this.warehouseRepository.update( - { tenantId, isDefault: true }, - { isDefault: false } - ); - } - - Object.assign(warehouse, dto); - return this.warehouseRepository.save(warehouse); - } - - async delete(id: string, tenantId: string): Promise { - const result = await this.warehouseRepository.softDelete({ id, tenantId }); - return (result.affected ?? 0) > 0; - } - - // ==================== Locations ==================== - // Note: Locations don't have tenantId directly, they belong to a Warehouse which has tenantId - - async findAllLocations(params: LocationSearchParams & { tenantId: string }): Promise<{ data: WarehouseLocation[]; total: number }> { - const { tenantId, warehouseId, parentId, locationType, isActive, limit = 50, offset = 0 } = params; - - // Build query to join with warehouse for tenant filtering - const queryBuilder = this.locationRepository - .createQueryBuilder('location') - .leftJoinAndSelect('location.warehouse', 'warehouse') - .where('warehouse.tenantId = :tenantId', { tenantId }) - .andWhere('location.deletedAt IS NULL'); - - if (warehouseId) { - queryBuilder.andWhere('location.warehouseId = :warehouseId', { warehouseId }); - } - - if (parentId) { - queryBuilder.andWhere('location.parentId = :parentId', { parentId }); - } - - if (locationType) { - queryBuilder.andWhere('location.locationType = :locationType', { locationType }); - } - - if (isActive !== undefined) { - queryBuilder.andWhere('location.isActive = :isActive', { isActive }); - } - - queryBuilder.orderBy('location.code', 'ASC'); - queryBuilder.skip(offset).take(limit); - - const [data, total] = await queryBuilder.getManyAndCount(); - return { data, total }; - } - - async findLocation(id: string, tenantId: string): Promise { - return this.locationRepository - .createQueryBuilder('location') - .leftJoinAndSelect('location.warehouse', 'warehouse') - .where('location.id = :id', { id }) - .andWhere('warehouse.tenantId = :tenantId', { tenantId }) - .andWhere('location.deletedAt IS NULL') - .getOne(); - } - - async createLocation(_tenantId: string, dto: CreateLocationDto, _createdBy?: string): Promise { - const location = this.locationRepository.create({ - warehouseId: dto.warehouseId, - code: dto.code, - name: dto.name, - parentId: dto.parentId, - locationType: dto.locationType || 'shelf', - barcode: dto.barcode, - isActive: dto.isActive ?? true, - }); - return this.locationRepository.save(location); - } - - async updateLocation(id: string, tenantId: string, dto: UpdateLocationDto, _updatedBy?: string): Promise { - const location = await this.findLocation(id, tenantId); - if (!location) return null; - Object.assign(location, dto); - return this.locationRepository.save(location); - } - - async deleteLocation(id: string, tenantId: string): Promise { - const location = await this.findLocation(id, tenantId); - if (!location) return false; - const result = await this.locationRepository.softDelete({ id }); - return (result.affected ?? 0) > 0; - } - - async getLocationsByWarehouse(warehouseId: string, tenantId: string): Promise { - return this.locationRepository - .createQueryBuilder('location') - .leftJoin('location.warehouse', 'warehouse') - .where('location.warehouseId = :warehouseId', { warehouseId }) - .andWhere('warehouse.tenantId = :tenantId', { tenantId }) - .andWhere('location.isActive = :isActive', { isActive: true }) - .andWhere('location.deletedAt IS NULL') - .orderBy('location.code', 'ASC') - .getMany(); - } -} - -// Export singleton instance -export const warehousesService = new WarehousesServiceClass(); diff --git a/src/shared/middleware/apiKeyAuth.middleware.ts b/src/shared/middleware/apiKeyAuth.middleware.ts index db513da..95d752c 100644 --- a/src/shared/middleware/apiKeyAuth.middleware.ts +++ b/src/shared/middleware/apiKeyAuth.middleware.ts @@ -1,5 +1,5 @@ import { Response, NextFunction } from 'express'; -import { apiKeysService } from '../../modules/auth/apiKeys.service.js'; +import { apiKeysService } from '../../modules/auth/services/apiKeys.service.js'; import { AuthenticatedRequest, UnauthorizedError, ForbiddenError } from '../types/index.js'; import { logger } from '../utils/logger.js';