[MAI-012] feat: Complete contracts module with schema fixes
- Fix 3 entities schema from 'contracts' to 'construction' to match DDL - Create ContractPartida entity for contrato_partidas table - Fix ContractService and SubcontractorService to use local ServiceContext - Fix ContractController and SubcontractorController pagination structure - Update entities/index.ts to export ContractPartida Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
60782a3cac
commit
24c2931209
@ -17,7 +17,11 @@ import { AuthService } from '../../auth/services/auth.service';
|
|||||||
import { User } from '../../core/entities/user.entity';
|
import { User } from '../../core/entities/user.entity';
|
||||||
import { Tenant } from '../../core/entities/tenant.entity';
|
import { Tenant } from '../../core/entities/tenant.entity';
|
||||||
import { RefreshToken } from '../../auth/entities/refresh-token.entity';
|
import { RefreshToken } from '../../auth/entities/refresh-token.entity';
|
||||||
import { ServiceContext } from '../../../shared/services/base.service';
|
|
||||||
|
interface ServiceContext {
|
||||||
|
tenantId: string;
|
||||||
|
userId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export function createContractController(dataSource: DataSource): Router {
|
export function createContractController(dataSource: DataSource): Router {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
@ -69,7 +73,7 @@ export function createContractController(dataSource: DataSource): Router {
|
|||||||
const limit = Math.min(parseInt(req.query.limit as string) || 20, 100);
|
const limit = Math.min(parseInt(req.query.limit as string) || 20, 100);
|
||||||
|
|
||||||
const result = await service.findWithFilters(getContext(req), filters, page, limit);
|
const result = await service.findWithFilters(getContext(req), filters, page, limit);
|
||||||
res.status(200).json({ success: true, data: result.data, pagination: result.meta });
|
res.status(200).json({ success: true, data: result.data, pagination: { total: result.total, page: result.page, limit: result.limit } });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,7 +16,11 @@ import { AuthService } from '../../auth/services/auth.service';
|
|||||||
import { User } from '../../core/entities/user.entity';
|
import { User } from '../../core/entities/user.entity';
|
||||||
import { Tenant } from '../../core/entities/tenant.entity';
|
import { Tenant } from '../../core/entities/tenant.entity';
|
||||||
import { RefreshToken } from '../../auth/entities/refresh-token.entity';
|
import { RefreshToken } from '../../auth/entities/refresh-token.entity';
|
||||||
import { ServiceContext } from '../../../shared/services/base.service';
|
|
||||||
|
interface ServiceContext {
|
||||||
|
tenantId: string;
|
||||||
|
userId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export function createSubcontractorController(dataSource: DataSource): Router {
|
export function createSubcontractorController(dataSource: DataSource): Router {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
@ -65,7 +69,7 @@ export function createSubcontractorController(dataSource: DataSource): Router {
|
|||||||
const limit = Math.min(parseInt(req.query.limit as string) || 20, 100);
|
const limit = Math.min(parseInt(req.query.limit as string) || 20, 100);
|
||||||
|
|
||||||
const result = await service.findWithFilters(getContext(req), filters, page, limit);
|
const result = await service.findWithFilters(getContext(req), filters, page, limit);
|
||||||
res.status(200).json({ success: true, data: result.data, pagination: result.meta });
|
res.status(200).json({ success: true, data: result.data, pagination: { total: result.total, page: result.page, limit: result.limit } });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,7 +23,7 @@ import { Contract } from './contract.entity';
|
|||||||
export type AddendumType = 'extension' | 'amount_increase' | 'amount_decrease' | 'scope_change' | 'termination' | 'other';
|
export type AddendumType = 'extension' | 'amount_increase' | 'amount_decrease' | 'scope_change' | 'termination' | 'other';
|
||||||
export type AddendumStatus = 'draft' | 'review' | 'approved' | 'rejected';
|
export type AddendumStatus = 'draft' | 'review' | 'approved' | 'rejected';
|
||||||
|
|
||||||
@Entity({ schema: 'contracts', name: 'contract_addendums' })
|
@Entity({ schema: 'construction', name: 'contrato_addendas' })
|
||||||
@Index(['tenantId', 'addendumNumber'], { unique: true })
|
@Index(['tenantId', 'addendumNumber'], { unique: true })
|
||||||
export class ContractAddendum {
|
export class ContractAddendum {
|
||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
|||||||
75
src/modules/contracts/entities/contract-partida.entity.ts
Normal file
75
src/modules/contracts/entities/contract-partida.entity.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
/**
|
||||||
|
* ContractPartida Entity
|
||||||
|
* Partidas/lineas de contratos con subcontratistas
|
||||||
|
*
|
||||||
|
* @module Contracts
|
||||||
|
* @table construction.contrato_partidas
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
ManyToOne,
|
||||||
|
JoinColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { Tenant } from '../../core/entities/tenant.entity';
|
||||||
|
import { User } from '../../core/entities/user.entity';
|
||||||
|
import { Contract } from './contract.entity';
|
||||||
|
|
||||||
|
@Entity({ schema: 'construction', name: 'contrato_partidas' })
|
||||||
|
export class ContractPartida {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column({ name: 'tenant_id', type: 'uuid' })
|
||||||
|
tenantId: string;
|
||||||
|
|
||||||
|
@Column({ name: 'contrato_id', type: 'uuid' })
|
||||||
|
contractId: string;
|
||||||
|
|
||||||
|
@Column({ name: 'concepto_id', type: 'uuid' })
|
||||||
|
conceptoId: string;
|
||||||
|
|
||||||
|
@Column({ type: 'decimal', precision: 12, scale: 4, default: 0 })
|
||||||
|
quantity: number;
|
||||||
|
|
||||||
|
@Column({ name: 'unit_price', type: 'decimal', precision: 12, scale: 4, default: 0 })
|
||||||
|
unitPrice: number;
|
||||||
|
|
||||||
|
@Column({ name: 'total_amount', type: 'decimal', precision: 14, scale: 2, insert: false, update: false })
|
||||||
|
totalAmount: number;
|
||||||
|
|
||||||
|
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@Column({ name: 'created_by', type: 'uuid', nullable: true })
|
||||||
|
createdById: string;
|
||||||
|
|
||||||
|
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
|
||||||
|
updatedAt: Date;
|
||||||
|
|
||||||
|
@Column({ name: 'updated_by', type: 'uuid', nullable: true })
|
||||||
|
updatedById: string;
|
||||||
|
|
||||||
|
@Column({ name: 'deleted_at', type: 'timestamptz', nullable: true })
|
||||||
|
deletedAt: Date;
|
||||||
|
|
||||||
|
@Column({ name: 'deleted_by', type: 'uuid', nullable: true })
|
||||||
|
deletedById: string;
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
@ManyToOne(() => Tenant)
|
||||||
|
@JoinColumn({ name: 'tenant_id' })
|
||||||
|
tenant: Tenant;
|
||||||
|
|
||||||
|
@ManyToOne(() => Contract, { onDelete: 'CASCADE' })
|
||||||
|
@JoinColumn({ name: 'contrato_id' })
|
||||||
|
contract: Contract;
|
||||||
|
|
||||||
|
@ManyToOne(() => User)
|
||||||
|
@JoinColumn({ name: 'created_by' })
|
||||||
|
createdBy: User;
|
||||||
|
}
|
||||||
@ -25,7 +25,7 @@ export type ContractType = 'client' | 'subcontractor';
|
|||||||
export type ContractStatus = 'draft' | 'review' | 'approved' | 'active' | 'completed' | 'terminated';
|
export type ContractStatus = 'draft' | 'review' | 'approved' | 'active' | 'completed' | 'terminated';
|
||||||
export type ClientContractType = 'desarrollo' | 'llave_en_mano' | 'administracion';
|
export type ClientContractType = 'desarrollo' | 'llave_en_mano' | 'administracion';
|
||||||
|
|
||||||
@Entity({ schema: 'contracts', name: 'contracts' })
|
@Entity({ schema: 'construction', name: 'contratos' })
|
||||||
@Index(['tenantId', 'contractNumber'], { unique: true })
|
@Index(['tenantId', 'contractNumber'], { unique: true })
|
||||||
export class Contract {
|
export class Contract {
|
||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
|||||||
@ -8,3 +8,4 @@
|
|||||||
export * from './contract.entity';
|
export * from './contract.entity';
|
||||||
export * from './subcontractor.entity';
|
export * from './subcontractor.entity';
|
||||||
export * from './contract-addendum.entity';
|
export * from './contract-addendum.entity';
|
||||||
|
export * from './contract-partida.entity';
|
||||||
|
|||||||
@ -22,7 +22,7 @@ import { User } from '../../core/entities/user.entity';
|
|||||||
export type SubcontractorSpecialty = 'cimentacion' | 'estructura' | 'instalaciones_electricas' | 'instalaciones_hidraulicas' | 'acabados' | 'urbanizacion' | 'carpinteria' | 'herreria' | 'otros';
|
export type SubcontractorSpecialty = 'cimentacion' | 'estructura' | 'instalaciones_electricas' | 'instalaciones_hidraulicas' | 'acabados' | 'urbanizacion' | 'carpinteria' | 'herreria' | 'otros';
|
||||||
export type SubcontractorStatus = 'active' | 'inactive' | 'blacklisted';
|
export type SubcontractorStatus = 'active' | 'inactive' | 'blacklisted';
|
||||||
|
|
||||||
@Entity({ schema: 'contracts', name: 'subcontractors' })
|
@Entity({ schema: 'construction', name: 'subcontratistas' })
|
||||||
@Index(['tenantId', 'code'], { unique: true })
|
@Index(['tenantId', 'code'], { unique: true })
|
||||||
@Index(['tenantId', 'rfc'], { unique: true })
|
@Index(['tenantId', 'rfc'], { unique: true })
|
||||||
export class Subcontractor {
|
export class Subcontractor {
|
||||||
|
|||||||
@ -9,7 +9,11 @@
|
|||||||
import { Repository, FindOptionsWhere, LessThan } from 'typeorm';
|
import { Repository, FindOptionsWhere, LessThan } from 'typeorm';
|
||||||
import { Contract, ContractStatus, ContractType } from '../entities/contract.entity';
|
import { Contract, ContractStatus, ContractType } from '../entities/contract.entity';
|
||||||
import { ContractAddendum } from '../entities/contract-addendum.entity';
|
import { ContractAddendum } from '../entities/contract-addendum.entity';
|
||||||
import { ServiceContext, PaginatedResult } from '../../../shared/services/base.service';
|
|
||||||
|
interface ServiceContext {
|
||||||
|
tenantId: string;
|
||||||
|
userId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CreateContractDto {
|
export interface CreateContractDto {
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
@ -77,7 +81,7 @@ export class ContractService {
|
|||||||
filters: ContractFilters = {},
|
filters: ContractFilters = {},
|
||||||
page: number = 1,
|
page: number = 1,
|
||||||
limit: number = 20
|
limit: number = 20
|
||||||
): Promise<PaginatedResult<Contract>> {
|
): Promise<{ data: Contract[]; total: number; page: number; limit: number }> {
|
||||||
const skip = (page - 1) * limit;
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
const queryBuilder = this.contractRepository
|
const queryBuilder = this.contractRepository
|
||||||
@ -126,15 +130,7 @@ export class ContractService {
|
|||||||
|
|
||||||
const [data, total] = await queryBuilder.getManyAndCount();
|
const [data, total] = await queryBuilder.getManyAndCount();
|
||||||
|
|
||||||
return {
|
return { data, total, page, limit };
|
||||||
data,
|
|
||||||
meta: {
|
|
||||||
total,
|
|
||||||
page,
|
|
||||||
limit,
|
|
||||||
totalPages: Math.ceil(total / limit),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async findById(ctx: ServiceContext, id: string): Promise<Contract | null> {
|
async findById(ctx: ServiceContext, id: string): Promise<Contract | null> {
|
||||||
|
|||||||
@ -8,7 +8,11 @@
|
|||||||
|
|
||||||
import { Repository, FindOptionsWhere } from 'typeorm';
|
import { Repository, FindOptionsWhere } from 'typeorm';
|
||||||
import { Subcontractor, SubcontractorStatus, SubcontractorSpecialty } from '../entities/subcontractor.entity';
|
import { Subcontractor, SubcontractorStatus, SubcontractorSpecialty } from '../entities/subcontractor.entity';
|
||||||
import { ServiceContext, PaginatedResult } from '../../../shared/services/base.service';
|
|
||||||
|
interface ServiceContext {
|
||||||
|
tenantId: string;
|
||||||
|
userId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CreateSubcontractorDto {
|
export interface CreateSubcontractorDto {
|
||||||
businessName: string;
|
businessName: string;
|
||||||
@ -63,7 +67,7 @@ export class SubcontractorService {
|
|||||||
filters: SubcontractorFilters = {},
|
filters: SubcontractorFilters = {},
|
||||||
page: number = 1,
|
page: number = 1,
|
||||||
limit: number = 20
|
limit: number = 20
|
||||||
): Promise<PaginatedResult<Subcontractor>> {
|
): Promise<{ data: Subcontractor[]; total: number; page: number; limit: number }> {
|
||||||
const skip = (page - 1) * limit;
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
const queryBuilder = this.subcontractorRepository
|
const queryBuilder = this.subcontractorRepository
|
||||||
@ -98,15 +102,7 @@ export class SubcontractorService {
|
|||||||
|
|
||||||
const [data, total] = await queryBuilder.getManyAndCount();
|
const [data, total] = await queryBuilder.getManyAndCount();
|
||||||
|
|
||||||
return {
|
return { data, total, page, limit };
|
||||||
data,
|
|
||||||
meta: {
|
|
||||||
total,
|
|
||||||
page,
|
|
||||||
limit,
|
|
||||||
totalPages: Math.ceil(total / limit),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async findById(ctx: ServiceContext, id: string): Promise<Subcontractor | null> {
|
async findById(ctx: ServiceContext, id: string): Promise<Subcontractor | null> {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user