Enhance Salesforce Case Integration and Update Eligibility Logic
- Added new fields for notes and case ID in the ServicesCdcSubscriber to improve eligibility detail tracking. - Refactored the SalesforceCaseService to unify case creation for various case types, including customer support and internal workflow cases. - Updated InternetServicesService to create internal workflow cases for eligibility checks, enhancing tracking and management. - Improved error handling and logging consistency across services when creating cases. - Cleaned up unused imports and optimized code structure for better maintainability.
This commit is contained in:
parent
a55967a31f
commit
a938c605c7
@ -273,6 +273,8 @@ export class ServicesCdcSubscriber implements OnModuleInit, OnModuleDestroy {
|
||||
const accountId = this.extractStringField(payload, ["AccountId__c", "AccountId", "Id"]);
|
||||
const eligibility = this.extractStringField(payload, ["Internet_Eligibility__c"]);
|
||||
const status = this.extractStringField(payload, ["Internet_Eligibility_Status__c"]);
|
||||
const notes = this.extractStringField(payload, ["Internet_Eligibility_Notes__c"]);
|
||||
const caseId = this.extractStringField(payload, ["Internet_Eligibility_Case_Id__c"]);
|
||||
const requestedAt = this.extractStringField(payload, [
|
||||
"Internet_Eligibility_Request_Date_Time__c",
|
||||
]);
|
||||
@ -280,8 +282,7 @@ export class ServicesCdcSubscriber implements OnModuleInit, OnModuleDestroy {
|
||||
"Internet_Eligibility_Checked_Date_Time__c",
|
||||
]);
|
||||
|
||||
// Note: Request ID field is not used in this environment
|
||||
const requestId = undefined;
|
||||
const requestId = caseId;
|
||||
|
||||
// Also extract ID verification fields for notifications
|
||||
const verificationStatus = this.extractStringField(payload, ["Id_Verification_Status__c"]);
|
||||
@ -303,7 +304,9 @@ export class ServicesCdcSubscriber implements OnModuleInit, OnModuleDestroy {
|
||||
});
|
||||
|
||||
await this.catalogCache.invalidateEligibility(accountId);
|
||||
const hasDetails = Boolean(status || eligibility || requestedAt || checkedAt || requestId);
|
||||
const hasDetails = Boolean(
|
||||
status || eligibility || requestedAt || checkedAt || requestId || notes
|
||||
);
|
||||
if (hasDetails) {
|
||||
await this.catalogCache.setEligibilityDetails(accountId, {
|
||||
status: this.mapEligibilityStatus(status, eligibility),
|
||||
@ -311,7 +314,7 @@ export class ServicesCdcSubscriber implements OnModuleInit, OnModuleDestroy {
|
||||
requestId: requestId ?? null,
|
||||
requestedAt: requestedAt ?? null,
|
||||
checkedAt: checkedAt ?? null,
|
||||
notes: null, // Field not used
|
||||
notes: notes ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
/**
|
||||
* Salesforce Case Integration Service
|
||||
*
|
||||
* Encapsulates all Salesforce Case operations for the portal.
|
||||
* - Queries cases filtered by Origin = 'Portal Website'
|
||||
* - Creates cases with portal-specific defaults
|
||||
* - Validates account ownership for security
|
||||
* Unified service for all Salesforce Case operations.
|
||||
*
|
||||
* Case Types:
|
||||
* - Customer Support Cases (Origin: Portal Support) - visible to customers
|
||||
* - Internal Workflow Cases (Origin: Portal Notification) - for CS team only
|
||||
* - Web-to-Case (Origin: Web) - public contact form submissions
|
||||
*
|
||||
* Uses domain types and mappers from @customer-portal/domain/support
|
||||
*/
|
||||
@ -15,31 +17,62 @@ import { SalesforceConnection } from "./salesforce-connection.service.js";
|
||||
import { assertSalesforceId } from "../utils/soql.util.js";
|
||||
import { extractErrorMessage } from "@bff/core/utils/error.util.js";
|
||||
import type { SalesforceResponse } from "@customer-portal/domain/common/providers";
|
||||
import type { SupportCase, CreateCaseRequest } from "@customer-portal/domain/support";
|
||||
import type { SupportCase } from "@customer-portal/domain/support";
|
||||
import type { SalesforceCaseRecord } from "@customer-portal/domain/support/providers";
|
||||
import {
|
||||
SALESFORCE_CASE_ORIGIN,
|
||||
SALESFORCE_CASE_STATUS,
|
||||
SALESFORCE_CASE_PRIORITY,
|
||||
toSalesforcePriority,
|
||||
type SalesforceCaseOrigin,
|
||||
} from "@customer-portal/domain/support/providers";
|
||||
import {
|
||||
buildCaseByIdQuery,
|
||||
buildCaseSelectFields,
|
||||
buildCasesForAccountQuery,
|
||||
toSalesforcePriority,
|
||||
transformSalesforceCaseToSupportCase,
|
||||
transformSalesforceCasesToSupportCases,
|
||||
} from "@customer-portal/domain/support/providers";
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Parameters for creating a case in Salesforce
|
||||
* Extends domain CreateCaseRequest with infrastructure-specific fields
|
||||
* Parameters for creating any case in Salesforce.
|
||||
*
|
||||
* The `origin` field determines case visibility:
|
||||
* - PORTAL_SUPPORT: Customer-visible support case
|
||||
* - PORTAL_NOTIFICATION: Internal CS workflow case (eligibility, verification, cancellation)
|
||||
*/
|
||||
export interface CreateCaseParams extends CreateCaseRequest {
|
||||
export interface CreateCaseParams {
|
||||
/** Salesforce Account ID */
|
||||
accountId: string;
|
||||
/** Case subject line */
|
||||
subject: string;
|
||||
/** Case description/body */
|
||||
description: string;
|
||||
/** Case origin - determines visibility and routing */
|
||||
origin: SalesforceCaseOrigin;
|
||||
/** Priority (defaults to Medium) */
|
||||
priority?: string;
|
||||
/** Optional Salesforce Contact ID */
|
||||
contactId?: string;
|
||||
/** Optional Opportunity ID for workflow cases */
|
||||
opportunityId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for Web-to-Case (public contact form, no account)
|
||||
*/
|
||||
export interface CreateWebCaseParams {
|
||||
subject: string;
|
||||
description: string;
|
||||
suppliedEmail: string;
|
||||
suppliedName: string;
|
||||
suppliedPhone?: string;
|
||||
origin?: string;
|
||||
priority?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@ -50,13 +83,13 @@ export class SalesforceCaseService {
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get all cases for an account filtered by Portal Website origin
|
||||
* Get all customer-visible support cases for an account
|
||||
*/
|
||||
async getCasesForAccount(accountId: string): Promise<SupportCase[]> {
|
||||
const safeAccountId = assertSalesforceId(accountId, "accountId");
|
||||
this.logger.debug({ accountId: safeAccountId }, "Fetching portal cases for account");
|
||||
|
||||
const soql = buildCasesForAccountQuery(safeAccountId, SALESFORCE_CASE_ORIGIN.PORTAL_WEBSITE);
|
||||
const soql = buildCasesForAccountQuery(safeAccountId, SALESFORCE_CASE_ORIGIN.PORTAL_SUPPORT);
|
||||
|
||||
try {
|
||||
const result = (await this.sf.query(soql, {
|
||||
@ -92,7 +125,7 @@ export class SalesforceCaseService {
|
||||
const soql = buildCaseByIdQuery(
|
||||
safeCaseId,
|
||||
safeAccountId,
|
||||
SALESFORCE_CASE_ORIGIN.PORTAL_WEBSITE
|
||||
SALESFORCE_CASE_ORIGIN.PORTAL_SUPPORT
|
||||
);
|
||||
|
||||
try {
|
||||
@ -118,27 +151,40 @@ export class SalesforceCaseService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new case with Portal Website origin
|
||||
* Create a case in Salesforce.
|
||||
*
|
||||
* This is the unified method for creating all case types:
|
||||
* - Customer support cases (origin: PORTAL_SUPPORT)
|
||||
* - Internal workflow cases (origin: PORTAL_NOTIFICATION)
|
||||
*
|
||||
* @param params - Case creation parameters
|
||||
* @returns Created case ID and case number
|
||||
*/
|
||||
async createCase(params: CreateCaseParams): Promise<{ id: string; caseNumber: string }> {
|
||||
const safeAccountId = assertSalesforceId(params.accountId, "accountId");
|
||||
const safeContactId = params.contactId
|
||||
? assertSalesforceId(params.contactId, "contactId")
|
||||
: undefined;
|
||||
const safeOpportunityId = params.opportunityId
|
||||
? assertSalesforceId(params.opportunityId, "opportunityId")
|
||||
: undefined;
|
||||
|
||||
this.logger.log(
|
||||
{ accountId: safeAccountId, subject: params.subject },
|
||||
"Creating portal support case"
|
||||
);
|
||||
const isInternalCase = params.origin === SALESFORCE_CASE_ORIGIN.PORTAL_NOTIFICATION;
|
||||
|
||||
// Build case payload with portal defaults
|
||||
// Convert portal display values to Salesforce API values
|
||||
this.logger.log("Creating Salesforce case", {
|
||||
accountIdTail: safeAccountId.slice(-4),
|
||||
origin: params.origin,
|
||||
isInternal: isInternalCase,
|
||||
hasOpportunity: !!safeOpportunityId,
|
||||
});
|
||||
|
||||
// Convert portal display priority to Salesforce API value
|
||||
const sfPriority = params.priority
|
||||
? toSalesforcePriority(params.priority)
|
||||
: SALESFORCE_CASE_PRIORITY.MEDIUM;
|
||||
|
||||
const casePayload: Record<string, unknown> = {
|
||||
Origin: SALESFORCE_CASE_ORIGIN.PORTAL_WEBSITE,
|
||||
Origin: params.origin,
|
||||
Status: SALESFORCE_CASE_STATUS.NEW,
|
||||
Priority: sfPriority,
|
||||
Subject: params.subject.trim(),
|
||||
@ -146,18 +192,17 @@ export class SalesforceCaseService {
|
||||
};
|
||||
|
||||
// Set ContactId if available - Salesforce will auto-populate AccountId from Contact
|
||||
// If no ContactId, we must set AccountId directly (requires FLS write permission)
|
||||
// If no ContactId, we must set AccountId directly
|
||||
if (safeContactId) {
|
||||
casePayload.ContactId = safeContactId;
|
||||
} else {
|
||||
// Only set AccountId when no ContactId is available
|
||||
// Note: This requires AccountId field-level security write permission
|
||||
casePayload.AccountId = safeAccountId;
|
||||
}
|
||||
|
||||
// Note: Category maps to Salesforce Type field
|
||||
// Only set if Type picklist is configured in Salesforce
|
||||
// Currently skipped as Type picklist values are unknown
|
||||
// Link to Opportunity if provided (used for workflow cases)
|
||||
if (safeOpportunityId) {
|
||||
casePayload.OpportunityId = safeOpportunityId;
|
||||
}
|
||||
|
||||
try {
|
||||
const created = (await this.sf.sobject("Case").create(casePayload)) as { id?: string };
|
||||
@ -170,34 +215,30 @@ export class SalesforceCaseService {
|
||||
const createdCase = await this.getCaseByIdInternal(created.id);
|
||||
const caseNumber = createdCase?.CaseNumber ?? created.id;
|
||||
|
||||
this.logger.log(
|
||||
{ caseId: created.id, caseNumber },
|
||||
"Portal support case created successfully"
|
||||
);
|
||||
this.logger.log("Salesforce case created successfully", {
|
||||
caseId: created.id,
|
||||
caseNumber,
|
||||
origin: params.origin,
|
||||
});
|
||||
|
||||
return { id: created.id, caseNumber };
|
||||
} catch (error: unknown) {
|
||||
this.logger.error("Failed to create support case", {
|
||||
this.logger.error("Failed to create Salesforce case", {
|
||||
error: extractErrorMessage(error),
|
||||
accountId: safeAccountId,
|
||||
accountIdTail: safeAccountId.slice(-4),
|
||||
origin: params.origin,
|
||||
});
|
||||
throw new Error("Failed to create support case");
|
||||
throw new Error("Failed to create case");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Web-to-Case for public contact form submissions
|
||||
* Does not require an Account - uses supplied contact info
|
||||
* Create a Web-to-Case for public contact form submissions.
|
||||
*
|
||||
* Does not require an Account - uses supplied contact info.
|
||||
* Separate from createCase() because it has a different payload structure.
|
||||
*/
|
||||
async createWebCase(params: {
|
||||
subject: string;
|
||||
description: string;
|
||||
suppliedEmail: string;
|
||||
suppliedName: string;
|
||||
suppliedPhone?: string;
|
||||
origin?: string;
|
||||
priority?: string;
|
||||
}): Promise<{ id: string; caseNumber: string }> {
|
||||
async createWebCase(params: CreateWebCaseParams): Promise<{ id: string; caseNumber: string }> {
|
||||
this.logger.log("Creating Web-to-Case", { email: params.suppliedEmail });
|
||||
|
||||
const casePayload: Record<string, unknown> = {
|
||||
@ -258,148 +299,4 @@ export class SalesforceCaseService {
|
||||
|
||||
return result.records?.[0] ?? null;
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Opportunity-Linked Cases
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Create an eligibility check case linked to an Opportunity
|
||||
*
|
||||
* @param params - Case parameters including Opportunity link
|
||||
* @returns Created case ID
|
||||
*/
|
||||
async createEligibilityCase(params: {
|
||||
accountId: string;
|
||||
opportunityId: string;
|
||||
subject: string;
|
||||
description: string;
|
||||
}): Promise<string> {
|
||||
const safeAccountId = assertSalesforceId(params.accountId, "accountId");
|
||||
const safeOpportunityId = assertSalesforceId(params.opportunityId, "opportunityId");
|
||||
|
||||
this.logger.log("Creating eligibility check case linked to Opportunity", {
|
||||
accountIdTail: safeAccountId.slice(-4),
|
||||
opportunityIdTail: safeOpportunityId.slice(-4),
|
||||
});
|
||||
|
||||
const casePayload: Record<string, unknown> = {
|
||||
Origin: SALESFORCE_CASE_ORIGIN.PORTAL_WEBSITE,
|
||||
Status: SALESFORCE_CASE_STATUS.NEW,
|
||||
Priority: SALESFORCE_CASE_PRIORITY.MEDIUM,
|
||||
Subject: params.subject,
|
||||
Description: params.description,
|
||||
AccountId: safeAccountId,
|
||||
// Link Case to Opportunity - this is a standard lookup field
|
||||
OpportunityId: safeOpportunityId,
|
||||
};
|
||||
|
||||
try {
|
||||
const created = (await this.sf.sobject("Case").create(casePayload)) as { id?: string };
|
||||
|
||||
if (!created.id) {
|
||||
throw new Error("Salesforce did not return a case ID");
|
||||
}
|
||||
|
||||
this.logger.log("Eligibility case created and linked to Opportunity", {
|
||||
caseId: created.id,
|
||||
opportunityIdTail: safeOpportunityId.slice(-4),
|
||||
});
|
||||
|
||||
return created.id;
|
||||
} catch (error: unknown) {
|
||||
this.logger.error("Failed to create eligibility case", {
|
||||
error: extractErrorMessage(error),
|
||||
accountIdTail: safeAccountId.slice(-4),
|
||||
});
|
||||
throw new Error("Failed to create eligibility check case");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a cancellation request case linked to an Opportunity
|
||||
*
|
||||
* All customer-provided details (comments, alternative email) go here.
|
||||
* The Opportunity only gets the core lifecycle fields (dates, status).
|
||||
*
|
||||
* @param params - Cancellation case parameters
|
||||
* @returns Created case ID
|
||||
*/
|
||||
async createCancellationCase(params: {
|
||||
accountId: string;
|
||||
opportunityId?: string;
|
||||
whmcsServiceId: number;
|
||||
productType: string;
|
||||
cancellationMonth: string;
|
||||
cancellationDate: string;
|
||||
alternativeEmail?: string;
|
||||
comments?: string;
|
||||
}): Promise<string> {
|
||||
const safeAccountId = assertSalesforceId(params.accountId, "accountId");
|
||||
const safeOpportunityId = params.opportunityId
|
||||
? assertSalesforceId(params.opportunityId, "opportunityId")
|
||||
: null;
|
||||
|
||||
this.logger.log("Creating cancellation request case", {
|
||||
accountIdTail: safeAccountId.slice(-4),
|
||||
opportunityId: safeOpportunityId ? safeOpportunityId.slice(-4) : "none",
|
||||
whmcsServiceId: params.whmcsServiceId,
|
||||
});
|
||||
|
||||
// Build description with all form data
|
||||
const descriptionLines = [
|
||||
`Cancellation Request from Portal`,
|
||||
``,
|
||||
`Product Type: ${params.productType}`,
|
||||
`WHMCS Service ID: ${params.whmcsServiceId}`,
|
||||
`Cancellation Month: ${params.cancellationMonth}`,
|
||||
`Service End Date: ${params.cancellationDate}`,
|
||||
``,
|
||||
];
|
||||
|
||||
if (params.alternativeEmail) {
|
||||
descriptionLines.push(`Alternative Contact Email: ${params.alternativeEmail}`);
|
||||
}
|
||||
|
||||
if (params.comments) {
|
||||
descriptionLines.push(``, `Customer Comments:`, params.comments);
|
||||
}
|
||||
|
||||
descriptionLines.push(``, `Submitted: ${new Date().toISOString()}`);
|
||||
|
||||
const casePayload: Record<string, unknown> = {
|
||||
Origin: "Portal",
|
||||
Status: SALESFORCE_CASE_STATUS.NEW,
|
||||
Priority: SALESFORCE_CASE_PRIORITY.HIGH,
|
||||
Subject: `Cancellation Request - ${params.productType} (${params.cancellationMonth})`,
|
||||
Description: descriptionLines.join("\n"),
|
||||
AccountId: safeAccountId,
|
||||
};
|
||||
|
||||
// Link to Opportunity if we have one
|
||||
if (safeOpportunityId) {
|
||||
casePayload.OpportunityId = safeOpportunityId;
|
||||
}
|
||||
|
||||
try {
|
||||
const created = (await this.sf.sobject("Case").create(casePayload)) as { id?: string };
|
||||
|
||||
if (!created.id) {
|
||||
throw new Error("Salesforce did not return a case ID");
|
||||
}
|
||||
|
||||
this.logger.log("Cancellation case created", {
|
||||
caseId: created.id,
|
||||
hasOpportunityLink: !!safeOpportunityId,
|
||||
});
|
||||
|
||||
return created.id;
|
||||
} catch (error: unknown) {
|
||||
this.logger.error("Failed to create cancellation case", {
|
||||
error: extractErrorMessage(error),
|
||||
accountIdTail: safeAccountId.slice(-4),
|
||||
});
|
||||
throw new Error("Failed to create cancellation request case");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,7 +29,7 @@ export class InvoiceRetrievalService {
|
||||
/**
|
||||
* Get paginated invoices for a user
|
||||
*/
|
||||
async getInvoices(userId: string, options: InvoiceListQuery = {}): Promise<InvoiceList> {
|
||||
async getInvoices(userId: string, options: Partial<InvoiceListQuery> = {}): Promise<InvoiceList> {
|
||||
const {
|
||||
page = INVOICE_PAGINATION.DEFAULT_PAGE,
|
||||
limit = INVOICE_PAGINATION.DEFAULT_LIMIT,
|
||||
|
||||
@ -25,7 +25,7 @@ import { MappingsService } from "@bff/modules/id-mappings/mappings.service.js";
|
||||
import { SalesforceConnection } from "@bff/integrations/salesforce/services/salesforce-connection.service.js";
|
||||
import { OpportunityResolutionService } from "@bff/integrations/salesforce/services/opportunity-resolution.service.js";
|
||||
import { SalesforceCaseService } from "@bff/integrations/salesforce/services/salesforce-case.service.js";
|
||||
import { DistributedLockService } from "@bff/infra/cache/distributed-lock.service.js";
|
||||
import { SALESFORCE_CASE_ORIGIN } from "@customer-portal/domain/support/providers";
|
||||
import { Logger } from "nestjs-pino";
|
||||
import { extractErrorMessage } from "@bff/core/utils/error.util.js";
|
||||
import { assertSalesforceId } from "@bff/integrations/salesforce/utils/soql.util.js";
|
||||
@ -42,7 +42,6 @@ export class InternetServicesService extends BaseServicesService {
|
||||
@Inject(Logger) logger: Logger,
|
||||
private mappingsService: MappingsService,
|
||||
private catalogCache: ServicesCacheService,
|
||||
private lockService: DistributedLockService,
|
||||
private opportunityResolution: OpportunityResolutionService,
|
||||
private caseService: SalesforceCaseService
|
||||
) {
|
||||
@ -300,45 +299,13 @@ export class InternetServicesService extends BaseServicesService {
|
||||
}
|
||||
|
||||
try {
|
||||
const lockKey = `internet:eligibility:${sfAccountId}`;
|
||||
const subject = "Internet availability check request (Portal)";
|
||||
|
||||
const caseId = await this.lockService.withLock(
|
||||
lockKey,
|
||||
async () => {
|
||||
// Idempotency: if we already have a pending request, do not create a new Case.
|
||||
// The Case creation is a signal of interest; if status is pending, interest is already signaled/active.
|
||||
const existing = await this.queryEligibilityDetails(sfAccountId);
|
||||
|
||||
if (existing.status === "pending") {
|
||||
this.logger.log("Eligibility request already pending; skipping new case creation", {
|
||||
userId,
|
||||
sfAccountIdTail: sfAccountId.slice(-4),
|
||||
});
|
||||
|
||||
// Try to find the existing open case to return its ID (best effort)
|
||||
try {
|
||||
const cases = await this.caseService.getCasesForAccount(sfAccountId);
|
||||
const openCase = cases.find(
|
||||
c => c.status !== "Closed" && c.subject.includes("Internet availability check")
|
||||
);
|
||||
if (openCase) {
|
||||
return openCase.id;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.warn("Failed to lookup existing case for pending request", { error });
|
||||
}
|
||||
|
||||
// If we can't find the case ID but status is pending, we return a placeholder or empty string.
|
||||
// The frontend primarily relies on the status change.
|
||||
return "";
|
||||
}
|
||||
|
||||
// 1) Find or create Opportunity for Internet eligibility
|
||||
// 1) Find or create Opportunity for Internet eligibility (this service remains locked internally)
|
||||
const { opportunityId, wasCreated: opportunityCreated } =
|
||||
await this.opportunityResolution.findOrCreateForInternetEligibility(sfAccountId);
|
||||
|
||||
// 2) Build case description
|
||||
const subject = "Internet availability check request (Portal)";
|
||||
const descriptionLines: string[] = [
|
||||
"Portal internet availability check requested.",
|
||||
"",
|
||||
@ -353,16 +320,20 @@ export class InternetServicesService extends BaseServicesService {
|
||||
`RequestedAt: ${new Date().toISOString()}`,
|
||||
].filter(Boolean);
|
||||
|
||||
// 3) Create Case linked to Opportunity
|
||||
const createdCaseId = await this.caseService.createEligibilityCase({
|
||||
// 3) Create Case linked to Opportunity (internal workflow case)
|
||||
const { id: createdCaseId } = await this.caseService.createCase({
|
||||
accountId: sfAccountId,
|
||||
opportunityId,
|
||||
subject,
|
||||
description: descriptionLines.join("\n"),
|
||||
origin: SALESFORCE_CASE_ORIGIN.PORTAL_NOTIFICATION,
|
||||
});
|
||||
|
||||
// 4) Update Account eligibility status
|
||||
await this.updateAccountEligibilityRequestState(sfAccountId);
|
||||
// 4) Update Account eligibility status (best-effort for optional fields)
|
||||
await this.updateAccountEligibilityRequestState(sfAccountId, {
|
||||
caseId: createdCaseId,
|
||||
notes: request.notes,
|
||||
});
|
||||
await this.catalogCache.invalidateEligibility(sfAccountId);
|
||||
|
||||
this.logger.log("Created eligibility Case linked to Opportunity", {
|
||||
@ -374,11 +345,6 @@ export class InternetServicesService extends BaseServicesService {
|
||||
});
|
||||
|
||||
return createdCaseId;
|
||||
},
|
||||
{ ttlMs: 10_000 }
|
||||
);
|
||||
|
||||
return caseId;
|
||||
} catch (error) {
|
||||
this.logger.error("Failed to create eligibility request", {
|
||||
userId,
|
||||
@ -415,18 +381,44 @@ export class InternetServicesService extends BaseServicesService {
|
||||
"Internet_Eligibility_Checked_Date_Time__c",
|
||||
"ACCOUNT_INTERNET_ELIGIBILITY_CHECKED_AT_FIELD"
|
||||
);
|
||||
// Note: Notes and Case ID fields removed as they are not present/needed in the Salesforce schema
|
||||
const notesField = assertSoqlFieldName(
|
||||
this.config.get<string>("ACCOUNT_INTERNET_ELIGIBILITY_NOTES_FIELD") ??
|
||||
"Internet_Eligibility_Notes__c",
|
||||
"ACCOUNT_INTERNET_ELIGIBILITY_NOTES_FIELD"
|
||||
);
|
||||
const caseIdField = assertSoqlFieldName(
|
||||
this.config.get<string>("ACCOUNT_INTERNET_ELIGIBILITY_CASE_ID_FIELD") ??
|
||||
"Internet_Eligibility_Case_Id__c",
|
||||
"ACCOUNT_INTERNET_ELIGIBILITY_CASE_ID_FIELD"
|
||||
);
|
||||
|
||||
const selectBase = `Id, ${eligibilityField}, ${statusField}, ${requestedAtField}, ${checkedAtField}`;
|
||||
const selectExtended = `${selectBase}, ${notesField}, ${caseIdField}`;
|
||||
|
||||
const queryAccount = async (selectFields: string, labelSuffix: string) => {
|
||||
const soql = `
|
||||
SELECT Id, ${eligibilityField}, ${statusField}, ${requestedAtField}, ${checkedAtField}
|
||||
SELECT ${selectFields}
|
||||
FROM Account
|
||||
WHERE Id = '${sfAccountId}'
|
||||
LIMIT 1
|
||||
`;
|
||||
|
||||
const res = (await this.sf.query(soql, {
|
||||
label: "services:internet:eligibility_details",
|
||||
return (await this.sf.query(soql, {
|
||||
label: `services:internet:eligibility_details:${labelSuffix}`,
|
||||
})) as SalesforceResponse<Record<string, unknown>>;
|
||||
};
|
||||
|
||||
let res: SalesforceResponse<Record<string, unknown>>;
|
||||
try {
|
||||
res = await queryAccount(selectExtended, "extended");
|
||||
} catch (error) {
|
||||
// Hardening: if optional fields aren't available (FLS / schema drift),
|
||||
// fall back to the minimal field set rather than failing the whole endpoint.
|
||||
this.logger.warn("Eligibility details query failed (extended); falling back to base fields", {
|
||||
sfAccountIdTail: sfAccountId.slice(-4),
|
||||
error: extractErrorMessage(error),
|
||||
});
|
||||
res = await queryAccount(selectBase, "base");
|
||||
}
|
||||
const record = (res.records?.[0] as Record<string, unknown> | undefined) ?? undefined;
|
||||
if (!record) {
|
||||
return internetEligibilityDetailsSchema.parse({
|
||||
@ -461,6 +453,8 @@ export class InternetServicesService extends BaseServicesService {
|
||||
|
||||
const requestedAtRaw = record[requestedAtField];
|
||||
const checkedAtRaw = record[checkedAtField];
|
||||
const notesRaw = record[notesField];
|
||||
const requestIdRaw = record[caseIdField];
|
||||
|
||||
const requestedAt =
|
||||
typeof requestedAtRaw === "string"
|
||||
@ -478,17 +472,23 @@ export class InternetServicesService extends BaseServicesService {
|
||||
return internetEligibilityDetailsSchema.parse({
|
||||
status,
|
||||
eligibility,
|
||||
requestId: null, // Always null as field is not used
|
||||
requestId:
|
||||
typeof requestIdRaw === "string" && requestIdRaw.trim().length > 0
|
||||
? requestIdRaw.trim()
|
||||
: null,
|
||||
requestedAt,
|
||||
checkedAt,
|
||||
notes: null, // Always null as field is not used
|
||||
notes: typeof notesRaw === "string" && notesRaw.trim().length > 0 ? notesRaw.trim() : null,
|
||||
});
|
||||
}
|
||||
|
||||
// Note: createEligibilityCaseOrTask was removed - now using this.caseService.createEligibilityCase()
|
||||
// which links the Case to the Opportunity
|
||||
|
||||
private async updateAccountEligibilityRequestState(sfAccountId: string): Promise<void> {
|
||||
private async updateAccountEligibilityRequestState(
|
||||
sfAccountId: string,
|
||||
params?: { caseId?: string; notes?: string }
|
||||
): Promise<void> {
|
||||
const statusField = assertSoqlFieldName(
|
||||
this.config.get<string>("ACCOUNT_INTERNET_ELIGIBILITY_STATUS_FIELD") ??
|
||||
"Internet_Eligibility_Status__c",
|
||||
@ -499,17 +499,45 @@ export class InternetServicesService extends BaseServicesService {
|
||||
"Internet_Eligibility_Request_Date_Time__c",
|
||||
"ACCOUNT_INTERNET_ELIGIBILITY_REQUESTED_AT_FIELD"
|
||||
);
|
||||
const caseIdField = assertSoqlFieldName(
|
||||
this.config.get<string>("ACCOUNT_INTERNET_ELIGIBILITY_CASE_ID_FIELD") ??
|
||||
"Internet_Eligibility_Case_Id__c",
|
||||
"ACCOUNT_INTERNET_ELIGIBILITY_CASE_ID_FIELD"
|
||||
);
|
||||
const notesField = assertSoqlFieldName(
|
||||
this.config.get<string>("ACCOUNT_INTERNET_ELIGIBILITY_NOTES_FIELD") ??
|
||||
"Internet_Eligibility_Notes__c",
|
||||
"ACCOUNT_INTERNET_ELIGIBILITY_NOTES_FIELD"
|
||||
);
|
||||
|
||||
const update = this.sf.sobject("Account")?.update;
|
||||
if (!update) {
|
||||
throw new Error("Salesforce Account update method not available");
|
||||
}
|
||||
|
||||
await update({
|
||||
const basePayload: { Id: string } & Record<string, unknown> = {
|
||||
Id: sfAccountId,
|
||||
[statusField]: "Pending",
|
||||
[requestedAtField]: new Date().toISOString(),
|
||||
};
|
||||
const extendedPayload: { Id: string } & Record<string, unknown> = { ...basePayload };
|
||||
if (params?.caseId) {
|
||||
extendedPayload[caseIdField] = params.caseId;
|
||||
}
|
||||
if (typeof params?.notes === "string") {
|
||||
extendedPayload[notesField] = params.notes.trim().length > 0 ? params.notes.trim() : null;
|
||||
}
|
||||
|
||||
try {
|
||||
await update(extendedPayload);
|
||||
} catch (error) {
|
||||
// Hardening: If optional fields fail (schema/FLS), still set core Pending fields.
|
||||
this.logger.warn("Eligibility Account update failed (extended); retrying base update", {
|
||||
sfAccountIdTail: sfAccountId.slice(-4),
|
||||
error: extractErrorMessage(error),
|
||||
});
|
||||
await update(basePayload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -18,6 +18,7 @@ import { WhmcsClientService } from "@bff/integrations/whmcs/services/whmcs-clien
|
||||
import { MappingsService } from "@bff/modules/id-mappings/mappings.service.js";
|
||||
import { SalesforceCaseService } from "@bff/integrations/salesforce/services/salesforce-case.service.js";
|
||||
import { SalesforceOpportunityService } from "@bff/integrations/salesforce/services/salesforce-opportunity.service.js";
|
||||
import { SALESFORCE_CASE_ORIGIN } from "@customer-portal/domain/support/providers";
|
||||
import { EmailService } from "@bff/infra/email/email.service.js";
|
||||
import { NotificationService } from "@bff/modules/notifications/notifications.service.js";
|
||||
import type {
|
||||
@ -225,16 +226,35 @@ export class InternetCancellationService {
|
||||
this.logger.warn("Could not find Opportunity for subscription", { subscriptionId });
|
||||
}
|
||||
|
||||
// Create Salesforce Case for cancellation
|
||||
const caseId = await this.caseService.createCancellationCase({
|
||||
// Build description with all form data
|
||||
const descriptionLines = [
|
||||
`Cancellation Request from Portal`,
|
||||
``,
|
||||
`Product Type: Internet`,
|
||||
`WHMCS Service ID: ${subscriptionId}`,
|
||||
`Cancellation Month: ${request.cancellationMonth}`,
|
||||
`Service End Date: ${cancellationDate}`,
|
||||
``,
|
||||
];
|
||||
|
||||
if (request.alternativeEmail) {
|
||||
descriptionLines.push(`Alternative Contact Email: ${request.alternativeEmail}`);
|
||||
}
|
||||
|
||||
if (request.comments) {
|
||||
descriptionLines.push(``, `Customer Comments:`, request.comments);
|
||||
}
|
||||
|
||||
descriptionLines.push(``, `Submitted: ${new Date().toISOString()}`);
|
||||
|
||||
// Create Salesforce Case for cancellation (internal workflow case)
|
||||
const { id: caseId } = await this.caseService.createCase({
|
||||
accountId: sfAccountId,
|
||||
opportunityId: opportunityId || undefined,
|
||||
whmcsServiceId: subscriptionId,
|
||||
productType: "Internet",
|
||||
cancellationMonth: request.cancellationMonth,
|
||||
cancellationDate,
|
||||
alternativeEmail: request.alternativeEmail || undefined,
|
||||
comments: request.comments,
|
||||
subject: `Cancellation Request - Internet (${request.cancellationMonth})`,
|
||||
description: descriptionLines.join("\n"),
|
||||
origin: SALESFORCE_CASE_ORIGIN.PORTAL_NOTIFICATION,
|
||||
priority: "High",
|
||||
});
|
||||
|
||||
this.logger.log("Cancellation case created", {
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
type CreateCaseResponse,
|
||||
type PublicContactRequest,
|
||||
} from "@customer-portal/domain/support";
|
||||
import { SALESFORCE_CASE_ORIGIN } from "@customer-portal/domain/support/providers";
|
||||
import { SalesforceCaseService } from "@bff/integrations/salesforce/services/salesforce-case.service.js";
|
||||
import { MappingsService } from "@bff/modules/id-mappings/mappings.service.js";
|
||||
import { extractErrorMessage } from "@bff/core/utils/error.util.js";
|
||||
@ -110,9 +111,9 @@ export class SupportService {
|
||||
const result = await this.caseService.createCase({
|
||||
subject: request.subject,
|
||||
description: request.description,
|
||||
category: request.category,
|
||||
priority: request.priority,
|
||||
accountId,
|
||||
origin: SALESFORCE_CASE_ORIGIN.PORTAL_SUPPORT,
|
||||
});
|
||||
|
||||
this.logger.log("Support case created", {
|
||||
|
||||
@ -3,6 +3,8 @@ import { ConfigService } from "@nestjs/config";
|
||||
import { Logger } from "nestjs-pino";
|
||||
import { SalesforceConnection } from "@bff/integrations/salesforce/services/salesforce-connection.service.js";
|
||||
import { MappingsService } from "@bff/modules/id-mappings/mappings.service.js";
|
||||
import { SalesforceCaseService } from "@bff/integrations/salesforce/services/salesforce-case.service.js";
|
||||
import { SALESFORCE_CASE_ORIGIN } from "@customer-portal/domain/support/providers";
|
||||
import {
|
||||
assertSalesforceId,
|
||||
assertSoqlFieldName,
|
||||
@ -34,6 +36,7 @@ export class ResidenceCardService {
|
||||
constructor(
|
||||
private readonly sf: SalesforceConnection,
|
||||
private readonly mappings: MappingsService,
|
||||
private readonly caseService: SalesforceCaseService,
|
||||
private readonly config: ConfigService,
|
||||
@Inject(Logger) private readonly logger: Logger
|
||||
) {}
|
||||
@ -98,7 +101,9 @@ export class ResidenceCardService {
|
||||
: null;
|
||||
|
||||
const fileMeta =
|
||||
status === "not_submitted" ? null : await this.getLatestAccountFileMetadata(sfAccountId);
|
||||
status === "not_submitted"
|
||||
? null
|
||||
: await this.getLatestIdVerificationFileMetadata(sfAccountId);
|
||||
|
||||
const payload = {
|
||||
status,
|
||||
@ -149,6 +154,33 @@ export class ResidenceCardService {
|
||||
}
|
||||
const sfAccountId = assertSalesforceId(mapping.sfAccountId, "sfAccountId");
|
||||
|
||||
// Create an internal workflow Case for CS to track this submission.
|
||||
// (No lock/dedupe: multiple submissions may create multiple cases by design.)
|
||||
const subject = "ID verification review (Portal)";
|
||||
const descriptionLines: string[] = [
|
||||
"Portal ID verification submitted (residence card).",
|
||||
"",
|
||||
`UserId: ${params.userId}`,
|
||||
// Email is not included here to reduce sensitive data in case bodies.
|
||||
`SalesforceAccountId: ${sfAccountId}`,
|
||||
"",
|
||||
`Filename: ${params.filename || "residence-card"}`,
|
||||
`MimeType: ${params.mimeType}`,
|
||||
`SizeBytes: ${params.sizeBytes}`,
|
||||
"",
|
||||
"The ID document is attached to this Case (see Files related list).",
|
||||
"",
|
||||
`SubmittedAt: ${new Date().toISOString()}`,
|
||||
];
|
||||
|
||||
const { id: caseId } = await this.caseService.createCase({
|
||||
accountId: sfAccountId,
|
||||
subject,
|
||||
description: descriptionLines.join("\n"),
|
||||
origin: SALESFORCE_CASE_ORIGIN.PORTAL_NOTIFICATION,
|
||||
});
|
||||
|
||||
// Upload file to Salesforce Files and attach to the Case
|
||||
const versionData = params.content.toString("base64");
|
||||
const extension = extname(params.filename || "").replace(/^\./, "");
|
||||
const title = basename(params.filename || "residence-card", extension ? `.${extension}` : "");
|
||||
@ -159,26 +191,33 @@ export class ResidenceCardService {
|
||||
}
|
||||
|
||||
try {
|
||||
// Attach file directly to the Case (not Account) so CS can see it in the Case's Files
|
||||
const result = await create({
|
||||
Title: title || "residence-card",
|
||||
PathOnClient: params.filename || "residence-card",
|
||||
VersionData: versionData,
|
||||
FirstPublishLocationId: sfAccountId,
|
||||
FirstPublishLocationId: caseId, // Attach to Case, not Account
|
||||
});
|
||||
const id = (result as { id?: unknown })?.id;
|
||||
if (typeof id !== "string" || id.trim().length === 0) {
|
||||
const contentVersionId = (result as { id?: unknown })?.id;
|
||||
if (typeof contentVersionId !== "string" || contentVersionId.trim().length === 0) {
|
||||
throw new DomainHttpException(
|
||||
ErrorCode.EXTERNAL_SERVICE_ERROR,
|
||||
HttpStatus.SERVICE_UNAVAILABLE
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log("ID verification file attached to Case", {
|
||||
caseIdTail: caseId.slice(-4),
|
||||
contentVersionId,
|
||||
filename: params.filename,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof DomainHttpException) {
|
||||
throw error;
|
||||
}
|
||||
this.logger.error("Failed to upload residence card to Salesforce Files", {
|
||||
userId: params.userId,
|
||||
sfAccountIdTail: sfAccountId.slice(-4),
|
||||
caseIdTail: caseId.slice(-4),
|
||||
error: extractErrorMessage(error),
|
||||
});
|
||||
throw new DomainHttpException(
|
||||
@ -239,17 +278,40 @@ export class ResidenceCardService {
|
||||
};
|
||||
}
|
||||
|
||||
private async getLatestAccountFileMetadata(accountId: string): Promise<{
|
||||
/**
|
||||
* Get the latest ID verification file metadata from Cases linked to the Account.
|
||||
*
|
||||
* Files are attached to ID verification Cases (not the Account directly).
|
||||
* We find the most recent Case with the ID verification subject and get its file.
|
||||
*/
|
||||
private async getLatestIdVerificationFileMetadata(accountId: string): Promise<{
|
||||
filename: string | null;
|
||||
mimeType: string | null;
|
||||
sizeBytes: number | null;
|
||||
submittedAt: string | null;
|
||||
} | null> {
|
||||
try {
|
||||
// Find the most recent ID verification case for this account
|
||||
const caseSoql = `
|
||||
SELECT Id
|
||||
FROM Case
|
||||
WHERE AccountId = '${accountId}'
|
||||
AND Origin = '${SALESFORCE_CASE_ORIGIN.PORTAL_NOTIFICATION}'
|
||||
AND Subject LIKE '%ID verification%'
|
||||
ORDER BY CreatedDate DESC
|
||||
LIMIT 1
|
||||
`;
|
||||
const caseRes = (await this.sf.query(caseSoql, {
|
||||
label: "verification:residence_card:latest_case",
|
||||
})) as SalesforceResponse<{ Id?: string }>;
|
||||
const caseId = caseRes.records?.[0]?.Id;
|
||||
if (!caseId) return null;
|
||||
|
||||
// Get files linked to that case
|
||||
const linkSoql = `
|
||||
SELECT ContentDocumentId
|
||||
FROM ContentDocumentLink
|
||||
WHERE LinkedEntityId = '${accountId}'
|
||||
WHERE LinkedEntityId = '${caseId}'
|
||||
ORDER BY SystemModstamp DESC
|
||||
LIMIT 1
|
||||
`;
|
||||
@ -292,7 +354,7 @@ export class ResidenceCardService {
|
||||
submittedAt,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.warn("Failed to load residence card file metadata from Salesforce", {
|
||||
this.logger.warn("Failed to load ID verification file metadata from Salesforce", {
|
||||
accountIdTail: accountId.slice(-4),
|
||||
error: extractErrorMessage(error),
|
||||
});
|
||||
|
||||
2
apps/portal/next-env.d.ts
vendored
2
apps/portal/next-env.d.ts
vendored
@ -1,6 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/types/routes.d.ts";
|
||||
import "./.next/dev/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
@ -128,6 +128,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "catalog:",
|
||||
"zod": "^4.2.1"
|
||||
"zod": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@ -69,6 +69,6 @@ export const SUPPORT_CASE_CATEGORY = {
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Portal Website origin - used to filter and create portal cases
|
||||
* Portal support origin - used to filter and create customer-visible portal support cases
|
||||
*/
|
||||
export const PORTAL_CASE_ORIGIN = "Portal Website" as const;
|
||||
export const PORTAL_CASE_ORIGIN = "Portal Support" as const;
|
||||
|
||||
@ -123,7 +123,7 @@ export function buildCaseSelectFields(additionalFields: string[] = []): string[]
|
||||
* Build a SOQL query for fetching cases for an account.
|
||||
*
|
||||
* @param accountId - Salesforce Account ID
|
||||
* @param origin - Case origin to filter by (e.g., "Portal Website")
|
||||
* @param origin - Case origin to filter by (e.g., "Portal Support")
|
||||
* @param additionalFields - Optional additional fields to include
|
||||
* @returns SOQL query string
|
||||
*/
|
||||
|
||||
@ -168,8 +168,11 @@ export type SalesforceCaseCreatePayload = z.infer<typeof salesforceCaseCreatePay
|
||||
* API Names from Salesforce Setup > Object Manager > Case > Fields > Case Origin
|
||||
*/
|
||||
export const SALESFORCE_CASE_ORIGIN = {
|
||||
// Portal origin (used for portal-created cases)
|
||||
PORTAL_WEBSITE: "Portal Website",
|
||||
// Customer-visible support cases created from the portal support UI
|
||||
PORTAL_SUPPORT: "Portal Support",
|
||||
|
||||
// Internal portal workflow cases (eligibility, verification, cancellations)
|
||||
PORTAL_NOTIFICATION: "Portal Notification",
|
||||
|
||||
// Phone/Email origins
|
||||
PHONE: "電話", // Japanese: Phone
|
||||
|
||||
33
pnpm-lock.yaml
generated
33
pnpm-lock.yaml
generated
@ -14,8 +14,8 @@ catalogs:
|
||||
specifier: 5.9.3
|
||||
version: 5.9.3
|
||||
zod:
|
||||
specifier: 4.1.13
|
||||
version: 4.1.13
|
||||
specifier: 4.2.1
|
||||
version: 4.2.1
|
||||
|
||||
overrides:
|
||||
js-yaml: ">=4.1.1"
|
||||
@ -64,7 +64,7 @@ importers:
|
||||
dependencies:
|
||||
"@customer-portal/domain":
|
||||
specifier: workspace:*
|
||||
version: file:packages/domain(zod@4.1.13)
|
||||
version: link:../../packages/domain
|
||||
"@nestjs/bullmq":
|
||||
specifier: ^11.0.4
|
||||
version: 11.0.4(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(bullmq@5.65.1)
|
||||
@ -121,7 +121,7 @@ importers:
|
||||
version: 4.5.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(pino-http@11.0.0)(pino@10.1.0)(rxjs@7.8.2)
|
||||
nestjs-zod:
|
||||
specifier: ^5.0.1
|
||||
version: 5.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/swagger@11.2.3(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2))(rxjs@7.8.2)(zod@4.1.13)
|
||||
version: 5.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/swagger@11.2.3(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2))(rxjs@7.8.2)(zod@4.2.1)
|
||||
p-queue:
|
||||
specifier: ^9.0.1
|
||||
version: 9.0.1
|
||||
@ -151,7 +151,7 @@ importers:
|
||||
version: 5.0.1(express@5.1.0)
|
||||
zod:
|
||||
specifier: "catalog:"
|
||||
version: 4.1.13
|
||||
version: 4.2.1
|
||||
devDependencies:
|
||||
"@nestjs/cli":
|
||||
specifier: ^11.0.14
|
||||
@ -260,7 +260,7 @@ importers:
|
||||
specifier: "catalog:"
|
||||
version: 5.9.3
|
||||
zod:
|
||||
specifier: ^4.2.1
|
||||
specifier: "catalog:"
|
||||
version: 4.2.1
|
||||
|
||||
packages:
|
||||
@ -508,11 +508,6 @@ packages:
|
||||
}
|
||||
engines: { node: ">=0.1.90" }
|
||||
|
||||
"@customer-portal/domain@file:packages/domain":
|
||||
resolution: { directory: packages/domain, type: directory }
|
||||
peerDependencies:
|
||||
zod: 4.1.13
|
||||
|
||||
"@discoveryjs/json-ext@0.5.7":
|
||||
resolution:
|
||||
{
|
||||
@ -7513,12 +7508,6 @@ packages:
|
||||
peerDependencies:
|
||||
zod: ^3.25.0 || ^4.0.0
|
||||
|
||||
zod@4.1.13:
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==,
|
||||
}
|
||||
|
||||
zod@4.2.1:
|
||||
resolution:
|
||||
{
|
||||
@ -7729,10 +7718,6 @@ snapshots:
|
||||
"@colors/colors@1.5.0":
|
||||
optional: true
|
||||
|
||||
"@customer-portal/domain@file:packages/domain(zod@4.1.13)":
|
||||
dependencies:
|
||||
zod: 4.1.13
|
||||
|
||||
"@discoveryjs/json-ext@0.5.7": {}
|
||||
|
||||
"@electric-sql/pglite-socket@0.0.6(@electric-sql/pglite@0.3.2)":
|
||||
@ -10757,12 +10742,12 @@ snapshots:
|
||||
pino-http: 11.0.0
|
||||
rxjs: 7.8.2
|
||||
|
||||
nestjs-zod@5.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/swagger@11.2.3(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2))(rxjs@7.8.2)(zod@4.1.13):
|
||||
nestjs-zod@5.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/swagger@11.2.3(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2))(rxjs@7.8.2)(zod@4.2.1):
|
||||
dependencies:
|
||||
"@nestjs/common": 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||
deepmerge: 4.3.1
|
||||
rxjs: 7.8.2
|
||||
zod: 4.1.13
|
||||
zod: 4.2.1
|
||||
optionalDependencies:
|
||||
"@nestjs/swagger": 11.2.3(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)
|
||||
|
||||
@ -11928,8 +11913,6 @@ snapshots:
|
||||
dependencies:
|
||||
zod: 4.2.1
|
||||
|
||||
zod@4.1.13: {}
|
||||
|
||||
zod@4.2.1: {}
|
||||
|
||||
zustand@5.0.9(@types/react@19.2.7)(react@19.2.3):
|
||||
|
||||
@ -5,4 +5,4 @@ packages:
|
||||
catalog:
|
||||
"@types/node": 24.10.3
|
||||
typescript: 5.9.3
|
||||
zod: 4.1.13
|
||||
zod: 4.2.1
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user