Merge pull request #1124 from Tria-plc/freight/nati-2

Audit log and presistent cbe pnr
This commit is contained in:
Nathnael Wondisha
2026-08-05 17:23:54 +03:00
committed by GitHub
28 changed files with 839 additions and 294 deletions

View File

@@ -1,5 +1,17 @@
# Copy to .env for local/docker compose (not committed).
PORT=3001
# @tria-plc/auditlog's client interceptor stamps every AuditLog row's
# `application` from this env var directly, bypassing MezgebModule.forRoot's
# applicationName option (package quirk). audit.controller.ts reads the same
# var when filtering reads, so this can be anything as long as it's set.
APPLICATION_NAME=freight-api
# Also required for @tria-plc/auditlog: its producer (AuditClientModule)
# reads the RMQ URL at package IMPORT time, before MezgebModule.forRoot's
# rmqUrl option ever runs, so only an env var reaches it — an in-code
# override is too late. Without this, audit events are silently dropped
# (no error, nothing published). Point it at whatever broker/vhost your
# RabbitMQ actually has a user provisioned on.
RABBITMQ_URL=amqp://localhost:5672
# GT06 GPS tracker TCP listener port (raw TCP, must be reachable by tracker SIMs). 0 disables.
GT06_TCP_PORT=5023
DB_HOST=localhost

View File

@@ -59,6 +59,7 @@
"@nestjs/typeorm": "^11.0.1",
"@nestjs/websockets": "^11.1.27",
"@tria-plc/api-common": "file:../../local-packages/tria-plc-api-common-1.6.0.tgz",
"@tria-plc/auditlog": "file:../../local-packages/tria-plc-auditlog-1.1.2.tgz",
"@tria-plc/iamapi-common": "file:../../local-packages/tria-plc-iamapi-common-1.0.0.tgz",
"amqp-connection-manager": "^5.0.0",
"amqplib": "^2.0.1",

View File

