mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Merge branch 'staging' into dev-to-staging
This commit is contained in:
@@ -0,0 +1,15 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class AddCbeBillPaymentMethod3050000000000 implements MigrationInterface {
|
||||||
|
name = "AddCbeBillPaymentMethod3050000000000";
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
// CBE Unified Bill Payment (docs/cbe/CBE_IMPLEMENTATION_PLAN.md §4.3) — lowercase-hyphen
|
||||||
|
// per the local convention (see 2460000000000-AddCacBankPaymentMethod).
|
||||||
|
await queryRunner.query(`ALTER TYPE freight.payments_method_enum ADD VALUE IF NOT EXISTS 'cbe-bill';`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(_queryRunner: QueryRunner): Promise<void> {
|
||||||
|
// PostgreSQL does not support removing enum values directly.
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,6 +55,27 @@ const OPEN_STATUSES: Freight.InvoiceStatus[] = [
|
|||||||
Freight.InvoiceStatus.Overdue,
|
Freight.InvoiceStatus.Overdue,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Why a non-open invoice can no longer be paid, in the vocabulary the payment service's CBE
|
||||||
|
* bill-query mapper understands. Kept specific: CBE reads this back to the payer at the counter,
|
||||||
|
* so "cancelled" must not stand in for "already paid" or "refunded".
|
||||||
|
*/
|
||||||
|
function closedInvoiceReason(status: Freight.InvoiceStatus): string {
|
||||||
|
switch (status) {
|
||||||
|
case Freight.InvoiceStatus.Paid:
|
||||||
|
return "ALREADY_PAID";
|
||||||
|
case Freight.InvoiceStatus.Refunded:
|
||||||
|
return "REFUNDED";
|
||||||
|
case Freight.InvoiceStatus.Cancelled:
|
||||||
|
return "CANCELLED";
|
||||||
|
case Freight.InvoiceStatus.Expired:
|
||||||
|
return "EXPIRED";
|
||||||
|
// Draft — issued to nobody yet, so there is nothing honest to say beyond "not payable".
|
||||||
|
default:
|
||||||
|
return "NOT_PAYABLE";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** A single line to bill on a generated invoice. */
|
/** A single line to bill on a generated invoice. */
|
||||||
export interface InvoiceLineInput {
|
export interface InvoiceLineInput {
|
||||||
chargeType: string;
|
chargeType: string;
|
||||||
@@ -1034,6 +1055,7 @@ export class BillingService {
|
|||||||
): Promise<InitiateResponseDto> {
|
): Promise<InitiateResponseDto> {
|
||||||
const invoice = await this.dataSource.getRepository(Invoice).findOne({
|
const invoice = await this.dataSource.getRepository(Invoice).findOne({
|
||||||
where: { id: invoiceId, status: In(OPEN_STATUSES) },
|
where: { id: invoiceId, status: In(OPEN_STATUSES) },
|
||||||
|
relations: { company: true },
|
||||||
});
|
});
|
||||||
if (!invoice) {
|
if (!invoice) {
|
||||||
throw new NotFoundException(
|
throw new NotFoundException(
|
||||||
@@ -1090,6 +1112,10 @@ export class BillingService {
|
|||||||
method: opts.method ?? "TELEBIRR",
|
method: opts.method ?? "TELEBIRR",
|
||||||
platform: opts.platform,
|
platform: opts.platform,
|
||||||
payerAccount: opts.payerAccount,
|
payerAccount: opts.payerAccount,
|
||||||
|
// CBE_BILL: payer identity + the invoice's own due date as the bill expiry
|
||||||
|
// (docs/cbe/CBE_IMPLEMENTATION_PLAN.md §6.4).
|
||||||
|
payerName: invoice.company?.name,
|
||||||
|
expiresAt: invoice.dueAt?.toISOString(),
|
||||||
returnUrl: opts.returnUrl,
|
returnUrl: opts.returnUrl,
|
||||||
failureUrl: opts.failureUrl,
|
failureUrl: opts.failureUrl,
|
||||||
});
|
});
|
||||||
@@ -1105,7 +1131,8 @@ export class BillingService {
|
|||||||
// code — so the demo shortcut must never fire for it.
|
// code — so the demo shortcut must never fire for it.
|
||||||
if (
|
if (
|
||||||
!result.immediateSuccess &&
|
!result.immediateSuccess &&
|
||||||
result.response.clientAction?.type !== "COLLECT_OTP"
|
result.response.clientAction?.type !== "COLLECT_OTP" &&
|
||||||
|
opts.method !== "CBE_BILL"
|
||||||
) {
|
) {
|
||||||
await this.payment.handlePaymentEvent({
|
await this.payment.handlePaymentEvent({
|
||||||
eventType: "payment.succeeded",
|
eventType: "payment.succeeded",
|
||||||
@@ -1151,4 +1178,67 @@ export class BillingService {
|
|||||||
paidAt,
|
paidAt,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CBE bill-query (docs/cbe/CBE_IMPLEMENTATION_PLAN.md Phase 4): live still-payable check for
|
||||||
|
* the invoice behind a payment reference. `referenceId` is the gateway intent's referenceId,
|
||||||
|
* i.e. the invoice `sourceId`. Read-only; called while a CBE teller/app is waiting.
|
||||||
|
*/
|
||||||
|
async billQuery(referenceId: string): Promise<{
|
||||||
|
stillPayable: boolean;
|
||||||
|
payerName?: string | null;
|
||||||
|
currentAmountMinor?: number | null;
|
||||||
|
currency?: string | null;
|
||||||
|
reason?: string | null;
|
||||||
|
paymentReason?: string | null;
|
||||||
|
}> {
|
||||||
|
const repo = this.dataSource.getRepository(Invoice);
|
||||||
|
const open = await repo.findOne({
|
||||||
|
where: { sourceId: referenceId, status: In(OPEN_STATUSES) },
|
||||||
|
relations: { company: true },
|
||||||
|
order: { issuedAt: "DESC" },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (open) {
|
||||||
|
const balance = Math.round(Number(open.balanceAmount ?? open.totalAmount));
|
||||||
|
const expired = open.dueAt && open.dueAt.getTime() < Date.now();
|
||||||
|
return {
|
||||||
|
stillPayable: balance > 0 && !expired,
|
||||||
|
payerName: open.company?.name ?? null,
|
||||||
|
currentAmountMinor: balance,
|
||||||
|
currency: open.currency,
|
||||||
|
// CBE shows this beside the amount on the confirmation screen — the invoice number
|
||||||
|
// the payer is holding, not our internal reference.
|
||||||
|
paymentReason: `Freight invoice ${open.invoiceNumber}`,
|
||||||
|
// Settled-in-full wins over past-due: an invoice with nothing left to pay is paid, not
|
||||||
|
// expired, and that is what the payer at the CBE counter must be told.
|
||||||
|
reason: balance > 0 ? (expired ? "EXPIRED" : null) : "ALREADY_PAID",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const latest = await repo.findOne({
|
||||||
|
where: { sourceId: referenceId },
|
||||||
|
relations: { company: true },
|
||||||
|
order: { createdAt: "DESC" },
|
||||||
|
});
|
||||||
|
// A bill reference whose invoice no longer exists at all — a data problem, not a
|
||||||
|
// cancellation the payer did anything to cause.
|
||||||
|
if (!latest) {
|
||||||
|
return {
|
||||||
|
stillPayable: false,
|
||||||
|
payerName: null,
|
||||||
|
currentAmountMinor: null,
|
||||||
|
currency: null,
|
||||||
|
reason: "NOT_FOUND",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
stillPayable: false,
|
||||||
|
payerName: latest.company?.name ?? null,
|
||||||
|
currentAmountMinor: Math.round(Number(latest.totalAmount)),
|
||||||
|
currency: latest.currency,
|
||||||
|
paymentReason: `Freight invoice ${latest.invoiceNumber}`,
|
||||||
|
reason: closedInvoiceReason(latest.status),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { PaymentRefundEntity } from "./payment-refund.entity";
|
|||||||
|
|
||||||
/** Invoice source that owns the intent ('booking', 'demurrage', …) — caller-supplied. */
|
/** Invoice source that owns the intent ('booking', 'demurrage', …) — caller-supplied. */
|
||||||
type PaymentType = string
|
type PaymentType = string
|
||||||
type PaymentMethod = "telebirr" | "cbe-birr" | "ebirr" | "waafi" | "card" | "dmoney" | "cac-bank"
|
type PaymentMethod = "telebirr" | "cbe-birr" | "ebirr" | "waafi" | "card" | "dmoney" | "cac-bank" | "cbe-bill"
|
||||||
type Currency = "ETB" | "USD"
|
type Currency = "ETB" | "USD"
|
||||||
export type PaymentStatus = "action-required" | "processing" | "success" | "failed" | "canceled" | "refunded"
|
export type PaymentStatus = "action-required" | "processing" | "success" | "failed" | "canceled" | "refunded"
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ export class PaymentEntity extends BaseEntity {
|
|||||||
@Column({ type: "varchar", length: 40, nullable: true, name: "reference_type" })
|
@Column({ type: "varchar", length: 40, nullable: true, name: "reference_type" })
|
||||||
referenceType?: string;
|
referenceType?: string;
|
||||||
|
|
||||||
@Column({ type: "enum", enum: ["telebirr", "cbe-birr", "ebirr", "waafi", "card", "dmoney", "cac-bank"] })
|
@Column({ type: "enum", enum: ["telebirr", "cbe-birr", "ebirr", "waafi", "card", "dmoney", "cac-bank", "cbe-bill"] })
|
||||||
method!: PaymentMethod
|
method!: PaymentMethod
|
||||||
|
|
||||||
@Column({ type: "enum", enum: ["ETB", "USD"] })
|
@Column({ type: "enum", enum: ["ETB", "USD"] })
|
||||||
|
|||||||
@@ -1,30 +1,42 @@
|
|||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
|
forwardRef,
|
||||||
HttpCode,
|
HttpCode,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
|
Inject,
|
||||||
Logger,
|
Logger,
|
||||||
Post,
|
Post,
|
||||||
|
UseGuards,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||||
import { Public } from "@edr/api-common";
|
import {
|
||||||
import { PaymentEventDto, MarkPaidResponseDto } from "./internal-payment.dto";
|
PaymentEventDto,
|
||||||
|
MarkPaidResponseDto,
|
||||||
|
BillQueryRequestDto,
|
||||||
|
BillQueryResponseDto,
|
||||||
|
} from "./internal-payment.dto";
|
||||||
import { PaymentService } from "./payment.service";
|
import { PaymentService } from "./payment.service";
|
||||||
|
import { BillingService } from "../billing/billing.service";
|
||||||
|
import { ServiceAuthGuard } from "../../common/guards/service-auth.guard";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Consumer side of the payment microservice's outbox relay.
|
* Consumer side of the payment microservice's outbox relay. Only the payment service may
|
||||||
* WARNING: currently unauthenticated — anyone who can reach the API can mark
|
* call this (shared service token — restored per docs/cbe/CBE_IMPLEMENTATION_PLAN.md R8).
|
||||||
* payments as paid. Re-add ServiceAuthGuard before exposing beyond a trusted network.
|
|
||||||
* Idempotent by design — the relay delivers at-least-once, so duplicates must be harmless.
|
* Idempotent by design — the relay delivers at-least-once, so duplicates must be harmless.
|
||||||
* Becomes a queue consumer via PaymentEventsConsumer when RabbitMQ is available;
|
* Becomes a queue consumer via PaymentEventsConsumer when RabbitMQ is available;
|
||||||
* this HTTP endpoint remains as a transport-agnostic fallback.
|
* this HTTP endpoint remains as a transport-agnostic fallback.
|
||||||
*/
|
*/
|
||||||
@ApiTags("Internal Payments")
|
@ApiTags("Internal Payments")
|
||||||
@Public()
|
@UseGuards(ServiceAuthGuard)
|
||||||
@Controller("internal/payments")
|
@Controller("internal/payments")
|
||||||
export class InternalPaymentController {
|
export class InternalPaymentController {
|
||||||
private readonly logger = new Logger(InternalPaymentController.name);
|
private readonly logger = new Logger(InternalPaymentController.name);
|
||||||
constructor(private readonly paymentService: PaymentService) { }
|
constructor(
|
||||||
|
private readonly paymentService: PaymentService,
|
||||||
|
@Inject(forwardRef(() => BillingService))
|
||||||
|
private readonly billingService: BillingService,
|
||||||
|
) { }
|
||||||
|
|
||||||
@Post("mark-paid")
|
@Post("mark-paid")
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@@ -36,4 +48,16 @@ export class InternalPaymentController {
|
|||||||
this.logger.log(`Marking payment ${event} as PAID`);
|
this.logger.log(`Marking payment ${event} as PAID`);
|
||||||
return this.paymentService.handlePaymentEvent(event);
|
return this.paymentService.handlePaymentEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post("bill-query")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@ApiOperation({
|
||||||
|
summary:
|
||||||
|
"Live still-payable check + payer name for a CBE bill (called while CBE is on the line)",
|
||||||
|
})
|
||||||
|
async billQuery(
|
||||||
|
@Body() request: BillQueryRequestDto,
|
||||||
|
): Promise<BillQueryResponseDto> {
|
||||||
|
return this.billingService.billQuery(request.referenceId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,3 +51,30 @@ export class MarkPaidResponseDto {
|
|||||||
@ApiPropertyOptional() alreadyFinalized?: boolean;
|
@ApiPropertyOptional() alreadyFinalized?: boolean;
|
||||||
@ApiPropertyOptional() reason?: string;
|
@ApiPropertyOptional() reason?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CBE bill-query hop (docs/cbe/CBE_IMPLEMENTATION_PLAN.md Phase 4): the payment service asks
|
||||||
|
* "is this invoice still payable, by whom, for how much" while a CBE channel is on the line.
|
||||||
|
*/
|
||||||
|
export class BillQueryRequestDto {
|
||||||
|
// Typed `string`, not the enum: the @nestjs/swagger CLI plugin resolves an enum-typed
|
||||||
|
// property to a relative require() into packages/types, which does not exist inside the
|
||||||
|
// Docker image (only /app is copied) and crashes at boot with MODULE_NOT_FOUND. The
|
||||||
|
// decorators below still give us enum docs + runtime validation.
|
||||||
|
@ApiProperty({ enum: PaymentReferenceType })
|
||||||
|
@IsEnum(PaymentReferenceType)
|
||||||
|
referenceType!: string;
|
||||||
|
|
||||||
|
@ApiProperty() @IsString() referenceId!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BillQueryResponseDto {
|
||||||
|
@ApiProperty() stillPayable!: boolean;
|
||||||
|
@ApiPropertyOptional() payerName?: string | null;
|
||||||
|
@ApiPropertyOptional() currentAmountMinor?: number | null;
|
||||||
|
@ApiPropertyOptional() currency?: string | null;
|
||||||
|
/** When stillPayable=false: "ALREADY_PAID" | "CANCELLED" | "REFUNDED" | "EXPIRED" | "NOT_FOUND" | "NOT_PAYABLE". */
|
||||||
|
@ApiPropertyOptional() reason?: string | null;
|
||||||
|
/** What the payer is paying for — CBE renders it beside the amount (Payment_Reason). */
|
||||||
|
@ApiPropertyOptional() paymentReason?: string | null;
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,10 @@ export interface InitiateIntentInput {
|
|||||||
payerAccount?: string;
|
payerAccount?: string;
|
||||||
returnUrl?: string;
|
returnUrl?: string;
|
||||||
failureUrl?: string;
|
failureUrl?: string;
|
||||||
|
/** CBE_BILL: payer full name snapshot (feeds CBE's mandatory Full_Name). */
|
||||||
|
payerName?: string;
|
||||||
|
/** CBE_BILL: intent expiry, ISO-8601 — the invoice due date, never a session TTL. */
|
||||||
|
expiresAt?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InitiateIntentResult {
|
export interface InitiateIntentResult {
|
||||||
@@ -79,6 +83,7 @@ const PROVIDER_TO_METHOD: Record<string, PaymentEntity["method"]> = {
|
|||||||
CARD: "card",
|
CARD: "card",
|
||||||
DMONEY: "dmoney",
|
DMONEY: "dmoney",
|
||||||
CAC_BANK: "cac-bank",
|
CAC_BANK: "cac-bank",
|
||||||
|
CBE_BILL: "cbe-bill",
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -214,8 +219,13 @@ export class PaymentService {
|
|||||||
|
|
||||||
async initiate(input: InitiateIntentInput): Promise<InitiateIntentResult> {
|
async initiate(input: InitiateIntentInput): Promise<InitiateIntentResult> {
|
||||||
try {
|
try {
|
||||||
|
const isCbeBill = input.method === ProviderMethod.CBE_BILL;
|
||||||
|
// CBE settles ETB only (docs/cbe/CBE_IMPLEMENTATION_PLAN.md D8).
|
||||||
|
if (isCbeBill && input.currency?.toUpperCase() !== "ETB") {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"CBE bill payment is only available for ETB invoices",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const snapshot = await this.paymentClient.initiate({
|
const snapshot = await this.paymentClient.initiate({
|
||||||
service: PaymentServiceEnum.FREIGHT,
|
service: PaymentServiceEnum.FREIGHT,
|
||||||
@@ -223,11 +233,15 @@ export class PaymentService {
|
|||||||
referenceId: input.referenceId,
|
referenceId: input.referenceId,
|
||||||
orderRef: input.orderRef,
|
orderRef: input.orderRef,
|
||||||
// amountMinor: input.amountMinor,
|
// amountMinor: input.amountMinor,
|
||||||
amountMinor:1,
|
// CBE_BILL must carry the REAL amount: /cbe/payment verifies what the customer was
|
||||||
|
// debited against the intent amount, so the 1-birr dev shortcut would break it.
|
||||||
|
amountMinor: isCbeBill ? input.amountMinor : 1,
|
||||||
currency: input.currency,
|
currency: input.currency,
|
||||||
provider: input.method as ProviderMethod,
|
provider: input.method as ProviderMethod,
|
||||||
platform: input.platform,
|
platform: input.platform,
|
||||||
payerAccount: input.payerAccount,
|
payerAccount: input.payerAccount,
|
||||||
|
payerName: input.payerName,
|
||||||
|
expiresAt: input.expiresAt,
|
||||||
returnUrl:
|
returnUrl:
|
||||||
input.returnUrl ?? "https://edrfreight.triaplc.com/payment/success",
|
input.returnUrl ?? "https://edrfreight.triaplc.com/payment/success",
|
||||||
failureUrl:
|
failureUrl:
|
||||||
|
|||||||
@@ -60,8 +60,10 @@ export class RefundDto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ClientActionDto {
|
export class ClientActionDto {
|
||||||
@ApiProperty({ enum: ["REDIRECT", "LAUNCH_APP", "COLLECT_OTP"] })
|
@ApiProperty({
|
||||||
type!: "REDIRECT" | "LAUNCH_APP" | "COLLECT_OTP";
|
enum: ["REDIRECT", "LAUNCH_APP", "COLLECT_OTP", "SHOW_BILL_REFERENCE"],
|
||||||
|
})
|
||||||
|
type!: "REDIRECT" | "LAUNCH_APP" | "COLLECT_OTP" | "SHOW_BILL_REFERENCE";
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: "Set when type=REDIRECT (web flow)" })
|
@ApiPropertyOptional({ description: "Set when type=REDIRECT (web flow)" })
|
||||||
url?: string;
|
url?: string;
|
||||||
@@ -80,6 +82,17 @@ export class ClientActionDto {
|
|||||||
|
|
||||||
@ApiPropertyOptional({ description: "Set when type=COLLECT_OTP" })
|
@ApiPropertyOptional({ description: "Set when type=COLLECT_OTP" })
|
||||||
message?: string;
|
message?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: "Set when type=SHOW_BILL_REFERENCE (CBE bill payment)",
|
||||||
|
})
|
||||||
|
billReference?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: "Set when type=SHOW_BILL_REFERENCE" })
|
||||||
|
instructions?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: "Set when type=SHOW_BILL_REFERENCE" })
|
||||||
|
expiresAt?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class InitiateResponseDto {
|
export class InitiateResponseDto {
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ const METHOD_OPTIONS: { value: PaymentMethod; label: string }[] = [
|
|||||||
{ value: "card", label: "Card" },
|
{ value: "card", label: "Card" },
|
||||||
{ value: "dmoney", label: "D-Money" },
|
{ value: "dmoney", label: "D-Money" },
|
||||||
{ value: "cac-bank", label: "CAC Bank" },
|
{ value: "cac-bank", label: "CAC Bank" },
|
||||||
|
{ value: "cbe-bill", label: "CBE Bill" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const STATUS_COLORS: Record<string, string> = {
|
const STATUS_COLORS: Record<string, string> = {
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ export type PaymentMethod =
|
|||||||
| "waafi"
|
| "waafi"
|
||||||
| "card"
|
| "card"
|
||||||
| "dmoney"
|
| "dmoney"
|
||||||
| "cac-bank";
|
| "cac-bank"
|
||||||
|
| "cbe-bill";
|
||||||
|
|
||||||
export interface PaymentRow {
|
export interface PaymentRow {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
Divider,
|
Divider,
|
||||||
Group,
|
Group,
|
||||||
Loader,
|
Loader,
|
||||||
|
Modal,
|
||||||
Paper,
|
Paper,
|
||||||
SimpleGrid,
|
SimpleGrid,
|
||||||
Stack,
|
Stack,
|
||||||
@@ -69,6 +70,12 @@ export default function InvoiceDetailPage() {
|
|||||||
} = useQuery(api.invoices.get.queryOptions({ input: { id } }));
|
} = useQuery(api.invoices.get.queryOptions({ input: { id } }));
|
||||||
|
|
||||||
const [payModalOpen, setPayModalOpen] = useState(false);
|
const [payModalOpen, setPayModalOpen] = useState(false);
|
||||||
|
// CBE bill payment: the bill reference to pay at any CBE channel (no redirect).
|
||||||
|
const [billAction, setBillAction] = useState<{
|
||||||
|
billReference?: string;
|
||||||
|
instructions?: string;
|
||||||
|
expiresAt?: string;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
// Ownership-checked: POST /billing/my-invoices/:id/pay only ever charges
|
// Ownership-checked: POST /billing/my-invoices/:id/pay only ever charges
|
||||||
// one of the signed-in customer's own invoices (unlike the admin-facing
|
// one of the signed-in customer's own invoices (unlike the admin-facing
|
||||||
@@ -377,6 +384,62 @@ export default function InvoiceDetailPage() {
|
|||||||
pay.pay(id, method, payerAccount)
|
pay.pay(id, method, payerAccount)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* CBE bill payment — show the bill number; settlement arrives via CBE, not the browser */}
|
||||||
|
<Modal
|
||||||
|
opened={!!billAction}
|
||||||
|
onClose={() => setBillAction(null)}
|
||||||
|
centered
|
||||||
|
radius={18}
|
||||||
|
size={440}
|
||||||
|
title={<Text fw={800}>Pay at CBE</Text>}
|
||||||
|
>
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Text fz="sm" c={MUTED}>
|
||||||
|
{billAction?.instructions ??
|
||||||
|
"Pay this bill at any CBE branch, the CBE Birr app, mobile banking or USSD."}
|
||||||
|
</Text>
|
||||||
|
<Group
|
||||||
|
justify="space-between"
|
||||||
|
px={16}
|
||||||
|
py={13}
|
||||||
|
style={{ borderRadius: 12, border: `1px solid ${BORDER}` }}
|
||||||
|
>
|
||||||
|
<Text ff="monospace" fz={24} fw={800} c={INK} style={{ letterSpacing: 3 }}>
|
||||||
|
{billAction?.billReference}
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
size="xs"
|
||||||
|
onClick={() => {
|
||||||
|
if (billAction?.billReference) {
|
||||||
|
navigator.clipboard?.writeText(billAction.billReference);
|
||||||
|
toast.success("Bill number copied");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Copy
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
<Text fz="sm" c={MUTED}>
|
||||||
|
Amount due:{" "}
|
||||||
|
<Text span fw={700} c={INK}>
|
||||||
|
{formatCurrency(amountDue, invoice.currency)}
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
|
{billAction?.expiresAt && (
|
||||||
|
<Text fz="sm" c={MUTED}>
|
||||||
|
Pay before:{" "}
|
||||||
|
<Text span fw={700} c={INK}>
|
||||||
|
{fmtDate(billAction.expiresAt)}
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
<Text fz="xs" c={MUTED}>
|
||||||
|
The invoice updates automatically once CBE confirms your payment.
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Modal>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ interface ProviderOption {
|
|||||||
accent: string;
|
accent: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only Telebirr, Waafi and CAC Bank are enabled for now.
|
// Only Telebirr, Waafi and CBE bill payment are enabled for now.
|
||||||
const PROVIDERS: ProviderOption[] = [
|
const PROVIDERS: ProviderOption[] = [
|
||||||
{
|
{
|
||||||
method: "TELEBIRR",
|
method: "TELEBIRR",
|
||||||
@@ -51,6 +51,14 @@ const PROVIDERS: ProviderOption[] = [
|
|||||||
currencies: ["USD"],
|
currencies: ["USD"],
|
||||||
accent: "#8A5A17",
|
accent: "#8A5A17",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
method: "CBE_BILL",
|
||||||
|
label: "CBE bill payment",
|
||||||
|
description: "Pay at any CBE branch, app or USSD · ETB",
|
||||||
|
logo: "/assets/edr-logo.png",
|
||||||
|
currencies: ["ETB"],
|
||||||
|
accent: "#5B2D8C",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/** Providers that debit against an SMS OTP instead of redirecting to a page. */
|
/** Providers that debit against an SMS OTP instead of redirecting to a page. */
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ export type PaymentMethod =
|
|||||||
| "WAAFI"
|
| "WAAFI"
|
||||||
| "CARD"
|
| "CARD"
|
||||||
| "DMONEY"
|
| "DMONEY"
|
||||||
| "CAC_BANK";
|
| "CAC_BANK"
|
||||||
|
| "CBE_BILL";
|
||||||
|
|
||||||
export type PaymentPlatform = "web" | "mobile";
|
export type PaymentPlatform = "web" | "mobile";
|
||||||
|
|
||||||
@@ -26,13 +27,17 @@ export interface InitiatePaymentPayload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ClientAction {
|
export interface ClientAction {
|
||||||
type: "REDIRECT" | "LAUNCH_APP" | "COLLECT_OTP";
|
type: "REDIRECT" | "LAUNCH_APP" | "COLLECT_OTP" | "SHOW_BILL_REFERENCE";
|
||||||
url?: string;
|
url?: string;
|
||||||
appId?: string;
|
appId?: string;
|
||||||
receiveCode?: string;
|
receiveCode?: string;
|
||||||
shortCode?: string;
|
shortCode?: string;
|
||||||
providerOrderId?: string;
|
providerOrderId?: string;
|
||||||
message?: string;
|
message?: string;
|
||||||
|
/** SHOW_BILL_REFERENCE (CBE bill payment) */
|
||||||
|
billReference?: string;
|
||||||
|
instructions?: string;
|
||||||
|
expiresAt?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InitiateResponse {
|
export interface InitiateResponse {
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- CBE Unified Bill Payment (docs/cbe/CBE_IMPLEMENTATION_PLAN.md §4.3): new inbound biller
|
||||||
|
-- method. Value must stay byte-identical to @edr/types ProviderMethod.CBE_BILL — the
|
||||||
|
-- passenger payments service casts between the two enums directly.
|
||||||
|
ALTER TYPE "passenger"."PaymentMethodType" ADD VALUE IF NOT EXISTS 'CBE_BILL';
|
||||||
@@ -147,6 +147,7 @@ enum PaymentMethodType {
|
|||||||
WAAFI
|
WAAFI
|
||||||
DMONEY
|
DMONEY
|
||||||
CAC_BANK
|
CAC_BANK
|
||||||
|
CBE_BILL
|
||||||
|
|
||||||
@@schema("passenger")
|
@@schema("passenger")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,17 @@ import {
|
|||||||
HttpCode,
|
HttpCode,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
Post,
|
Post,
|
||||||
|
SetMetadata,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||||
import { ServiceAuthGuard } from "../../common/guards/service-auth.guard";
|
import { ServiceAuthGuard } from "../../common/guards/service-auth.guard";
|
||||||
import { PaymentEventDto, MarkPaidResponseDto } from "./internal-payments.dto";
|
import {
|
||||||
|
PaymentEventDto,
|
||||||
|
MarkPaidResponseDto,
|
||||||
|
BillQueryRequestDto,
|
||||||
|
BillQueryResponseDto,
|
||||||
|
} from "./internal-payments.dto";
|
||||||
import { PaymentsService } from "./payments.service";
|
import { PaymentsService } from "./payments.service";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -18,6 +24,9 @@ import { PaymentsService } from "./payments.service";
|
|||||||
* consumer when RabbitMQ lands — the handler logic is transport-agnostic.
|
* consumer when RabbitMQ lands — the handler logic is transport-agnostic.
|
||||||
*/
|
*/
|
||||||
@ApiTags("Internal Payments")
|
@ApiTags("Internal Payments")
|
||||||
|
// isPublic only skips the global IAM user-JWT guard — these routes stay protected by
|
||||||
|
// ServiceAuthGuard's shared service token (the payment service is not an IAM user).
|
||||||
|
@SetMetadata("isPublic", true)
|
||||||
@UseGuards(ServiceAuthGuard)
|
@UseGuards(ServiceAuthGuard)
|
||||||
@Controller("internal/payments")
|
@Controller("internal/payments")
|
||||||
export class InternalPaymentsController {
|
export class InternalPaymentsController {
|
||||||
@@ -32,4 +41,16 @@ export class InternalPaymentsController {
|
|||||||
async markPaid(@Body() event: PaymentEventDto): Promise<MarkPaidResponseDto> {
|
async markPaid(@Body() event: PaymentEventDto): Promise<MarkPaidResponseDto> {
|
||||||
return this.paymentsService.handlePaymentEvent(event);
|
return this.paymentsService.handlePaymentEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post("bill-query")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@ApiOperation({
|
||||||
|
summary:
|
||||||
|
"Live still-payable check + payer name for a CBE bill (called while CBE is on the line)",
|
||||||
|
})
|
||||||
|
async billQuery(
|
||||||
|
@Body() request: BillQueryRequestDto,
|
||||||
|
): Promise<BillQueryResponseDto> {
|
||||||
|
return this.paymentsService.billQuery(request.referenceId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,3 +55,30 @@ export class MarkPaidResponseDto {
|
|||||||
@ApiPropertyOptional() alreadyFinalized?: boolean;
|
@ApiPropertyOptional() alreadyFinalized?: boolean;
|
||||||
@ApiPropertyOptional() reason?: string;
|
@ApiPropertyOptional() reason?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CBE bill-query hop (docs/cbe/CBE_IMPLEMENTATION_PLAN.md Phase 4): the payment service asks
|
||||||
|
* "is this order still payable, by whom, for how much" while a CBE teller/app is on the line.
|
||||||
|
*/
|
||||||
|
export class BillQueryRequestDto {
|
||||||
|
// Typed `string`, not the enum: the @nestjs/swagger CLI plugin resolves an enum-typed
|
||||||
|
// property to a relative require() into packages/types, which does not exist inside the
|
||||||
|
// Docker image (only /app is copied) and crashes at boot with MODULE_NOT_FOUND. The
|
||||||
|
// decorators below still give us enum docs + runtime validation.
|
||||||
|
@ApiProperty({ enum: PaymentReferenceType })
|
||||||
|
@IsEnum(PaymentReferenceType)
|
||||||
|
referenceType!: string;
|
||||||
|
|
||||||
|
@ApiProperty() @IsString() referenceId!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BillQueryResponseDto {
|
||||||
|
@ApiProperty() stillPayable!: boolean;
|
||||||
|
@ApiPropertyOptional() payerName?: string | null;
|
||||||
|
@ApiPropertyOptional() currentAmountMinor?: number | null;
|
||||||
|
@ApiPropertyOptional() currency?: string | null;
|
||||||
|
/** When stillPayable=false: "ALREADY_PAID" | "CANCELLED" | "REFUNDED" | "EXPIRED" | "NOT_FOUND" | "NOT_PAYABLE". */
|
||||||
|
@ApiPropertyOptional() reason?: string | null;
|
||||||
|
/** What the payer is paying for — CBE renders it beside the amount (Payment_Reason). */
|
||||||
|
@ApiPropertyOptional() paymentReason?: string | null;
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export enum PaymentMethodTypeEnum {
|
|||||||
CAC_BANK = "CAC_BANK", // Djibouti (OTP debit)
|
CAC_BANK = "CAC_BANK", // Djibouti (OTP debit)
|
||||||
CARD = "CARD", // International
|
CARD = "CARD", // International
|
||||||
WALLET = "WALLET", // Internal
|
WALLET = "WALLET", // Internal
|
||||||
|
CBE_BILL = "CBE_BILL", // Ethiopia (pay at any CBE channel by bill number)
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PaymentPlatformDto = "web" | "mobile";
|
export type PaymentPlatformDto = "web" | "mobile";
|
||||||
@@ -115,8 +116,10 @@ export class SupportedPaymentMethodDto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ClientActionDto {
|
export class ClientActionDto {
|
||||||
@ApiProperty({ enum: ["REDIRECT", "LAUNCH_APP", "COLLECT_OTP"] })
|
@ApiProperty({
|
||||||
type: "REDIRECT" | "LAUNCH_APP" | "COLLECT_OTP";
|
enum: ["REDIRECT", "LAUNCH_APP", "COLLECT_OTP", "SHOW_BILL_REFERENCE"],
|
||||||
|
})
|
||||||
|
type: "REDIRECT" | "LAUNCH_APP" | "COLLECT_OTP" | "SHOW_BILL_REFERENCE";
|
||||||
@ApiPropertyOptional({ description: "Set when type=REDIRECT (web flow)" })
|
@ApiPropertyOptional({ description: "Set when type=REDIRECT (web flow)" })
|
||||||
url?: string;
|
url?: string;
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({
|
||||||
@@ -135,6 +138,14 @@ export class ClientActionDto {
|
|||||||
providerOrderId?: string;
|
providerOrderId?: string;
|
||||||
@ApiPropertyOptional({ description: "Set when type=COLLECT_OTP" })
|
@ApiPropertyOptional({ description: "Set when type=COLLECT_OTP" })
|
||||||
message?: string;
|
message?: string;
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: "Set when type=SHOW_BILL_REFERENCE (CBE bill payment)",
|
||||||
|
})
|
||||||
|
billReference?: string;
|
||||||
|
@ApiPropertyOptional({ description: "Set when type=SHOW_BILL_REFERENCE" })
|
||||||
|
instructions?: string;
|
||||||
|
@ApiPropertyOptional({ description: "Set when type=SHOW_BILL_REFERENCE" })
|
||||||
|
expiresAt?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class InitiateResponseDto {
|
export class InitiateResponseDto {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { PaymentsService } from "./payments.service";
|
|||||||
import { PaymentClientService } from "./payment-client.service";
|
import { PaymentClientService } from "./payment-client.service";
|
||||||
import { CurrencyService } from "../currency/currency.service";
|
import { CurrencyService } from "../currency/currency.service";
|
||||||
import { PrismaService } from "../../common/prisma.service";
|
import { PrismaService } from "../../common/prisma.service";
|
||||||
|
import { AuditService } from "../../common/audit.service";
|
||||||
import { SeatsService } from "../seats/seats.service";
|
import { SeatsService } from "../seats/seats.service";
|
||||||
import { TicketsService } from "../tickets/tickets.service";
|
import { TicketsService } from "../tickets/tickets.service";
|
||||||
import { EventEmitter2 } from "@nestjs/event-emitter";
|
import { EventEmitter2 } from "@nestjs/event-emitter";
|
||||||
@@ -27,6 +28,7 @@ describe("PaymentsService", () => {
|
|||||||
booking: {
|
booking: {
|
||||||
findUnique: jest.fn(),
|
findUnique: jest.fn(),
|
||||||
update: jest.fn(),
|
update: jest.fn(),
|
||||||
|
updateMany: jest.fn().mockResolvedValue({ count: 1 }),
|
||||||
},
|
},
|
||||||
paymentIntent: {
|
paymentIntent: {
|
||||||
findUnique: jest.fn(),
|
findUnique: jest.fn(),
|
||||||
@@ -81,6 +83,8 @@ describe("PaymentsService", () => {
|
|||||||
convertEtbMinorToChargeMajor: jest.fn((minor: number) =>
|
convertEtbMinorToChargeMajor: jest.fn((minor: number) =>
|
||||||
Promise.resolve(minor),
|
Promise.resolve(minor),
|
||||||
),
|
),
|
||||||
|
displayMinorToChargeMajor: jest.fn((minor: number) => minor / 100),
|
||||||
|
convertMinorToChargeMajor: jest.fn(async (minor: number) => minor / 100),
|
||||||
getRateOrThrow: jest.fn(),
|
getRateOrThrow: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -109,6 +113,7 @@ describe("PaymentsService", () => {
|
|||||||
{ provide: EventEmitter2, useValue: mockEventEmitter },
|
{ provide: EventEmitter2, useValue: mockEventEmitter },
|
||||||
{ provide: PaymentClientService, useValue: mockPaymentClient },
|
{ provide: PaymentClientService, useValue: mockPaymentClient },
|
||||||
{ provide: CurrencyService, useValue: mockCurrencyService },
|
{ provide: CurrencyService, useValue: mockCurrencyService },
|
||||||
|
{ provide: AuditService, useValue: { log: jest.fn() } },
|
||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,12 @@ import {
|
|||||||
PaymentRegionEnum,
|
PaymentRegionEnum,
|
||||||
ForceConfirmDto,
|
ForceConfirmDto,
|
||||||
} from "./payments.dto";
|
} from "./payments.dto";
|
||||||
import { PaymentEventDto, MarkPaidResponseDto } from "./internal-payments.dto";
|
import {
|
||||||
|
PaymentEventDto,
|
||||||
|
MarkPaidResponseDto,
|
||||||
|
BillQueryResponseDto,
|
||||||
|
} from "./internal-payments.dto";
|
||||||
|
import { computePaymentDeadline } from "../../common/utils/payment-deadline.utils";
|
||||||
import {
|
import {
|
||||||
PaymentClientService,
|
PaymentClientService,
|
||||||
PaymentDiagnostic,
|
PaymentDiagnostic,
|
||||||
@@ -222,6 +227,17 @@ export class PaymentsService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CBE settles ETB only (docs/cbe/CBE_IMPLEMENTATION_PLAN.md D8). payerAccount is NOT
|
||||||
|
// required — CBE identifies the payer at its own channel.
|
||||||
|
if (
|
||||||
|
method === PaymentMethodType.CBE_BILL &&
|
||||||
|
(booking.currency ?? "ETB").toUpperCase() !== "ETB"
|
||||||
|
) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"CBE bill payment is only available for bookings charged in ETB",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const correctTotalMinor = await this.resolveBookingTotal(booking as any);
|
const correctTotalMinor = await this.resolveBookingTotal(booking as any);
|
||||||
|
|
||||||
// Patch the DB if the stored total is wrong (single-leg for a round-trip package booking)
|
// Patch the DB if the stored total is wrong (single-leg for a round-trip package booking)
|
||||||
@@ -259,15 +275,22 @@ export class PaymentsService {
|
|||||||
const paymentMethod = await this.prisma.paymentMethod.findUnique({
|
const paymentMethod = await this.prisma.paymentMethod.findUnique({
|
||||||
where: { type: method },
|
where: { type: method },
|
||||||
});
|
});
|
||||||
const chargeCurrency = (
|
const chargeCurrency =
|
||||||
paymentMethod?.currency ?? booking.currency
|
method === PaymentMethodType.CBE_BILL
|
||||||
).toUpperCase();
|
? "ETB"
|
||||||
|
: (paymentMethod?.currency ?? booking.currency).toUpperCase();
|
||||||
|
|
||||||
const bookingDisplayCurrency = ((booking as any).displayCurrency ?? 'ETB').toUpperCase();
|
const bookingDisplayCurrency = ((booking as any).displayCurrency ?? 'ETB').toUpperCase();
|
||||||
const bookingDisplayTotalMinor = (booking as any).displayTotalMinor as number | null;
|
const bookingDisplayTotalMinor = (booking as any).displayTotalMinor as number | null;
|
||||||
|
|
||||||
let chargeAmount: number;
|
let chargeAmount: number;
|
||||||
if (
|
if (method === PaymentMethodType.CBE_BILL) {
|
||||||
|
// Force ETB, no conversion (D8) — eligibility was already checked above.
|
||||||
|
chargeAmount = this.currencyService.displayMinorToChargeMajor(
|
||||||
|
booking.totalMinor,
|
||||||
|
"ETB",
|
||||||
|
);
|
||||||
|
} else if (
|
||||||
chargeCurrency === bookingDisplayCurrency &&
|
chargeCurrency === bookingDisplayCurrency &&
|
||||||
chargeCurrency !== 'ETB' &&
|
chargeCurrency !== 'ETB' &&
|
||||||
bookingDisplayTotalMinor != null
|
bookingDisplayTotalMinor != null
|
||||||
@@ -285,6 +308,20 @@ export class PaymentsService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CBE_BILL: the bill lives in CBE's system for as long as the booking is payable, so the
|
||||||
|
// intent expiry is the booking's own payment deadline — never a provider-session TTL
|
||||||
|
// (plan §6.4); payerName feeds the mandatory Full_Name of CBE's query response.
|
||||||
|
let payerName: string | undefined;
|
||||||
|
let expiresAt: string | undefined;
|
||||||
|
if (method === PaymentMethodType.CBE_BILL) {
|
||||||
|
payerName =
|
||||||
|
booking.seats.find((s) => s.leg === 1)?.passengerName ??
|
||||||
|
booking.seats[0]?.passengerName;
|
||||||
|
expiresAt = (
|
||||||
|
await this.computeBookingPaymentDeadline(booking.id)
|
||||||
|
)?.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
const snapshot = await this.paymentClient.initiate({
|
const snapshot = await this.paymentClient.initiate({
|
||||||
service: PaymentServiceEnum.PASSENGER,
|
service: PaymentServiceEnum.PASSENGER,
|
||||||
referenceType: PaymentReferenceType.BOOKING,
|
referenceType: PaymentReferenceType.BOOKING,
|
||||||
@@ -297,6 +334,8 @@ export class PaymentsService {
|
|||||||
payerAccount: dto.payerAccount,
|
payerAccount: dto.payerAccount,
|
||||||
returnUrl,
|
returnUrl,
|
||||||
failureUrl,
|
failureUrl,
|
||||||
|
payerName,
|
||||||
|
expiresAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
let intent = await this.syncIntentProjection(booking.id, snapshot);
|
let intent = await this.syncIntentProjection(booking.id, snapshot);
|
||||||
@@ -350,6 +389,111 @@ export class PaymentsService {
|
|||||||
return this.formatIntentStatus(intent);
|
return this.formatIntentStatus(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CBE bill-query (docs/cbe/CBE_IMPLEMENTATION_PLAN.md Phase 4): live still-payable check +
|
||||||
|
* payer identity for a booking. Called by the payment service while a CBE teller/app is
|
||||||
|
* waiting — read-only and fast. This is the double-payment guard: once the booking is
|
||||||
|
* confirmed by ANY method, stillPayable=false and CBE refuses the bill (§6.3).
|
||||||
|
*/
|
||||||
|
async billQuery(bookingId: string): Promise<BillQueryResponseDto> {
|
||||||
|
const booking = await this.prisma.booking.findUnique({
|
||||||
|
where: { id: bookingId },
|
||||||
|
include: { seats: true, passenger: { include: { user: true } } },
|
||||||
|
});
|
||||||
|
// Distinct from CANCELLED: the payment service issued a bill reference for a booking that
|
||||||
|
// no longer exists at all, which is a data problem, not a customer-facing cancellation.
|
||||||
|
if (!booking) return { stillPayable: false, reason: "NOT_FOUND" };
|
||||||
|
|
||||||
|
const base = {
|
||||||
|
// Full_Name is mandatory in CBE's envelope: lead passenger first, then account holder.
|
||||||
|
payerName:
|
||||||
|
booking.seats.find((s) => s.leg === 1)?.passengerName ??
|
||||||
|
booking.seats[0]?.passengerName ??
|
||||||
|
booking.passenger?.user?.fullName ??
|
||||||
|
null,
|
||||||
|
currentAmountMinor: this.currencyService.displayMinorToChargeMajor(
|
||||||
|
booking.totalMinor,
|
||||||
|
"ETB",
|
||||||
|
),
|
||||||
|
currency: "ETB",
|
||||||
|
// CBE shows this beside the amount on the confirmation screen. bookingRef is the same
|
||||||
|
// code on the customer's ticket, so they can match the two before confirming.
|
||||||
|
paymentReason: `Train ticket booking ${booking.bookingRef}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Paid first: a booking that was paid and then boarded/refunded must never be reported as
|
||||||
|
// merely "not payable" — the payer needs to hear that their money already went through.
|
||||||
|
if (booking.status === "CONFIRMED" || booking.paidAt) {
|
||||||
|
return { ...base, stillPayable: false, reason: "ALREADY_PAID" };
|
||||||
|
}
|
||||||
|
if (booking.status === "REFUNDED") {
|
||||||
|
return { ...base, stillPayable: false, reason: "REFUNDED" };
|
||||||
|
}
|
||||||
|
if (booking.status === "CANCELLED") {
|
||||||
|
return { ...base, stillPayable: false, reason: "CANCELLED" };
|
||||||
|
}
|
||||||
|
// DRAFT / BOARDED / NO_SHOW without a payment: no honest specific wording exists.
|
||||||
|
if (booking.status !== "PENDING_PAYMENT") {
|
||||||
|
return { ...base, stillPayable: false, reason: "NOT_PAYABLE" };
|
||||||
|
}
|
||||||
|
const deadline = await this.computeBookingPaymentDeadline(booking.id);
|
||||||
|
if (deadline && deadline.getTime() < Date.now()) {
|
||||||
|
return { ...base, stillPayable: false, reason: "EXPIRED" };
|
||||||
|
}
|
||||||
|
return { ...base, stillPayable: true, reason: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The booking's payment deadline, resolved exactly like the auto-cancel job: the booking's
|
||||||
|
* origin-segment time and that stop's own check-in window, falling back to the route default.
|
||||||
|
*/
|
||||||
|
private async computeBookingPaymentDeadline(
|
||||||
|
bookingId: string,
|
||||||
|
): Promise<Date | null> {
|
||||||
|
const booking = await this.prisma.booking.findUnique({
|
||||||
|
where: { id: bookingId },
|
||||||
|
select: {
|
||||||
|
createdAt: true,
|
||||||
|
originStationId: true,
|
||||||
|
schedule: {
|
||||||
|
select: {
|
||||||
|
departureAt: true,
|
||||||
|
stopTimes: {
|
||||||
|
select: {
|
||||||
|
stationId: true,
|
||||||
|
plannedArrivalAt: true,
|
||||||
|
plannedDepartureAt: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
route: {
|
||||||
|
select: {
|
||||||
|
checkinMinutesBefore: true,
|
||||||
|
stops: {
|
||||||
|
select: { stationId: true, checkinMinutesBefore: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!booking?.schedule) return null;
|
||||||
|
const originStop = booking.schedule.stopTimes?.find(
|
||||||
|
(s) => s.stationId === booking.originStationId,
|
||||||
|
);
|
||||||
|
const dep = (originStop?.plannedArrivalAt ??
|
||||||
|
originStop?.plannedDepartureAt ??
|
||||||
|
booking.schedule.departureAt) as Date;
|
||||||
|
const originRouteStop = booking.schedule.route?.stops?.find(
|
||||||
|
(s) => s.stationId === booking.originStationId,
|
||||||
|
);
|
||||||
|
const checkinMinutes =
|
||||||
|
originRouteStop?.checkinMinutesBefore ??
|
||||||
|
booking.schedule.route?.checkinMinutesBefore ??
|
||||||
|
undefined;
|
||||||
|
return computePaymentDeadline(booking.createdAt, dep, checkinMinutes);
|
||||||
|
}
|
||||||
|
|
||||||
private resolveReturnUrls(
|
private resolveReturnUrls(
|
||||||
method: PaymentMethodType,
|
method: PaymentMethodType,
|
||||||
requestOrigin?: string | null,
|
requestOrigin?: string | null,
|
||||||
@@ -1117,15 +1261,17 @@ export class PaymentsService {
|
|||||||
return { processed: false, reason: "booking-not-found" };
|
return { processed: false, reason: "booking-not-found" };
|
||||||
}
|
}
|
||||||
|
|
||||||
// C-4 guard: a settlement must cover what the passenger was quoted. Compare the provider-settled
|
// C-4 guard: a settlement must cover what the passenger was quoted. `event.amountMinor`
|
||||||
// amount against the booking's display-currency total (the amount the customer agreed to pay);
|
// carries the charge amount in MAJOR units (the intent's "real/major price" — what
|
||||||
// a short payment must NOT confirm the booking. Amount-only — the display↔charge currency
|
// initiate sent, e.g. 1500.00 ETB), while booking totals are stored in minor units, so
|
||||||
// divergence is tracked separately under the USD/DJF findings. The 1% tolerance absorbs rounding.
|
// normalize before comparing; a short payment must NOT confirm the booking. Amount-only —
|
||||||
const expectedMinor = booking.displayTotalMinor ?? booking.totalMinor;
|
// the display↔charge currency divergence is tracked separately under the USD/DJF
|
||||||
const shortPayTolerance = Math.max(1, Math.round(expectedMinor * 0.01));
|
// findings. The 1% tolerance absorbs rounding.
|
||||||
if (event.amountMinor < expectedMinor - shortPayTolerance) {
|
const expectedMajor = (booking.displayTotalMinor ?? booking.totalMinor) / 100;
|
||||||
|
const shortPayTolerance = Math.max(0.01, expectedMajor * 0.01);
|
||||||
|
if (event.amountMinor < expectedMajor - shortPayTolerance) {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
`mark-paid: short payment for booking ${booking.id} — settled ${event.amountMinor} ${event.currency} < expected ${expectedMinor} ${booking.displayCurrency}; not confirming`,
|
`mark-paid: short payment for booking ${booking.id} — settled ${event.amountMinor} ${event.currency} < expected ${expectedMajor} ${booking.displayCurrency}; not confirming`,
|
||||||
);
|
);
|
||||||
return { processed: false, reason: "amount-mismatch" };
|
return { processed: false, reason: "amount-mismatch" };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,4 +69,9 @@ export const EDR_PASSENGER_ROLES: PassengerSeedRole[] = [
|
|||||||
name: { en: 'EDR Passenger Finance Manager' },
|
name: { en: 'EDR Passenger Finance Manager' },
|
||||||
permissionKeys: [...ROLE_PERMISSION_PRESETS.financeManager],
|
permissionKeys: [...ROLE_PERMISSION_PRESETS.financeManager],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'edr_passenger_director',
|
||||||
|
name: { en: 'EDR Passenger Director' },
|
||||||
|
permissionKeys: [...ROLE_PERMISSION_PRESETS.director],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -166,9 +166,18 @@ export const PASSENGER_PERMS = {
|
|||||||
admin: 'edr_passenger_app:admin',
|
admin: 'edr_passenger_app:admin',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
// Every view-only permission (`:view` and `:view_all`), derived from the registry
|
||||||
|
// so newly-added view permissions are automatically included.
|
||||||
|
export const PASSENGER_VIEW_PERMISSION_KEYS = PASSENGER_PERMISSION_KEYS.filter(
|
||||||
|
(key) => key.endsWith(':view') || key.endsWith(':view_all'),
|
||||||
|
);
|
||||||
|
|
||||||
export const ROLE_PERMISSION_PRESETS = {
|
export const ROLE_PERMISSION_PRESETS = {
|
||||||
backofficeAdmin: [...PASSENGER_PERMISSION_KEYS],
|
backofficeAdmin: [...PASSENGER_PERMISSION_KEYS],
|
||||||
|
|
||||||
|
// Director: read-only across the whole app — can view every resource, cannot manage/mutate anything.
|
||||||
|
director: [...PASSENGER_VIEW_PERMISSION_KEYS],
|
||||||
|
|
||||||
stationMaster: [
|
stationMaster: [
|
||||||
PASSENGER_PERMS.bookings.view,
|
PASSENGER_PERMS.bookings.view,
|
||||||
PASSENGER_PERMS.bookings.manage,
|
PASSENGER_PERMS.bookings.manage,
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ const STAFF_USERS = [
|
|||||||
{ email: 'passenger.staff@edr.local', username: 'passenger_staff', roleKey: 'edr_passenger_backoffice_staff' },
|
{ email: 'passenger.staff@edr.local', username: 'passenger_staff', roleKey: 'edr_passenger_backoffice_staff' },
|
||||||
{ email: 'passenger.agent@edr.local', username: 'passenger_agent', roleKey: 'edr_passenger_agent' },
|
{ email: 'passenger.agent@edr.local', username: 'passenger_agent', roleKey: 'edr_passenger_agent' },
|
||||||
{ email: 'passenger.finance@edr.local', username: 'passenger_finance', roleKey: 'edr_passenger_finance' },
|
{ email: 'passenger.finance@edr.local', username: 'passenger_finance', roleKey: 'edr_passenger_finance' },
|
||||||
|
{ email: 'passenger.director@edr.local', username: 'passenger_director', roleKey: 'edr_passenger_director' },
|
||||||
|
{ email: 'passenger.ticketofficer@edr.local', username: 'passenger_ticket_officer', roleKey: 'edr_passenger_ticket_officer' },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|||||||
@@ -200,6 +200,7 @@ export default function PaymentMethodsPage() {
|
|||||||
const paymentTypes = [
|
const paymentTypes = [
|
||||||
{ value: 'TELEBIRR', label: 'Telebirr' },
|
{ value: 'TELEBIRR', label: 'Telebirr' },
|
||||||
{ value: 'CBE_BIRR', label: 'CBE Birr' },
|
{ value: 'CBE_BIRR', label: 'CBE Birr' },
|
||||||
|
{ value: 'CBE_BILL', label: 'CBE Bill Payment' },
|
||||||
{ value: 'EBIRR', label: 'eBirr' },
|
{ value: 'EBIRR', label: 'eBirr' },
|
||||||
{ value: 'WAAFI', label: 'Waafi' },
|
{ value: 'WAAFI', label: 'Waafi' },
|
||||||
{ value: 'DMONEY', label: 'dMoney' },
|
{ value: 'DMONEY', label: 'dMoney' },
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import {
|
|||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
KeyRound,
|
KeyRound,
|
||||||
Landmark,
|
Landmark,
|
||||||
|
Copy,
|
||||||
|
Check,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
const getIconForMethod = (methodId: string) => {
|
const getIconForMethod = (methodId: string) => {
|
||||||
@@ -45,6 +47,13 @@ export default function PaymentPage() {
|
|||||||
const [otpCode, setOtpCode] = useState("");
|
const [otpCode, setOtpCode] = useState("");
|
||||||
const [otpMessage, setOtpMessage] = useState<string | null>(null);
|
const [otpMessage, setOtpMessage] = useState<string | null>(null);
|
||||||
const [otpError, setOtpError] = useState<string | null>(null);
|
const [otpError, setOtpError] = useState<string | null>(null);
|
||||||
|
// CBE bill payment: the bill reference the customer pays at any CBE channel.
|
||||||
|
const [billAction, setBillAction] = useState<{
|
||||||
|
billReference: string;
|
||||||
|
instructions?: string;
|
||||||
|
expiresAt?: string;
|
||||||
|
} | null>(null);
|
||||||
|
const [billCopied, setBillCopied] = useState(false);
|
||||||
|
|
||||||
const isRoundTrip = searchCriteria?.tripType === 'ROUND_TRIP';
|
const isRoundTrip = searchCriteria?.tripType === 'ROUND_TRIP';
|
||||||
|
|
||||||
@@ -144,6 +153,16 @@ export default function PaymentPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CBE bill: no redirect — show the bill reference and wait for the customer to pay
|
||||||
|
// at a CBE channel. Confirmation only ever comes from polling the intent status.
|
||||||
|
if (data?.clientAction?.type === 'SHOW_BILL_REFERENCE') {
|
||||||
|
setPaymentIntent(data.intentId);
|
||||||
|
updateStatus("REQUIRES_ACTION");
|
||||||
|
setBillAction(data.clientAction);
|
||||||
|
setIsProcessing(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if ((selectedMethod === 'TELEBIRR' || selectedMethod === 'WAAFI' || selectedMethod === 'DMONEY') && data?.clientAction?.type === 'REDIRECT') {
|
if ((selectedMethod === 'TELEBIRR' || selectedMethod === 'WAAFI' || selectedMethod === 'DMONEY') && data?.clientAction?.type === 'REDIRECT') {
|
||||||
setPaymentIntent(data.intentId);
|
setPaymentIntent(data.intentId);
|
||||||
updateStatus("REQUIRES_ACTION");
|
updateStatus("REQUIRES_ACTION");
|
||||||
@@ -190,6 +209,34 @@ export default function PaymentPage() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// While the CBE bill dialog is open, poll the intent; the booking confirms server-side
|
||||||
|
// once CBE settles the bill. Never claim success on any client-side signal.
|
||||||
|
const { data: billIntentStatus } = useQuery<any>({
|
||||||
|
queryKey: ["cbe-bill-intent-status", bookingId],
|
||||||
|
queryFn: () => apiClient.get(`/payments/intents/${bookingId}`),
|
||||||
|
enabled: !!billAction && !!bookingId,
|
||||||
|
refetchInterval: 5_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (billAction && billIntentStatus?.status === "SUCCEEDED") {
|
||||||
|
updateStatus("SUCCEEDED");
|
||||||
|
router.push("/booking/confirmation");
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [billAction, billIntentStatus?.status]);
|
||||||
|
|
||||||
|
const copyBillReference = async () => {
|
||||||
|
if (!billAction) return;
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(billAction.billReference);
|
||||||
|
setBillCopied(true);
|
||||||
|
setTimeout(() => setBillCopied(false), 2000);
|
||||||
|
} catch {
|
||||||
|
/* clipboard unavailable — the number is still shown on screen */
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Fire the actual initiate. `mobile` is only used for CAC (OTP debit).
|
// Fire the actual initiate. `mobile` is only used for CAC (OTP debit).
|
||||||
const startPayment = (mobile?: string) => {
|
const startPayment = (mobile?: string) => {
|
||||||
if (!selectedMethod || !bookingId || !selectedPaymentMethod) return;
|
if (!selectedMethod || !bookingId || !selectedPaymentMethod) return;
|
||||||
@@ -569,6 +616,62 @@ export default function PaymentPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* CBE bill payment — show the bill reference; confirmation comes from polling */}
|
||||||
|
{billAction && (
|
||||||
|
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 px-4">
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 max-w-md w-full shadow-2xl">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<Landmark className="w-5 h-5 text-primary" />
|
||||||
|
<h3 className="text-lg font-bold text-gray-900 dark:text-gray-100">Pay at CBE</h3>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4">
|
||||||
|
{billAction.instructions ??
|
||||||
|
"Pay this bill at any CBE branch, the CBE Birr app, mobile banking or USSD."}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center justify-between gap-2 bg-gray-50 dark:bg-gray-900/40 border border-gray-200 dark:border-gray-700 rounded-lg px-4 py-3">
|
||||||
|
<span className="font-mono text-2xl font-bold tracking-widest text-gray-900 dark:text-gray-100 select-all">
|
||||||
|
{billAction.billReference}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={copyBillReference}
|
||||||
|
className="btn-secondary px-3 py-2 flex items-center gap-1 text-sm"
|
||||||
|
title="Copy bill number"
|
||||||
|
>
|
||||||
|
{billCopied ? <Check className="w-4 h-4 text-green-600" /> : <Copy className="w-4 h-4" />}
|
||||||
|
{billCopied ? "Copied" : "Copy"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-600 dark:text-gray-300 mt-3 space-y-1">
|
||||||
|
<p>
|
||||||
|
Amount:{" "}
|
||||||
|
<span className="font-semibold">
|
||||||
|
ETB {(totalAmountDisplay ?? 0).toFixed(2)}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
{billAction.expiresAt && (
|
||||||
|
<p>
|
||||||
|
Pay before:{" "}
|
||||||
|
<span className="font-semibold">
|
||||||
|
{format(new Date(billAction.expiresAt), "MMM d, yyyy HH:mm")}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 mt-4 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin flex-shrink-0" />
|
||||||
|
Waiting for payment confirmation — this page updates automatically once CBE
|
||||||
|
confirms your payment.
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setBillAction(null)}
|
||||||
|
className="btn-secondary w-full py-2.5 mt-4"
|
||||||
|
>
|
||||||
|
Close (pay later)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Two-column grid */}
|
{/* Two-column grid */}
|
||||||
<div className="lg:grid lg:grid-cols-3 lg:gap-6 lg:items-start">
|
<div className="lg:grid lg:grid-cols-3 lg:gap-6 lg:items-start">
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
"@nestjs/common": "^11.0.0",
|
"@nestjs/common": "^11.0.0",
|
||||||
"@nestjs/config": "^4.0.0",
|
"@nestjs/config": "^4.0.0",
|
||||||
"@nestjs/core": "^11.0.0",
|
"@nestjs/core": "^11.0.0",
|
||||||
|
"@nestjs/jwt": "^11.0.2",
|
||||||
"@nestjs/platform-express": "^11.0.0",
|
"@nestjs/platform-express": "^11.0.0",
|
||||||
"@nestjs/schedule": "^6.0.0",
|
"@nestjs/schedule": "^6.0.0",
|
||||||
"@nestjs/swagger": "^11.4.2",
|
"@nestjs/swagger": "^11.4.2",
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import ebirrConfig from "./config/ebirr.config";
|
|||||||
import cardConfig from "./config/card.config";
|
import cardConfig from "./config/card.config";
|
||||||
import dmoneyConfig from "./config/dmoney.config";
|
import dmoneyConfig from "./config/dmoney.config";
|
||||||
import cacConfig from "./config/cac.config";
|
import cacConfig from "./config/cac.config";
|
||||||
|
import cbeBillConfig from "./config/cbe-bill.config";
|
||||||
|
import { CbeBillModule } from "./modules/cbe-bill/cbe-bill.module";
|
||||||
import { HealthModule } from "./modules/health/health.module";
|
import { HealthModule } from "./modules/health/health.module";
|
||||||
import { IntentsModule } from "./modules/intents/intents.module";
|
import { IntentsModule } from "./modules/intents/intents.module";
|
||||||
import { OutboxModule } from "./modules/outbox/outbox.module";
|
import { OutboxModule } from "./modules/outbox/outbox.module";
|
||||||
@@ -36,6 +38,7 @@ import { WebhooksModule } from "./modules/webhooks/webhooks.module";
|
|||||||
cardConfig,
|
cardConfig,
|
||||||
dmoneyConfig,
|
dmoneyConfig,
|
||||||
cacConfig,
|
cacConfig,
|
||||||
|
cbeBillConfig,
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
TypeOrmModule.forRootAsync({
|
TypeOrmModule.forRootAsync({
|
||||||
@@ -47,6 +50,7 @@ import { WebhooksModule } from "./modules/webhooks/webhooks.module";
|
|||||||
HealthModule,
|
HealthModule,
|
||||||
ProvidersModule,
|
ProvidersModule,
|
||||||
IntentsModule,
|
IntentsModule,
|
||||||
|
CbeBillModule,
|
||||||
WebhooksModule,
|
WebhooksModule,
|
||||||
OutboxModule,
|
OutboxModule,
|
||||||
ReconciliationModule,
|
ReconciliationModule,
|
||||||
|
|||||||
26
apps/edr-payment-api/src/config/cbe-bill.config.ts
Normal file
26
apps/edr-payment-api/src/config/cbe-bill.config.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { registerAs } from "@nestjs/config";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CBE Unified Bill Payment — the INBOUND biller integration (docs/cbe/). Deliberately separate
|
||||||
|
* from cbe.config.ts, which belongs to the outbound CBE_BIRR wallet gateway: two disjoint
|
||||||
|
* credential/auth domains that rotate independently (plan D7).
|
||||||
|
*/
|
||||||
|
export default registerAs("cbeBill", () => ({
|
||||||
|
/** Kill switch — all /cbe/* endpoints answer 503 while false. */
|
||||||
|
enabled: process.env.CBE_BILL_ENABLED === "true",
|
||||||
|
/** Credentials CBE presents to /cbe/oauth/token. */
|
||||||
|
clientId: process.env.CBE_BILL_CLIENT_ID || "",
|
||||||
|
clientSecret: process.env.CBE_BILL_CLIENT_SECRET || "",
|
||||||
|
/** Signs/verifies the bearer tokens WE issue to CBE — never shared with ServiceAuthGuard. */
|
||||||
|
jwtSecret: process.env.CBE_BILL_JWT_SECRET || "",
|
||||||
|
tokenExpiresIn: Number(process.env.CBE_BILL_TOKEN_EXPIRES_IN || 3600),
|
||||||
|
scope: process.env.CBE_BILL_SCOPE || "Unified_Outgoing",
|
||||||
|
/** bill-query hop to the owning domain app. Short — CBE holds its own timeout over ours. */
|
||||||
|
domainTimeoutMs: Number(process.env.CBE_BILL_DOMAIN_TIMEOUT_MS || 3000),
|
||||||
|
passengerApiBaseUrl: (
|
||||||
|
process.env.PASSENGER_API_BASE_URL || "http://localhost:3002"
|
||||||
|
).replace(/\/$/, ""),
|
||||||
|
freightApiBaseUrl: (
|
||||||
|
process.env.FREIGHT_API_BASE_URL || "http://localhost:3001"
|
||||||
|
).replace(/\/$/, ""),
|
||||||
|
}));
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CBE Unified Bill Payment (docs/cbe/CBE_IMPLEMENTATION_PLAN.md §4.1, §5):
|
||||||
|
* - `bill_reference` — the short numeric Bill_Id CBE presents back to us; unique, null for
|
||||||
|
* every non-CBE intent.
|
||||||
|
* - `payer_name` — payer snapshot captured at initiate; fallback for /cbe/query Full_Name.
|
||||||
|
* - `cbe_bill_reference_seq` — backs the 11-digit sequence body of the bill reference.
|
||||||
|
*
|
||||||
|
* DATA SAFETY: purely additive — new nullable columns and a new sequence; no existing rows
|
||||||
|
* or values are touched.
|
||||||
|
*/
|
||||||
|
export class AddCbeBillReference1782300000000 implements MigrationInterface {
|
||||||
|
name = "AddCbeBillReference1782300000000";
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "edr_payment"."payment_intent" ADD COLUMN IF NOT EXISTS "bill_reference" varchar(32)`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "edr_payment"."payment_intent" ADD COLUMN IF NOT EXISTS "payer_name" varchar(128)`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE UNIQUE INDEX IF NOT EXISTS "uq_payment_intent_bill_reference" ON "edr_payment"."payment_intent" ("bill_reference") WHERE "bill_reference" IS NOT NULL`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE SEQUENCE IF NOT EXISTS "edr_payment"."cbe_bill_reference_seq" START 10000001`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`DROP SEQUENCE IF EXISTS "edr_payment"."cbe_bill_reference_seq"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`DROP INDEX IF EXISTS "edr_payment"."uq_payment_intent_bill_reference"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "edr_payment"."payment_intent" DROP COLUMN IF EXISTS "payer_name"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "edr_payment"."payment_intent" DROP COLUMN IF EXISTS "bill_reference"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CBE-protocol audit/idempotency ledger for the Unified Bill Payment integration
|
||||||
|
* (docs/cbe/CBE_IMPLEMENTATION_PLAN.md §4.2).
|
||||||
|
*
|
||||||
|
* - UNIQUE (end_to_end_txn_id, operation): DB-level backstop for the application idempotency
|
||||||
|
* checks — a concurrent duplicate of the same CBE attempt cannot create two rows.
|
||||||
|
* - Partial UNIQUE (cbe_txn_ref) on settled PAYMENTs: blocks Cbe_Txn_Ref replay across bills.
|
||||||
|
*
|
||||||
|
* DATA SAFETY: new table only; nothing existing is touched.
|
||||||
|
*/
|
||||||
|
export class CreateCbeBillOperation1782400000000 implements MigrationInterface {
|
||||||
|
name = "CreateCbeBillOperation1782400000000";
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS "edr_payment"."cbe_bill_operation" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
"operation" varchar(16) NOT NULL,
|
||||||
|
"bill_id" varchar(32) NOT NULL,
|
||||||
|
"end_to_end_txn_id" varchar(128) NOT NULL,
|
||||||
|
"cbe_txn_ref" varchar(128),
|
||||||
|
"destination_api_name" varchar(64),
|
||||||
|
"intent_id" uuid,
|
||||||
|
"trade_status" varchar(16) NOT NULL,
|
||||||
|
"failure_class" varchar(16),
|
||||||
|
"response_code" varchar(8),
|
||||||
|
"response_description" text,
|
||||||
|
"request_payload" jsonb NOT NULL,
|
||||||
|
"response_payload" jsonb,
|
||||||
|
"created_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
"updated_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
"deleted_at" timestamptz
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE UNIQUE INDEX IF NOT EXISTS "uq_cbe_bill_operation_e2e" ON "edr_payment"."cbe_bill_operation" ("end_to_end_txn_id", "operation")`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE UNIQUE INDEX IF NOT EXISTS "uq_cbe_bill_operation_txn_ref" ON "edr_payment"."cbe_bill_operation" ("cbe_txn_ref") WHERE "operation" = 'PAYMENT' AND "trade_status" = 'SUCCESS'`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE INDEX IF NOT EXISTS "idx_cbe_bill_operation_bill" ON "edr_payment"."cbe_bill_operation" ("bill_id", "operation")`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`DROP TABLE IF EXISTS "edr_payment"."cbe_bill_operation"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
import { Injectable, Logger } from "@nestjs/common";
|
||||||
|
import { ConfigService } from "@nestjs/config";
|
||||||
|
import { HttpService } from "@nestjs/axios";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
import { PaymentReferenceType, PaymentService } from "@edr/types";
|
||||||
|
import { PaymentIntent } from "../intents/entities/payment-intent.entity";
|
||||||
|
import { CbeBillError } from "./mappers/cbe-error.mapper";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Why a bill is no longer payable. Shared vocabulary between both domain apps and the local
|
||||||
|
* intent check, so one mapper produces every Response_Description CBE sees.
|
||||||
|
* `NOT_PAYABLE` is the catch-all for domain states with no better name (booking DRAFT/BOARDED,
|
||||||
|
* invoice DRAFT) — it must stay last-resort, never a substitute for a specific reason.
|
||||||
|
*/
|
||||||
|
export type BillNotPayableReason =
|
||||||
|
| "ALREADY_PAID"
|
||||||
|
| "CANCELLED"
|
||||||
|
| "REFUNDED"
|
||||||
|
| "EXPIRED"
|
||||||
|
| "NOT_FOUND"
|
||||||
|
| "NOT_PAYABLE";
|
||||||
|
|
||||||
|
/** Contract of POST /internal/payments/bill-query on the domain apps (plan Phase 4). */
|
||||||
|
export interface BillQueryResult {
|
||||||
|
stillPayable: boolean;
|
||||||
|
payerName?: string | null;
|
||||||
|
currentAmountMinor?: number | null;
|
||||||
|
currency?: string | null;
|
||||||
|
/** When stillPayable=false — see {@link BillNotPayableReason}. */
|
||||||
|
reason?: BillNotPayableReason | string | null;
|
||||||
|
/**
|
||||||
|
* What the payer is paying for, shown on CBE's confirmation screen next to the amount —
|
||||||
|
* the domain's own human reference (booking ref / invoice number), not our internal ids.
|
||||||
|
*/
|
||||||
|
paymentReason?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Payment_Reason when the domain app sends none (older build, or an order with no human
|
||||||
|
* reference). Generic but never blank: CBE renders this field to the payer, and a bill with
|
||||||
|
* an amount and no stated purpose is what a customer refuses to confirm.
|
||||||
|
*/
|
||||||
|
export function defaultPaymentReason(
|
||||||
|
referenceType: PaymentReferenceType,
|
||||||
|
): string {
|
||||||
|
return referenceType === PaymentReferenceType.BOOKING
|
||||||
|
? "Train ticket booking"
|
||||||
|
: "Freight invoice";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CBE reads Response_Description back to the payer at the counter or in the USSD prompt, so it
|
||||||
|
* has to name the thing they are actually holding — a passenger booking or a freight invoice —
|
||||||
|
* rather than our internal "bill" abstraction (plan §6.6).
|
||||||
|
*/
|
||||||
|
function subjectOf(referenceType: PaymentReferenceType): string {
|
||||||
|
return referenceType === PaymentReferenceType.BOOKING ? "booking" : "invoice";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reasonToDescription(
|
||||||
|
reason: string | null | undefined,
|
||||||
|
referenceType: PaymentReferenceType,
|
||||||
|
): string {
|
||||||
|
const subject = subjectOf(referenceType);
|
||||||
|
switch (reason) {
|
||||||
|
case "ALREADY_PAID":
|
||||||
|
return `This ${subject} has already been paid.`;
|
||||||
|
case "CANCELLED":
|
||||||
|
return `This ${subject} has been cancelled.`;
|
||||||
|
case "REFUNDED":
|
||||||
|
return `This ${subject} has been refunded.`;
|
||||||
|
case "EXPIRED":
|
||||||
|
return `This ${subject} has expired and can no longer be paid.`;
|
||||||
|
// A bill reference we issued whose order has since vanished from the domain app. Same
|
||||||
|
// wording as an unknown Bill_Id — from the teller's side it is the same situation.
|
||||||
|
case "NOT_FOUND":
|
||||||
|
return "Bill not found.";
|
||||||
|
default:
|
||||||
|
return `This ${subject} is no longer payable.`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The live "still payable?" hop to the owning domain app — routing comes from
|
||||||
|
* `intent.service` (plan D3). This hop is the double-payment guard (§6.3) and the source of
|
||||||
|
* the mandatory Full_Name: NOT optional, and on the /cbe/payment path never served from cache.
|
||||||
|
* Short timeout, no retries — CBE holds its own timeout over ours.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class BillResolverService {
|
||||||
|
private readonly logger = new Logger(BillResolverService.name);
|
||||||
|
private readonly serviceToken = process.env.SERVICE_AUTH_TOKEN ?? "";
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly http: HttpService,
|
||||||
|
private readonly config: ConfigService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async billQuery(intent: PaymentIntent): Promise<BillQueryResult> {
|
||||||
|
const base =
|
||||||
|
intent.service === PaymentService.PASSENGER
|
||||||
|
? this.config.get<string>("cbeBill.passengerApiBaseUrl")
|
||||||
|
: this.config.get<string>("cbeBill.freightApiBaseUrl");
|
||||||
|
const url = `${base}/internal/payments/bill-query`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await firstValueFrom(
|
||||||
|
this.http.post<BillQueryResult>(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
referenceType: intent.referenceType,
|
||||||
|
referenceId: intent.referenceId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timeout: this.config.get<number>("cbeBill.domainTimeoutMs") ?? 3000,
|
||||||
|
headers: this.serviceToken
|
||||||
|
? { "x-service-token": this.serviceToken }
|
||||||
|
: {},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
// The passenger API wraps every response in a { success, data } envelope
|
||||||
|
// (global transform interceptor); freight returns the body bare. Accept both.
|
||||||
|
const body = response.data as unknown as {
|
||||||
|
success?: boolean;
|
||||||
|
data?: BillQueryResult;
|
||||||
|
};
|
||||||
|
return body && typeof body === "object" && "success" in body && body.data
|
||||||
|
? body.data
|
||||||
|
: (response.data as BillQueryResult);
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.warn(
|
||||||
|
`bill-query ${intent.service}/${intent.referenceId} unreachable: ${
|
||||||
|
err instanceof Error ? err.message : String(err)
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
// TRANSIENT so CBE may retry the same End_To_End_Txn_Id once we recover (plan R5).
|
||||||
|
throw new CbeBillError("Service temporarily unavailable.", "TRANSIENT");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
39
apps/edr-payment-api/src/modules/cbe-bill/cbe-auth.guard.ts
Normal file
39
apps/edr-payment-api/src/modules/cbe-bill/cbe-auth.guard.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import {
|
||||||
|
CanActivate,
|
||||||
|
ExecutionContext,
|
||||||
|
Injectable,
|
||||||
|
UnauthorizedException,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import { ConfigService } from "@nestjs/config";
|
||||||
|
import { JwtService } from "@nestjs/jwt";
|
||||||
|
import { Request } from "express";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies the bearer tokens WE minted for CBE at /cbe/oauth/token (plan D7). Its secret
|
||||||
|
* (CBE_BILL_JWT_SECRET) is disjoint from ServiceAuthGuard's shared token: a CBE token must
|
||||||
|
* never authenticate a call to /payments/*, and vice versa.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class CbeAuthGuard implements CanActivate {
|
||||||
|
constructor(
|
||||||
|
private readonly jwtService: JwtService,
|
||||||
|
private readonly config: ConfigService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
|
const request = context.switchToHttp().getRequest<Request>();
|
||||||
|
const auth = request.headers.authorization;
|
||||||
|
if (!auth?.startsWith("Bearer ")) {
|
||||||
|
throw new UnauthorizedException("Missing bearer token");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.jwtService.verifyAsync(auth.substring(7), {
|
||||||
|
secret: this.config.get<string>("cbeBill.jwtSecret"),
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
throw new UnauthorizedException("Invalid or expired token");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
HttpCode,
|
||||||
|
HttpStatus,
|
||||||
|
Post,
|
||||||
|
UseFilters,
|
||||||
|
UseGuards,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||||
|
import { CbeAuthGuard } from "./cbe-auth.guard";
|
||||||
|
import { CbeExceptionFilter } from "./cbe-exception.filter";
|
||||||
|
import { CbeBillService } from "./cbe-bill.service";
|
||||||
|
import { TokenRequestDto } from "./dto/token-request.dto";
|
||||||
|
import { TokenResponseDto } from "./dto/token-response.dto";
|
||||||
|
import { CbeQueryRequestDto } from "./dto/cbe-query-request.dto";
|
||||||
|
import { CbeQueryResponseDto } from "./dto/cbe-query-response.dto";
|
||||||
|
import { CbePaymentRequestDto } from "./dto/cbe-payment-request.dto";
|
||||||
|
import { CbePaymentResponseDto } from "./dto/cbe-payment-response.dto";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CBE Unified Bill Payment — the INBOUND surface CBE core banking calls (docs/cbe/). We are
|
||||||
|
* the biller: CBE authenticates against /cbe/oauth/token with credentials we issued, then
|
||||||
|
* presents the bearer token on /cbe/query and /cbe/payment. Business failures answer HTTP 200
|
||||||
|
* with Response_Code "3"; only authentication answers 401 (plan D6/D7).
|
||||||
|
*/
|
||||||
|
@ApiTags("CBE Unified Bill (inbound)")
|
||||||
|
@Controller("cbe")
|
||||||
|
@UseFilters(CbeExceptionFilter)
|
||||||
|
export class CbeBillController {
|
||||||
|
constructor(private readonly cbeBillService: CbeBillService) {}
|
||||||
|
|
||||||
|
@Post("oauth/token")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@ApiOperation({ summary: "OAuth client_credentials token for CBE (we are the auth server)" })
|
||||||
|
async token(@Body() dto: TokenRequestDto): Promise<TokenResponseDto> {
|
||||||
|
return this.cbeBillService.generateToken(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("query")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@UseGuards(CbeAuthGuard)
|
||||||
|
@ApiOperation({ summary: "Bill lookup — amount due + payer name for a Bill_Id" })
|
||||||
|
async query(@Body() dto: CbeQueryRequestDto): Promise<CbeQueryResponseDto> {
|
||||||
|
return this.cbeBillService.query(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("payment")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@UseGuards(CbeAuthGuard)
|
||||||
|
@ApiOperation({ summary: "Settle a bill — customer already debited by CBE" })
|
||||||
|
async payment(
|
||||||
|
@Body() dto: CbePaymentRequestDto,
|
||||||
|
): Promise<CbePaymentResponseDto> {
|
||||||
|
return this.cbeBillService.pay(dto);
|
||||||
|
}
|
||||||
|
}
|
||||||
28
apps/edr-payment-api/src/modules/cbe-bill/cbe-bill.module.ts
Normal file
28
apps/edr-payment-api/src/modules/cbe-bill/cbe-bill.module.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
import { HttpModule } from "@nestjs/axios";
|
||||||
|
import { JwtModule } from "@nestjs/jwt";
|
||||||
|
import { TypeOrmModule } from "@nestjs/typeorm";
|
||||||
|
import { IntentsModule } from "../intents/intents.module";
|
||||||
|
import { CbeBillOperation } from "./entities/cbe-bill-operation.entity";
|
||||||
|
import { BillResolverService } from "./bill-resolver.service";
|
||||||
|
import { CbeAuthGuard } from "./cbe-auth.guard";
|
||||||
|
import { CbeBillController } from "./cbe-bill.controller";
|
||||||
|
import { CbeBillRepository } from "./cbe-bill.repository";
|
||||||
|
import { CbeBillService } from "./cbe-bill.service";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inbound CBE Unified Bill Payment module (docs/cbe/). Its auth domain is disjoint from the
|
||||||
|
* rest of the app: tokens are minted and verified with CBE_BILL_JWT_SECRET only (plan D7) —
|
||||||
|
* JwtModule is registered bare and the secret passed explicitly at sign/verify time.
|
||||||
|
*/
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([CbeBillOperation]),
|
||||||
|
HttpModule,
|
||||||
|
JwtModule.register({}),
|
||||||
|
IntentsModule,
|
||||||
|
],
|
||||||
|
controllers: [CbeBillController],
|
||||||
|
providers: [CbeBillService, CbeBillRepository, BillResolverService, CbeAuthGuard],
|
||||||
|
})
|
||||||
|
export class CbeBillModule {}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { InjectRepository } from "@nestjs/typeorm";
|
||||||
|
import { Repository } from "typeorm";
|
||||||
|
import { BaseRepository } from "@edr/api-common";
|
||||||
|
import {
|
||||||
|
CbeBillOperation,
|
||||||
|
CbeOperation,
|
||||||
|
} from "./entities/cbe-bill-operation.entity";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CbeBillRepository extends BaseRepository<CbeBillOperation> {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(CbeBillOperation)
|
||||||
|
repository: Repository<CbeBillOperation>,
|
||||||
|
) {
|
||||||
|
super(repository);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The prior attempt for CBE's per-attempt id — the §6.5 idempotency lookup. */
|
||||||
|
async findByEndToEndTxnId(
|
||||||
|
endToEndTxnId: string,
|
||||||
|
operation: CbeOperation,
|
||||||
|
): Promise<CbeBillOperation | null> {
|
||||||
|
return this.repository.findOne({ where: { endToEndTxnId, operation } });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A SUCCESSful settlement already carrying this Cbe_Txn_Ref — blocks replay across bills. */
|
||||||
|
async findSettledByCbeTxnRef(
|
||||||
|
cbeTxnRef: string,
|
||||||
|
): Promise<CbeBillOperation | null> {
|
||||||
|
return this.repository.findOne({
|
||||||
|
where: { cbeTxnRef, operation: "PAYMENT", tradeStatus: "SUCCESS" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
387
apps/edr-payment-api/src/modules/cbe-bill/cbe-bill.service.ts
Normal file
387
apps/edr-payment-api/src/modules/cbe-bill/cbe-bill.service.ts
Normal file
@@ -0,0 +1,387 @@
|
|||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
Logger,
|
||||||
|
ServiceUnavailableException,
|
||||||
|
UnauthorizedException,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import { ConfigService } from "@nestjs/config";
|
||||||
|
import { JwtService } from "@nestjs/jwt";
|
||||||
|
import { ProviderMethod, ProviderPaymentStatus } from "@edr/types";
|
||||||
|
import { BillReferenceService } from "../intents/bill-reference.service";
|
||||||
|
import { IntentsRepository } from "../intents/intents.repository";
|
||||||
|
import { IntentsService } from "../intents/intents.service";
|
||||||
|
import { PaymentIntent } from "../intents/entities/payment-intent.entity";
|
||||||
|
import {
|
||||||
|
BillNotPayableReason,
|
||||||
|
BillResolverService,
|
||||||
|
defaultPaymentReason,
|
||||||
|
reasonToDescription,
|
||||||
|
} from "./bill-resolver.service";
|
||||||
|
import { CbeBillRepository } from "./cbe-bill.repository";
|
||||||
|
import { CbeBillOperation } from "./entities/cbe-bill-operation.entity";
|
||||||
|
import { TokenRequestDto } from "./dto/token-request.dto";
|
||||||
|
import { TokenResponseDto } from "./dto/token-response.dto";
|
||||||
|
import { CbeQueryRequestDto } from "./dto/cbe-query-request.dto";
|
||||||
|
import { CbeQueryResponseDto } from "./dto/cbe-query-response.dto";
|
||||||
|
import { CbePaymentRequestDto } from "./dto/cbe-payment-request.dto";
|
||||||
|
import { CbePaymentResponseDto } from "./dto/cbe-payment-response.dto";
|
||||||
|
import { CbeBillError, toCbeFailure } from "./mappers/cbe-error.mapper";
|
||||||
|
import {
|
||||||
|
mapQueryFailure,
|
||||||
|
mapQuerySuccess,
|
||||||
|
} from "./mappers/cbe-query.mapper";
|
||||||
|
import {
|
||||||
|
mapPaymentFailure,
|
||||||
|
mapPaymentSuccess,
|
||||||
|
} from "./mappers/cbe-payment.mapper";
|
||||||
|
|
||||||
|
/** Postgres unique_violation — the DB-level idempotency backstop firing on a concurrent duplicate. */
|
||||||
|
const PG_UNIQUE_VIOLATION = "23505";
|
||||||
|
|
||||||
|
/** Mirrors the short-pay tolerance already applied in handlePaymentEvent. */
|
||||||
|
const AMOUNT_TOLERANCE = 0.01;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translate an intent's own terminal state into the same reason vocabulary the domain apps
|
||||||
|
* speak, so both paths flow through one description mapper.
|
||||||
|
*/
|
||||||
|
function localReason(intent: PaymentIntent): BillNotPayableReason {
|
||||||
|
switch (intent.status) {
|
||||||
|
case ProviderPaymentStatus.SUCCEEDED:
|
||||||
|
return "ALREADY_PAID";
|
||||||
|
case ProviderPaymentStatus.CANCELLED:
|
||||||
|
// expireIntent() cancels with failureCode EXPIRED — an abandoned bill, not a cancellation.
|
||||||
|
return intent.failureCode === "EXPIRED" ? "EXPIRED" : "CANCELLED";
|
||||||
|
default:
|
||||||
|
return "NOT_PAYABLE";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Orchestration for CBE's three inbound calls (docs/cbe/CBE_IMPLEMENTATION_PLAN.md Phase 3).
|
||||||
|
* Business failures return HTTP 200 + Response_Code "3" envelopes (never throw past the
|
||||||
|
* controller); the exception filter only catches auth, validation, and the unexpected.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class CbeBillService {
|
||||||
|
private readonly logger = new Logger(CbeBillService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly config: ConfigService,
|
||||||
|
private readonly jwtService: JwtService,
|
||||||
|
private readonly cbeBillRepository: CbeBillRepository,
|
||||||
|
private readonly intentsRepository: IntentsRepository,
|
||||||
|
private readonly intentsService: IntentsService,
|
||||||
|
private readonly billReferenceService: BillReferenceService,
|
||||||
|
private readonly billResolver: BillResolverService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ token */
|
||||||
|
|
||||||
|
async generateToken(dto: TokenRequestDto): Promise<TokenResponseDto> {
|
||||||
|
this.assertEnabled();
|
||||||
|
|
||||||
|
const clientId = this.config.get<string>("cbeBill.clientId");
|
||||||
|
const clientSecret = this.config.get<string>("cbeBill.clientSecret");
|
||||||
|
// Unset credentials must fail closed — never let "" === "" mint a token.
|
||||||
|
const valid =
|
||||||
|
!!clientId &&
|
||||||
|
!!clientSecret &&
|
||||||
|
dto.client_id === clientId &&
|
||||||
|
dto.client_secret === clientSecret &&
|
||||||
|
dto.grant_type === "client_credentials" &&
|
||||||
|
dto.scope === this.config.get<string>("cbeBill.scope");
|
||||||
|
if (!valid) throw new UnauthorizedException("Invalid credentials");
|
||||||
|
|
||||||
|
const expiresIn = this.config.get<number>("cbeBill.tokenExpiresIn") ?? 3600;
|
||||||
|
const accessToken = await this.jwtService.signAsync(
|
||||||
|
{ iss: "edr-payment-api", clientId: dto.client_id, scope: dto.scope },
|
||||||
|
{ secret: this.config.get<string>("cbeBill.jwtSecret"), expiresIn },
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
token_type: "Bearer",
|
||||||
|
access_token: accessToken,
|
||||||
|
expires_in: expiresIn,
|
||||||
|
scope: dto.scope,
|
||||||
|
consented_on: Math.floor(Date.now() / 1000),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ query */
|
||||||
|
|
||||||
|
async query(dto: CbeQueryRequestDto): Promise<CbeQueryResponseDto> {
|
||||||
|
this.assertEnabled();
|
||||||
|
|
||||||
|
// Audit first — unlike the reference (which left this commented out), every query attempt
|
||||||
|
// is persisted; counter disputes are exactly what this row is for (plan Phase 3.2).
|
||||||
|
const audit = await this.upsertAudit("QUERY", dto.End_To_End_Txn_Id, {
|
||||||
|
billId: dto.Bill_Id,
|
||||||
|
destinationApiName: dto.Destination_Api_Name,
|
||||||
|
requestPayload: dto as unknown as Record<string, unknown>,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const intent = await this.resolveIntent(dto.Bill_Id);
|
||||||
|
this.assertIntentPayable(intent);
|
||||||
|
|
||||||
|
// The live domain hop — the double-payment guard (§6.3). Not optional.
|
||||||
|
const billQuery = await this.billResolver.billQuery(intent);
|
||||||
|
if (!billQuery.stillPayable) {
|
||||||
|
throw new CbeBillError(
|
||||||
|
reasonToDescription(billQuery.reason, intent.referenceType),
|
||||||
|
"BUSINESS",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = mapQuerySuccess(dto, {
|
||||||
|
amountMajor: billQuery.currentAmountMinor ?? intent.amountMinor,
|
||||||
|
fullName: billQuery.payerName || intent.payerName || "",
|
||||||
|
paymentReason:
|
||||||
|
billQuery.paymentReason || defaultPaymentReason(intent.referenceType),
|
||||||
|
});
|
||||||
|
await this.finishAudit(audit, {
|
||||||
|
intentId: intent.id,
|
||||||
|
tradeStatus: "SUCCESS",
|
||||||
|
response,
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
} catch (err) {
|
||||||
|
const failure = toCbeFailure(err);
|
||||||
|
if (!(err instanceof CbeBillError)) {
|
||||||
|
this.logger.error(
|
||||||
|
`/cbe/query ${dto.Bill_Id} failed unexpectedly: ${err instanceof Error ? err.stack : String(err)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const response = mapQueryFailure(dto, failure.description);
|
||||||
|
await this.finishAudit(audit, {
|
||||||
|
tradeStatus: "FAILED",
|
||||||
|
failureClass: failure.failureClass,
|
||||||
|
response,
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ payment */
|
||||||
|
|
||||||
|
async pay(dto: CbePaymentRequestDto): Promise<CbePaymentResponseDto> {
|
||||||
|
this.assertEnabled();
|
||||||
|
|
||||||
|
// §6.5 idempotency on CBE's per-attempt id, in order.
|
||||||
|
const prior = await this.cbeBillRepository.findByEndToEndTxnId(
|
||||||
|
dto.End_To_End_Txn_Id,
|
||||||
|
"PAYMENT",
|
||||||
|
);
|
||||||
|
if (prior) {
|
||||||
|
if (prior.tradeStatus === "SUCCESS") {
|
||||||
|
// Replay the stored body verbatim. Never re-settle.
|
||||||
|
return prior.responsePayload as unknown as CbePaymentResponseDto;
|
||||||
|
}
|
||||||
|
if (prior.tradeStatus === "PENDING") {
|
||||||
|
return mapPaymentFailure(dto, "Payment is being processed.");
|
||||||
|
}
|
||||||
|
if (prior.failureClass === "BUSINESS") {
|
||||||
|
// Final — retrying cannot change the answer. Replay what we told CBE last time.
|
||||||
|
return (
|
||||||
|
(prior.responsePayload as unknown as CbePaymentResponseDto) ??
|
||||||
|
mapPaymentFailure(
|
||||||
|
dto,
|
||||||
|
prior.responseDescription ?? "Payment already failed.",
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// FAILED + TRANSIENT: allowed retry — fall through and re-run the settlement.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cbe_Txn_Ref replay across different bills/attempts (partial-unique backstop in the DB).
|
||||||
|
const settled = await this.cbeBillRepository.findSettledByCbeTxnRef(
|
||||||
|
dto.Cbe_Txn_Ref,
|
||||||
|
);
|
||||||
|
if (settled) {
|
||||||
|
return mapPaymentFailure(
|
||||||
|
dto,
|
||||||
|
`Invalid transaction reference number ${dto.Cbe_Txn_Ref}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let audit: CbeBillOperation;
|
||||||
|
if (prior) {
|
||||||
|
// Transient retry reuses the row — UNIQUE (end_to_end_txn_id, operation) forbids a second.
|
||||||
|
audit =
|
||||||
|
(await this.cbeBillRepository.update(prior.id, {
|
||||||
|
tradeStatus: "PENDING",
|
||||||
|
failureClass: null,
|
||||||
|
cbeTxnRef: dto.Cbe_Txn_Ref,
|
||||||
|
requestPayload: dto as unknown as Record<string, unknown>,
|
||||||
|
})) ?? prior;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
audit = await this.cbeBillRepository.create({
|
||||||
|
operation: "PAYMENT",
|
||||||
|
billId: dto.Bill_Id,
|
||||||
|
endToEndTxnId: dto.End_To_End_Txn_Id,
|
||||||
|
cbeTxnRef: dto.Cbe_Txn_Ref,
|
||||||
|
destinationApiName: dto.Destination_Api_Name,
|
||||||
|
tradeStatus: "PENDING",
|
||||||
|
requestPayload: dto as unknown as Record<string, unknown>,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if ((err as { code?: string }).code === PG_UNIQUE_VIOLATION) {
|
||||||
|
// Concurrent duplicate of the same attempt lost the insert race.
|
||||||
|
return mapPaymentFailure(dto, "Payment is being processed.");
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let intent: PaymentIntent | undefined;
|
||||||
|
try {
|
||||||
|
intent = await this.resolveIntent(dto.Bill_Id);
|
||||||
|
|
||||||
|
if (dto.Currency && dto.Currency !== intent.currency) {
|
||||||
|
throw new CbeBillError("Payment currency does not match.", "BUSINESS");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-run bill-query — fresh, never cached. Last legitimate point for a synchronous
|
||||||
|
// failure (§6.1): after this we settle and reconcile downstream.
|
||||||
|
const billQuery = await this.billResolver.billQuery(intent);
|
||||||
|
if (!billQuery.stillPayable) {
|
||||||
|
throw new CbeBillError(
|
||||||
|
reasonToDescription(billQuery.reason, intent.referenceType),
|
||||||
|
"BUSINESS",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const amount = Number(dto.Amount);
|
||||||
|
if (
|
||||||
|
!Number.isFinite(amount) ||
|
||||||
|
Math.abs(amount - intent.amountMinor) >
|
||||||
|
intent.amountMinor * AMOUNT_TOLERANCE
|
||||||
|
) {
|
||||||
|
throw new CbeBillError("Payment amount does not match.", "BUSINESS");
|
||||||
|
}
|
||||||
|
|
||||||
|
const paidAt = new Date(dto.Timestamp);
|
||||||
|
// Existing state machine, unmodified — intent + outbox commit in one transaction.
|
||||||
|
await this.intentsService.applyProviderResult(intent.id, {
|
||||||
|
status: ProviderPaymentStatus.SUCCEEDED,
|
||||||
|
providerTxnId: dto.Cbe_Txn_Ref,
|
||||||
|
paidAt: Number.isNaN(paidAt.getTime()) ? new Date() : paidAt,
|
||||||
|
confirmedAmountMinor: amount,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = mapPaymentSuccess(dto, intent.merchantOrderId);
|
||||||
|
await this.finishAudit(audit, {
|
||||||
|
intentId: intent.id,
|
||||||
|
tradeStatus: "SUCCESS",
|
||||||
|
response,
|
||||||
|
});
|
||||||
|
this.logger.log(
|
||||||
|
`bill ${dto.Bill_Id} settled by CBE txn ${dto.Cbe_Txn_Ref} (intent ${intent.id})`,
|
||||||
|
);
|
||||||
|
return response;
|
||||||
|
} catch (err) {
|
||||||
|
const failure = toCbeFailure(err);
|
||||||
|
if (!(err instanceof CbeBillError)) {
|
||||||
|
this.logger.error(
|
||||||
|
`/cbe/payment ${dto.Bill_Id} failed unexpectedly: ${err instanceof Error ? err.stack : String(err)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const response = mapPaymentFailure(dto, failure.description);
|
||||||
|
await this.finishAudit(audit, {
|
||||||
|
intentId: intent?.id,
|
||||||
|
tradeStatus: "FAILED",
|
||||||
|
failureClass: failure.failureClass,
|
||||||
|
response,
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ helpers */
|
||||||
|
|
||||||
|
/** Kill switch (CBE_BILL_ENABLED) — 503 so CBE classes it as transport error and retries. */
|
||||||
|
assertEnabled(): void {
|
||||||
|
if (!this.config.get<boolean>("cbeBill.enabled")) {
|
||||||
|
throw new ServiceUnavailableException("CBE bill payment is disabled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The cheap local gate before the domain hop: what OUR record of this attempt says. Only
|
||||||
|
* REQUIRES_ACTION is payable; every other status gets a description naming the actual reason,
|
||||||
|
* because CBE reads it back to the payer standing at the counter. All BUSINESS — none of these
|
||||||
|
* states can change back, so a same-End_To_End_Txn_Id retry cannot produce a different answer.
|
||||||
|
*/
|
||||||
|
private assertIntentPayable(intent: PaymentIntent): void {
|
||||||
|
if (intent.status === ProviderPaymentStatus.REQUIRES_ACTION) return;
|
||||||
|
if (intent.status === ProviderPaymentStatus.PROCESSING) {
|
||||||
|
throw new CbeBillError("Payment is being processed.", "BUSINESS");
|
||||||
|
}
|
||||||
|
throw new CbeBillError(
|
||||||
|
reasonToDescription(localReason(intent), intent.referenceType),
|
||||||
|
"BUSINESS",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check digit first (cheap reject), then the unique bill_reference lookup. */
|
||||||
|
private async resolveIntent(billId: string): Promise<PaymentIntent> {
|
||||||
|
if (!this.billReferenceService.isValid(billId)) {
|
||||||
|
throw new CbeBillError("Bill not found.", "BUSINESS");
|
||||||
|
}
|
||||||
|
const intent = await this.intentsRepository.findByBillReference(billId);
|
||||||
|
if (!intent || intent.provider !== ProviderMethod.CBE_BILL) {
|
||||||
|
throw new CbeBillError("Bill not found.", "BUSINESS");
|
||||||
|
}
|
||||||
|
return intent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Insert the audit row; a CBE retry of the same QUERY attempt reuses (and refreshes) its row. */
|
||||||
|
private async upsertAudit(
|
||||||
|
operation: "QUERY",
|
||||||
|
endToEndTxnId: string,
|
||||||
|
data: {
|
||||||
|
billId: string;
|
||||||
|
destinationApiName: string;
|
||||||
|
requestPayload: Record<string, unknown>;
|
||||||
|
},
|
||||||
|
): Promise<CbeBillOperation> {
|
||||||
|
try {
|
||||||
|
return await this.cbeBillRepository.create({
|
||||||
|
operation,
|
||||||
|
endToEndTxnId,
|
||||||
|
tradeStatus: "PENDING",
|
||||||
|
...data,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if ((err as { code?: string }).code === PG_UNIQUE_VIOLATION) {
|
||||||
|
const existing = await this.cbeBillRepository.findByEndToEndTxnId(
|
||||||
|
endToEndTxnId,
|
||||||
|
operation,
|
||||||
|
);
|
||||||
|
if (existing) return existing;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async finishAudit(
|
||||||
|
audit: CbeBillOperation,
|
||||||
|
outcome: {
|
||||||
|
intentId?: string;
|
||||||
|
tradeStatus: "SUCCESS" | "FAILED";
|
||||||
|
failureClass?: "BUSINESS" | "TRANSIENT";
|
||||||
|
response: { Response_Code: string; Response_Description: string };
|
||||||
|
},
|
||||||
|
): Promise<void> {
|
||||||
|
await this.cbeBillRepository.update(audit.id, {
|
||||||
|
intentId: outcome.intentId ?? audit.intentId,
|
||||||
|
tradeStatus: outcome.tradeStatus,
|
||||||
|
failureClass: outcome.failureClass ?? null,
|
||||||
|
responseCode: outcome.response.Response_Code,
|
||||||
|
responseDescription: outcome.response.Response_Description,
|
||||||
|
responsePayload: outcome.response as unknown as Record<string, unknown>,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import {
|
||||||
|
ArgumentsHost,
|
||||||
|
Catch,
|
||||||
|
ExceptionFilter,
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
Logger,
|
||||||
|
ServiceUnavailableException,
|
||||||
|
UnauthorizedException,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import { Response } from "express";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller-scoped safety net for anything that escapes CbeBillService's own error handling
|
||||||
|
* (auth failures, DTO validation, unhandled throws). CBE's contract (plan D6): HTTP 200 for
|
||||||
|
* every business outcome, 401 only for authentication.
|
||||||
|
*
|
||||||
|
* Exception: the CBE_BILL_ENABLED kill switch throws ServiceUnavailableException and stays
|
||||||
|
* HTTP 503 with a non-0/1/3 code — the spec classes "any other code" as a transport error
|
||||||
|
* ("retry, contact admin"), which is exactly what a kill switch should signal; a 200/code-3
|
||||||
|
* would tell CBE the failure is final.
|
||||||
|
*/
|
||||||
|
@Catch()
|
||||||
|
export class CbeExceptionFilter implements ExceptionFilter {
|
||||||
|
private readonly logger = new Logger(CbeExceptionFilter.name);
|
||||||
|
|
||||||
|
catch(exception: unknown, host: ArgumentsHost): void {
|
||||||
|
const response = host.switchToHttp().getResponse<Response>();
|
||||||
|
|
||||||
|
if (exception instanceof UnauthorizedException) {
|
||||||
|
response.status(HttpStatus.UNAUTHORIZED).json({
|
||||||
|
Status: "FAILED",
|
||||||
|
Response_Code: "1",
|
||||||
|
Response_Description: exception.message || "Unauthorized",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exception instanceof ServiceUnavailableException) {
|
||||||
|
response.status(HttpStatus.SERVICE_UNAVAILABLE).json({
|
||||||
|
Status: "FAILED",
|
||||||
|
Response_Code: "9",
|
||||||
|
Response_Description: "Service temporarily unavailable.",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exception instanceof HttpException) {
|
||||||
|
// class-validator errors arrive as BadRequestException with message: string[].
|
||||||
|
const body = exception.getResponse();
|
||||||
|
const message =
|
||||||
|
typeof body === "object" && body !== null && "message" in body
|
||||||
|
? ([] as string[]).concat((body as { message: string }).message).join("; ")
|
||||||
|
: exception.message;
|
||||||
|
response.status(HttpStatus.OK).json({
|
||||||
|
Status: "FAILED",
|
||||||
|
Response_Code: "3",
|
||||||
|
Response_Description: message || "Invalid request",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.error(
|
||||||
|
`unhandled /cbe/* error: ${exception instanceof Error ? exception.stack : String(exception)}`,
|
||||||
|
);
|
||||||
|
response.status(HttpStatus.OK).json({
|
||||||
|
Status: "FAILED",
|
||||||
|
Response_Code: "3",
|
||||||
|
Response_Description: "Internal server error.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
|
import { Type } from "class-transformer";
|
||||||
|
import {
|
||||||
|
IsArray,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
ValidateNested,
|
||||||
|
} from "class-validator";
|
||||||
|
import { AdditionalFieldDto } from "./cbe-query-request.dto";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CBE → us, POST /cbe/payment (AAFDA spec §3.5). Mandatory fields per spec; the optional tail
|
||||||
|
* (payer identity, channel) mirrors the reference implementation pending the Q1 sample files.
|
||||||
|
*/
|
||||||
|
export class CbePaymentRequestDto {
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
Destination_Api_Name!: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
End_To_End_Txn_Id!: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
Cbe_Txn_Ref!: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
Timestamp!: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
Bill_Id!: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
Amount!: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
Currency!: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
Phone_No?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
Credit_Acct_Number?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
First_Name?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
Last_Name?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
Full_Name?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
Tin_Number?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
Cheque_No?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
Bank_Code?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
Payment_Method?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ type: [AdditionalFieldDto] })
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => AdditionalFieldDto)
|
||||||
|
Additional_Fields?: AdditionalFieldDto[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
/** Us → CBE, POST /cbe/payment response (AAFDA spec §3.8). */
|
||||||
|
export class CbePaymentResponseDto {
|
||||||
|
Destination_Api_Name!: string;
|
||||||
|
End_To_End_Txn_Id!: string;
|
||||||
|
Cbe_Txn_Ref!: string;
|
||||||
|
Destination_Txn_Ref!: string;
|
||||||
|
Status!: string;
|
||||||
|
Response_Code!: string;
|
||||||
|
Response_Description!: string;
|
||||||
|
Additional_Fields!: { Key: string; Value: string }[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
|
import { Type } from "class-transformer";
|
||||||
|
import {
|
||||||
|
IsArray,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
ValidateNested,
|
||||||
|
} from "class-validator";
|
||||||
|
|
||||||
|
export class AdditionalFieldDto {
|
||||||
|
@IsString()
|
||||||
|
Key!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
Value!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** CBE → us, POST /cbe/query (AAFDA spec §2.5). Field names are CBE's, Pascal_Snake. */
|
||||||
|
export class CbeQueryRequestDto {
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
Destination_Api_Name!: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
End_To_End_Txn_Id!: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
Bill_Id!: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ type: [AdditionalFieldDto] })
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => AdditionalFieldDto)
|
||||||
|
Additional_Fields?: AdditionalFieldDto[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* Us → CBE, POST /cbe/query response (AAFDA spec §2.8, shape mirrored from the reference
|
||||||
|
* implementation pending the Q1 sample files). Empty-string fields are deliberate — the
|
||||||
|
* reference sends the full envelope with blanks rather than omitting keys.
|
||||||
|
*/
|
||||||
|
export class CbeQueryResponseDto {
|
||||||
|
Destination_Api_Name!: string;
|
||||||
|
End_To_End_Txn_Id!: string;
|
||||||
|
Bill_Id!: string;
|
||||||
|
Total_Amount!: string;
|
||||||
|
Penalty_Amount!: string;
|
||||||
|
Bill_Amount!: string;
|
||||||
|
First_Name!: string;
|
||||||
|
Last_Name!: string;
|
||||||
|
Full_Name!: string;
|
||||||
|
Payment_Reason!: string;
|
||||||
|
Tin_Number!: string;
|
||||||
|
Credit_Acct_Number!: string;
|
||||||
|
Transaction_Type!: string;
|
||||||
|
Timestamp!: string;
|
||||||
|
Status!: string;
|
||||||
|
Response_Code!: string;
|
||||||
|
Response_Description!: string;
|
||||||
|
Additional_Fields!: { Key: string; Value: string }[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { IsNotEmpty, IsString } from "class-validator";
|
||||||
|
|
||||||
|
/** CBE → us, POST /cbe/oauth/token (AAFDA spec §1.5). Field names are CBE's, snake_case. */
|
||||||
|
export class TokenRequestDto {
|
||||||
|
@ApiProperty({ example: "client_credentials" })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
grant_type!: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
client_id!: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
client_secret!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: "Unified_Outgoing" })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
scope!: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
/** Us → CBE, POST /cbe/oauth/token response (AAFDA spec §1.7). */
|
||||||
|
export class TokenResponseDto {
|
||||||
|
token_type!: string;
|
||||||
|
access_token!: string;
|
||||||
|
expires_in!: number;
|
||||||
|
scope!: string;
|
||||||
|
/** Unix seconds at issue time. */
|
||||||
|
consented_on!: number;
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { Column, Entity, Index } from "typeorm";
|
||||||
|
import { BaseEntity } from "@edr/api-common";
|
||||||
|
|
||||||
|
export type CbeOperation = "QUERY" | "PAYMENT";
|
||||||
|
export type CbeTradeStatus = "PENDING" | "SUCCESS" | "FAILED";
|
||||||
|
/** Drives the same-End_To_End_Txn_Id retry policy (plan §6.5): BUSINESS is final, TRANSIENT retryable. */
|
||||||
|
export type CbeFailureClass = "BUSINESS" | "TRANSIENT";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CBE-protocol-level audit and idempotency ledger (docs/cbe/CBE_IMPLEMENTATION_PLAN.md §4.2).
|
||||||
|
* Separate from payment_intent because it tracks CBE's transaction identity — not ours — and
|
||||||
|
* must retain the exact response body we returned so a retry replays it byte-for-byte.
|
||||||
|
*
|
||||||
|
* The UNIQUE (end_to_end_txn_id, operation) and partial-unique cbe_txn_ref indexes live in the
|
||||||
|
* CreateCbeBillOperation migration.
|
||||||
|
*/
|
||||||
|
@Entity({ name: "cbe_bill_operation" })
|
||||||
|
@Index("idx_cbe_bill_operation_bill", ["billId", "operation"])
|
||||||
|
export class CbeBillOperation extends BaseEntity {
|
||||||
|
@Column({ name: "operation", type: "varchar", length: 16 })
|
||||||
|
operation!: CbeOperation;
|
||||||
|
|
||||||
|
/** Bill_Id exactly as received from CBE. */
|
||||||
|
@Column({ name: "bill_id", type: "varchar", length: 32 })
|
||||||
|
billId!: string;
|
||||||
|
|
||||||
|
/** CBE's per-attempt id — the idempotency key of the protocol. */
|
||||||
|
@Column({ name: "end_to_end_txn_id", type: "varchar", length: 128 })
|
||||||
|
endToEndTxnId!: string;
|
||||||
|
|
||||||
|
/** CBE core-banking reference; set on PAYMENT. */
|
||||||
|
@Column({ name: "cbe_txn_ref", type: "varchar", length: 128, nullable: true })
|
||||||
|
cbeTxnRef?: string | null;
|
||||||
|
|
||||||
|
/** Echoed back in every response. */
|
||||||
|
@Column({
|
||||||
|
name: "destination_api_name",
|
||||||
|
type: "varchar",
|
||||||
|
length: 64,
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
destinationApiName?: string | null;
|
||||||
|
|
||||||
|
/** Our payment_intent.id once the bill resolved to an intent. Soft reference, no FK. */
|
||||||
|
@Column({ name: "intent_id", type: "uuid", nullable: true })
|
||||||
|
intentId?: string | null;
|
||||||
|
|
||||||
|
@Column({ name: "trade_status", type: "varchar", length: 16 })
|
||||||
|
tradeStatus!: CbeTradeStatus;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
name: "failure_class",
|
||||||
|
type: "varchar",
|
||||||
|
length: 16,
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
failureClass?: CbeFailureClass | null;
|
||||||
|
|
||||||
|
@Column({ name: "response_code", type: "varchar", length: 8, nullable: true })
|
||||||
|
responseCode?: string | null;
|
||||||
|
|
||||||
|
@Column({ name: "response_description", type: "text", nullable: true })
|
||||||
|
responseDescription?: string | null;
|
||||||
|
|
||||||
|
/** Raw inbound body, verbatim. */
|
||||||
|
@Column({ name: "request_payload", type: "jsonb" })
|
||||||
|
requestPayload!: Record<string, unknown>;
|
||||||
|
|
||||||
|
/** EXACT body we returned — replayed verbatim when CBE retries a settled End_To_End_Txn_Id. */
|
||||||
|
@Column({ name: "response_payload", type: "jsonb", nullable: true })
|
||||||
|
responsePayload?: Record<string, unknown> | null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { CbeFailureClass } from "../entities/cbe-bill-operation.entity";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A CBE business/transport outcome we detected ourselves. `failureClass` drives the §6.5
|
||||||
|
* same-End_To_End_Txn_Id retry policy: BUSINESS outcomes are final (retrying cannot change
|
||||||
|
* the answer), TRANSIENT ones (our 5xx, domain app unreachable) may be retried by CBE.
|
||||||
|
*/
|
||||||
|
export class CbeBillError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
readonly failureClass: CbeFailureClass,
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.name = "CbeBillError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Every failure maps to Response_Code "3" — the AAFDA spec (§2.10, §3.10) defines only
|
||||||
|
* 0 (success), 1 (auth), 3 (business); only the description is specific (plan §6.6).
|
||||||
|
*/
|
||||||
|
export function toCbeFailure(err: unknown): {
|
||||||
|
description: string;
|
||||||
|
failureClass: CbeFailureClass;
|
||||||
|
} {
|
||||||
|
if (err instanceof CbeBillError) {
|
||||||
|
return { description: err.message, failureClass: err.failureClass };
|
||||||
|
}
|
||||||
|
return { description: "Internal server error.", failureClass: "TRANSIENT" };
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { CbePaymentRequestDto } from "../dto/cbe-payment-request.dto";
|
||||||
|
import { CbePaymentResponseDto } from "../dto/cbe-payment-response.dto";
|
||||||
|
|
||||||
|
export function mapPaymentSuccess(
|
||||||
|
request: CbePaymentRequestDto,
|
||||||
|
destinationTxnRef: string,
|
||||||
|
): CbePaymentResponseDto {
|
||||||
|
return {
|
||||||
|
Destination_Api_Name: request.Destination_Api_Name,
|
||||||
|
End_To_End_Txn_Id: request.End_To_End_Txn_Id,
|
||||||
|
Cbe_Txn_Ref: request.Cbe_Txn_Ref,
|
||||||
|
Destination_Txn_Ref: destinationTxnRef,
|
||||||
|
Status: "SUCCESS",
|
||||||
|
Response_Code: "0",
|
||||||
|
Response_Description: "Success",
|
||||||
|
Additional_Fields: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapPaymentFailure(
|
||||||
|
request: CbePaymentRequestDto,
|
||||||
|
description: string,
|
||||||
|
): CbePaymentResponseDto {
|
||||||
|
return {
|
||||||
|
Destination_Api_Name: request.Destination_Api_Name,
|
||||||
|
End_To_End_Txn_Id: request.End_To_End_Txn_Id,
|
||||||
|
Cbe_Txn_Ref: request.Cbe_Txn_Ref,
|
||||||
|
Destination_Txn_Ref: "",
|
||||||
|
Status: "FAILED",
|
||||||
|
Response_Code: "3",
|
||||||
|
Response_Description: description,
|
||||||
|
Additional_Fields: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { CbeQueryRequestDto } from "../dto/cbe-query-request.dto";
|
||||||
|
import { CbeQueryResponseDto } from "../dto/cbe-query-response.dto";
|
||||||
|
|
||||||
|
export function mapQuerySuccess(
|
||||||
|
request: CbeQueryRequestDto,
|
||||||
|
input: { amountMajor: number; fullName: string; paymentReason: string },
|
||||||
|
): CbeQueryResponseDto {
|
||||||
|
const amount = input.amountMajor.toFixed(2);
|
||||||
|
return {
|
||||||
|
Destination_Api_Name: request.Destination_Api_Name,
|
||||||
|
End_To_End_Txn_Id: request.End_To_End_Txn_Id,
|
||||||
|
Bill_Id: request.Bill_Id,
|
||||||
|
Total_Amount: amount,
|
||||||
|
Penalty_Amount: "0.00",
|
||||||
|
Bill_Amount: amount,
|
||||||
|
First_Name: "",
|
||||||
|
Last_Name: "",
|
||||||
|
Full_Name: input.fullName,
|
||||||
|
Payment_Reason: input.paymentReason,
|
||||||
|
Tin_Number: "",
|
||||||
|
Credit_Acct_Number: "",
|
||||||
|
Transaction_Type: "",
|
||||||
|
Timestamp: new Date().toISOString(),
|
||||||
|
Status: "SUCCESS",
|
||||||
|
Response_Code: "0",
|
||||||
|
Response_Description: "Success",
|
||||||
|
Additional_Fields: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapQueryFailure(
|
||||||
|
request: CbeQueryRequestDto,
|
||||||
|
description: string,
|
||||||
|
): CbeQueryResponseDto {
|
||||||
|
return {
|
||||||
|
Destination_Api_Name: request.Destination_Api_Name,
|
||||||
|
End_To_End_Txn_Id: request.End_To_End_Txn_Id,
|
||||||
|
Bill_Id: request.Bill_Id,
|
||||||
|
Total_Amount: "",
|
||||||
|
Penalty_Amount: "",
|
||||||
|
Bill_Amount: "",
|
||||||
|
First_Name: "",
|
||||||
|
Last_Name: "",
|
||||||
|
Full_Name: "",
|
||||||
|
Payment_Reason: "",
|
||||||
|
Tin_Number: "",
|
||||||
|
Credit_Acct_Number: "",
|
||||||
|
Transaction_Type: "",
|
||||||
|
Timestamp: new Date().toISOString(),
|
||||||
|
Status: "FAILED",
|
||||||
|
Response_Code: "3",
|
||||||
|
Response_Description: description,
|
||||||
|
Additional_Fields: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { DataSource } from "typeorm";
|
||||||
|
import { BillReferenceService } from "./bill-reference.service";
|
||||||
|
|
||||||
|
describe("BillReferenceService", () => {
|
||||||
|
const dataSource = {
|
||||||
|
query: jest.fn().mockResolvedValue([{ nextval: "10000001" }]),
|
||||||
|
} as unknown as DataSource;
|
||||||
|
const service = new BillReferenceService(dataSource);
|
||||||
|
|
||||||
|
it("generates a 12-digit numeric reference that validates", async () => {
|
||||||
|
const ref = await service.generate();
|
||||||
|
expect(ref).toMatch(/^\d{12}$/);
|
||||||
|
expect(ref.startsWith("00010000001")).toBe(true);
|
||||||
|
expect(service.isValid(ref)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects a single mistyped digit", async () => {
|
||||||
|
const ref = await service.generate();
|
||||||
|
const flipped =
|
||||||
|
ref.slice(0, 5) + ((Number(ref[5]) + 1) % 10) + ref.slice(6);
|
||||||
|
expect(service.isValid(flipped)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects adjacent transpositions", async () => {
|
||||||
|
const ref = await service.generate();
|
||||||
|
// Transpose the last two differing adjacent body digits.
|
||||||
|
const digits = ref.split("");
|
||||||
|
const i = digits.findIndex((d, idx) => idx < 11 && d !== digits[idx + 1]);
|
||||||
|
[digits[i], digits[i + 1]] = [digits[i + 1], digits[i]];
|
||||||
|
expect(service.isValid(digits.join(""))).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects wrong length and non-numeric input", () => {
|
||||||
|
expect(service.isValid("12345")).toBe(false);
|
||||||
|
expect(service.isValid("00045123389A")).toBe(false);
|
||||||
|
expect(service.isValid("")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { DataSource } from "typeorm";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CBE_BILL bill reference numbers (docs/cbe/CBE_IMPLEMENTATION_PLAN.md §5).
|
||||||
|
*
|
||||||
|
* 12 numeric digits: an 11-digit Postgres-sequence value, zero-padded, plus a trailing Luhn
|
||||||
|
* check digit. Numeric-only so it is typeable on any USSD keypad; the check digit rejects
|
||||||
|
* most single-digit typos and adjacent transpositions before any DB lookup; sequence-backed
|
||||||
|
* so uniqueness is guaranteed without a collision-retry loop.
|
||||||
|
*
|
||||||
|
* Subject to Phase 0-Q2 — if CBE imposes their own length/charset constraint, theirs wins.
|
||||||
|
*/
|
||||||
|
const SEQUENCE = "edr_payment.cbe_bill_reference_seq";
|
||||||
|
const TOTAL_LENGTH = 12;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BillReferenceService {
|
||||||
|
constructor(private readonly dataSource: DataSource) {}
|
||||||
|
|
||||||
|
async generate(): Promise<string> {
|
||||||
|
const rows: [{ nextval: string }] = await this.dataSource.query(
|
||||||
|
`SELECT nextval('${SEQUENCE}')`,
|
||||||
|
);
|
||||||
|
const body = rows[0].nextval.padStart(TOTAL_LENGTH - 1, "0");
|
||||||
|
return body + luhnCheckDigit(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Format + check-digit validation — the cheap reject before any DB hit. */
|
||||||
|
isValid(billReference: string): boolean {
|
||||||
|
if (!/^\d+$/.test(billReference) || billReference.length !== TOTAL_LENGTH) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const body = billReference.slice(0, -1);
|
||||||
|
return luhnCheckDigit(body) === billReference.slice(-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Standard Luhn check digit over a numeric string. */
|
||||||
|
function luhnCheckDigit(digits: string): string {
|
||||||
|
let sum = 0;
|
||||||
|
// Rightmost body digit is doubled (it sits next to the check digit position).
|
||||||
|
for (let i = 0; i < digits.length; i++) {
|
||||||
|
let d = Number(digits[digits.length - 1 - i]);
|
||||||
|
if (i % 2 === 0) {
|
||||||
|
d *= 2;
|
||||||
|
if (d > 9) d -= 9;
|
||||||
|
}
|
||||||
|
sum += d;
|
||||||
|
}
|
||||||
|
return String((10 - (sum % 10)) % 10);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
IsEnum,
|
IsEnum,
|
||||||
IsIn,
|
IsIn,
|
||||||
|
IsISO8601,
|
||||||
IsNumber,
|
IsNumber,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
IsPositive,
|
IsPositive,
|
||||||
@@ -101,6 +102,25 @@ export class InitiatePaymentRequestDto implements InitiatePaymentRequest {
|
|||||||
@IsString()
|
@IsString()
|
||||||
@MaxLength(128)
|
@MaxLength(128)
|
||||||
idempotencyKey?: string;
|
idempotencyKey?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description:
|
||||||
|
"Payer full name snapshot (CBE_BILL: fallback Full_Name for /cbe/query when the " +
|
||||||
|
"domain app is unreachable)",
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(128)
|
||||||
|
payerName?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description:
|
||||||
|
"Intent expiry, ISO-8601 (CBE_BILL: the booking's own payment deadline — never a " +
|
||||||
|
"provider session TTL)",
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsISO8601()
|
||||||
|
expiresAt?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class IntentReferenceQueryDto {
|
export class IntentReferenceQueryDto {
|
||||||
|
|||||||
@@ -109,6 +109,23 @@ export class PaymentIntent extends BaseEntity {
|
|||||||
})
|
})
|
||||||
idempotencyKey?: string | null;
|
idempotencyKey?: string | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CBE_BILL only: the short numeric Bill_Id the customer types at a CBE channel
|
||||||
|
* (docs/cbe/CBE_IMPLEMENTATION_PLAN.md §5). Null for every other provider.
|
||||||
|
*/
|
||||||
|
@Column({
|
||||||
|
name: "bill_reference",
|
||||||
|
type: "varchar",
|
||||||
|
length: 32,
|
||||||
|
nullable: true,
|
||||||
|
unique: true,
|
||||||
|
})
|
||||||
|
billReference?: string | null;
|
||||||
|
|
||||||
|
/** Payer full name snapshot — fallback for CBE /cbe/query Full_Name when bill-query is down. */
|
||||||
|
@Column({ name: "payer_name", type: "varchar", length: 128, nullable: true })
|
||||||
|
payerName?: string | null;
|
||||||
|
|
||||||
@Column({ name: "expires_at", type: "timestamptz", nullable: true })
|
@Column({ name: "expires_at", type: "timestamptz", nullable: true })
|
||||||
expiresAt?: Date | null;
|
expiresAt?: Date | null;
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { TypeOrmModule } from "@nestjs/typeorm";
|
|||||||
import { ProvidersModule } from "../providers/providers.module";
|
import { ProvidersModule } from "../providers/providers.module";
|
||||||
import { NotificationOutbox } from "../outbox/entities/notification-outbox.entity";
|
import { NotificationOutbox } from "../outbox/entities/notification-outbox.entity";
|
||||||
import { PaymentIntent } from "./entities/payment-intent.entity";
|
import { PaymentIntent } from "./entities/payment-intent.entity";
|
||||||
|
import { BillReferenceService } from "./bill-reference.service";
|
||||||
import { IntentsController } from "./intents.controller";
|
import { IntentsController } from "./intents.controller";
|
||||||
import { IntentsRepository } from "./intents.repository";
|
import { IntentsRepository } from "./intents.repository";
|
||||||
import { IntentsService } from "./intents.service";
|
import { IntentsService } from "./intents.service";
|
||||||
@@ -15,7 +16,7 @@ import { IntentsService } from "./intents.service";
|
|||||||
ProvidersModule,
|
ProvidersModule,
|
||||||
],
|
],
|
||||||
controllers: [IntentsController],
|
controllers: [IntentsController],
|
||||||
providers: [IntentsService, IntentsRepository],
|
providers: [IntentsService, IntentsRepository, BillReferenceService],
|
||||||
exports: [IntentsService, IntentsRepository],
|
exports: [IntentsService, IntentsRepository, BillReferenceService],
|
||||||
})
|
})
|
||||||
export class IntentsModule {}
|
export class IntentsModule {}
|
||||||
|
|||||||
@@ -70,6 +70,13 @@ export class IntentsRepository extends BaseRepository<PaymentIntent> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** CBE_BILL: resolve the intent behind a Bill_Id presented by CBE. */
|
||||||
|
async findByBillReference(
|
||||||
|
billReference: string,
|
||||||
|
): Promise<PaymentIntent | null> {
|
||||||
|
return this.repository.findOne({ where: { billReference } });
|
||||||
|
}
|
||||||
|
|
||||||
async findByMerchantOrderId(
|
async findByMerchantOrderId(
|
||||||
merchantOrderId: string,
|
merchantOrderId: string,
|
||||||
): Promise<PaymentIntent | null> {
|
): Promise<PaymentIntent | null> {
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { BadRequestException } from "@nestjs/common";
|
||||||
|
import { DataSource } from "typeorm";
|
||||||
|
import {
|
||||||
|
InitiatePaymentRequest,
|
||||||
|
PaymentReferenceType,
|
||||||
|
PaymentService,
|
||||||
|
ProviderMethod,
|
||||||
|
ProviderPaymentStatus,
|
||||||
|
} from "@edr/types";
|
||||||
|
import { CacBankProvider } from "@edr/payment-providers";
|
||||||
|
import { IntentsService } from "./intents.service";
|
||||||
|
import { IntentsRepository } from "./intents.repository";
|
||||||
|
import { BillReferenceService } from "./bill-reference.service";
|
||||||
|
import { PaymentIntent } from "./entities/payment-intent.entity";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CBE_BILL regression tests for plan D5 (docs/cbe/CBE_IMPLEMENTATION_PLAN.md): the provider is
|
||||||
|
* deliberately absent from PAYMENT_PROVIDER_MAP, so the pull-side refresh must return the
|
||||||
|
* cached intent untouched instead of calling a provider. This is load-bearing — a stub
|
||||||
|
* provider entry would make the reconciliation sweep expire live CBE bills.
|
||||||
|
*/
|
||||||
|
describe("IntentsService CBE_BILL", () => {
|
||||||
|
const providers = new Map();
|
||||||
|
let repository: jest.Mocked<
|
||||||
|
Pick<
|
||||||
|
IntentsRepository,
|
||||||
|
"create" | "findById" | "findByIdempotencyKey" | "update"
|
||||||
|
>
|
||||||
|
>;
|
||||||
|
let billReferenceService: { generate: jest.Mock };
|
||||||
|
let service: IntentsService;
|
||||||
|
|
||||||
|
const request: InitiatePaymentRequest = {
|
||||||
|
service: PaymentService.PASSENGER,
|
||||||
|
referenceType: PaymentReferenceType.BOOKING,
|
||||||
|
referenceId: "booking-1",
|
||||||
|
amountMinor: 1500,
|
||||||
|
currency: "ETB",
|
||||||
|
provider: ProviderMethod.CBE_BILL,
|
||||||
|
payerName: "Abebe Kebede",
|
||||||
|
expiresAt: "2026-08-01T12:00:00.000Z",
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
repository = {
|
||||||
|
create: jest.fn(async (data) => ({ id: "intent-1", ...data })),
|
||||||
|
findById: jest.fn(),
|
||||||
|
findByIdempotencyKey: jest.fn().mockResolvedValue(null),
|
||||||
|
update: jest.fn(),
|
||||||
|
} as never;
|
||||||
|
billReferenceService = {
|
||||||
|
generate: jest.fn().mockResolvedValue("000100000015"),
|
||||||
|
};
|
||||||
|
service = new IntentsService(
|
||||||
|
repository as unknown as IntentsRepository,
|
||||||
|
{} as DataSource,
|
||||||
|
providers as never,
|
||||||
|
{} as CacBankProvider,
|
||||||
|
billReferenceService as unknown as BillReferenceService,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("initiates without a provider session: REQUIRES_ACTION + SHOW_BILL_REFERENCE", async () => {
|
||||||
|
const snapshot = await service.initiate(request);
|
||||||
|
|
||||||
|
expect(snapshot.status).toBe(ProviderPaymentStatus.REQUIRES_ACTION);
|
||||||
|
expect(snapshot.billReference).toBe("000100000015");
|
||||||
|
expect(snapshot.clientAction).toMatchObject({
|
||||||
|
type: "SHOW_BILL_REFERENCE",
|
||||||
|
billReference: "000100000015",
|
||||||
|
});
|
||||||
|
// The booking's own deadline, not a provider-session TTL (plan §6.4).
|
||||||
|
expect(snapshot.expiresAt).toBe("2026-08-01T12:00:00.000Z");
|
||||||
|
expect(repository.create).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
billReference: "000100000015",
|
||||||
|
payerName: "Abebe Kebede",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects non-ETB currency (plan D8)", async () => {
|
||||||
|
await expect(
|
||||||
|
service.initiate({ ...request, currency: "DJF" }),
|
||||||
|
).rejects.toBeInstanceOf(BadRequestException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("getIntent leaves a stale CBE_BILL intent untouched (no provider in map — plan D5)", async () => {
|
||||||
|
const intent = {
|
||||||
|
id: "intent-1",
|
||||||
|
service: PaymentService.PASSENGER,
|
||||||
|
referenceType: PaymentReferenceType.BOOKING,
|
||||||
|
referenceId: "booking-1",
|
||||||
|
merchantOrderId: "PSG-x",
|
||||||
|
provider: ProviderMethod.CBE_BILL,
|
||||||
|
status: ProviderPaymentStatus.REQUIRES_ACTION,
|
||||||
|
amountMinor: 1500,
|
||||||
|
currency: "ETB",
|
||||||
|
billReference: "000100000015",
|
||||||
|
// Stale enough that a mapped provider WOULD be queried.
|
||||||
|
updatedAt: new Date(Date.now() - 60_000),
|
||||||
|
} as unknown as PaymentIntent;
|
||||||
|
repository.findById.mockResolvedValue(intent);
|
||||||
|
const applySpy = jest.spyOn(service, "applyProviderResult");
|
||||||
|
|
||||||
|
const snapshot = await service.getIntent("intent-1");
|
||||||
|
|
||||||
|
expect(snapshot.status).toBe(ProviderPaymentStatus.REQUIRES_ACTION);
|
||||||
|
expect(applySpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
TERMINAL_INTENT_STATUSES,
|
TERMINAL_INTENT_STATUSES,
|
||||||
} from "./entities/payment-intent.entity";
|
} from "./entities/payment-intent.entity";
|
||||||
import { IntentsRepository } from "./intents.repository";
|
import { IntentsRepository } from "./intents.repository";
|
||||||
|
import { BillReferenceService } from "./bill-reference.service";
|
||||||
|
|
||||||
/** Don't hit the provider again if the intent was refreshed this recently. */
|
/** Don't hit the provider again if the intent was refreshed this recently. */
|
||||||
const REFRESH_MIN_AGE_MS = 5_000;
|
const REFRESH_MIN_AGE_MS = 5_000;
|
||||||
@@ -69,6 +70,7 @@ export class IntentsService {
|
|||||||
@Inject(PAYMENT_PROVIDER_MAP)
|
@Inject(PAYMENT_PROVIDER_MAP)
|
||||||
private readonly providers: PaymentProviderMap,
|
private readonly providers: PaymentProviderMap,
|
||||||
private readonly cacBankProvider: CacBankProvider,
|
private readonly cacBankProvider: CacBankProvider,
|
||||||
|
private readonly billReferenceService: BillReferenceService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ initiate */
|
/* ------------------------------------------------------------------ initiate */
|
||||||
@@ -85,6 +87,12 @@ export class IntentsService {
|
|||||||
if (byKey) return this.toSnapshot(byKey);
|
if (byKey) return this.toSnapshot(byKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CBE_BILL is inbound-only: there is no provider session to open and deliberately no entry
|
||||||
|
// in PAYMENT_PROVIDER_MAP (plan D5 — the sweep and refreshIfStale must no-op on it).
|
||||||
|
if (request.provider === ProviderMethod.CBE_BILL) {
|
||||||
|
return this.initiateCbeBill(request);
|
||||||
|
}
|
||||||
|
|
||||||
// Free method changes: no reuse/supersede. Every initiate opens a fresh intent, so a booking
|
// Free method changes: no reuse/supersede. Every initiate opens a fresh intent, so a booking
|
||||||
// may accumulate many intents (each method attempt is its own row). The `idempotencyKey` check
|
// may accumulate many intents (each method attempt is its own row). The `idempotencyKey` check
|
||||||
// above still collapses exact duplicate submissions (e.g. a double-click). Confirm-once is
|
// above still collapses exact duplicate submissions (e.g. a double-click). Confirm-once is
|
||||||
@@ -140,6 +148,54 @@ export class IntentsService {
|
|||||||
return this.toSnapshot(intent);
|
return this.toSnapshot(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CBE Unified Bill Payment (docs/cbe/CBE_IMPLEMENTATION_PLAN.md). Intent-first: the bill
|
||||||
|
* reference is created here, before CBE ever sees the bill; settlement arrives later through
|
||||||
|
* the inbound /cbe/payment endpoint and the unchanged applyProviderResult() state machine.
|
||||||
|
*/
|
||||||
|
private async initiateCbeBill(
|
||||||
|
request: InitiatePaymentRequest,
|
||||||
|
): Promise<PaymentIntentSnapshot> {
|
||||||
|
// D8: CBE settles ETB only. The domain app must price/charge the order in ETB.
|
||||||
|
if (request.currency !== "ETB") {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`CBE_BILL supports ETB only (got ${request.currency})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const merchantOrderId = createMerchantOrderId();
|
||||||
|
const billReference = await this.billReferenceService.generate();
|
||||||
|
// expiresAt is the BOOKING's payment deadline passed by the domain app — never a provider
|
||||||
|
// session TTL (plan §6.4: a short TTL would make the sweep cancel the booking within the hour).
|
||||||
|
const expiresAt = request.expiresAt ? new Date(request.expiresAt) : null;
|
||||||
|
|
||||||
|
const intent = await this.intentsRepository.create({
|
||||||
|
service: request.service,
|
||||||
|
referenceType: request.referenceType,
|
||||||
|
referenceId: request.referenceId,
|
||||||
|
merchantOrderId,
|
||||||
|
provider: request.provider,
|
||||||
|
amountMinor: request.amountMinor,
|
||||||
|
currency: request.currency,
|
||||||
|
status: ProviderPaymentStatus.REQUIRES_ACTION,
|
||||||
|
clientAction: {
|
||||||
|
type: "SHOW_BILL_REFERENCE",
|
||||||
|
billReference,
|
||||||
|
instructions:
|
||||||
|
"Pay this bill at any CBE branch, CBE Birr app, mobile banking or USSD.",
|
||||||
|
expiresAt: expiresAt?.toISOString(),
|
||||||
|
},
|
||||||
|
idempotencyKey: request.idempotencyKey ?? null,
|
||||||
|
expiresAt,
|
||||||
|
billReference,
|
||||||
|
payerName: request.payerName ?? null,
|
||||||
|
});
|
||||||
|
this.logger.log(
|
||||||
|
`intent ${intent.id} created: ${request.service}/${request.referenceType}/${request.referenceId} via CBE_BILL (bill ${billReference})`,
|
||||||
|
);
|
||||||
|
return this.toSnapshot(intent);
|
||||||
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ confirm (OTP providers) */
|
/* ------------------------------------------------------------------ confirm (OTP providers) */
|
||||||
|
|
||||||
async confirm(
|
async confirm(
|
||||||
@@ -626,6 +682,7 @@ export class IntentsService {
|
|||||||
failureCode: intent.failureCode ?? undefined,
|
failureCode: intent.failureCode ?? undefined,
|
||||||
failureMessage: intent.failureMessage ?? undefined,
|
failureMessage: intent.failureMessage ?? undefined,
|
||||||
expiresAt: intent.expiresAt?.toISOString(),
|
expiresAt: intent.expiresAt?.toISOString(),
|
||||||
|
billReference: intent.billReference ?? undefined,
|
||||||
providerResponse: intent.rawInitiation ?? undefined,
|
providerResponse: intent.rawInitiation ?? undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ export enum ProviderMethod {
|
|||||||
CARD = "CARD",
|
CARD = "CARD",
|
||||||
DMONEY = "DMONEY",
|
DMONEY = "DMONEY",
|
||||||
CAC_BANK = "CAC_BANK",
|
CAC_BANK = "CAC_BANK",
|
||||||
|
/**
|
||||||
|
* CBE Unified Bill Payment — inbound biller integration. We never call CBE: the customer
|
||||||
|
* takes the bill reference to any CBE channel and CBE calls payment-api's /cbe/* endpoints.
|
||||||
|
* No entry in PAYMENT_PROVIDER_MAP by design (docs/cbe/CBE_IMPLEMENTATION_PLAN.md D5).
|
||||||
|
*/
|
||||||
|
CBE_BILL = "CBE_BILL",
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PaymentPlatform = "web" | "mobile";
|
export type PaymentPlatform = "web" | "mobile";
|
||||||
@@ -40,6 +46,13 @@ export type ClientAction =
|
|||||||
type: "COLLECT_OTP";
|
type: "COLLECT_OTP";
|
||||||
providerOrderId: string;
|
providerOrderId: string;
|
||||||
message?: string;
|
message?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
/** CBE_BILL: show the bill reference the customer pays at any CBE channel. */
|
||||||
|
type: "SHOW_BILL_REFERENCE";
|
||||||
|
billReference: string;
|
||||||
|
instructions?: string;
|
||||||
|
expiresAt?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface ProviderInitiationInput {
|
export interface ProviderInitiationInput {
|
||||||
@@ -130,6 +143,16 @@ export interface InitiatePaymentRequest {
|
|||||||
failureUrl?: string;
|
failureUrl?: string;
|
||||||
/** Optional caller key to dedupe retried initiations beyond the per-reference upsert. */
|
/** Optional caller key to dedupe retried initiations beyond the per-reference upsert. */
|
||||||
idempotencyKey?: string;
|
idempotencyKey?: string;
|
||||||
|
/**
|
||||||
|
* Payer full name snapshot (CBE_BILL: fallback for the mandatory Full_Name in /cbe/query
|
||||||
|
* responses when the domain app's bill-query is unreachable).
|
||||||
|
*/
|
||||||
|
payerName?: string;
|
||||||
|
/**
|
||||||
|
* Intent expiry, ISO-8601. CBE_BILL: the booking's own payment deadline — NOT a provider
|
||||||
|
* session TTL (the reconciliation sweep cancels the intent when this passes).
|
||||||
|
*/
|
||||||
|
expiresAt?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Body of `POST /payments/intents/:id/confirm` (OTP-based providers such as CAC Bank). */
|
/** Body of `POST /payments/intents/:id/confirm` (OTP-based providers such as CAC Bank). */
|
||||||
@@ -154,6 +177,8 @@ export type PaymentIntentSnapshot ={
|
|||||||
failureCode?: string;
|
failureCode?: string;
|
||||||
failureMessage?: string;
|
failureMessage?: string;
|
||||||
expiresAt?: string;
|
expiresAt?: string;
|
||||||
|
/** CBE_BILL only: the short numeric Bill_Id the customer pays at a CBE channel. */
|
||||||
|
billReference?: string;
|
||||||
/**
|
/**
|
||||||
* Raw provider payload for inspection/debugging — the audit copy of the provider
|
* Raw provider payload for inspection/debugging — the audit copy of the provider
|
||||||
* initiation response merged with the latest status-query response (secrets redacted
|
* initiation response merged with the latest status-query response (secrets redacted
|
||||||
|
|||||||
377
pnpm-lock.yaml
generated
377
pnpm-lock.yaml
generated
@@ -589,7 +589,7 @@ importers:
|
|||||||
version: 5.101.0(react@19.2.6)
|
version: 5.101.0(react@19.2.6)
|
||||||
'@tria-plc/iamui':
|
'@tria-plc/iamui':
|
||||||
specifier: file:../../../local-packages/tria-plc-iamui-0.1.1.tgz
|
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':
|
'@vis.gl/react-google-maps':
|
||||||
specifier: ^1.8.3
|
specifier: ^1.8.3
|
||||||
version: 1.8.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
version: 1.8.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
||||||
@@ -816,10 +816,10 @@ importers:
|
|||||||
version: 8.1.6
|
version: 8.1.6
|
||||||
'@tria-plc/api-common':
|
'@tria-plc/api-common':
|
||||||
specifier: file:../../local-packages/tria-plc-api-common-1.4.3.tgz
|
specifier: file:../../local-packages/tria-plc-api-common-1.4.3.tgz
|
||||||
version: file:local-packages/tria-plc-api-common-1.4.3.tgz(59a15a37c5b1c12685ed78e172f27e65)
|
version: file:local-packages/tria-plc-api-common-1.4.3.tgz(aaad3d77da283ea37b052677c39644c3)
|
||||||
'@tria-plc/iamapi-common':
|
'@tria-plc/iamapi-common':
|
||||||
specifier: file:../../local-packages/tria-plc-iamapi-common-0.7.9.tgz
|
specifier: file:../../local-packages/tria-plc-iamapi-common-0.7.9.tgz
|
||||||
version: file:local-packages/tria-plc-iamapi-common-0.7.9.tgz(c97ba831ddde82920910406ab5262991)
|
version: file:local-packages/tria-plc-iamapi-common-0.7.9.tgz(4d1d275441e80423228c2d8370f9d999)
|
||||||
'@types/bcrypt':
|
'@types/bcrypt':
|
||||||
specifier: ^6.0.0
|
specifier: ^6.0.0
|
||||||
version: 6.0.0
|
version: 6.0.0
|
||||||
@@ -1135,6 +1135,9 @@ importers:
|
|||||||
'@nestjs/core':
|
'@nestjs/core':
|
||||||
specifier: ^11.0.0
|
specifier: ^11.0.0
|
||||||
version: 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)
|
version: 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/jwt':
|
||||||
|
specifier: ^11.0.2
|
||||||
|
version: 11.0.2(@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/platform-express':
|
'@nestjs/platform-express':
|
||||||
specifier: ^11.0.0
|
specifier: ^11.0.0
|
||||||
version: 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)
|
version: 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)
|
||||||
@@ -2830,10 +2833,10 @@ packages:
|
|||||||
'@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0
|
'@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0
|
||||||
'@nestjs/core': ^8.0.0 || ^9.0.0 || ^10.0.0
|
'@nestjs/core': ^8.0.0 || ^9.0.0 || ^10.0.0
|
||||||
|
|
||||||
'@nestjs/jwt@10.2.0':
|
'@nestjs/jwt@11.0.2':
|
||||||
resolution: {integrity: sha512-x8cG90SURkEiLOehNaN2aRlotxT0KZESUliOPKKnjWiyJOcWurkF3w345WOX0P4MgFzUjGoZ1Sy0aZnxeihT0g==}
|
resolution: {integrity: sha512-rK8aE/3/Ma45gAWfCksAXUNbOoSOUudU0Kn3rT39htPF7wsYXtKfjALKeKKJbFrIWbLjsbqfXX5bIJNvgBugGA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0
|
'@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0
|
||||||
|
|
||||||
'@nestjs/mapped-types@2.0.5':
|
'@nestjs/mapped-types@2.0.5':
|
||||||
resolution: {integrity: sha512-bSJv4pd6EY99NX9CjBIyn4TVDoSit82DUZlL4I3bqNfy5Gt+gXTa86i3I/i0iIV9P4hntcGM5GyO+FhZAhxtyg==}
|
resolution: {integrity: sha512-bSJv4pd6EY99NX9CjBIyn4TVDoSit82DUZlL4I3bqNfy5Gt+gXTa86i3I/i0iIV9P4hntcGM5GyO+FhZAhxtyg==}
|
||||||
@@ -4760,8 +4763,8 @@ packages:
|
|||||||
'@types/json5@0.0.29':
|
'@types/json5@0.0.29':
|
||||||
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
|
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
|
||||||
|
|
||||||
'@types/jsonwebtoken@9.0.5':
|
'@types/jsonwebtoken@9.0.10':
|
||||||
resolution: {integrity: sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==}
|
resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==}
|
||||||
|
|
||||||
'@types/lodash@4.17.24':
|
'@types/lodash@4.17.24':
|
||||||
resolution: {integrity: sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==}
|
resolution: {integrity: sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==}
|
||||||
@@ -4775,6 +4778,9 @@ packages:
|
|||||||
'@types/mime@1.3.5':
|
'@types/mime@1.3.5':
|
||||||
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
|
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
|
||||||
|
|
||||||
|
'@types/ms@2.1.0':
|
||||||
|
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
|
||||||
|
|
||||||
'@types/multer@2.1.0':
|
'@types/multer@2.1.0':
|
||||||
resolution: {integrity: sha512-zYZb0+nJhOHtPpGDb3vqPjwpdeGlGC157VpkqNQL+UU2qwoacoQ7MpsAmUptI/0Oa127X32JzWDqQVEXp2RcIA==}
|
resolution: {integrity: sha512-zYZb0+nJhOHtPpGDb3vqPjwpdeGlGC157VpkqNQL+UU2qwoacoQ7MpsAmUptI/0Oa127X32JzWDqQVEXp2RcIA==}
|
||||||
|
|
||||||
@@ -8565,10 +8571,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==}
|
resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==}
|
||||||
engines: {'0': node >= 0.2.0}
|
engines: {'0': node >= 0.2.0}
|
||||||
|
|
||||||
jsonwebtoken@9.0.2:
|
|
||||||
resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==}
|
|
||||||
engines: {node: '>=12', npm: '>=6'}
|
|
||||||
|
|
||||||
jsonwebtoken@9.0.3:
|
jsonwebtoken@9.0.3:
|
||||||
resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==}
|
resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==}
|
||||||
engines: {node: '>=12', npm: '>=6'}
|
engines: {node: '>=12', npm: '>=6'}
|
||||||
@@ -8595,15 +8597,9 @@ packages:
|
|||||||
jszip@3.10.1:
|
jszip@3.10.1:
|
||||||
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
|
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
|
||||||
|
|
||||||
jwa@1.4.2:
|
|
||||||
resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==}
|
|
||||||
|
|
||||||
jwa@2.0.1:
|
jwa@2.0.1:
|
||||||
resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==}
|
resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==}
|
||||||
|
|
||||||
jws@3.2.3:
|
|
||||||
resolution: {integrity: sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==}
|
|
||||||
|
|
||||||
jws@4.0.1:
|
jws@4.0.1:
|
||||||
resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==}
|
resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==}
|
||||||
|
|
||||||
@@ -12250,11 +12246,11 @@ snapshots:
|
|||||||
'@babel/helpers': 7.29.7
|
'@babel/helpers': 7.29.7
|
||||||
'@babel/parser': 7.29.7
|
'@babel/parser': 7.29.7
|
||||||
'@babel/template': 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
|
'@babel/types': 7.29.7
|
||||||
'@jridgewell/remapping': 2.3.5
|
'@jridgewell/remapping': 2.3.5
|
||||||
convert-source-map: 2.0.0
|
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
|
gensync: 1.0.0-beta.2
|
||||||
json5: 2.2.3
|
json5: 2.2.3
|
||||||
semver: 6.3.1
|
semver: 6.3.1
|
||||||
@@ -12289,7 +12285,7 @@ snapshots:
|
|||||||
'@babel/helper-optimise-call-expression': 7.29.7
|
'@babel/helper-optimise-call-expression': 7.29.7
|
||||||
'@babel/helper-replace-supers': 7.29.7(@babel/core@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/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
|
semver: 6.3.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -12298,14 +12294,7 @@ snapshots:
|
|||||||
|
|
||||||
'@babel/helper-member-expression-to-functions@7.29.7':
|
'@babel/helper-member-expression-to-functions@7.29.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/traverse': 7.29.7
|
'@babel/traverse': 7.29.7(supports-color@5.5.0)
|
||||||
'@babel/types': 7.29.7
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
'@babel/helper-module-imports@7.29.7':
|
|
||||||
dependencies:
|
|
||||||
'@babel/traverse': 7.29.7
|
|
||||||
'@babel/types': 7.29.7
|
'@babel/types': 7.29.7
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -12320,9 +12309,9 @@ snapshots:
|
|||||||
'@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)':
|
'@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.7
|
'@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/helper-validator-identifier': 7.29.7
|
||||||
'@babel/traverse': 7.29.7
|
'@babel/traverse': 7.29.7(supports-color@5.5.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -12337,13 +12326,13 @@ snapshots:
|
|||||||
'@babel/core': 7.29.7
|
'@babel/core': 7.29.7
|
||||||
'@babel/helper-member-expression-to-functions': 7.29.7
|
'@babel/helper-member-expression-to-functions': 7.29.7
|
||||||
'@babel/helper-optimise-call-expression': 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:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@babel/helper-skip-transparent-expression-wrappers@7.29.7':
|
'@babel/helper-skip-transparent-expression-wrappers@7.29.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/traverse': 7.29.7
|
'@babel/traverse': 7.29.7(supports-color@5.5.0)
|
||||||
'@babel/types': 7.29.7
|
'@babel/types': 7.29.7
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -12496,18 +12485,6 @@ snapshots:
|
|||||||
'@babel/parser': 7.29.7
|
'@babel/parser': 7.29.7
|
||||||
'@babel/types': 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)':
|
'@babel/traverse@7.29.7(supports-color@5.5.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/code-frame': 7.29.7
|
'@babel/code-frame': 7.29.7
|
||||||
@@ -12731,7 +12708,7 @@ snapshots:
|
|||||||
|
|
||||||
'@emotion/babel-plugin@11.13.5':
|
'@emotion/babel-plugin@11.13.5':
|
||||||
dependencies:
|
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
|
'@babel/runtime': 7.29.7
|
||||||
'@emotion/hash': 0.9.2
|
'@emotion/hash': 0.9.2
|
||||||
'@emotion/memoize': 0.9.0
|
'@emotion/memoize': 0.9.0
|
||||||
@@ -12897,7 +12874,7 @@ snapshots:
|
|||||||
'@eslint/eslintrc@2.1.4':
|
'@eslint/eslintrc@2.1.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
ajv: 6.15.0
|
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
|
espree: 9.6.1
|
||||||
globals: 13.24.0
|
globals: 13.24.0
|
||||||
ignore: 5.3.2
|
ignore: 5.3.2
|
||||||
@@ -13043,7 +13020,7 @@ snapshots:
|
|||||||
'@humanwhocodes/config-array@0.13.0':
|
'@humanwhocodes/config-array@0.13.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@humanwhocodes/object-schema': 2.0.3
|
'@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
|
minimatch: 3.1.5
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -13853,7 +13830,7 @@ snapshots:
|
|||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
uid: 2.0.2
|
uid: 2.0.2
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@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/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@0.10.9))(amqplib@0.10.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
'@nestjs/platform-express': 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/platform-express': 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(@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/platform-socket.io@11.1.27)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
'@nestjs/websockets': 11.1.27(@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/platform-socket.io@11.1.27)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
|
|
||||||
@@ -13863,11 +13840,11 @@ snapshots:
|
|||||||
'@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/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)
|
||||||
eventemitter2: 6.4.9
|
eventemitter2: 6.4.9
|
||||||
|
|
||||||
'@nestjs/jwt@10.2.0(@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/jwt@11.0.2(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))':
|
||||||
dependencies:
|
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/common': 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
'@types/jsonwebtoken': 9.0.5
|
'@types/jsonwebtoken': 9.0.10
|
||||||
jsonwebtoken: 9.0.2
|
jsonwebtoken: 9.0.3
|
||||||
|
|
||||||
'@nestjs/mapped-types@2.0.5(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)':
|
'@nestjs/mapped-types@2.0.5(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -13885,6 +13862,20 @@ snapshots:
|
|||||||
class-transformer: 0.5.1
|
class-transformer: 0.5.1
|
||||||
class-validator: 0.14.4
|
class-validator: 0.14.4
|
||||||
|
|
||||||
|
'@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@0.10.9))(amqplib@0.10.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)':
|
||||||
|
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)
|
||||||
|
iterare: 1.2.1
|
||||||
|
reflect-metadata: 0.2.2
|
||||||
|
rxjs: 7.8.2
|
||||||
|
tslib: 2.8.1
|
||||||
|
optionalDependencies:
|
||||||
|
'@nestjs/websockets': 11.1.27(@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/platform-socket.io@11.1.27)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
|
amqp-connection-manager: 5.0.0(amqplib@0.10.9)
|
||||||
|
amqplib: 0.10.9
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@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/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)':
|
||||||
dependencies:
|
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/common': 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
@@ -13982,7 +13973,7 @@ snapshots:
|
|||||||
'@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/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)
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@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/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@0.10.9))(amqplib@0.10.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
'@nestjs/platform-express': 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/platform-express': 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/throttler@6.5.0(@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)':
|
'@nestjs/throttler@6.5.0(@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)':
|
||||||
@@ -14214,7 +14205,7 @@ snapshots:
|
|||||||
|
|
||||||
'@puppeteer/browsers@2.13.2':
|
'@puppeteer/browsers@2.13.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
extract-zip: 2.0.1
|
extract-zip: 2.0.1
|
||||||
progress: 2.0.3
|
progress: 2.0.3
|
||||||
proxy-agent: 6.5.0
|
proxy-agent: 6.5.0
|
||||||
@@ -16282,7 +16273,7 @@ snapshots:
|
|||||||
|
|
||||||
'@tokenizer/inflate@0.4.1':
|
'@tokenizer/inflate@0.4.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
token-types: 6.1.2
|
token-types: 6.1.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -16296,7 +16287,7 @@ snapshots:
|
|||||||
'@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)
|
'@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)
|
||||||
'@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/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/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/jwt': 10.2.0(@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/jwt': 11.0.2(@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/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/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/passport': 10.0.3(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)
|
'@nestjs/passport': 10.0.3(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)
|
||||||
'@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/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)
|
||||||
@@ -16340,7 +16331,7 @@ snapshots:
|
|||||||
'@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)
|
'@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)
|
||||||
'@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/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/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/jwt': 10.2.0(@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/jwt': 11.0.2(@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/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/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/passport': 10.0.3(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)
|
'@nestjs/passport': 10.0.3(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)
|
||||||
'@nestjs/swagger': 7.4.2(@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/swagger': 7.4.2(@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)
|
||||||
@@ -16384,7 +16375,7 @@ snapshots:
|
|||||||
'@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)
|
'@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)
|
||||||
'@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/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/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/jwt': 10.2.0(@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/jwt': 11.0.2(@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/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/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/passport': 10.0.3(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)
|
'@nestjs/passport': 10.0.3(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)
|
||||||
'@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/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)
|
||||||
@@ -16414,18 +16405,18 @@ snapshots:
|
|||||||
- '@faker-js/faker'
|
- '@faker-js/faker'
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@tria-plc/iamapi-common@file:local-packages/tria-plc-iamapi-common-0.7.9.tgz(c97ba831ddde82920910406ab5262991)':
|
'@tria-plc/iamapi-common@file:local-packages/tria-plc-iamapi-common-0.7.9.tgz(4d1d275441e80423228c2d8370f9d999)':
|
||||||
dependencies:
|
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)
|
'@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)
|
||||||
'@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/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/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/jwt': 10.2.0(@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/jwt': 11.0.2(@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/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/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/passport': 10.0.3(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)
|
'@nestjs/passport': 10.0.3(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)
|
||||||
'@nestjs/swagger': 7.4.2(@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/swagger': 7.4.2(@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/throttler': 6.5.0(@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)
|
'@nestjs/throttler': 6.5.0(@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)
|
||||||
'@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)))
|
'@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)))
|
||||||
'@tria-plc/api-common': file:local-packages/tria-plc-api-common-1.4.3.tgz(59a15a37c5b1c12685ed78e172f27e65)
|
'@tria-plc/api-common': file:local-packages/tria-plc-api-common-1.4.3.tgz(aaad3d77da283ea37b052677c39644c3)
|
||||||
api-common: 1.2.2
|
api-common: 1.2.2
|
||||||
argon2: 0.43.1
|
argon2: 0.43.1
|
||||||
axios: 1.17.0
|
axios: 1.17.0
|
||||||
@@ -16573,130 +16564,6 @@ snapshots:
|
|||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
- vite
|
- 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':
|
'@ts-morph/common@0.27.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
fast-glob: 3.3.3
|
fast-glob: 3.3.3
|
||||||
@@ -16889,8 +16756,9 @@ snapshots:
|
|||||||
|
|
||||||
'@types/json5@0.0.29': {}
|
'@types/json5@0.0.29': {}
|
||||||
|
|
||||||
'@types/jsonwebtoken@9.0.5':
|
'@types/jsonwebtoken@9.0.10':
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@types/ms': 2.1.0
|
||||||
'@types/node': 20.19.42
|
'@types/node': 20.19.42
|
||||||
|
|
||||||
'@types/lodash@4.17.24': {}
|
'@types/lodash@4.17.24': {}
|
||||||
@@ -16901,6 +16769,8 @@ snapshots:
|
|||||||
|
|
||||||
'@types/mime@1.3.5': {}
|
'@types/mime@1.3.5': {}
|
||||||
|
|
||||||
|
'@types/ms@2.1.0': {}
|
||||||
|
|
||||||
'@types/multer@2.1.0':
|
'@types/multer@2.1.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/express': 5.0.6
|
'@types/express': 5.0.6
|
||||||
@@ -17057,7 +16927,7 @@ snapshots:
|
|||||||
'@typescript-eslint/types': 8.60.1
|
'@typescript-eslint/types': 8.60.1
|
||||||
'@typescript-eslint/typescript-estree': 8.60.1(typescript@5.9.3)
|
'@typescript-eslint/typescript-estree': 8.60.1(typescript@5.9.3)
|
||||||
'@typescript-eslint/visitor-keys': 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)
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -17067,7 +16937,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/tsconfig-utils': 8.60.1(typescript@5.9.3)
|
'@typescript-eslint/tsconfig-utils': 8.60.1(typescript@5.9.3)
|
||||||
'@typescript-eslint/types': 8.60.1
|
'@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
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -17086,7 +16956,7 @@ snapshots:
|
|||||||
'@typescript-eslint/types': 8.60.1
|
'@typescript-eslint/types': 8.60.1
|
||||||
'@typescript-eslint/typescript-estree': 8.60.1(typescript@5.9.3)
|
'@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)
|
'@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
|
eslint: 8.57.1
|
||||||
ts-api-utils: 2.5.0(typescript@5.9.3)
|
ts-api-utils: 2.5.0(typescript@5.9.3)
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
@@ -17101,7 +16971,7 @@ snapshots:
|
|||||||
'@typescript-eslint/tsconfig-utils': 8.60.1(typescript@5.9.3)
|
'@typescript-eslint/tsconfig-utils': 8.60.1(typescript@5.9.3)
|
||||||
'@typescript-eslint/types': 8.60.1
|
'@typescript-eslint/types': 8.60.1
|
||||||
'@typescript-eslint/visitor-keys': 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
|
minimatch: 10.2.5
|
||||||
semver: 7.8.2
|
semver: 7.8.2
|
||||||
tinyglobby: 0.2.17
|
tinyglobby: 0.2.17
|
||||||
@@ -17381,7 +17251,7 @@ snapshots:
|
|||||||
|
|
||||||
agent-base@6.0.2:
|
agent-base@6.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -17434,6 +17304,12 @@ snapshots:
|
|||||||
amqplib: 0.10.9
|
amqplib: 0.10.9
|
||||||
promise-breaker: 6.0.0
|
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
|
||||||
|
optional: true
|
||||||
|
|
||||||
amqp-connection-manager@5.0.0(amqplib@2.0.1):
|
amqp-connection-manager@5.0.0(amqplib@2.0.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
amqplib: 2.0.1
|
amqplib: 2.0.1
|
||||||
@@ -17905,16 +17781,6 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- 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:
|
babel-polyfill@6.26.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
babel-runtime: 6.26.0
|
babel-runtime: 6.26.0
|
||||||
@@ -18068,7 +17934,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
bytes: 3.1.2
|
bytes: 3.1.2
|
||||||
content-type: 1.0.5
|
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
|
http-errors: 2.0.1
|
||||||
iconv-lite: 0.7.2
|
iconv-lite: 0.7.2
|
||||||
on-finished: 2.4.1
|
on-finished: 2.4.1
|
||||||
@@ -19069,7 +18935,7 @@ snapshots:
|
|||||||
engine.io-client@6.6.5:
|
engine.io-client@6.6.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@socket.io/component-emitter': 3.1.2
|
'@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
|
engine.io-parser: 5.2.3
|
||||||
ws: 8.20.1
|
ws: 8.20.1
|
||||||
xmlhttprequest-ssl: 2.1.2
|
xmlhttprequest-ssl: 2.1.2
|
||||||
@@ -19089,7 +18955,7 @@ snapshots:
|
|||||||
base64id: 2.0.0
|
base64id: 2.0.0
|
||||||
cookie: 0.7.2
|
cookie: 0.7.2
|
||||||
cors: 2.8.6
|
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
|
engine.io-parser: 5.2.3
|
||||||
ws: 8.21.0
|
ws: 8.21.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -19322,7 +19188,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):
|
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:
|
dependencies:
|
||||||
'@nolyfill/is-core-module': 1.0.39
|
'@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
|
eslint: 8.57.1
|
||||||
get-tsconfig: 4.14.0
|
get-tsconfig: 4.14.0
|
||||||
is-bun-module: 2.0.0
|
is-bun-module: 2.0.0
|
||||||
@@ -19450,7 +19316,7 @@ snapshots:
|
|||||||
ajv: 6.15.0
|
ajv: 6.15.0
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
cross-spawn: 7.0.6
|
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
|
doctrine: 3.0.0
|
||||||
escape-string-regexp: 4.0.0
|
escape-string-regexp: 4.0.0
|
||||||
eslint-scope: 7.2.2
|
eslint-scope: 7.2.2
|
||||||
@@ -19680,7 +19546,7 @@ snapshots:
|
|||||||
content-type: 1.0.5
|
content-type: 1.0.5
|
||||||
cookie: 0.7.2
|
cookie: 0.7.2
|
||||||
cookie-signature: 1.2.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
|
depd: 2.0.0
|
||||||
encodeurl: 2.0.0
|
encodeurl: 2.0.0
|
||||||
escape-html: 1.0.3
|
escape-html: 1.0.3
|
||||||
@@ -19733,7 +19599,7 @@ snapshots:
|
|||||||
|
|
||||||
extract-zip@2.0.1:
|
extract-zip@2.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
get-stream: 5.2.0
|
get-stream: 5.2.0
|
||||||
yauzl: 2.10.0
|
yauzl: 2.10.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
@@ -19886,7 +19752,7 @@ snapshots:
|
|||||||
|
|
||||||
finalhandler@2.1.1:
|
finalhandler@2.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
encodeurl: 2.0.0
|
encodeurl: 2.0.0
|
||||||
escape-html: 1.0.3
|
escape-html: 1.0.3
|
||||||
on-finished: 2.4.1
|
on-finished: 2.4.1
|
||||||
@@ -20132,7 +19998,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
basic-ftp: 5.3.1
|
basic-ftp: 5.3.1
|
||||||
data-uri-to-buffer: 6.0.2
|
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:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -20413,7 +20279,7 @@ snapshots:
|
|||||||
http-proxy-agent@7.0.2:
|
http-proxy-agent@7.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
agent-base: 7.1.4
|
agent-base: 7.1.4
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -20426,14 +20292,14 @@ snapshots:
|
|||||||
https-proxy-agent@5.0.1:
|
https-proxy-agent@5.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
agent-base: 6.0.2
|
agent-base: 6.0.2
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
https-proxy-agent@7.0.6:
|
https-proxy-agent@7.0.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
agent-base: 7.1.4
|
agent-base: 7.1.4
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -20858,7 +20724,7 @@ snapshots:
|
|||||||
|
|
||||||
istanbul-lib-source-maps@4.0.1:
|
istanbul-lib-source-maps@4.0.1:
|
||||||
dependencies:
|
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
|
istanbul-lib-coverage: 3.2.2
|
||||||
source-map: 0.6.1
|
source-map: 0.6.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -21319,19 +21185,6 @@ snapshots:
|
|||||||
|
|
||||||
jsonparse@1.3.1: {}
|
jsonparse@1.3.1: {}
|
||||||
|
|
||||||
jsonwebtoken@9.0.2:
|
|
||||||
dependencies:
|
|
||||||
jws: 3.2.3
|
|
||||||
lodash.includes: 4.3.0
|
|
||||||
lodash.isboolean: 3.0.3
|
|
||||||
lodash.isinteger: 4.0.4
|
|
||||||
lodash.isnumber: 3.0.3
|
|
||||||
lodash.isplainobject: 4.0.6
|
|
||||||
lodash.isstring: 4.0.1
|
|
||||||
lodash.once: 4.1.1
|
|
||||||
ms: 2.1.3
|
|
||||||
semver: 7.8.2
|
|
||||||
|
|
||||||
jsonwebtoken@9.0.3:
|
jsonwebtoken@9.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
jws: 4.0.1
|
jws: 4.0.1
|
||||||
@@ -21392,23 +21245,12 @@ snapshots:
|
|||||||
readable-stream: 2.3.8
|
readable-stream: 2.3.8
|
||||||
setimmediate: 1.0.5
|
setimmediate: 1.0.5
|
||||||
|
|
||||||
jwa@1.4.2:
|
|
||||||
dependencies:
|
|
||||||
buffer-equal-constant-time: 1.0.1
|
|
||||||
ecdsa-sig-formatter: 1.0.11
|
|
||||||
safe-buffer: 5.2.1
|
|
||||||
|
|
||||||
jwa@2.0.1:
|
jwa@2.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
buffer-equal-constant-time: 1.0.1
|
buffer-equal-constant-time: 1.0.1
|
||||||
ecdsa-sig-formatter: 1.0.11
|
ecdsa-sig-formatter: 1.0.11
|
||||||
safe-buffer: 5.2.1
|
safe-buffer: 5.2.1
|
||||||
|
|
||||||
jws@3.2.3:
|
|
||||||
dependencies:
|
|
||||||
jwa: 1.4.2
|
|
||||||
safe-buffer: 5.2.1
|
|
||||||
|
|
||||||
jws@4.0.1:
|
jws@4.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
jwa: 2.0.1
|
jwa: 2.0.1
|
||||||
@@ -21530,7 +21372,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
chalk: 5.6.2
|
chalk: 5.6.2
|
||||||
commander: 13.1.0
|
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
|
execa: 8.0.1
|
||||||
lilconfig: 3.1.3
|
lilconfig: 3.1.3
|
||||||
listr2: 8.3.3
|
listr2: 8.3.3
|
||||||
@@ -22305,7 +22147,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@tootallnate/quickjs-emscripten': 0.23.0
|
'@tootallnate/quickjs-emscripten': 0.23.0
|
||||||
agent-base: 7.1.4
|
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
|
get-uri: 6.0.5
|
||||||
http-proxy-agent: 7.0.2
|
http-proxy-agent: 7.0.2
|
||||||
https-proxy-agent: 7.0.6
|
https-proxy-agent: 7.0.6
|
||||||
@@ -22645,7 +22487,7 @@ snapshots:
|
|||||||
proxy-agent@6.5.0:
|
proxy-agent@6.5.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
agent-base: 7.1.4
|
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
|
http-proxy-agent: 7.0.2
|
||||||
https-proxy-agent: 7.0.6
|
https-proxy-agent: 7.0.6
|
||||||
lru-cache: 7.18.3
|
lru-cache: 7.18.3
|
||||||
@@ -22674,7 +22516,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@puppeteer/browsers': 2.13.2
|
'@puppeteer/browsers': 2.13.2
|
||||||
chromium-bidi: 14.0.0(devtools-protocol@0.0.1608973)
|
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
|
devtools-protocol: 0.0.1608973
|
||||||
typed-query-selector: 2.12.2
|
typed-query-selector: 2.12.2
|
||||||
webdriver-bidi-protocol: 0.4.1
|
webdriver-bidi-protocol: 0.4.1
|
||||||
@@ -22927,15 +22769,6 @@ snapshots:
|
|||||||
- '@babel/core'
|
- '@babel/core'
|
||||||
- react-is
|
- 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):
|
react-day-picker@8.10.2(date-fns@3.6.0)(react@19.2.6):
|
||||||
dependencies:
|
dependencies:
|
||||||
date-fns: 3.6.0
|
date-fns: 3.6.0
|
||||||
@@ -23509,7 +23342,7 @@ snapshots:
|
|||||||
|
|
||||||
router@2.2.0:
|
router@2.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
depd: 2.0.0
|
depd: 2.0.0
|
||||||
is-promise: 4.0.0
|
is-promise: 4.0.0
|
||||||
parseurl: 1.3.3
|
parseurl: 1.3.3
|
||||||
@@ -23627,7 +23460,7 @@ snapshots:
|
|||||||
|
|
||||||
send@1.2.1:
|
send@1.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
encodeurl: 2.0.0
|
encodeurl: 2.0.0
|
||||||
escape-html: 1.0.3
|
escape-html: 1.0.3
|
||||||
etag: 1.8.1
|
etag: 1.8.1
|
||||||
@@ -23845,7 +23678,7 @@ snapshots:
|
|||||||
|
|
||||||
socket.io-adapter@2.5.8:
|
socket.io-adapter@2.5.8:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
ws: 8.21.0
|
ws: 8.21.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- bufferutil
|
- bufferutil
|
||||||
@@ -23855,7 +23688,7 @@ snapshots:
|
|||||||
socket.io-client@4.8.3:
|
socket.io-client@4.8.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@socket.io/component-emitter': 3.1.2
|
'@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
|
engine.io-client: 6.6.5
|
||||||
socket.io-parser: 4.2.6
|
socket.io-parser: 4.2.6
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -23866,7 +23699,7 @@ snapshots:
|
|||||||
socket.io-parser@4.2.6:
|
socket.io-parser@4.2.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@socket.io/component-emitter': 3.1.2
|
'@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:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -23875,7 +23708,7 @@ snapshots:
|
|||||||
accepts: 1.3.8
|
accepts: 1.3.8
|
||||||
base64id: 2.0.0
|
base64id: 2.0.0
|
||||||
cors: 2.8.6
|
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
|
engine.io: 6.6.9
|
||||||
socket.io-adapter: 2.5.8
|
socket.io-adapter: 2.5.8
|
||||||
socket.io-parser: 4.2.6
|
socket.io-parser: 4.2.6
|
||||||
@@ -23887,7 +23720,7 @@ snapshots:
|
|||||||
socks-proxy-agent@8.0.5:
|
socks-proxy-agent@8.0.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
agent-base: 7.1.4
|
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
|
socks: 2.8.9
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -24167,24 +24000,6 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@babel/core'
|
- '@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):
|
styled-jsx@5.1.1(babel-plugin-macros@3.1.0)(react@18.3.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
client-only: 0.0.1
|
client-only: 0.0.1
|
||||||
@@ -24210,7 +24025,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
component-emitter: 1.3.1
|
component-emitter: 1.3.1
|
||||||
cookiejar: 2.1.4
|
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
|
fast-safe-stringify: 2.1.1
|
||||||
form-data: 4.0.5
|
form-data: 4.0.5
|
||||||
formidable: 3.5.4
|
formidable: 3.5.4
|
||||||
@@ -24717,7 +24532,7 @@ snapshots:
|
|||||||
app-root-path: 3.1.0
|
app-root-path: 3.1.0
|
||||||
buffer: 6.0.3
|
buffer: 6.0.3
|
||||||
dayjs: 1.11.21
|
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)
|
dedent: 1.7.2(babel-plugin-macros@3.1.0)
|
||||||
dotenv: 16.6.1
|
dotenv: 16.6.1
|
||||||
glob: 10.5.0
|
glob: 10.5.0
|
||||||
@@ -24741,7 +24556,7 @@ snapshots:
|
|||||||
app-root-path: 3.1.0
|
app-root-path: 3.1.0
|
||||||
buffer: 6.0.3
|
buffer: 6.0.3
|
||||||
dayjs: 1.11.21
|
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)
|
dedent: 1.7.2(babel-plugin-macros@3.1.0)
|
||||||
dotenv: 16.6.1
|
dotenv: 16.6.1
|
||||||
glob: 10.5.0
|
glob: 10.5.0
|
||||||
@@ -25044,7 +24859,7 @@ snapshots:
|
|||||||
vite-node@2.1.9(@types/node@24.13.1)(lightningcss@1.32.0)(terser@5.48.0):
|
vite-node@2.1.9(@types/node@24.13.1)(lightningcss@1.32.0)(terser@5.48.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
cac: 6.7.14
|
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
|
es-module-lexer: 1.7.0
|
||||||
pathe: 1.1.2
|
pathe: 1.1.2
|
||||||
vite: 5.4.21(@types/node@24.13.1)(lightningcss@1.32.0)(terser@5.48.0)
|
vite: 5.4.21(@types/node@24.13.1)(lightningcss@1.32.0)(terser@5.48.0)
|
||||||
@@ -25080,7 +24895,7 @@ snapshots:
|
|||||||
'@vitest/spy': 2.1.9
|
'@vitest/spy': 2.1.9
|
||||||
'@vitest/utils': 2.1.9
|
'@vitest/utils': 2.1.9
|
||||||
chai: 5.3.3
|
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
|
expect-type: 1.3.0
|
||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
pathe: 1.1.2
|
pathe: 1.1.2
|
||||||
|
|||||||
Reference in New Issue
Block a user