mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
feat: setup the company profiles
This commit is contained in:
@@ -114,7 +114,9 @@ export class ContractViewModelBuilder {
|
||||
tinNumber: this.valueOrDash(booking.company?.tin),
|
||||
vatNumber: this.valueOrDash(booking.company?.vatNumber),
|
||||
fanNumber: this.valueOrDash(booking.company?.fanNumber),
|
||||
businessLicense: this.valueOrDash(booking.company?.businessLicense),
|
||||
businessLicense: this.valueOrDash(
|
||||
booking.company?.companyProfiles?.[0]?.businessLicense,
|
||||
),
|
||||
},
|
||||
provider: {
|
||||
name: 'Ethio-Djibouti Standard Gauge Railway Share Company',
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class MoveBusinessLicenseToProfile1752000000001
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'MoveBusinessLicenseToProfile1752000000001';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
UPDATE freight.company_profiles cp
|
||||
SET business_license = c.business_license
|
||||
FROM freight.companies c
|
||||
WHERE cp.company_id = c.id AND c.business_license IS NOT NULL
|
||||
`);
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE freight.companies DROP COLUMN IF EXISTS business_license`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE freight.companies ADD COLUMN business_license varchar(100) NULL`,
|
||||
);
|
||||
|
||||
await queryRunner.query(`
|
||||
UPDATE freight.companies c
|
||||
SET business_license = cp.business_license
|
||||
FROM (
|
||||
SELECT DISTINCT ON (cp2.company_id)
|
||||
cp2.company_id, cp2.business_license
|
||||
FROM freight.company_profiles cp2
|
||||
WHERE cp2.business_license IS NOT NULL
|
||||
ORDER BY cp2.company_id, cp2.created_at
|
||||
) cp
|
||||
WHERE cp.company_id = c.id
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -85,6 +85,7 @@ export class CompaniesController {
|
||||
return this.companiesService.updateProfile(user.id, dto);
|
||||
}
|
||||
|
||||
// Used by portal
|
||||
@Post("create")
|
||||
@ApiOperation({
|
||||
summary:
|
||||
@@ -109,10 +110,12 @@ export class CompaniesController {
|
||||
return new CompanyInfoResponseDto(profile, company);
|
||||
}
|
||||
|
||||
// Used by backoffice
|
||||
@Post()
|
||||
@FreightAdmin()
|
||||
@ApiOperation({
|
||||
summary: "Create a new company (customer, forwarder, transporter, broker)",
|
||||
summary:
|
||||
"Create a new company (customer, freight_forwarder, dj_freight_forwarder, transporter)",
|
||||
})
|
||||
async create(@Body() dto: CreateCompanyDto): Promise<ResponseCompanyDto> {
|
||||
const company = await this.companiesService.createCompany(dto);
|
||||
|
||||
@@ -8,11 +8,13 @@ import { ExternalProfileRepository } from "./external-profile.repository";
|
||||
import { CompanyDashboardRepository } from "./company-dashboard.repository";
|
||||
import { Company } from "./entities/company.entity";
|
||||
import { ExternalProfile } from "./entities/external-profile.entity";
|
||||
import { CompanyProfile } from "./entities/company-profile.entity";
|
||||
import { Booking } from "../bookings/entities/booking.entity";
|
||||
import { CompanyProfileRepository } from "./company-profile.repository";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Company, ExternalProfile, Booking]),
|
||||
TypeOrmModule.forFeature([Company, ExternalProfile, CompanyProfile, Booking]),
|
||||
FilesModule,
|
||||
],
|
||||
controllers: [CompaniesController],
|
||||
@@ -20,6 +22,7 @@ import { Booking } from "../bookings/entities/booking.entity";
|
||||
CompaniesService,
|
||||
CompaniesRepository,
|
||||
ExternalProfileRepository,
|
||||
CompanyProfileRepository,
|
||||
CompanyDashboardRepository,
|
||||
],
|
||||
exports: [CompaniesService],
|
||||
|
||||
@@ -2,8 +2,10 @@ import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
ConflictException,
|
||||
BadRequestException,
|
||||
} from "@nestjs/common";
|
||||
import { CompaniesRepository } from "./companies.repository";
|
||||
import { CompanyProfileRepository } from "./company-profile.repository";
|
||||
import { ExternalProfileRepository } from "./external-profile.repository";
|
||||
import { CompanyDashboardRepository } from "./company-dashboard.repository";
|
||||
import { CreateCompanyDto } from "./dto/create-company.dto";
|
||||
@@ -15,6 +17,11 @@ import { ProfileResponseDto } from "./dto/profile-response.dto";
|
||||
import { DashboardSummaryResponseDto } from "./dto/dashboard-summary-response.dto";
|
||||
import { Company } from "./entities/company.entity";
|
||||
import { ExternalProfile } from "./entities/external-profile.entity";
|
||||
import {
|
||||
CompanyProfile,
|
||||
ProfileType,
|
||||
ProfileStatus,
|
||||
} from "./entities/company-profile.entity";
|
||||
|
||||
export interface UserIdentity {
|
||||
userId: string;
|
||||
@@ -28,6 +35,7 @@ export interface UserIdentity {
|
||||
export class CompaniesService {
|
||||
constructor(
|
||||
private readonly companiesRepo: CompaniesRepository,
|
||||
private readonly companyProfilesRepo: CompanyProfileRepository,
|
||||
private readonly profilesRepo: ExternalProfileRepository,
|
||||
private readonly dashboardRepo: CompanyDashboardRepository,
|
||||
) { }
|
||||
@@ -65,7 +73,6 @@ export class CompaniesService {
|
||||
type: dto.companyType,
|
||||
tin: dto.tin ?? "",
|
||||
vatNumber: dto.vatNumber ?? null,
|
||||
businessLicense: dto.fanNumber ?? null,
|
||||
fanNumber: dto.fanNumber ?? null,
|
||||
country: dto.companyLocation ?? "Ethiopia",
|
||||
address: dto.companyAddress ?? null,
|
||||
@@ -89,7 +96,7 @@ export class CompaniesService {
|
||||
}
|
||||
|
||||
async findAllCompanies(): Promise<Company[]> {
|
||||
return this.companiesRepo.findAll({ order: { name: "ASC" as any } });
|
||||
return this.companiesRepo.findAll({ order: { name: "ASC" } });
|
||||
}
|
||||
|
||||
async findCompanyById(id: string): Promise<Company> {
|
||||
@@ -111,6 +118,9 @@ export class CompaniesService {
|
||||
`Company for profile ${profile.id} not found`,
|
||||
);
|
||||
|
||||
company.companyProfiles =
|
||||
await this.companyProfilesRepo.findByCompanyId(company.id);
|
||||
|
||||
return { profile, company };
|
||||
}
|
||||
|
||||
@@ -295,7 +305,6 @@ export class CompaniesService {
|
||||
if (dto.tin !== undefined) companyUpdates.tin = dto.tin;
|
||||
if (dto.vatNumber !== undefined) companyUpdates.vatNumber = dto.vatNumber;
|
||||
if (dto.fanNumber !== undefined) {
|
||||
companyUpdates.businessLicense = dto.fanNumber;
|
||||
companyUpdates.fanNumber = dto.fanNumber;
|
||||
}
|
||||
|
||||
@@ -352,4 +361,77 @@ export class CompaniesService {
|
||||
async findProfilesByCompany(companyId: string): Promise<ExternalProfile[]> {
|
||||
return this.profilesRepo.findByCompanyId(companyId);
|
||||
}
|
||||
|
||||
private getProfileTypeForCompanyType(companyType: string): ProfileType[] {
|
||||
switch (companyType) {
|
||||
case "customer":
|
||||
return [ProfileType.importer, ProfileType.exporter];
|
||||
case "freight_forwarder":
|
||||
return [ProfileType.freightForwarder];
|
||||
case "dj_freight_forwarder":
|
||||
return [ProfileType.djFreightForwarder];
|
||||
case "transporter":
|
||||
return [ProfileType.transporter];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async createCompanyProfile(
|
||||
companyId: string,
|
||||
profileType?: ProfileType,
|
||||
): Promise<CompanyProfile> {
|
||||
const company = await this.findCompanyById(companyId);
|
||||
|
||||
const allowedTypes = this.getProfileTypeForCompanyType(company.type);
|
||||
|
||||
const type = profileType ?? allowedTypes[0];
|
||||
if (!allowedTypes.includes(type)) {
|
||||
throw new BadRequestException(
|
||||
`Profile type "${type}" is not allowed for company type "${company.type}"`,
|
||||
);
|
||||
}
|
||||
|
||||
const existing = await this.companyProfilesRepo.findByType(companyId, type);
|
||||
if (existing) {
|
||||
throw new ConflictException(
|
||||
`Company already has a ${type} profile (${existing.reference})`,
|
||||
);
|
||||
}
|
||||
|
||||
const reference = await this.companyProfilesRepo.generateReference(type);
|
||||
|
||||
return this.companyProfilesRepo.create({
|
||||
companyId,
|
||||
type,
|
||||
reference,
|
||||
status: ProfileStatus.Active,
|
||||
});
|
||||
}
|
||||
|
||||
async createDefaultProfilesForCompany(
|
||||
companyId: string,
|
||||
): Promise<CompanyProfile[]> {
|
||||
const company = await this.findCompanyById(companyId);
|
||||
const types = this.getProfileTypeForCompanyType(company.type);
|
||||
|
||||
const profiles: CompanyProfile[] = [];
|
||||
for (const type of types) {
|
||||
const existing = await this.companyProfilesRepo.findByType(
|
||||
companyId,
|
||||
type,
|
||||
);
|
||||
if (!existing) {
|
||||
profiles.push(await this.createCompanyProfile(companyId, type));
|
||||
}
|
||||
}
|
||||
|
||||
if (profiles.length === 0) {
|
||||
throw new BadRequestException(
|
||||
`Company of type "${company.type}" must have at least one operational profile`,
|
||||
);
|
||||
}
|
||||
|
||||
return profiles;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
import { Repository } from "typeorm";
|
||||
import { BaseRepository } from "@edr/api-common";
|
||||
import { CompanyProfile, ProfileType } from "./entities/company-profile.entity";
|
||||
|
||||
const SEQUENCE_MAP: Record<ProfileType, string> = {
|
||||
[ProfileType.exporter]: "seq_company_profile_ex",
|
||||
[ProfileType.importer]: "seq_company_profile_im",
|
||||
[ProfileType.freightForwarder]: "seq_company_profile_ffe",
|
||||
[ProfileType.djFreightForwarder]: "seq_company_profile_fwj",
|
||||
[ProfileType.transporter]: "seq_company_profile_tr",
|
||||
};
|
||||
|
||||
const PREFIX_MAP: Record<ProfileType, string> = {
|
||||
[ProfileType.exporter]: "EX",
|
||||
[ProfileType.importer]: "IM",
|
||||
[ProfileType.freightForwarder]: "FFE",
|
||||
[ProfileType.djFreightForwarder]: "FWJ",
|
||||
[ProfileType.transporter]: "TR",
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class CompanyProfileRepository extends BaseRepository<CompanyProfile> {
|
||||
constructor(
|
||||
@InjectRepository(CompanyProfile)
|
||||
repo: Repository<CompanyProfile>,
|
||||
) {
|
||||
super(repo);
|
||||
}
|
||||
|
||||
async generateReference(type: ProfileType): Promise<string> {
|
||||
const seqName = SEQUENCE_MAP[type];
|
||||
const result = await this.repository.query(
|
||||
`SELECT nextval('${seqName}') AS next_id`,
|
||||
);
|
||||
const nextId = result[0].next_id as number;
|
||||
const prefix = PREFIX_MAP[type];
|
||||
return `${prefix}-${String(nextId).padStart(5, "0")}`;
|
||||
}
|
||||
|
||||
async findByCompanyId(companyId: string): Promise<CompanyProfile[]> {
|
||||
return this.repository.find({
|
||||
where: { companyId },
|
||||
relations: ["company"],
|
||||
});
|
||||
}
|
||||
|
||||
async findByType(
|
||||
companyId: string,
|
||||
type: ProfileType,
|
||||
): Promise<CompanyProfile | null> {
|
||||
return this.repository.findOne({
|
||||
where: { companyId, type },
|
||||
});
|
||||
}
|
||||
|
||||
async findByReference(reference: string): Promise<CompanyProfile | null> {
|
||||
return this.repository.findOne({ where: { reference } });
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,17 @@
|
||||
import { IsString, IsNotEmpty, IsOptional, IsEmail, MaxLength, IsBoolean, IsEnum } from 'class-validator';
|
||||
import { IsString, IsNotEmpty, IsOptional, IsEmail, MaxLength, IsBoolean, IsEnum, IsArray, ValidateNested, ArrayMinSize } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { CompanyType } from '../entities/company.entity';
|
||||
import { ProfileType } from '../entities/company-profile.entity';
|
||||
|
||||
export class CompanyProfileInputDto {
|
||||
@IsEnum(ProfileType)
|
||||
type!: ProfileType;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(100)
|
||||
businessLicense?: string;
|
||||
}
|
||||
|
||||
export class CreateCompanyWithProfileDto {
|
||||
@IsEnum(CompanyType)
|
||||
@@ -55,4 +67,11 @@ export class CreateCompanyWithProfileDto {
|
||||
|
||||
@IsOptional()
|
||||
attributes?: Record<string, any>;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ArrayMinSize(1)
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => CompanyProfileInputDto)
|
||||
companyProfiles?: CompanyProfileInputDto[];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,29 @@
|
||||
import { Company, CompanyType, CompanyStatus } from '../entities/company.entity';
|
||||
import { CompanyProfile } from '../entities/company-profile.entity';
|
||||
import { ResponseExternalProfileDto } from './response-external-profile.dto';
|
||||
|
||||
export class ResponseCompanyProfileDto {
|
||||
id: string;
|
||||
type: string;
|
||||
reference: string;
|
||||
status: string;
|
||||
businessLicense?: string | null;
|
||||
attributes?: Record<string, any> | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
|
||||
constructor(profile: CompanyProfile) {
|
||||
this.id = profile.id;
|
||||
this.type = profile.type;
|
||||
this.reference = profile.reference;
|
||||
this.status = profile.status;
|
||||
this.businessLicense = profile.businessLicense;
|
||||
this.attributes = profile.attributes;
|
||||
this.createdAt = profile.createdAt;
|
||||
this.updatedAt = profile.updatedAt;
|
||||
}
|
||||
}
|
||||
|
||||
export class ResponseCompanyDto {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -16,6 +39,7 @@ export class ResponseCompanyDto {
|
||||
website?: string | null;
|
||||
attributes?: Record<string, any> | null;
|
||||
profiles?: ResponseExternalProfileDto[];
|
||||
companyProfiles?: ResponseCompanyProfileDto[];
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
|
||||
@@ -34,6 +58,7 @@ export class ResponseCompanyDto {
|
||||
this.website = company.website;
|
||||
this.attributes = company.attributes;
|
||||
this.profiles = company.profiles?.map((p) => new ResponseExternalProfileDto(p));
|
||||
this.companyProfiles = company.companyProfiles?.map((p) => new ResponseCompanyProfileDto(p));
|
||||
this.createdAt = company.createdAt;
|
||||
this.updatedAt = company.updatedAt;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user