@@ -16,6 +16,7 @@ import {
import { IamBaselineSeeder, IamSeedModule } from "@edr/iam-seed";
import { IamModule } from "@tria-plc/iamapi-common";
import { SharedAuthModule } from "@tria-plc/api-common/modules/auth/shared-auth.module";
import { MezgebModule } from "@tria-plc/auditlog";
import appConfig from "./config/app.config";
import databaseConfig from "./config/database.config";
@@ -105,9 +106,14 @@ import { LastMileModule } from "./modules/last-mile/last-mile.module";
import { InterchangeDocumentsModule } from "./modules/interchange-documents/interchange-documents.module";
import { ImportOperationsModule } from "./modules/import-operations/import-operations.module";
import { AiModule } from "./modules/ai/ai.module";
import { AuditModule } from "./modules/audit/audit.module";
import { LoggerMiddleware } from "./logger.middleware";
import { LoginAudienceMiddleware } from "./modules/auth/login-audience.middleware";
if (!process.env.APPLICATION_NAME) {
process.env.APPLICATION_NAME = "freight";
}
@Module({
imports: [
ConfigModule.forRoot({
@@ -155,6 +161,19 @@ import { LoginAudienceMiddleware } from "./modules/auth/login-audience.middlewar
return dataSource;
},
}),
// Request + entity-level audit logging over RabbitMQ (@tria-plc/auditlog).
// Must come after TypeOrmModule above so it picks up this app's DataSource.
// rmqUrl falls back the same way notifications.module.ts's RABBITMQ_URL
// does: the dev broker only provisions the `edr` user on the `payment`
// vhost (docker-compose's RABBITMQ_DEFAULT_USER/VHOST), so an unset
// RABBITMQ_URL must land there too, not on guest@'/' (403 ACCESS_REFUSED).
MezgebModule.forRoot({
applicationName: "freight-api",
rmqUrl:
process.env.RABBITMQ_URL ??
process.env.PAYMENT_RABBITMQ_URL ??
"amqp://localhost:5672",
}),
SharedAuthModule,
IamModule.forRoot({
applications: [EDR_FREIGHT_APPLICATION],
@@ -227,6 +246,7 @@ import { LoginAudienceMiddleware } from "./modules/auth/login-audience.middlewar
VerifaydaModule,
FleetHistoryModule,
AiModule,
AuditModule,
],
providers: [
EdrOrgSeeder,

View File

@@ -56,6 +56,11 @@ import { EmployeePositionActivePeriod } from "@tria-plc/iamapi-common/entities/i
import { UnitConfiguration } from "@tria-plc/iamapi-common/entities/iam/organization-structure/unit-configuration.entity";
import { Site } from "@tria-plc/iamapi-common/entities/iam/site/site.entity";
import { SiteSetting } from "@tria-plc/iamapi-common/entities/iam/site/site-setting.entity";
import { AuditLog, AuditLogCommand } from "@tria-plc/auditlog";
// @tria-plc/auditlog's entities live in node_modules, same as the iam ones —
// the glob below only matches this app's own src/**/*.entity.ts.
const auditEntities = [AuditLog, AuditLogCommand];
const iamEntities = [
UnitSetting,
@@ -177,7 +182,11 @@ export function buildDataSourceOptions(): DataSourceOptions {
return {
...buildConnectionOptions(),
schema: "public",
entities: [__dirname + "/../**/*.entity.{ts,js}", ...iamEntities],
entities: [
__dirname + "/../**/*.entity.{ts,js}",
...iamEntities,
...auditEntities,
],
migrations: [],
};
}

View File

@@ -10,6 +10,7 @@ import {
ResponseTransformInterceptor,
createValidationPipe,
} from "@edr/api-common";
import { getAuditLoggerConfig } from "@tria-plc/auditlog";
import { AppModule } from "./app.module";
@@ -160,6 +161,13 @@ export async function createFreightApp(): Promise<NestExpressApplication> {
app.useGlobalFilters(new HttpExceptionFilter());
app.useGlobalInterceptors(new ResponseTransformInterceptor());
// Audit listener: consumes the RMQ events MezgebModule's client interceptor
// (app.module.ts) emits and persists them via the AuditLogController /
// AuditLogCommandController @EventPattern handlers. Same queue config the
// client side uses, reused from the package so the two never drift apart.
app.connectMicroservice(getAuditLoggerConfig());
await app.startAllMicroservices();
const config = new DocumentBuilder()
.setTitle("EDR Freight API")
.setDescription("API for the EDR Freight Management application")

View File

@@ -0,0 +1,32 @@
import { Controller, Get, Query } from "@nestjs/common";
import { ApiOperation, ApiQuery, ApiTags } from "@nestjs/swagger";
import { BookingStaff } from "../../common/booking-guards";
import { FREIGHT_PERMS } from "../../seed/freight-permissions.registry";
import { AuditService } from "./audit.service";
@ApiTags("audit")
@Controller("audit")
@BookingStaff(FREIGHT_PERMS.audit.view)
export class AuditController {
constructor(private readonly auditService: AuditService) {}
@Get("logs")
@ApiOperation({ summary: "List freight-api audit log commands" })
@ApiQuery({ name: "skip", type: Number, required: false })
@ApiQuery({ name: "take", type: Number, required: false })
list(@Query("skip") skip?: string, @Query("take") take?: string) {
// Same fallback chain @tria-plc/auditlog's client interceptor uses to
// stamp AuditLog.application (mezgeb/client/client-audit.interceptor.js)
// — reading it here instead of a hardcoded literal means this can't
// silently drift out of sync with whatever APPLICATION_NAME/APP_NAME
// actually is at runtime.
const application =
process.env.APPLICATION_NAME ?? process.env.APP_NAME ?? "DEFAULT";
return this.auditService.list(
application,
skip !== undefined ? parseInt(skip, 10) : undefined,
take !== undefined ? parseInt(take, 10) : undefined,
);
}
}

View File

@@ -0,0 +1,13 @@
import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { AuditLogCommand } from "@tria-plc/auditlog";
import { AuditController } from "./audit.controller";
import { AuditService } from "./audit.service";
@Module({
imports: [TypeOrmModule.forFeature([AuditLogCommand])],
controllers: [AuditController],
providers: [AuditService],
})
export class AuditModule {}

View File

@@ -0,0 +1,70 @@
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { AuditLogCommand } from "@tria-plc/auditlog";
import { CLIENT_APP_HEADER } from "../auth/login-audience.middleware";
export interface AuditLogListResult {
count: number;
items: AuditLogCommand[];
}
/**
* Own read path onto @tria-plc/auditlog's tables, gated by AuditController's
* @BookingStaff — the package's own AuditLogCommandController (mounted at
* /api/audit-log-commands) ships with no guards at all, so it can't be used
* directly for a permission-gated UI. Query mirrors the package's
* AuditLogCommandService.buildAuditLogQuery/getAllAuditLogs exactly.
*/
@Injectable()
export class AuditService {
constructor(
@InjectRepository(AuditLogCommand)
private readonly auditLogCommandRepository: Repository<AuditLogCommand>,
) {}
async list(
application: string,
skip = 0,
take = 10,
): Promise<AuditLogListResult> {
const [items, count] = await this.auditLogCommandRepository
.createQueryBuilder("audit_log_commands")
.leftJoinAndSelect("audit_log_commands.auditLog", "auditLog")
.andWhere(
"(audit_log_commands.auditLogId IS NULL OR auditLog.application = :application)",
{ application },
)
.andWhere(
"(audit_log_commands.auditLogId IS NULL OR auditLog.status = :status)",
{ status: "Commit" },
)
// Backoffice-only view: portal (customer-facing) writes carry the same
// request-header set by every axios call from that app — see
// login-audience.middleware.ts. Rows with no linked auditLog (child/
// event commands with no request context) stay visible; they aren't
// attributable to any frontend, so they're not portal noise either.
.andWhere(
"(audit_log_commands.auditLogId IS NULL OR auditLog.requestHeader ->> :clientAppHeader = :clientApp)",
{ clientAppHeader: CLIENT_APP_HEADER, clientApp: "backoffice" },
)
.select([
"audit_log_commands.id",
"audit_log_commands.createdAt",
"audit_log_commands.deletedAt",
"audit_log_commands.entityName",
"audit_log_commands.queryMethod",
"audit_log_commands.changes",
"audit_log_commands.payload",
"auditLog.id",
"auditLog.user",
])
.addOrderBy("audit_log_commands.createdAt", "DESC")
.skip(skip)
.take(take)
.getManyAndCount();
return { count, items };
}
}

View File

@@ -10,6 +10,7 @@ import {
import { EventEmitter2 } from "@nestjs/event-emitter";
import { DataSource, EntityManager, In } from "typeorm";
import { Booking } from "../bookings/entities/booking.entity";
import { CompaniesService } from "../companies/companies.service";
import { applyBookingRefDirectionScope } from "../user-trade-access/trade-scope.util";
import { PaymentService } from "../payment/payment.service";
@@ -1192,6 +1193,17 @@ export class BillingService {
.getRepository(Invoice)
.update({ id: invoice.id }, { paymentId: result.intentId });
// CBE_BILL: the bill reference IS the booking's PNR — the number the customer pays against
// at any CBE channel. Persist it on the booking so it survives the initiate response and
// shows on the booking/contract everywhere. The payment service reissues the same reference
// while the bill stays open, so re-initiating overwrites with an identical value.
const billReference = result.response.clientAction?.billReference;
if (billReference && invoice.source === Freight.InvoiceSource.Booking) {
await this.dataSource
.getRepository(Booking)
.update({ id: invoice.sourceId }, { pnrCode: billReference });
}
// Settlement is driven by the payment API (webhook/outbox → payment.succeeded);
// billing must not simulate it. Kept for local demos only.
// An OTP intent (CAC Bank) is NOT paid yet — the payer still has to enter the

View File

@@ -280,15 +280,104 @@ describe("Fayda identity verification binds a person to the company", () => {
expect(ctx.attributes.poaFaydaSub).toBe("new-sub");
});
it("refuses to rename a verified person by hand", async () => {
const { service } = makeService({
it("stages nothing for a verified field an approved company resubmits", async () => {
// Approving it could not move the live row — the verified value is written
// back over it — so it must never reach a reviewer as a pending change.
const { service, deps } = makeService({
status: CompanyStatus.Active,
attributes: { ...OWNER_VERIFIED, ownerEmail: "abebe@example.com" },
});
await expect(
service.updateProfile("user-1", {
companyEmail: "someone-else@example.com",
} as never),
).resolves.toBeDefined();
expect(deps.changeRequestRepo.create).not.toHaveBeenCalled();
expect(deps.changeRequestRepo.update).not.toHaveBeenCalled();
expect(deps.companiesRepo.update).not.toHaveBeenCalled();
});
// The verified value wins, and it wins by overwriting rather than by
// rejecting: nobody types these fields, so a submission that disagrees is a
// stale form echoing itself back, not an edit. Failing it would block a save
// the customer never made — and leave them no way through, since re-verifying
// returns the same value they are being 400'd for.
it("overwrites a hand-renamed verified person with the verified name", async () => {
const { service, ctx } = makeService({
attributes: { ...OWNER_VERIFIED, ...POA_VERIFIED },
files: [paper()],
});
await expect(
service.updateProfile("user-1", { poaName: "Someone Else" } as never),
).rejects.toBeInstanceOf(BadRequestException);
).resolves.toBeDefined();
expect(ctx.attributes.poaName).toBe(POA_VERIFIED.poaName);
});
// Fayda's email and phone claims are optional — a verification can prove the
// person and return neither. Holding the company mirrors to "the owner is
// verified" rather than to "the verification supplied this value" would
// clobber the fallbacks the portal is built to send (account email, eTrade's
// registered phone) with nothing at all. OWNER_VERIFIED is exactly that
// shape: a sub, no contact details.
it("keeps company contact details a Fayda verification never supplied", async () => {
const { service, deps } = makeService({
attributes: { ...OWNER_VERIFIED },
});
await expect(
service.updateProfile("user-1", {
companyEmail: "account@example.com",
companyPhone: "+251911777777",
} as never),
).resolves.toBeDefined();
const [, patch] = deps.companiesRepo.update.mock.calls.at(-1)!;
expect(patch.email).toBe("account@example.com");
expect(patch.phone).toBe("+251911777777");
});
it("overwrites company contact details the verification did supply", async () => {
const { service, deps } = makeService({
attributes: {
...OWNER_VERIFIED,
ownerEmail: "abebe@example.com",
ownerPhone: "+251911000000",
},
});
await expect(
service.updateProfile("user-1", {
companyEmail: "someone-else@example.com",
companyPhone: "+251911999999",
} as never),
).resolves.toBeDefined();
const [, patch] = deps.companiesRepo.update.mock.calls.at(-1)!;
expect(patch.email).toBe("abebe@example.com");
expect(patch.phone).toBe("+251911000000");
});
// "Same as owner" copies `ownerEmail ?? null` onto the GM while setting
// `gmFaydaSub`. Locking that null made generalManagerEmail required by
// onboarding, hidden by the portal's link card and unwritable at once.
it("lets the GM's details be typed when the copied owner identity carried none", async () => {
const { service } = makeService({
attributes: {
...OWNER_VERIFIED,
gmSameAsOwner: true,
gmFaydaSub: "owner-sub",
generalManagerName: "Abebe Bikila",
generalManagerEmail: null,
generalManagerPhone: null,
},
});
await expect(
service.updateProfile("user-1", {
generalManagerEmail: "gm@example.com",
generalManagerPhone: "+251911888888",
} as never),
).resolves.toBeDefined();
});
it("never locks or gates the general manager — it is not the verified subject", async () => {

View File

@@ -739,6 +739,39 @@ export class CompaniesService {
return out;
}
/**
* `UpdateProfileDto` keys this company's completed verifications own — the
* ones `mapProfileDtoToCompanyUpdates` overwrites with the verified value
* whatever a request submits for them.
*
* A key only lands here once there is a verified value to hold it to: Fayda's
* email and phone claims are optional, and a verification that returned
* neither owns nothing to overwrite with.
*
* The map is the enforcement; this is the list used to keep those keys out of
* a change request in the first place. If the two ever drift the map still
* wins — the cost is a staged field that approving turns out not to move.
*/
private faydaOwnedKeys(company: Company): string[] {
const attrs = company.attributes ?? {};
const held = (key: string) => {
const v = attrs[key];
return v !== null && v !== undefined && v !== "";
};
const keys: string[] = [];
if (attrs.ownerFaydaSub) {
// The Company-column mirrors of the owner's verified contact details.
if (held("ownerEmail")) keys.push("companyEmail");
if (held("ownerPhone")) keys.push("companyPhone");
}
for (const subject of IDENTITY_SUBJECTS) {
if (!attrs[`${IDENTITY_PREFIX[subject]}FaydaSub`]) continue;
keys.push(...IDENTITY_OWNED_FIELDS[subject].filter(held));
}
return keys;
}
/**
* Translate an UpdateProfileDto (or a staged change-request snapshot) into a
* `Company` patch: scalar columns plus a merged `attributes` blob (contact/GM/
@@ -829,49 +862,46 @@ export class CompaniesService {
// lets the customer type them once verified) — lock them the same way
// ownerEmail/ownerPhone themselves are locked below, once there is a
// verified owner to lock them to.
//
// Keyed on the verified VALUE, not on `ownerFaydaSub`: Fayda's email and
// phone claims are optional, so a verification can prove the person while
// supplying neither (see completeIdentityVerification's conditional
// spreads). The portal falls back to the account email / eTrade's
// registered phone in exactly that case and submits it on every save of
// the company step — locking against an absent value would 400 that
// forever, and re-verifying could never clear it because Fayda still has
// nothing to return.
if (attrUpdates.ownerFaydaSub) {
if (
dto.companyEmail !== undefined &&
dto.companyEmail !== attrUpdates.ownerEmail
) {
throw new BadRequestException(
"companyEmail is set by the owner's Fayda verification and cannot be edited. Re-verify to change it.",
);
}
if (
dto.companyPhone !== undefined &&
normalizeE164(dto.companyPhone) !==
normalizeE164(String(attrUpdates.ownerPhone ?? ""))
) {
throw new BadRequestException(
"companyPhone is set by the owner's Fayda verification and cannot be edited. Re-verify to change it.",
);
}
if (attrUpdates.ownerEmail && dto.companyEmail !== undefined)
companyUpdates.email = attrUpdates.ownerEmail;
if (attrUpdates.ownerPhone && dto.companyPhone !== undefined)
companyUpdates.phone = normalizeE164(String(attrUpdates.ownerPhone));
}
// Renaming a Fayda-verified person by hand would launder the guarantee
// away, so the fields the verification owns are refused once it exists.
// away, so the verification keeps these fields: a submission that disagrees
// is overwritten with the verified value rather than rejected — the same
// doctrine `applyEtradeSourcedFields` uses for eTrade's fields, and for the
// same reason. The customer never types these (the portal derives them, and
// a stale form or a re-render can echo back something else entirely), so a
// 400 punishes a save they never made while an overwrite lands the truth.
for (const subject of IDENTITY_SUBJECTS) {
if (!attrUpdates[`${IDENTITY_PREFIX[subject]}FaydaSub`]) continue;
for (const field of IDENTITY_OWNED_FIELDS[subject]) {
const incoming = (dto as Record<string, unknown>)[field];
if (incoming === undefined) continue;
// The verification itself is allowed to write them; anything else is
// compared against what is already stored, not against the value this
// same call just copied into the patch. Phones are compared normalized:
// a form that re-renders +251911000000 as 0911000000 is echoing the
// stored value back, not trying to change it.
if ((dto as Record<string, unknown>)[field] === undefined) continue;
// The verification itself is what writes them; it must not be undone by
// the value this same call just copied into the patch.
if (dto.faydaIdentity && field in dto.faydaIdentity) continue;
const stored = company.attributes?.[field];
const same = field.endsWith("Phone")
? normalizeE164(String(incoming)) ===
normalizeE164(String(stored ?? ""))
: incoming === stored;
if (!same) {
throw new BadRequestException(
`${field} is set by the Fayda verification of this company's ${IDENTITY_LABEL[subject]} and cannot be edited. Re-verify to change it.`,
);
}
// A verification that supplied nothing for this field left no guarantee
// to protect, so it stays typeable. Matters most for the GM —
// `setGmSameAsOwner` copies `ownerEmail ?? null` onto
// `generalManagerEmail` while setting `gmFaydaSub`, and
// REQUIRED_COMPANY_INFO still demands that email, so holding a null
// here makes it required, hidden by the portal's "same as owner" card,
// and unwritable all at once.
if (stored === null || stored === undefined || stored === "") continue;
attrUpdates[field] = stored;
}
}
@@ -977,6 +1007,11 @@ export class CompaniesService {
// for review with the live row left intact.
await this.assertTinAvailable(company, dto.tin);
const fields = this.pickDefined(dto);
// Drop what the verifications own before anything is staged. Approving one
// of these could not change the live row — mapProfileDtoToCompanyUpdates
// writes the verified value back over it — so showing it to a reviewer
// asks them to rule on a change that does not exist.
for (const key of this.faydaOwnedKeys(company)) delete fields[key];
const selfService: Record<string, any> = {};
const staged: Record<string, any> = {};
for (const [key, value] of Object.entries(fields)) {

View File

@@ -366,6 +366,7 @@ export const CONFIG_SETTINGS_PERMISSIONS: FreightPermissionSeed[] = [
perm('b4a00001-0001-4000-8000-000000000002', 'edr_freight_app:settings:file_upload:manage', 'Manage file-upload settings'),
perm('b4b00001-0001-4000-8000-000000000001', 'edr_freight_app:settings:dropdown:view', 'View dropdown settings'),
perm('b4b00001-0001-4000-8000-000000000002', 'edr_freight_app:settings:dropdown:manage', 'Manage dropdown settings'),
perm('b4c00001-0001-4000-8000-000000000001', 'edr_freight_app:audit:view', 'View audit logs'),
];
// M. Staff / IAM admin — NEW keys only. The employee_registration / role_assignment
@@ -700,6 +701,9 @@ export const FREIGHT_PERMS = {
manage: 'edr_freight_app:settings:dropdown:manage',
},
},
audit: {
view: 'edr_freight_app:audit:view',
},
staff: {
roles: {
view: 'edr_freight_app:staff:roles:view',

View File

@@ -1,3 +1,6 @@
# Dev server port. Default: 5283.
PORT=5283
VITE_API_URL=http://localhost:3001
VITE_BASE_API_URL=http://localhost:3001

View File

@@ -4,7 +4,7 @@
"private": true,
"type": "module",
"scripts": {
"dev": "vite --port 5183 --clearScreen false",
"dev": "vite --clearScreen false",
"prebuild": "node -e \"const fs=require('fs'); fs.rmSync('dist',{recursive:true,force:true});\"",
"build": "vite build",
"preview": "vite preview --port 5183",

View File

@@ -7,6 +7,7 @@ import {
FileSignature,
FileText,
Hammer,
History,
LayoutDashboard,
LayoutGrid,
MapPin,
@@ -76,6 +77,7 @@ import ReportsHubPage from "./pages/reports/ReportsHubPage";
import ReportPage from "./pages/reports/ReportPage";
import AiBookingMockTestPage from "./pages/ai/AiBookingMockTestPage";
import PaymentsPage from "./pages/payments/PaymentsPage";
import AuditLogsPage from "./pages/audit/AuditLogsPage";
//import EmployeesPage from "./pages/dashboard/user-management/EmployeesPage";
import { RequirePermission } from "./components/auth/RequirePermission";
import {
@@ -582,6 +584,12 @@ const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [
icon: <ScrollText />,
permission: FREIGHT_PERMS.admin,
},
{
label: "Audit logs",
href: "/dashboard/audit-logs",
icon: <History />,
permission: FREIGHT_PERMS.audit.view,
},
{
label: "Configuration",
href: "/dashboard/configuration",
@@ -1595,6 +1603,14 @@ const App = () => {
</RequirePermission>
}
/>
<Route
path="audit-logs"
element={
<RequirePermission permission={FREIGHT_PERMS.audit.view}>
<AuditLogsPage />
</RequirePermission>
}
/>
<Route
path="contract-templates"
element={

View File

@@ -184,6 +184,13 @@ const ROUTE_META: Array<{ prefix: string; meta: PageMeta }> = [
subtitle: "Manage dropdown options used across the platform",
},
},
{
prefix: "/dashboard/audit-logs",
meta: {
title: "Audit Logs",
subtitle: "Request and entity-level activity recorded across the freight API",
},
},
{
prefix: "/dashboard/configuration/contract-validity-periods",
meta: {

View File

@@ -309,6 +309,10 @@ export const URL_CONSTANTS = {
SUMMARY: "/payments/summary",
},
AUDIT: {
LOGS: "/audit/logs",
},
LOCOMOTIVES: {
BASE: "/locomotives",
BY_ID: (id: string) => `/locomotives/${id}`,

View File

@@ -276,6 +276,9 @@ export const FREIGHT_PERMS = {
manage: "edr_freight_app:settings:dropdown:manage",
},
},
audit: {
view: "edr_freight_app:audit:view",
},
staff: {
roles: {
view: "edr_freight_app:staff:roles:view",

View File

@@ -0,0 +1,190 @@
import { Badge, Box, Card, Stack, Text } from "@mantine/core";
import { useQuery } from "@tanstack/react-query";
import { PageContainer, PageHeader } from "@/components/page";
import { api } from "@/services/api";
import type {
AuditLogRow,
AuditQueryMethod,
AuditUser,
LocalizedText,
} from "@/services/audit.service";
import {
DataTable,
DataTableFooter,
usePagination,
type ColumnDef,
} from "@edr/ui-common";
const ACTION_LABELS: Record<AuditQueryMethod, string> = {
INSERT: "Created",
UPDATE: "Updated",
DELETE: "Deleted",
INSERT_CHILD: "Linked child",
DELETE_CHILD: "Unlinked child",
};
const ACTION_COLORS: Record<AuditQueryMethod, string> = {
INSERT: "edr-green",
UPDATE: "yellow",
DELETE: "red",
INSERT_CHILD: "indigo",
DELETE_CHILD: "gray",
};
function formatDateTime(iso: string): string {
const d = new Date(iso);
return Number.isNaN(d.getTime())
? "—"
: d.toLocaleString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
});
}
// See LocalizedText: `name`/`title` lifted from a raw audited entity can be
// a plain string or IAM's { am, en } — never render either directly.
// "undefined undefined" is the producer's own broken template when no user
// was attached at all (unauthenticated/customer flows, e.g. Fayda
// verification) — filtered out here rather than shown as raw garbage.
function localize(value: LocalizedText | null | undefined): string | undefined {
if (!value) return undefined;
if (typeof value === "object") return value.en ?? value.am ?? undefined;
if (/^undefined(\s+undefined)?$/.test(value.trim())) return undefined;
return value;
}
function formatUser(user: AuditUser | null | undefined): string {
return localize(user?.name) ?? user?.id ?? "—";
}
function summarize(row: AuditLogRow): string {
if (row.changes?.length) {
return row.changes
.slice(0, 2)
.map((c) => c.field)
.join(", ") + (row.changes.length > 2 ? `, +${row.changes.length - 2} more` : "");
}
if (row.payload) {
return (
localize(row.payload.name) ?? localize(row.payload.title) ?? row.payload.id ?? "—"
);
}
return "—";
}
const tableHeader =
"text-xs font-semibold uppercase tracking-wide text-muted-foreground";
export default function AuditLogsPage() {
const { pagination, setPagination } = usePagination({ pageSize: 20 });
const filter = {
skip: pagination.pageIndex * pagination.pageSize,
take: pagination.pageSize,
};
const { data, isLoading, isError } = useQuery(
api.audit.list.queryOptions({ input: { filter } }),
);
const rows = data?.items ?? [];
const total = data?.count ?? 0;
const pageCount = Math.max(1, Math.ceil(total / pagination.pageSize));
const columns: ColumnDef<AuditLogRow>[] = [
{
id: "time",
header: () => <span className={tableHeader}>Time</span>,
cell: ({ row }) => (
<span className="text-sm text-muted-foreground">
{formatDateTime(row.original.createdAt)}
</span>
),
},
{
id: "action",
header: () => <span className={tableHeader}>Action</span>,
cell: ({ row }) => (
<Badge
color={ACTION_COLORS[row.original.queryMethod] ?? "gray"}
variant="light"
radius="sm"
>
{ACTION_LABELS[row.original.queryMethod] ?? row.original.queryMethod}
</Badge>
),
},
{
id: "entity",
header: () => <span className={tableHeader}>Entity</span>,
cell: ({ row }) => (
<span className="font-mono text-sm text-foreground">
{row.original.entityName}
</span>
),
},
{
id: "user",
header: () => <span className={tableHeader}>User</span>,
cell: ({ row }) => (
<span className="text-sm text-foreground">
{formatUser(row.original.auditLog?.user)}
</span>
),
},
{
id: "summary",
header: () => <span className={tableHeader}>Summary</span>,
cell: ({ row }) => (
<span className="truncate text-sm text-muted-foreground">
{summarize(row.original)}
</span>
),
},
];
return (
<PageContainer>
<PageHeader
title="Audit Logs"
subtitle="Request and entity-level activity recorded across the freight API."
/>
<Card p={0}>
<Stack gap={0}>
<Box px="md" pt="md" pb="sm" w="100%">
<Text size="sm" c="dimmed">
{total} record{total !== 1 ? "s" : ""}
</Text>
</Box>
<Box style={{ overflowX: "auto" }} w="100%">
<DataTable
columns={columns}
data={rows}
status={isLoading ? "loading" : isError ? "error" : "success"}
pagination={{
pageIndex: pagination.pageIndex,
pageSize: pagination.pageSize,
pageCount,
totalCount: total,
}}
tableOptions={{
state: { pagination },
onPaginationChange: setPagination,
manualPagination: true,
pageCount,
}}
containerClassName="border-0 shadow-none bg-transparent"
footer={DataTableFooter}
/>
</Box>
</Stack>
</Card>
</PageContainer>
);
}

View File

@@ -163,6 +163,11 @@ import {
type SaveLocomotivePayload,
} from "./locomotives.service";
import { overviewService } from "./overview.service";
import {
auditService,
type AuditLogListFilter,
type PaginatedAuditLogs,
} from "./audit.service";
import { reportsService } from "./reports.service";
import type { ReportQueryInput, ReportResult } from "@/types/reports";
import {
@@ -2136,6 +2141,15 @@ export const api = {
),
},
audit: {
list: endpoint<{ filter?: AuditLogListFilter }, PaginatedAuditLogs>(
"audit",
"list",
({ filter }) => auditService.list(filter),
({ filter }) => ["audit", "list", filter ?? {}],
),
},
signatures: {
mySignature: endpoint<void, SavedSignature | null>(
"me",

View File

@@ -0,0 +1,70 @@
import { api as client } from "../auth/http";
import { unwrap } from "@/utils/endpoint";
import { URL_CONSTANTS } from "@/constants/URLS";
const A = URL_CONSTANTS.AUDIT;
// Shape from @tria-plc/auditlog's AuditLogCommandController — see
// local-packages/FRONTEND_GUIDE.md.
export type AuditQueryMethod =
| "INSERT"
| "UPDATE"
| "DELETE"
| "INSERT_CHILD"
| "DELETE_CHILD";
export interface AuditFieldChange {
field: string;
from: unknown;
to: unknown;
}
// IAM entities (users, orgs, positions, ...) name themselves bilingually —
// see edr-org.seeder.ts. Any `name`/`title` field lifted from a raw audited
// entity (auditLog.user, payload) can come back as either a plain string or
// this shape; both `name` fields below reflect that.
export type LocalizedText = string | { am?: string; en?: string };
// The vendored interceptor's own broken template produces a plain string
// ("undefined undefined") when no user was attached at all (unauthenticated/
// customer flows) — that's the non-bilingual string case for `name` here.
export interface AuditUser {
id?: string;
name?: LocalizedText;
organizationId?: string;
organizationName?: string;
[key: string]: unknown;
}
export interface AuditLogRow {
id?: string;
createdAt: string;
deletedAt?: string | null;
entityName: string;
queryMethod: AuditQueryMethod;
changes?: AuditFieldChange[] | null;
payload?: { name?: LocalizedText; title?: LocalizedText; id?: string } | null;
auditLog?: { id?: string; user?: AuditUser | null };
}
export interface AuditLogListFilter {
skip?: number;
take?: number;
}
export interface PaginatedAuditLogs {
items: AuditLogRow[];
count: number;
}
export const auditService = {
list: async (filter?: AuditLogListFilter): Promise<PaginatedAuditLogs> => {
const params: Record<string, number | undefined> = {
skip: filter?.skip,
take: filter?.take,
};
const response = await client.get<PaginatedAuditLogs>(A.LOGS, { params });
const data = unwrap(response.data) as PaginatedAuditLogs;
return { items: data.items ?? [], count: data.count ?? 0 };
},
};

View File

@@ -2,6 +2,7 @@ import path from "node:path";
import { fileURLToPath } from "node:url";
import { createRequire } from "node:module";
import { loadEnv } from "vite";
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
@@ -10,7 +11,9 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
const require = createRequire(import.meta.url);
const streamBrowserifyPath = require.resolve("stream-browserify");
export default defineConfig(() => {
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, __dirname, "");
return {
plugins: [react(), tailwindcss()],
resolve: {
@@ -31,7 +34,7 @@ export default defineConfig(() => {
dedupe: ["react", "react-dom", "@mantine/core", "@mantine/hooks"],
},
server: {
port: 5183,
port: Number(env.PORT) || 5283,
host: "0.0.0.0",
},
test: {

View File

@@ -4,7 +4,7 @@
"private": true,
"type": "module",
"scripts": {
"dev": "vite --port 3000 --clearScreen false",
"dev": "vite --clearScreen false",
"build": "tsc -b && vite build",
"preview": "vite preview --port 5173",
"lint": "eslint src",

View File

@@ -1,6 +1,7 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
import { loadEnv } from "vite";
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
@@ -13,26 +14,30 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
const mantineCore = path.resolve(__dirname, "node_modules/@mantine/core");
const mantineHooks = path.resolve(__dirname, "node_modules/@mantine/hooks");
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
// Resolve from TS source so Vite gets ESM named exports (dist is CommonJS).
"@edr/types": path.resolve(__dirname, "../../../packages/types/src/index.ts"),
"@mantine/core": mantineCore,
"@mantine/hooks": mantineHooks,
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, __dirname, "");
return {
plugins: [react(), tailwindcss()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
// Resolve from TS source so Vite gets ESM named exports (dist is CommonJS).
"@edr/types": path.resolve(__dirname, "../../../packages/types/src/index.ts"),
"@mantine/core": mantineCore,
"@mantine/hooks": mantineHooks,
},
dedupe: ["react", "react-dom", "@mantine/core", "@mantine/hooks"],
},
dedupe: ["react", "react-dom", "@mantine/core", "@mantine/hooks"],
},
optimizeDeps: {
include: ["@mantine/core", "@mantine/hooks", "@edr/ui-common"],
},
server: {
port: 5173,
host: "0.0.0.0",
},
test: {
environment: "node",
},
optimizeDeps: {
include: ["@mantine/core", "@mantine/hooks", "@edr/ui-common"],
},
server: {
port: Number(env.PORT) || 5273,
host: "0.0.0.0",
},
test: {
environment: "node",
},
};
});

View File

@@ -24,7 +24,11 @@ describe("IntentsService CBE_BILL", () => {
let repository: jest.Mocked<
Pick<
IntentsRepository,
"create" | "findById" | "findByIdempotencyKey" | "update"
| "create"
| "findById"
| "findByIdempotencyKey"
| "findAllByReference"
| "update"
>
>;
let billReferenceService: { generate: jest.Mock };
@@ -46,6 +50,7 @@ describe("IntentsService CBE_BILL", () => {
create: jest.fn(async (data) => ({ id: "intent-1", ...data })),
findById: jest.fn(),
findByIdempotencyKey: jest.fn().mockResolvedValue(null),
findAllByReference: jest.fn().mockResolvedValue([]),
update: jest.fn(),
} as never;
billReferenceService = {
@@ -79,6 +84,47 @@ describe("IntentsService CBE_BILL", () => {
);
});
it("reuses the open bill instead of minting a second reference", async () => {
repository.findAllByReference.mockResolvedValue([
{
id: "intent-1",
provider: ProviderMethod.CBE_BILL,
status: ProviderPaymentStatus.REQUIRES_ACTION,
billReference: "000100000015",
amountMinor: 1500,
currency: "ETB",
clientAction: {
type: "SHOW_BILL_REFERENCE",
billReference: "000100000015",
},
},
] as never);
const snapshot = await service.initiate(request);
expect(snapshot.billReference).toBe("000100000015");
expect(billReferenceService.generate).not.toHaveBeenCalled();
expect(repository.create).not.toHaveBeenCalled();
});
it("mints a new bill when the amount changed", async () => {
repository.findAllByReference.mockResolvedValue([
{
id: "intent-1",
provider: ProviderMethod.CBE_BILL,
status: ProviderPaymentStatus.REQUIRES_ACTION,
billReference: "000100000015",
amountMinor: 900,
currency: "ETB",
},
] as never);
billReferenceService.generate.mockResolvedValue("000100000023");
const snapshot = await service.initiate(request);
expect(snapshot.billReference).toBe("000100000023");
});
it("rejects non-ETB currency (plan D8)", async () => {
await expect(
service.initiate({ ...request, currency: "DJF" }),

View File

@@ -163,6 +163,32 @@ export class IntentsService {
);
}
// The bill reference is issued ONCE per order: the domain app persists it (freight stores it
// as the booking's PNR) and the payer may already have written it down, so re-initiating the
// same open bill must hand back the same number. A different amount/currency means a
// different debt — /cbe/payment verifies the debited amount against the intent — so that
// case mints a fresh bill instead of silently repricing an outstanding one.
const open = (
await this.intentsRepository.findAllByReference(
request.service,
request.referenceType,
request.referenceId,
)
).find(
(i) =>
i.provider === ProviderMethod.CBE_BILL &&
i.status === ProviderPaymentStatus.REQUIRES_ACTION &&
!!i.billReference &&
i.amountMinor === request.amountMinor &&
i.currency === request.currency,
);
if (open) {
this.logger.log(
`intent ${open.id} reused for ${request.service}/${request.referenceType}/${request.referenceId} via CBE_BILL (bill ${open.billReference})`,
);
return this.toSnapshot(open);
}
const merchantOrderId = createMerchantOrderId();
const billReference = await this.billReferenceService.generate();
// expiresAt is the BOOKING's payment deadline passed by the domain app — never a provider

Binary file not shown.

313
pnpm-lock.yaml generated
View File

@@ -99,6 +99,9 @@ importers:
'@tria-plc/api-common':
specifier: file:../../local-packages/tria-plc-api-common-1.6.0.tgz
version: file:local-packages/tria-plc-api-common-1.6.0.tgz(ae9a56cc1c6d93629dd85f7605ccd5b6)
'@tria-plc/auditlog':
specifier: file:../../local-packages/tria-plc-auditlog-1.1.2.tgz
version: file:local-packages/tria-plc-auditlog-1.1.2.tgz(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(@nestjs/microservices@11.1.24)(@nestjs/swagger@11.4.4(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2))(@nestjs/typeorm@11.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.30(babel-plugin-macros@3.1.0)(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.42)(typescript@5.9.3))))(rxjs@7.8.2)(typeorm@0.3.30(babel-plugin-macros@3.1.0)(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.42)(typescript@5.9.3)))
'@tria-plc/iamapi-common':
specifier: file:../../local-packages/tria-plc-iamapi-common-1.0.0.tgz
version: file:local-packages/tria-plc-iamapi-common-1.0.0.tgz(d0be280d95adfc1b38e59bdc80c5dec5)
@@ -595,7 +598,7 @@ importers:
version: 5.101.0(react@19.2.6)
'@tria-plc/iamui':
specifier: file:../../../local-packages/tria-plc-iamui-0.1.1.tgz
version: file:local-packages/tria-plc-iamui-0.1.1.tgz(a5d0bdee55164ae56064fb273b530e00)
version: file:local-packages/tria-plc-iamui-0.1.1.tgz(0ce39b7e349029277dcd938d06eeb0f7)
'@vis.gl/react-google-maps':
specifier: ^1.8.3
version: 1.8.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
@@ -4647,6 +4650,19 @@ packages:
rxjs: ^7.8.0
typeorm: ^0.3.0
'@tria-plc/auditlog@file:local-packages/tria-plc-auditlog-1.1.2.tgz':
resolution: {integrity: sha512-3kRaAtETvM9wVRSbwd2jyrts9DCcSV9yFUDoEpxJfVQDsIGY2d9K0c+h1LAjfNNaogZ8eeBcroyCrnWjwBmkYA==, tarball: file:local-packages/tria-plc-auditlog-1.1.2.tgz}
version: 1.1.2
engines: {node: '>=18'}
peerDependencies:
'@nestjs/common': ^10.0.0 || ^11.0.0
'@nestjs/core': ^10.0.0 || ^11.0.0
'@nestjs/microservices': ^10.0.0 || ^11.0.0
'@nestjs/swagger': ^10.0.0 || ^11.0.0
'@nestjs/typeorm': ^10.0.0 || ^11.0.0
rxjs: ^7.0.0
typeorm: ^0.3.0
'@tria-plc/iamapi-common@file:local-packages/tria-plc-iamapi-common-1.0.0.tgz':
resolution: {integrity: sha512-rfHSXOm/0VUMTj7HrvYysrEAqxItqfPs4Rd35qWJyaAk+snF1wTKFRVz6cRGPoQNj8fhplkVAefnvuKBuHT+xQ==, tarball: file:local-packages/tria-plc-iamapi-common-1.0.0.tgz}
version: 1.0.0
@@ -12298,11 +12314,11 @@ snapshots:
'@babel/helpers': 7.29.7
'@babel/parser': 7.29.7
'@babel/template': 7.29.7
'@babel/traverse': 7.29.7
'@babel/traverse': 7.29.7(supports-color@5.5.0)
'@babel/types': 7.29.7
'@jridgewell/remapping': 2.3.5
convert-source-map: 2.0.0
debug: 4.4.3(supports-color@8.1.1)
debug: 4.4.3(supports-color@5.5.0)
gensync: 1.0.0-beta.2
json5: 2.2.3
semver: 6.3.1
@@ -12337,7 +12353,7 @@ snapshots:
'@babel/helper-optimise-call-expression': 7.29.7
'@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7)
'@babel/helper-skip-transparent-expression-wrappers': 7.29.7
'@babel/traverse': 7.29.7
'@babel/traverse': 7.29.7(supports-color@5.5.0)
semver: 6.3.1
transitivePeerDependencies:
- supports-color
@@ -12346,14 +12362,7 @@ snapshots:
'@babel/helper-member-expression-to-functions@7.29.7':
dependencies:
'@babel/traverse': 7.29.7
'@babel/types': 7.29.7
transitivePeerDependencies:
- supports-color
'@babel/helper-module-imports@7.29.7':
dependencies:
'@babel/traverse': 7.29.7
'@babel/traverse': 7.29.7(supports-color@5.5.0)
'@babel/types': 7.29.7
transitivePeerDependencies:
- supports-color
@@ -12368,9 +12377,9 @@ snapshots:
'@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)':
dependencies:
'@babel/core': 7.29.7
'@babel/helper-module-imports': 7.29.7
'@babel/helper-module-imports': 7.29.7(supports-color@5.5.0)
'@babel/helper-validator-identifier': 7.29.7
'@babel/traverse': 7.29.7
'@babel/traverse': 7.29.7(supports-color@5.5.0)
transitivePeerDependencies:
- supports-color
@@ -12385,13 +12394,13 @@ snapshots:
'@babel/core': 7.29.7
'@babel/helper-member-expression-to-functions': 7.29.7
'@babel/helper-optimise-call-expression': 7.29.7
'@babel/traverse': 7.29.7
'@babel/traverse': 7.29.7(supports-color@5.5.0)
transitivePeerDependencies:
- supports-color
'@babel/helper-skip-transparent-expression-wrappers@7.29.7':
dependencies:
'@babel/traverse': 7.29.7
'@babel/traverse': 7.29.7(supports-color@5.5.0)
'@babel/types': 7.29.7
transitivePeerDependencies:
- supports-color
@@ -12544,18 +12553,6 @@ snapshots:
'@babel/parser': 7.29.7
'@babel/types': 7.29.7
'@babel/traverse@7.29.7':
dependencies:
'@babel/code-frame': 7.29.7
'@babel/generator': 7.29.7
'@babel/helper-globals': 7.29.7
'@babel/parser': 7.29.7
'@babel/template': 7.29.7
'@babel/types': 7.29.7
debug: 4.4.3(supports-color@8.1.1)
transitivePeerDependencies:
- supports-color
'@babel/traverse@7.29.7(supports-color@5.5.0)':
dependencies:
'@babel/code-frame': 7.29.7
@@ -12781,7 +12778,7 @@ snapshots:
'@emotion/babel-plugin@11.13.5':
dependencies:
'@babel/helper-module-imports': 7.29.7
'@babel/helper-module-imports': 7.29.7(supports-color@5.5.0)
'@babel/runtime': 7.29.7
'@emotion/hash': 0.9.2
'@emotion/memoize': 0.9.0
@@ -12947,7 +12944,7 @@ snapshots:
'@eslint/eslintrc@2.1.4':
dependencies:
ajv: 6.15.0
debug: 4.4.3(supports-color@8.1.1)
debug: 4.4.3(supports-color@5.5.0)
espree: 9.6.1
globals: 13.24.0
ignore: 5.3.2
@@ -13107,7 +13104,7 @@ snapshots:
'@humanwhocodes/config-array@0.13.0':
dependencies:
'@humanwhocodes/object-schema': 2.0.3
debug: 4.4.3(supports-color@8.1.1)
debug: 4.4.3(supports-color@5.5.0)
minimatch: 3.1.5
transitivePeerDependencies:
- supports-color
@@ -14304,7 +14301,7 @@ snapshots:
'@puppeteer/browsers@2.13.2':
dependencies:
debug: 4.4.3(supports-color@8.1.1)
debug: 4.4.3(supports-color@5.5.0)
extract-zip: 2.0.1
progress: 2.0.3
proxy-agent: 6.5.0
@@ -16372,7 +16369,7 @@ snapshots:
'@tokenizer/inflate@0.4.1':
dependencies:
debug: 4.4.3(supports-color@8.1.1)
debug: 4.4.3(supports-color@5.5.0)
token-types: 6.1.2
transitivePeerDependencies:
- supports-color
@@ -16467,6 +16464,18 @@ snapshots:
- debug
- supports-color
'@tria-plc/auditlog@file:local-packages/tria-plc-auditlog-1.1.2.tgz(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(@nestjs/microservices@11.1.24)(@nestjs/swagger@11.4.4(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2))(@nestjs/typeorm@11.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.30(babel-plugin-macros@3.1.0)(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.42)(typescript@5.9.3))))(rxjs@7.8.2)(typeorm@0.3.30(babel-plugin-macros@3.1.0)(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.42)(typescript@5.9.3)))':
dependencies:
'@nestjs/common': 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/core': 11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.24)(@nestjs/platform-express@11.1.24)(@nestjs/websockets@11.1.27)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/microservices': 11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(@nestjs/websockets@11.1.27)(amqp-connection-manager@5.0.0(amqplib@2.0.1))(amqplib@2.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/swagger': 11.4.4(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)
'@nestjs/typeorm': 11.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.30(babel-plugin-macros@3.1.0)(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.42)(typescript@5.9.3)))
amqp-connection-manager: 5.0.0(amqplib@0.10.9)
amqplib: 0.10.9
rxjs: 7.8.2
typeorm: 0.3.30(babel-plugin-macros@3.1.0)(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.42)(typescript@5.9.3))
'@tria-plc/iamapi-common@file:local-packages/tria-plc-iamapi-common-1.0.0.tgz(cc085a020c559b355f168432c579a024)':
dependencies:
'@nestjs/axios': 4.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.17.0)(rxjs@7.8.2)
@@ -16659,130 +16668,6 @@ snapshots:
- utf-8-validate
- vite
'@tria-plc/iamui@file:local-packages/tria-plc-iamui-0.1.1.tgz(a5d0bdee55164ae56064fb273b530e00)':
dependencies:
'@emotion/react': 11.14.0(@types/react@18.3.31)(react@19.2.6)
'@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@18.3.31)(react@19.2.6))(@types/react@18.3.31)(react@19.2.6)
'@hookform/resolvers': 5.4.0(react-hook-form@7.77.0(react@19.2.6))
'@lottiefiles/react-lottie-player': 3.6.0(react@19.2.6)
'@mantine/charts': 7.17.8(@mantine/core@7.17.8(@mantine/hooks@7.17.8(react@19.2.6))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@7.17.8(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(recharts@3.8.1(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react-is@19.2.7)(react@19.2.6)(redux@5.0.1))
'@mantine/core': 7.17.8(@mantine/hooks@7.17.8(react@19.2.6))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@mantine/dates': 7.17.8(@mantine/core@7.17.8(@mantine/hooks@7.17.8(react@19.2.6))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@7.17.8(react@19.2.6))(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@mantine/hooks': 7.17.8(react@19.2.6)
'@mantine/notifications': 7.17.8(@mantine/core@7.17.8(@mantine/hooks@7.17.8(react@19.2.6))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@7.17.8(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@radix-ui/react-accordion': 1.2.13(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@radix-ui/react-alert-dialog': 1.1.16(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@radix-ui/react-avatar': 1.1.12(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@radix-ui/react-checkbox': 1.3.4(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@radix-ui/react-collapsible': 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@radix-ui/react-context-menu': 2.3.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@radix-ui/react-dialog': 1.1.16(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@radix-ui/react-dropdown-menu': 2.1.17(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@radix-ui/react-hover-card': 1.1.16(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@radix-ui/react-label': 2.1.9(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@radix-ui/react-navigation-menu': 1.2.15(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@radix-ui/react-popover': 1.1.16(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@radix-ui/react-progress': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@radix-ui/react-radio-group': 1.4.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@radix-ui/react-scroll-area': 1.2.11(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@radix-ui/react-select': 2.3.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@radix-ui/react-separator': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@radix-ui/react-slot': 1.2.5(@types/react@18.3.31)(react@19.2.6)
'@radix-ui/react-switch': 1.3.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@radix-ui/react-tabs': 1.1.14(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@radix-ui/react-toast': 1.2.16(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@radix-ui/react-tooltip': 1.2.9(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@react-pdf-viewer/core': 3.12.0(pdfjs-dist@5.4.296)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@react-pdf-viewer/default-layout': 3.12.0(pdfjs-dist@5.4.296)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@react-pdf/renderer': 4.5.1(react@19.2.6)
'@reduxjs/toolkit': 2.12.0(react-redux@9.3.0(@types/react@18.3.31)(react@19.2.6)(redux@5.0.1))(react@19.2.6)
'@tabler/icons-react': 3.44.0(react@19.2.6)
'@tailwindcss/vite': 4.3.0(vite@5.4.21(@types/node@24.13.1)(lightningcss@1.32.0)(terser@5.48.0))
'@tanstack/react-query': 5.101.0(react@19.2.6)
'@tanstack/react-query-devtools': 5.101.0(@tanstack/react-query@5.101.0(react@19.2.6))(react@19.2.6)
'@tanstack/react-table': 8.21.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@tinymce/tinymce-react': 6.3.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(tinymce@7.9.3)
'@types/dompurify': 3.2.0
'@types/node': 24.13.1
'@types/tinymce': 4.6.9
axios: 1.17.0
class-variance-authority: 0.7.1
clsx: 2.1.1
cmdk: 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
date-fns: 3.6.0
dayjs: 1.11.21
dompurify: 3.4.8
ethiopian-calendar-date-converter: 2.1.6
ethiopian-calendar-new: 1.1.0
file-type: 18.7.0
framer-motion: 12.40.0(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
html2canvas: 1.4.1
i18next: 25.10.10(typescript@5.9.3)
i18next-browser-languagedetector: 8.2.1
jquery: 3.7.1
js-cookie: 3.0.8
jspdf: 3.0.4
lodash: 4.18.1
lucide-react: 0.513.0(react@19.2.6)
mantine-react-table: 2.0.0-beta.9(@mantine/core@7.17.8(@mantine/hooks@7.17.8(react@19.2.6))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/dates@7.17.8(@mantine/core@7.17.8(@mantine/hooks@7.17.8(react@19.2.6))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@7.17.8(react@19.2.6))(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@7.17.8(react@19.2.6))(@tabler/icons-react@3.44.0(react@19.2.6))(clsx@2.1.1)(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
mui-ethiopian-datepicker: 0.3.2(4b3af212eafdf0059f009b005d7e343d)
next-themes: 0.4.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
path: 0.12.7
pdf-lib: 1.17.1
qs: 6.15.2
react: 19.2.6
react-cookie: 8.1.2(@types/react@18.3.31)(react@19.2.6)
react-css-nocode-editor: 1.0.13(react-dom@19.2.6(react@19.2.6))(react-is@19.2.7)(react@19.2.6)
react-day-picker: 8.10.2(date-fns@3.6.0)(react@19.2.6)
react-dom: 19.2.6(react@19.2.6)
react-dropzone: 14.4.1(react@19.2.6)
react-hook-form: 7.77.0(react@19.2.6)
react-i18next: 15.7.4(i18next@25.10.10(typescript@5.9.3))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@5.9.3)
react-icons: 5.6.0(react@19.2.6)
react-image-crop: 11.0.10(react@19.2.6)
react-intersection-observer: 9.16.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
react-pdf: 10.4.1(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
react-pdf-html: 2.1.5(@react-pdf/renderer@4.5.1(react@19.2.6))(react@19.2.6)
react-redux: 9.3.0(@types/react@18.3.31)(react@19.2.6)(redux@5.0.1)
react-resizable-panels: 3.0.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
react-router-dom: 7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
react-signature-canvas: 1.1.0-alpha.2(@types/prop-types@15.7.15)(@types/react@18.3.31)(prop-types@15.8.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
recharts: 3.8.1(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react-is@19.2.7)(react@19.2.6)(redux@5.0.1)
rollup-plugin-visualizer: 7.0.1(rollup@4.61.1)
socket.io-client: 4.8.3
sonner: 2.0.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
tailwind-merge: 3.6.0
tailwind-scrollbar-hide: 4.0.0(tailwindcss@4.3.0)
tailwindcss: 4.3.0
tailwindcss-animate: 1.0.7(tailwindcss@4.3.0)
tinymce: 7.9.3
url: 0.11.4
vaul: 1.1.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
xlsx: 0.18.5
zod: 3.25.76
transitivePeerDependencies:
- '@babel/core'
- '@emotion/is-prop-valid'
- '@mui/icons-material'
- '@mui/material'
- '@mui/x-date-pickers'
- '@types/prop-types'
- '@types/react'
- '@types/react-dom'
- bufferutil
- debug
- pdfjs-dist
- prop-types
- react-is
- react-native
- redux
- rolldown
- rollup
- supports-color
- typescript
- utf-8-validate
- vite
'@ts-morph/common@0.27.0':
dependencies:
fast-glob: 3.3.3
@@ -17161,7 +17046,7 @@ snapshots:
'@typescript-eslint/types': 8.60.1
'@typescript-eslint/typescript-estree': 8.60.1(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.60.1
debug: 4.4.3(supports-color@8.1.1)
debug: 4.4.3(supports-color@5.5.0)
eslint: 8.57.1
typescript: 5.9.3
transitivePeerDependencies:
@@ -17171,7 +17056,7 @@ snapshots:
dependencies:
'@typescript-eslint/tsconfig-utils': 8.60.1(typescript@5.9.3)
'@typescript-eslint/types': 8.60.1
debug: 4.4.3(supports-color@8.1.1)
debug: 4.4.3(supports-color@5.5.0)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
@@ -17190,7 +17075,7 @@ snapshots:
'@typescript-eslint/types': 8.60.1
'@typescript-eslint/typescript-estree': 8.60.1(typescript@5.9.3)
'@typescript-eslint/utils': 8.60.1(eslint@8.57.1)(typescript@5.9.3)
debug: 4.4.3(supports-color@8.1.1)
debug: 4.4.3(supports-color@5.5.0)
eslint: 8.57.1
ts-api-utils: 2.5.0(typescript@5.9.3)
typescript: 5.9.3
@@ -17205,7 +17090,7 @@ snapshots:
'@typescript-eslint/tsconfig-utils': 8.60.1(typescript@5.9.3)
'@typescript-eslint/types': 8.60.1
'@typescript-eslint/visitor-keys': 8.60.1
debug: 4.4.3(supports-color@8.1.1)
debug: 4.4.3(supports-color@5.5.0)
minimatch: 10.2.5
semver: 7.8.2
tinyglobby: 0.2.17
@@ -17494,7 +17379,7 @@ snapshots:
agent-base@6.0.2:
dependencies:
debug: 4.4.3(supports-color@8.1.1)
debug: 4.4.3(supports-color@5.5.0)
transitivePeerDependencies:
- supports-color
@@ -17547,6 +17432,11 @@ snapshots:
amqplib: 0.10.9
promise-breaker: 6.0.0
amqp-connection-manager@5.0.0(amqplib@0.10.9):
dependencies:
amqplib: 0.10.9
promise-breaker: 6.0.0
amqp-connection-manager@5.0.0(amqplib@2.0.1):
dependencies:
amqplib: 2.0.1
@@ -18003,16 +17893,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
babel-plugin-styled-components@2.3.0(styled-components@5.3.11(react-dom@19.2.6(react@19.2.6))(react-is@19.2.7)(react@19.2.6))(supports-color@5.5.0):
dependencies:
'@babel/helper-annotate-as-pure': 7.29.7
'@babel/helper-module-imports': 7.29.7(supports-color@5.5.0)
'@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7)
picomatch: 4.0.4
styled-components: 5.3.11(react-dom@19.2.6(react@19.2.6))(react-is@19.2.7)(react@19.2.6)
transitivePeerDependencies:
- supports-color
babel-polyfill@6.26.0:
dependencies:
babel-runtime: 6.26.0
@@ -18166,7 +18046,7 @@ snapshots:
dependencies:
bytes: 3.1.2
content-type: 1.0.5
debug: 4.4.3(supports-color@8.1.1)
debug: 4.4.3(supports-color@5.5.0)
http-errors: 2.0.1
iconv-lite: 0.7.2
on-finished: 2.4.1
@@ -19163,7 +19043,7 @@ snapshots:
engine.io-client@6.6.5:
dependencies:
'@socket.io/component-emitter': 3.1.2
debug: 4.4.3(supports-color@8.1.1)
debug: 4.4.3(supports-color@5.5.0)
engine.io-parser: 5.2.3
ws: 8.20.1
xmlhttprequest-ssl: 2.1.2
@@ -19183,7 +19063,7 @@ snapshots:
base64id: 2.0.0
cookie: 0.7.2
cors: 2.8.6
debug: 4.4.3(supports-color@8.1.1)
debug: 4.4.3(supports-color@5.5.0)
engine.io-parser: 5.2.3
ws: 8.21.0
transitivePeerDependencies:
@@ -19412,7 +19292,7 @@ snapshots:
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1):
dependencies:
'@nolyfill/is-core-module': 1.0.39
debug: 4.4.3(supports-color@8.1.1)
debug: 4.4.3(supports-color@5.5.0)
eslint: 8.57.1
get-tsconfig: 4.14.0
is-bun-module: 2.0.0
@@ -19540,7 +19420,7 @@ snapshots:
ajv: 6.15.0
chalk: 4.1.2
cross-spawn: 7.0.6
debug: 4.4.3(supports-color@8.1.1)
debug: 4.4.3(supports-color@5.5.0)
doctrine: 3.0.0
escape-string-regexp: 4.0.0
eslint-scope: 7.2.2
@@ -19770,7 +19650,7 @@ snapshots:
content-type: 1.0.5
cookie: 0.7.2
cookie-signature: 1.2.2
debug: 4.4.3(supports-color@8.1.1)
debug: 4.4.3(supports-color@5.5.0)
depd: 2.0.0
encodeurl: 2.0.0
escape-html: 1.0.3
@@ -19823,7 +19703,7 @@ snapshots:
extract-zip@2.0.1:
dependencies:
debug: 4.4.3(supports-color@8.1.1)
debug: 4.4.3(supports-color@5.5.0)
get-stream: 5.2.0
yauzl: 2.10.0
optionalDependencies:
@@ -19974,7 +19854,7 @@ snapshots:
finalhandler@2.1.1:
dependencies:
debug: 4.4.3(supports-color@8.1.1)
debug: 4.4.3(supports-color@5.5.0)
encodeurl: 2.0.0
escape-html: 1.0.3
on-finished: 2.4.1
@@ -20220,7 +20100,7 @@ snapshots:
dependencies:
basic-ftp: 5.3.1
data-uri-to-buffer: 6.0.2
debug: 4.4.3(supports-color@8.1.1)
debug: 4.4.3(supports-color@5.5.0)
transitivePeerDependencies:
- supports-color
@@ -20501,7 +20381,7 @@ snapshots:
http-proxy-agent@7.0.2:
dependencies:
agent-base: 7.1.4
debug: 4.4.3(supports-color@8.1.1)
debug: 4.4.3(supports-color@5.5.0)
transitivePeerDependencies:
- supports-color
@@ -20514,14 +20394,14 @@ snapshots:
https-proxy-agent@5.0.1:
dependencies:
agent-base: 6.0.2
debug: 4.4.3(supports-color@8.1.1)
debug: 4.4.3(supports-color@5.5.0)
transitivePeerDependencies:
- supports-color
https-proxy-agent@7.0.6:
dependencies:
agent-base: 7.1.4
debug: 4.4.3(supports-color@8.1.1)
debug: 4.4.3(supports-color@5.5.0)
transitivePeerDependencies:
- supports-color
@@ -20946,7 +20826,7 @@ snapshots:
istanbul-lib-source-maps@4.0.1:
dependencies:
debug: 4.4.3(supports-color@8.1.1)
debug: 4.4.3(supports-color@5.5.0)
istanbul-lib-coverage: 3.2.2
source-map: 0.6.1
transitivePeerDependencies:
@@ -21592,7 +21472,7 @@ snapshots:
dependencies:
chalk: 5.6.2
commander: 13.1.0
debug: 4.4.3(supports-color@8.1.1)
debug: 4.4.3(supports-color@5.5.0)
execa: 8.0.1
lilconfig: 3.1.3
listr2: 8.3.3
@@ -22389,7 +22269,7 @@ snapshots:
dependencies:
'@tootallnate/quickjs-emscripten': 0.23.0
agent-base: 7.1.4
debug: 4.4.3(supports-color@8.1.1)
debug: 4.4.3(supports-color@5.5.0)
get-uri: 6.0.5
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.6
@@ -22727,7 +22607,7 @@ snapshots:
proxy-agent@6.5.0:
dependencies:
agent-base: 7.1.4
debug: 4.4.3(supports-color@8.1.1)
debug: 4.4.3(supports-color@5.5.0)
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.6
lru-cache: 7.18.3
@@ -22756,7 +22636,7 @@ snapshots:
dependencies:
'@puppeteer/browsers': 2.13.2
chromium-bidi: 14.0.0(devtools-protocol@0.0.1608973)
debug: 4.4.3(supports-color@8.1.1)
debug: 4.4.3(supports-color@5.5.0)
devtools-protocol: 0.0.1608973
typed-query-selector: 2.12.2
webdriver-bidi-protocol: 0.4.1
@@ -23009,15 +22889,6 @@ snapshots:
- '@babel/core'
- react-is
react-css-nocode-editor@1.0.13(react-dom@19.2.6(react@19.2.6))(react-is@19.2.7)(react@19.2.6):
dependencies:
react: 19.2.6
react-dom: 19.2.6(react@19.2.6)
styled-components: 5.3.11(react-dom@19.2.6(react@19.2.6))(react-is@19.2.7)(react@19.2.6)
transitivePeerDependencies:
- '@babel/core'
- react-is
react-day-picker@8.10.2(date-fns@3.6.0)(react@19.2.6):
dependencies:
date-fns: 3.6.0
@@ -23591,7 +23462,7 @@ snapshots:
router@2.2.0:
dependencies:
debug: 4.4.3(supports-color@8.1.1)
debug: 4.4.3(supports-color@5.5.0)
depd: 2.0.0
is-promise: 4.0.0
parseurl: 1.3.3
@@ -23709,7 +23580,7 @@ snapshots:
send@1.2.1:
dependencies:
debug: 4.4.3(supports-color@8.1.1)
debug: 4.4.3(supports-color@5.5.0)
encodeurl: 2.0.0
escape-html: 1.0.3
etag: 1.8.1
@@ -23925,7 +23796,7 @@ snapshots:
socket.io-adapter@2.5.8:
dependencies:
debug: 4.4.3(supports-color@8.1.1)
debug: 4.4.3(supports-color@5.5.0)
ws: 8.21.0
transitivePeerDependencies:
- bufferutil
@@ -23935,7 +23806,7 @@ snapshots:
socket.io-client@4.8.3:
dependencies:
'@socket.io/component-emitter': 3.1.2
debug: 4.4.3(supports-color@8.1.1)
debug: 4.4.3(supports-color@5.5.0)
engine.io-client: 6.6.5
socket.io-parser: 4.2.6
transitivePeerDependencies:
@@ -23946,7 +23817,7 @@ snapshots:
socket.io-parser@4.2.6:
dependencies:
'@socket.io/component-emitter': 3.1.2
debug: 4.4.3(supports-color@8.1.1)
debug: 4.4.3(supports-color@5.5.0)
transitivePeerDependencies:
- supports-color
@@ -23955,7 +23826,7 @@ snapshots:
accepts: 1.3.8
base64id: 2.0.0
cors: 2.8.6
debug: 4.4.3(supports-color@8.1.1)
debug: 4.4.3(supports-color@5.5.0)
engine.io: 6.6.9
socket.io-adapter: 2.5.8
socket.io-parser: 4.2.6
@@ -23967,7 +23838,7 @@ snapshots:
socks-proxy-agent@8.0.5:
dependencies:
agent-base: 7.1.4
debug: 4.4.3(supports-color@8.1.1)
debug: 4.4.3(supports-color@5.5.0)
socks: 2.8.9
transitivePeerDependencies:
- supports-color
@@ -24245,24 +24116,6 @@ snapshots:
transitivePeerDependencies:
- '@babel/core'
styled-components@5.3.11(react-dom@19.2.6(react@19.2.6))(react-is@19.2.7)(react@19.2.6):
dependencies:
'@babel/helper-module-imports': 7.29.7(supports-color@5.5.0)
'@babel/traverse': 7.29.7(supports-color@5.5.0)
'@emotion/is-prop-valid': 1.4.0
'@emotion/stylis': 0.8.5
'@emotion/unitless': 0.7.5
babel-plugin-styled-components: 2.3.0(styled-components@5.3.11(react-dom@19.2.6(react@19.2.6))(react-is@19.2.7)(react@19.2.6))(supports-color@5.5.0)
css-to-react-native: 3.2.0
hoist-non-react-statics: 3.3.2
react: 19.2.6
react-dom: 19.2.6(react@19.2.6)
react-is: 19.2.7
shallowequal: 1.1.0
supports-color: 5.5.0
transitivePeerDependencies:
- '@babel/core'
styled-jsx@5.1.1(babel-plugin-macros@3.1.0)(react@18.3.1):
dependencies:
client-only: 0.0.1
@@ -24288,7 +24141,7 @@ snapshots:
dependencies:
component-emitter: 1.3.1
cookiejar: 2.1.4
debug: 4.4.3(supports-color@8.1.1)
debug: 4.4.3(supports-color@5.5.0)
fast-safe-stringify: 2.1.1
form-data: 4.0.5
formidable: 3.5.4
@@ -24797,7 +24650,7 @@ snapshots:
app-root-path: 3.1.0
buffer: 6.0.3
dayjs: 1.11.21
debug: 4.4.3(supports-color@8.1.1)
debug: 4.4.3(supports-color@5.5.0)
dedent: 1.7.2(babel-plugin-macros@3.1.0)
dotenv: 16.6.1
glob: 10.5.0
@@ -24821,7 +24674,7 @@ snapshots:
app-root-path: 3.1.0
buffer: 6.0.3
dayjs: 1.11.21
debug: 4.4.3(supports-color@8.1.1)
debug: 4.4.3(supports-color@5.5.0)
dedent: 1.7.2(babel-plugin-macros@3.1.0)
dotenv: 16.6.1
glob: 10.5.0
@@ -25124,7 +24977,7 @@ snapshots:
vite-node@2.1.9(@types/node@22.20.1)(lightningcss@1.32.0)(terser@5.48.0):
dependencies:
cac: 6.7.14
debug: 4.4.3(supports-color@8.1.1)
debug: 4.4.3(supports-color@5.5.0)
es-module-lexer: 1.7.0
pathe: 1.1.2
vite: 5.4.21(@types/node@22.20.1)(lightningcss@1.32.0)(terser@5.48.0)
@@ -25142,7 +24995,7 @@ snapshots:
vite-node@2.1.9(@types/node@24.13.1)(lightningcss@1.32.0)(terser@5.48.0):
dependencies:
cac: 6.7.14
debug: 4.4.3(supports-color@8.1.1)
debug: 4.4.3(supports-color@5.5.0)
es-module-lexer: 1.7.0
pathe: 1.1.2
vite: 5.4.21(@types/node@24.13.1)(lightningcss@1.32.0)(terser@5.48.0)
@@ -25189,7 +25042,7 @@ snapshots:
'@vitest/spy': 2.1.9
'@vitest/utils': 2.1.9
chai: 5.3.3
debug: 4.4.3(supports-color@8.1.1)
debug: 4.4.3(supports-color@5.5.0)
expect-type: 1.3.0
magic-string: 0.30.21
pathe: 1.1.2
@@ -25225,7 +25078,7 @@ snapshots:
'@vitest/spy': 2.1.9
'@vitest/utils': 2.1.9
chai: 5.3.3
debug: 4.4.3(supports-color@8.1.1)
debug: 4.4.3(supports-color@5.5.0)
expect-type: 1.3.0
magic-string: 0.30.21
pathe: 1.1.2