mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
feat(freight): record when a company is approved
Ticket #420 — add approved_at to companies (migration), stamped at both promotion sites (first-profile auto-approve and manual staff status change). Surfaced as Submitted on/Approved on in the backoffice customer list and detail views.
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
/**
|
||||
* Pending→Active is the only company-level approval event; `updatedAt` can't
|
||||
* stand in for it since any field edit bumps that too. Nullable — existing
|
||||
* companies (approved before this column existed) have no recorded moment.
|
||||
*/
|
||||
export class AddApprovedAtToCompanies3120000000000 implements MigrationInterface {
|
||||
name = "AddApprovedAtToCompanies3120000000000";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE freight.companies
|
||||
ADD COLUMN IF NOT EXISTS approved_at timestamptz`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE freight.companies
|
||||
DROP COLUMN IF EXISTS approved_at`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -682,7 +682,16 @@ export class CompaniesService {
|
||||
|
||||
async updateCompany(id: string, dto: UpdateCompanyDto): Promise<Company> {
|
||||
const before = await this.findCompanyById(id);
|
||||
const updated = await this.companiesRepo.update(id, dto);
|
||||
const patch: UpdateCompanyDto & { approvedAt?: Date } = { ...dto };
|
||||
// Staff can also promote Pending -> Active directly through this generic
|
||||
// endpoint (not just via the first-profile-approval path), so stamp it here too.
|
||||
if (
|
||||
dto.status === CompanyStatus.Active &&
|
||||
before.status !== CompanyStatus.Active
|
||||
) {
|
||||
patch.approvedAt = new Date();
|
||||
}
|
||||
const updated = await this.companiesRepo.update(id, patch);
|
||||
if (!updated) throw new NotFoundException(`Company ${id} not found`);
|
||||
|
||||
// Suspending or blacklisting locks the customer out, so they must be told.
|
||||
@@ -1472,6 +1481,7 @@ export class CompaniesService {
|
||||
) {
|
||||
await companyRepo.update(updated.companyId, {
|
||||
status: CompanyStatus.Active,
|
||||
approvedAt: new Date(),
|
||||
});
|
||||
this.companyNotifier.companyApproved(company);
|
||||
}
|
||||
|
||||
@@ -97,6 +97,7 @@ export class ResponseCompanyDto {
|
||||
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
approvedAt: Date | null;
|
||||
|
||||
constructor(company: Company) {
|
||||
this.id = company.id;
|
||||
@@ -135,5 +136,6 @@ export class ResponseCompanyDto {
|
||||
this.identity = buildCompanyIdentityState(company);
|
||||
this.createdAt = company.createdAt;
|
||||
this.updatedAt = company.updatedAt;
|
||||
this.approvedAt = company.approvedAt ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,10 @@ export class Company extends BaseEntity {
|
||||
})
|
||||
status!: CompanyStatus;
|
||||
|
||||
/** Set when the company is first promoted Pending → Active. Null for companies approved before this column existed. */
|
||||
@Column({ name: "approved_at", type: "timestamptz", nullable: true })
|
||||
approvedAt?: Date | null;
|
||||
|
||||
@Column({ name: "tin", type: "varchar", length: 10, unique: true })
|
||||
tin!: string;
|
||||
|
||||
|
||||
@@ -757,6 +757,18 @@ export default function CustomerDetailPage() {
|
||||
<InfoField label="TIN" value={company.tin} />
|
||||
<InfoField label="VAT number" value={company.vatNumber} />
|
||||
<InfoField label="FAN number" value={company.fanNumber} />
|
||||
<InfoField
|
||||
label="Submitted on"
|
||||
value={formatDate(company.createdAt)}
|
||||
/>
|
||||
<InfoField
|
||||
label="Approved on"
|
||||
value={
|
||||
company.approvedAt
|
||||
? formatDate(company.approvedAt)
|
||||
: "Not yet approved"
|
||||
}
|
||||
/>
|
||||
<InfoField
|
||||
label="Owner identity"
|
||||
value={
|
||||
|
||||
@@ -245,6 +245,16 @@ export default function CustomersPage() {
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "approved",
|
||||
header: "Approved",
|
||||
meta: { headerClassName: "text-right", cellClassName: "text-right" },
|
||||
cell: ({ row }) => (
|
||||
<Text size="sm" c="dimmed">
|
||||
{row.original.approvedAt ? formatDate(row.original.approvedAt) : "—"}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
@@ -110,6 +110,27 @@ export interface CompanyChangeRequest {
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/** One field/document change recorded on a company revision. */
|
||||
export interface CompanyRevisionChange {
|
||||
field: string;
|
||||
label: string;
|
||||
from: string | null;
|
||||
to: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Onboarding-phase edit history — records what changed on a company record
|
||||
* before it reached Active, the write path that has no approval gate.
|
||||
*/
|
||||
export interface CompanyRevision {
|
||||
id: string;
|
||||
companyId: string;
|
||||
actorId: string | null;
|
||||
summary: string;
|
||||
changes: CompanyRevisionChange[];
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
/** The channel a customer's password-reset link is delivered over. */
|
||||
export type ResetChannel = "email" | "phone";
|
||||
|
||||
@@ -215,6 +236,7 @@ export interface Company {
|
||||
onboardingCompleted?: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
approvedAt?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user