From c9fdbf7a4a630848e1cceb36283d616884b35fb3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 21 Feb 2026 11:23:56 +0000 Subject: [PATCH 1/6] Initial plan From 20d17f5129dc7fd92326e7e9b8065007578d9193 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 21 Feb 2026 11:41:46 +0000 Subject: [PATCH 2/6] Add type casts in hook helper functions for HookContext properties MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After changing ctx: any to ctx: HookContext in helper functions, TypeScript errors occurred because HookContext.result is unknown, HookContext.ql is unknown, and HookContext.input/previous use Record. Added minimal type casts at variable assignment points: - ctx.result → ctx.result as Record - ctx.input?.doc || ctx.result → (...) as Record - ctx.ql.find/doc.create/etc → (ctx.ql as any).find/doc.create/etc - ctx.previous.Field → (ctx.previous as Record).Field Files fixed: quote.hook.ts, pricebook.hook.ts, product.hook.ts, opportunity.hook.ts, offer.hook.ts, performance_review.hook.ts, roi.hook.ts, knowledge.hook.ts, campaign.hook.ts, employee.hook.ts Reduces TypeScript errors from 300 to 1 (pre-existing unrelated error). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/crm/src/hooks/opportunity.hook.ts | 36 ++++----- packages/hr/src/hooks/employee.hook.ts | 20 ++--- packages/hr/src/hooks/offer.hook.ts | 52 ++++++------- .../hr/src/hooks/performance_review.hook.ts | 42 +++++----- packages/marketing/src/hooks/campaign.hook.ts | 16 ++-- packages/marketing/src/hooks/roi.hook.ts | 8 +- packages/products/src/hooks/pricebook.hook.ts | 76 ++++++++++--------- packages/products/src/hooks/product.hook.ts | 64 +++++++++------- packages/products/src/hooks/quote.hook.ts | 72 +++++++++--------- packages/support/src/hooks/knowledge.hook.ts | 24 +++--- 10 files changed, 213 insertions(+), 197 deletions(-) diff --git a/packages/crm/src/hooks/opportunity.hook.ts b/packages/crm/src/hooks/opportunity.hook.ts index b26972e48..acb52360f 100644 --- a/packages/crm/src/hooks/opportunity.hook.ts +++ b/packages/crm/src/hooks/opportunity.hook.ts @@ -82,9 +82,9 @@ const OpportunityStageChange: Hook = { /** * Handle Closed Won automation */ -async function handleClosedWon(ctx: any): Promise { +async function handleClosedWon(ctx: HookContext): Promise { console.log('✅ Processing Closed Won automation...'); - const opportunity = ctx.result; + const opportunity = ctx.result as Record; if (!opportunity.AccountId) { console.error('❌ Cannot process: Opportunity has no AccountId'); @@ -96,7 +96,7 @@ async function handleClosedWon(ctx: any): Promise { // 1. Create Contract let contractId; try { - const contract = await ctx.ql.doc.create('contract', { + const contract = await (ctx.ql as any).doc.create('contract', { AccountId: opportunity.AccountId, OpportunityId: opportunity.Id, Status: 'draft', @@ -118,7 +118,7 @@ async function handleClosedWon(ctx: any): Promise { // 2. Update Account Status try { - await ctx.ql.doc.update('account', opportunity.AccountId, { + await (ctx.ql as any).doc.update('account', opportunity.AccountId, { CustomerStatus: 'active_customer' }); console.log('✅ Account status updated to Active Customer'); @@ -129,7 +129,7 @@ async function handleClosedWon(ctx: any): Promise { // 3. Log activity try { - await ctx.ql.doc.create('activity', { + await (ctx.ql as any).doc.create('activity', { Subject: `Deal Won: ${opportunity.Name}`, Type: 'Milestone', Status: 'completed', @@ -160,9 +160,9 @@ async function handleClosedWon(ctx: any): Promise { } } -async function handleClosedLost(ctx: any): Promise { +async function handleClosedLost(ctx: HookContext): Promise { console.log('❌ Processing Closed Lost automation...'); - const opportunity = ctx.result; + const opportunity = ctx.result as Record; if (!opportunity.AccountId) { return; @@ -170,7 +170,7 @@ async function handleClosedLost(ctx: any): Promise { // Log activity for lost opportunity try { - await ctx.ql.doc.create('activity', { + await (ctx.ql as any).doc.create('activity', { Subject: `Deal Lost: ${opportunity.Name}`, Type: 'Milestone', Status: 'completed', @@ -192,12 +192,12 @@ async function handleClosedLost(ctx: any): Promise { /** * Log activity when stage changes */ -async function logStageChange(ctx: any): Promise { +async function logStageChange(ctx: HookContext): Promise { try { - const opportunity = ctx.result; - const oldStage = ctx.previous?.Stage || 'unknown'; - await ctx.ql.doc.create('activity', { - Subject: `Opportunity Stage Change: ${oldStage} → ${ctx.result.Stage}`, + const opportunity = ctx.result as Record; + const oldStage = (ctx.previous as Record)?.Stage || 'unknown'; + await (ctx.ql as any).doc.create('activity', { + Subject: `Opportunity Stage Change: ${oldStage} → ${(ctx.result as Record).Stage}`, Type: 'Stage Change', Status: 'completed', Priority: 'normal', @@ -205,7 +205,7 @@ async function logStageChange(ctx: any): Promise { WhatId: opportunity.Id, OwnerId: ctx.user.id, ActivityDate: new Date().toISOString().split('T')[0], - Description: `Opportunity stage changed from "${oldStage}" to "${ctx.result.Stage}"` + Description: `Opportunity stage changed from "${oldStage}" to "${(ctx.result as Record).Stage}"` }); } catch (error) { console.error('❌ Failed to log stage change activity:', error); @@ -215,8 +215,8 @@ async function logStageChange(ctx: any): Promise { /** * Validate required fields for advanced stages */ -async function validateStageRequirements(ctx: any): Promise { - const opportunity = ctx.result; +async function validateStageRequirements(ctx: HookContext): Promise { + const opportunity = ctx.result as Record; const stage = opportunity.Stage; const warnings: string[] = []; @@ -252,13 +252,13 @@ async function validateStageRequirements(ctx: any): Promise { /** * Helper: Count related quotes */ -async function countRelatedQuotes(ctx: any, opportunityId: string): Promise { +async function countRelatedQuotes(ctx: HookContext, opportunityId: string): Promise { // Check if quote object exists first (it's in products package) try { // In a real monorepo with strict boundaries, we might use a decoupled service. // Here we assume the broker can find 'quote' across packages. // Mocking for now since we don't have the full runtime - const quotes = await ctx.ql.find('quote', { + const quotes = await (ctx.ql as any).find('quote', { filters: [['opportunity', '=', opportunityId]] }); return quotes.length; diff --git a/packages/hr/src/hooks/employee.hook.ts b/packages/hr/src/hooks/employee.hook.ts index 6bdd88b8f..0675e4fcf 100644 --- a/packages/hr/src/hooks/employee.hook.ts +++ b/packages/hr/src/hooks/employee.hook.ts @@ -40,14 +40,14 @@ const EmployeeOnboardingTrigger: Hook = { /** * Create onboarding record for new employee */ -async function createOnboardingRecord(employee: any, ctx: any): Promise { +async function createOnboardingRecord(employee: Record, ctx: HookContext): Promise { try { // Calculate onboarding completion date (typically 30-90 days) const startDate = new Date(employee.hire_date || new Date()); const targetDate = new Date(startDate); targetDate.setDate(targetDate.getDate() + 90); // 90-day onboarding - await ctx.ql.doc.create('onboarding', { + await (ctx.ql as any).doc.create('onboarding', { employee_id: employee.id, start_date: startDate.toISOString().split('T')[0], target_completion_date: targetDate.toISOString().split('T')[0], @@ -66,7 +66,7 @@ async function createOnboardingRecord(employee: any, ctx: any): Promise { /** * Notify manager about new team member */ -async function notifyManager(employee: any, ctx: any): Promise { +async function notifyManager(employee: Record, ctx: HookContext): Promise { try { if (!employee.manager_id) { console.log('⚠️ No manager assigned, skipping notification'); @@ -86,13 +86,13 @@ async function notifyManager(employee: any, ctx: any): Promise { /** * Create initial goals for probation period */ -async function createProbationGoals(employee: any, ctx: any): Promise { +async function createProbationGoals(employee: Record, ctx: HookContext): Promise { try { const hireDate = new Date(employee.hire_date || new Date()); const probationEndDate = new Date(hireDate); probationEndDate.setDate(probationEndDate.getDate() + 90); // 90-day probation - await ctx.ql.doc.create('goal', { + await (ctx.ql as any).doc.create('goal', { employee_id: employee.id, goal_name: `Complete Onboarding - ${employee.full_name}`, description: 'Successfully complete all onboarding tasks and probation requirements', @@ -157,7 +157,7 @@ const EmployeeStatusChangeTrigger: Hook = { /** * Handle employee activation */ -async function handleActivation(employee: any, ctx: any): Promise { +async function handleActivation(employee: Record, ctx: HookContext): Promise { console.log(`✅ Employee ${employee.full_name} activated`); console.debug(`[employee.hook] Activation pending for ${employee.full_name}: enable system accounts, grant access permissions, and add to team channels`); @@ -166,7 +166,7 @@ async function handleActivation(employee: any, ctx: any): Promise { /** * Handle employee termination */ -async function handleTermination(employee: any, ctx: any): Promise { +async function handleTermination(employee: Record, ctx: HookContext): Promise { console.log(`👋 Processing termination for ${employee.full_name}`); try { @@ -191,7 +191,7 @@ async function handleTermination(employee: any, ctx: any): Promise { /** * Handle leave start */ -async function handleLeaveStart(employee: any, ctx: any): Promise { +async function handleLeaveStart(employee: Record, ctx: HookContext): Promise { console.log(`🏖️ Employee ${employee.full_name} started leave`); console.debug(`[employee.hook] Leave process pending for ${employee.full_name}: update team calendars, set up out-of-office auto-responder, and reassign urgent tasks`); @@ -201,10 +201,10 @@ async function handleLeaveStart(employee: any, ctx: any): Promise { * Log employee status change */ async function logEmployeeStatusChange( - employee: any, + employee: Record, oldStatus: string, newStatus: string, - ctx: any + ctx: HookContext ): Promise { try { console.log(`📝 Logging status change: ${oldStatus} → ${newStatus} for ${employee.full_name}`); diff --git a/packages/hr/src/hooks/offer.hook.ts b/packages/hr/src/hooks/offer.hook.ts index 5adef5527..6e1155aaa 100644 --- a/packages/hr/src/hooks/offer.hook.ts +++ b/packages/hr/src/hooks/offer.hook.ts @@ -47,12 +47,12 @@ const OfferCreationTrigger: Hook = { /** * Generate unique offer number */ -async function generateOfferNumber(ctx: any): Promise { +async function generateOfferNumber(ctx: HookContext): Promise { const year = new Date().getFullYear(); const month = String(new Date().getMonth() + 1).padStart(2, '0'); // Find latest offer number for this year/month - const latestOffers = await ctx.ql.find('offer', { + const latestOffers = await (ctx.ql as any).find('offer', { filters: [ ['offer_number', 'like', `OFF-${year}${month}%`] ], @@ -123,23 +123,23 @@ const OfferStatusChangeTrigger: Hook = { /** * Handle offer sent */ -async function handleOfferSent(offer: any, ctx: any): Promise { +async function handleOfferSent(offer: Record, ctx: HookContext): Promise { console.log(`📧 Offer ${offer.offer_number} sent to candidate`); try { // Update candidate status - await ctx.ql.doc.update('candidate', offer.candidate_id, { + await (ctx.ql as any).doc.update('candidate', offer.candidate_id, { status: 'Offer Sent' }); // Update application status - await ctx.ql.doc.update('application', offer.application_id, { + await (ctx.ql as any).doc.update('application', offer.application_id, { status: 'Offer Sent' }); // Set sent date if not already set if (!offer.sent_date) { - await ctx.ql.doc.update('offer', offer.id, { + await (ctx.ql as any).doc.update('offer', offer.id, { sent_date: new Date().toISOString().split('T')[0] }); } @@ -156,23 +156,23 @@ async function handleOfferSent(offer: any, ctx: any): Promise { /** * Handle offer accepted */ -async function handleOfferAccepted(offer: any, ctx: any): Promise { +async function handleOfferAccepted(offer: Record, ctx: HookContext): Promise { console.log(`🎉 Offer ${offer.offer_number} accepted by candidate`); try { // Update candidate status - await ctx.ql.doc.update('candidate', offer.candidate_id, { + await (ctx.ql as any).doc.update('candidate', offer.candidate_id, { status: 'hired' }); // Update application status - await ctx.ql.doc.update('application', offer.application_id, { + await (ctx.ql as any).doc.update('application', offer.application_id, { status: 'hired' }); // Set acceptance date if not already set if (!offer.acceptance_date) { - await ctx.ql.doc.update('offer', offer.id, { + await (ctx.ql as any).doc.update('offer', offer.id, { acceptance_date: new Date().toISOString().split('T')[0] }); } @@ -192,10 +192,10 @@ async function handleOfferAccepted(offer: any, ctx: any): Promise { /** * Create employee record from accepted offer */ -async function createEmployeeFromOffer(offer: any, ctx: any): Promise { +async function createEmployeeFromOffer(offer: Record, ctx: HookContext): Promise { try { // Fetch candidate details - const candidate = await ctx.ql.doc.get('candidate', offer.candidate_id, { + const candidate = await (ctx.ql as any).doc.get('candidate', offer.candidate_id, { fields: ['first_name', 'last_name', 'email', 'mobile_phone', 'date_of_birth', 'gender'] }); @@ -207,7 +207,7 @@ async function createEmployeeFromOffer(offer: any, ctx: any): Promise { const employeeNumber = await generateEmployeeNumber(ctx); // Create employee record - const employee = await ctx.ql.doc.create('employee', { + const employee = await (ctx.ql as any).doc.create('employee', { employee_number: employeeNumber, first_name: candidate.first_name, last_name: candidate.last_name, @@ -226,7 +226,7 @@ async function createEmployeeFromOffer(offer: any, ctx: any): Promise { }); // Link employee to offer - await ctx.ql.doc.update('offer', offer.id, { + await (ctx.ql as any).doc.update('offer', offer.id, { employee_id: employee.id }); @@ -241,11 +241,11 @@ async function createEmployeeFromOffer(offer: any, ctx: any): Promise { /** * Generate unique employee number */ -async function generateEmployeeNumber(ctx: any): Promise { +async function generateEmployeeNumber(ctx: HookContext): Promise { const year = new Date().getFullYear(); // Find latest employee number for this year - const latestEmployees = await ctx.ql.find('employee', { + const latestEmployees = await (ctx.ql as any).find('employee', { filters: [ ['employee_number', 'like', `EMP${year}%`] ], @@ -266,23 +266,23 @@ async function generateEmployeeNumber(ctx: any): Promise { /** * Handle offer rejected */ -async function handleOfferRejected(offer: any, ctx: any): Promise { +async function handleOfferRejected(offer: Record, ctx: HookContext): Promise { console.log(`❌ Offer ${offer.offer_number} rejected by candidate`); try { // Update candidate status - await ctx.ql.doc.update('candidate', offer.candidate_id, { + await (ctx.ql as any).doc.update('candidate', offer.candidate_id, { status: 'Offer Rejected' }); // Update application status - await ctx.ql.doc.update('application', offer.application_id, { + await (ctx.ql as any).doc.update('application', offer.application_id, { status: 'rejected' }); // Set rejection date if not already set if (!offer.rejection_date) { - await ctx.ql.doc.update('offer', offer.id, { + await (ctx.ql as any).doc.update('offer', offer.id, { rejection_date: new Date().toISOString().split('T')[0] }); } @@ -297,12 +297,12 @@ async function handleOfferRejected(offer: any, ctx: any): Promise { /** * Handle offer expired */ -async function handleOfferExpired(offer: any, ctx: any): Promise { +async function handleOfferExpired(offer: Record, ctx: HookContext): Promise { console.log(`⏰ Offer ${offer.offer_number} has expired`); try { // Update candidate status - await ctx.ql.doc.update('candidate', offer.candidate_id, { + await (ctx.ql as any).doc.update('candidate', offer.candidate_id, { status: 'Offer Expired' }); @@ -316,12 +316,12 @@ async function handleOfferExpired(offer: any, ctx: any): Promise { /** * Handle offer withdrawn */ -async function handleOfferWithdrawn(offer: any, ctx: any): Promise { +async function handleOfferWithdrawn(offer: Record, ctx: HookContext): Promise { console.log(`🚫 Offer ${offer.offer_number} has been withdrawn`); try { // Update candidate status - await ctx.ql.doc.update('candidate', offer.candidate_id, { + await (ctx.ql as any).doc.update('candidate', offer.candidate_id, { status: 'Offer Withdrawn' }); @@ -336,10 +336,10 @@ async function handleOfferWithdrawn(offer: any, ctx: any): Promise { * Log offer status change */ async function logOfferStatusChange( - offer: any, + offer: Record, oldStatus: string, newStatus: string, - ctx: any + ctx: HookContext ): Promise { try { console.log(`📝 Logging offer status change: ${oldStatus} → ${newStatus} for offer ${offer.offer_number}`); diff --git a/packages/hr/src/hooks/performance_review.hook.ts b/packages/hr/src/hooks/performance_review.hook.ts index 9e9460303..78fc14f54 100644 --- a/packages/hr/src/hooks/performance_review.hook.ts +++ b/packages/hr/src/hooks/performance_review.hook.ts @@ -53,7 +53,7 @@ const PerformanceReviewRatingTrigger: Hook = { /** * Check if all component scores are provided */ -function hasAllComponentScores(review: any): boolean { +function hasAllComponentScores(review: Record): boolean { const requiredScores = [ 'technical_skills_rating', 'leadership_rating', @@ -74,7 +74,7 @@ function hasAllComponentScores(review: any): boolean { /** * Calculate overall rating from weighted component scores */ -function calculateOverallRating(review: any): number { +function calculateOverallRating(review: Record): number { const weightedScore = (review.technical_skills_rating || 0) * RATING_WEIGHTS.TECHNICAL_SKILLS + (review.leadership_rating || 0) * RATING_WEIGHTS.LEADERSHIP + @@ -101,7 +101,7 @@ function determinePerformanceLevel(rating: number): string { /** * Calculate completion percentage */ -function calculateCompletionPercentage(review: any): number { +function calculateCompletionPercentage(review: Record): number { const requiredFields = [ 'technical_skills_rating', 'leadership_rating', @@ -176,7 +176,7 @@ const PerformanceReviewWorkflowTrigger: Hook = { /** * Handle review started */ -async function handleReviewStarted(review: any, ctx: any): Promise { +async function handleReviewStarted(review: Record, ctx: HookContext): Promise { console.log(`📝 Performance review started: ${review.review_name}`); try { @@ -190,12 +190,12 @@ async function handleReviewStarted(review: any, ctx: any): Promise { /** * Handle review submitted for approval */ -async function handleReviewSubmitted(review: any, ctx: any): Promise { +async function handleReviewSubmitted(review: Record, ctx: HookContext): Promise { console.log(`📤 Performance review submitted for approval: ${review.review_name}`); try { // Set submission date - await ctx.ql.doc.update('performance_review', review.id, { + await (ctx.ql as any).doc.update('performance_review', review.id, { submitted_date: new Date().toISOString().split('T')[0], submitted_by: ctx.session?.userId }); @@ -212,12 +212,12 @@ async function handleReviewSubmitted(review: any, ctx: any): Promise { /** * Handle review approved */ -async function handleReviewApproved(review: any, ctx: any): Promise { +async function handleReviewApproved(review: Record, ctx: HookContext): Promise { console.log(`✅ Performance review approved: ${review.review_name}`); try { // Set approval date - await ctx.ql.doc.update('performance_review', review.id, { + await (ctx.ql as any).doc.update('performance_review', review.id, { approved_date: new Date().toISOString().split('T')[0], approved_by: ctx.session?.userId }); @@ -240,7 +240,7 @@ async function handleReviewApproved(review: any, ctx: any): Promise { /** * Trigger compensation review for high performers */ -async function triggerCompensationReview(review: any, ctx: any): Promise { +async function triggerCompensationReview(review: Record, ctx: HookContext): Promise { try { console.log(`💰 Triggering compensation review for high performer: ${review.employee_id}`); @@ -254,7 +254,7 @@ async function triggerCompensationReview(review: any, ctx: any): Promise { /** * Create development goals from review feedback */ -async function createDevelopmentGoals(review: any, ctx: any): Promise { +async function createDevelopmentGoals(review: Record, ctx: HookContext): Promise { try { if (!review.development_plan || review.development_plan.trim() === '') { console.log('ℹ️ No development plan specified, skipping goal creation'); @@ -281,7 +281,7 @@ async function createDevelopmentGoals(review: any, ctx: any): Promise { } // Create development goal - await ctx.ql.doc.create('goal', { + await (ctx.ql as any).doc.create('goal', { employee_id: review.employee_id, goal_name: `Development Plan - ${review.review_period} Review`, description: review.development_plan, @@ -303,12 +303,12 @@ async function createDevelopmentGoals(review: any, ctx: any): Promise { /** * Handle review completed */ -async function handleReviewCompleted(review: any, ctx: any): Promise { +async function handleReviewCompleted(review: Record, ctx: HookContext): Promise { console.log(`🎉 Performance review completed: ${review.review_name}`); try { // Set completion date - await ctx.ql.doc.update('performance_review', review.id, { + await (ctx.ql as any).doc.update('performance_review', review.id, { completion_date: new Date().toISOString().split('T')[0] }); @@ -327,9 +327,9 @@ async function handleReviewCompleted(review: any, ctx: any): Promise { /** * Update employee record with review statistics */ -async function updateEmployeeReviewStats(review: any, ctx: any): Promise { +async function updateEmployeeReviewStats(review: Record, ctx: HookContext): Promise { try { - await ctx.ql.doc.update('employee', review.employee_id, { + await (ctx.ql as any).doc.update('employee', review.employee_id, { last_review_date: review.end_date, last_review_rating: review.overall_rating, last_review_level: review.performance_level @@ -345,12 +345,12 @@ async function updateEmployeeReviewStats(review: any, ctx: any): Promise { /** * Handle review rejected */ -async function handleReviewRejected(review: any, ctx: any): Promise { +async function handleReviewRejected(review: Record, ctx: HookContext): Promise { console.log(`❌ Performance review rejected: ${review.review_name}`); try { // Send back to reviewer for revision - await ctx.ql.doc.update('performance_review', review.id, { + await (ctx.ql as any).doc.update('performance_review', review.id, { status: 'in_progress' }); @@ -365,10 +365,10 @@ async function handleReviewRejected(review: any, ctx: any): Promise { * Log review status change */ async function logReviewStatusChange( - review: any, + review: Record, oldStatus: string, newStatus: string, - ctx: any + ctx: HookContext ): Promise { try { console.log(`📝 Logging review status change: ${oldStatus} → ${newStatus} for ${review.review_name}`); @@ -385,14 +385,14 @@ async function logReviewStatusChange( * Checks for upcoming due dates and sends reminders * (This would typically be called by a scheduled job, not a trigger) */ -export async function checkPerformanceReviewDueDates(ctx: any): Promise { +export async function checkPerformanceReviewDueDates(ctx: HookContext): Promise { try { const today = new Date(); const reminderDate = new Date(today); reminderDate.setDate(reminderDate.getDate() + 7); // 7 days before due // Find reviews due soon - const upcomingReviews = await ctx.ql.find('performance_review', { + const upcomingReviews = await (ctx.ql as any).find('performance_review', { filters: [ ['status', 'in', ['not_started', 'in_progress']], ['due_date', '<=', reminderDate.toISOString().split('T')[0]], diff --git a/packages/marketing/src/hooks/campaign.hook.ts b/packages/marketing/src/hooks/campaign.hook.ts index 13f029a1d..e0955b745 100644 --- a/packages/marketing/src/hooks/campaign.hook.ts +++ b/packages/marketing/src/hooks/campaign.hook.ts @@ -139,7 +139,7 @@ const CampaignStatusChangeTrigger: Hook = { /** * Handle campaign transition to In Progress */ -async function handleInProgressStatus(campaign: any, ctx: any): Promise { +async function handleInProgressStatus(campaign: Record, ctx: HookContext): Promise { console.log(`🚀 Campaign "${campaign.name}" started`); console.debug(`[campaign.hook] Campaign start pending for "${campaign.name}": member notifications, workflow activation, and start event logging`); @@ -148,7 +148,7 @@ async function handleInProgressStatus(campaign: any, ctx: any): Promise { /** * Handle campaign completion */ -async function handleCompletedStatus(campaign: any, ctx: any): Promise { +async function handleCompletedStatus(campaign: Record, ctx: HookContext): Promise { console.log(`✅ Campaign "${campaign.name}" completed`); console.debug(`[campaign.hook] Campaign completion pending for "${campaign.name}": final metrics calculation, report generation, data archival, and stakeholder notification`); @@ -157,7 +157,7 @@ async function handleCompletedStatus(campaign: any, ctx: any): Promise { /** * Handle campaign abortion */ -async function handleAbortedStatus(campaign: any, ctx: any): Promise { +async function handleAbortedStatus(campaign: Record, ctx: HookContext): Promise { console.log(`❌ Campaign "${campaign.name}" was aborted`); console.debug(`[campaign.hook] Campaign abort pending for "${campaign.name}": abort reason logging, scheduled activity cleanup, and team notification`); @@ -203,12 +203,12 @@ const CampaignDateValidationTrigger: Hook = { * Update campaign metrics from member engagement * Called by campaign_member hooks */ -export async function updateCampaignMetrics(campaignId: string, ctx: any): Promise { +export async function updateCampaignMetrics(campaignId: string, ctx: HookContext): Promise { try { console.log(`🔄 Updating campaign metrics for: ${campaignId}`); // Aggregate campaign member statistics - const members = await ctx.ql.find('campaign_member', { + const members = await (ctx.ql as any).find('campaign_member', { filters: [['campaign', '=', campaignId]] }); @@ -234,7 +234,7 @@ export async function updateCampaignMetrics(campaignId: string, ctx: any): Promi const unsubscribeRate = totalMembers > 0 ? (unsubscribed / totalMembers) * 100 : 0; // Update campaign with aggregated metrics - await ctx.ql.update('campaign', campaignId, { + await (ctx.ql as any).update('campaign', campaignId, { total_members: totalMembers, total_opened: opened, total_clicked: clicked, @@ -256,9 +256,9 @@ export async function updateCampaignMetrics(campaignId: string, ctx: any): Promi /** * Calculate cost per lead/opportunity/customer */ -export async function calculateCostMetrics(campaignId: string, ctx: any): Promise { +export async function calculateCostMetrics(campaignId: string, ctx: HookContext): Promise { try { - const campaign = await ctx.ql.findOne('campaign', { + const campaign = await (ctx.ql as any).findOne('campaign', { filters: [['id', '=', campaignId]] }); diff --git a/packages/marketing/src/hooks/roi.hook.ts b/packages/marketing/src/hooks/roi.hook.ts index a9c9b5bf9..d57c2b4ec 100644 --- a/packages/marketing/src/hooks/roi.hook.ts +++ b/packages/marketing/src/hooks/roi.hook.ts @@ -9,7 +9,7 @@ const CampaignROIHook: Hook = { name: 'CampaignROIHook', object: 'opportunity', // Listening to Opportunity events from Marketing package events: ['afterUpdate'], - handler: async (ctx: any) => { + handler: async (ctx: HookContext) => { try { const newOpp = ctx.result as Record; const oldOpp = ctx.previous as Record | undefined; @@ -42,18 +42,18 @@ const CampaignROIHook: Hook = { // Note: In a real concurrent system, use $inc operator. // objectstack/runtime usually supports partial updates. // If we can't use $inc, we risk race conditions, but for this prototype: - const campaigns = await ctx.ql.find('campaign', { filters: [['_id', '=', newOpp.campaign_id]] }); + const campaigns = await (ctx.ql as any).find('campaign', { filters: [['_id', '=', newOpp.campaign_id]] }); if (campaigns && campaigns.length > 0) { const campaign: any = campaigns[0]; const currentRevenue = campaign.actual_revenue || 0; - await ctx.ql.doc.update('campaign', newOpp.campaign_id, { + await (ctx.ql as any).doc.update('campaign', newOpp.campaign_id, { actual_revenue: currentRevenue + delta }); } } } catch (error) { - console.error(`❌ Error in CampaignROIHook for campaign ${ctx.result?.campaign_id}:`, error); + console.error(`❌ Error in CampaignROIHook for campaign ${(ctx.result as Record)?.campaign_id}:`, error); // Don't throw - allow opportunity update to complete even if ROI calculation fails } } diff --git a/packages/products/src/hooks/pricebook.hook.ts b/packages/products/src/hooks/pricebook.hook.ts index be8fc3f4c..3a513fb0f 100644 --- a/packages/products/src/hooks/pricebook.hook.ts +++ b/packages/products/src/hooks/pricebook.hook.ts @@ -44,8 +44,8 @@ const PricebookHook: Hook = { /** * Validate pricebook date ranges */ -async function validatePricebookDates(ctx: any): Promise { - const pricebook = ctx.input?.doc || ctx.result; +async function validatePricebookDates(ctx: HookContext): Promise { + const pricebook = (ctx.input?.doc || ctx.result) as Record; try { // Validate effective dates @@ -60,7 +60,7 @@ async function validatePricebookDates(ctx: any): Promise { // Check for overlapping pricebooks (if this is a standard pricebook) if (pricebook.IsStandard && pricebook.EffectiveDate) { - const overlapping = await ctx.ql.find('pricebook', { + const overlapping = await (ctx.ql as any).find('pricebook', { filters: [ ['IsStandard', '=', true], ['Status', '=', 'active'], @@ -84,8 +84,8 @@ async function validatePricebookDates(ctx: any): Promise { /** * Validate currency configuration */ -async function validateCurrencyConfiguration(ctx: any): Promise { - const pricebook = ctx.input?.doc || ctx.result; +async function validateCurrencyConfiguration(ctx: HookContext): Promise { + const pricebook = (ctx.input?.doc || ctx.result) as Record; try { // Validate currency code @@ -116,15 +116,17 @@ async function validateCurrencyConfiguration(ctx: any): Promise { /** * Handle effective date changes */ -async function handleEffectiveDateChange(ctx: any): Promise { +async function handleEffectiveDateChange(ctx: HookContext): Promise { if (!ctx.previous || !ctx.result) return; - const effectiveDateChanged = ctx.previous.EffectiveDate !== ctx.result.EffectiveDate; - const expirationDateChanged = ctx.previous.ExpirationDate !== ctx.result.ExpirationDate; + const prev = ctx.previous as Record; + const result = ctx.result as Record; + const effectiveDateChanged = prev.EffectiveDate !== result.EffectiveDate; + const expirationDateChanged = prev.ExpirationDate !== result.ExpirationDate; if (!effectiveDateChanged && !expirationDateChanged) return; - const pricebook = ctx.result; + const pricebook = result; const today = new Date(); today.setHours(0, 0, 0, 0); @@ -137,7 +139,7 @@ async function handleEffectiveDateChange(ctx: any): Promise { if (effectiveDateOnly <= today && pricebook.Status === 'draft') { console.log(`✅ Pricebook effective date reached, activating: ${pricebook.Name}`); - await ctx.ql.doc.update('pricebook', pricebook.Id, { + await (ctx.ql as any).doc.update('pricebook', pricebook.Id, { Status: 'active' }); } @@ -151,7 +153,7 @@ async function handleEffectiveDateChange(ctx: any): Promise { if (expirationDateOnly < today && pricebook.Status === 'active') { console.log(`⏰ Pricebook expired, deactivating: ${pricebook.Name}`); - await ctx.ql.doc.update('pricebook', pricebook.Id, { + await (ctx.ql as any).doc.update('pricebook', pricebook.Id, { Status: 'expired' }); } @@ -161,13 +163,13 @@ async function handleEffectiveDateChange(ctx: any): Promise { if (effectiveDateChanged || expirationDateChanged) { const changes = []; if (effectiveDateChanged) { - changes.push(`Effective Date: ${ctx.old.EffectiveDate} → ${pricebook.EffectiveDate}`); + changes.push(`Effective Date: ${prev.EffectiveDate} → ${pricebook.EffectiveDate}`); } if (expirationDateChanged) { - changes.push(`Expiration Date: ${ctx.old.ExpirationDate} → ${pricebook.ExpirationDate}`); + changes.push(`Expiration Date: ${prev.ExpirationDate} → ${pricebook.ExpirationDate}`); } - await ctx.ql.doc.create('activity', { + await (ctx.ql as any).doc.create('activity', { Subject: `Pricebook Dates Updated: ${pricebook.Name}`, Type: 'Pricebook Update', Status: 'completed', @@ -186,16 +188,18 @@ async function handleEffectiveDateChange(ctx: any): Promise { /** * Handle pricebook status changes */ -async function handleStatusChange(ctx: any): Promise { +async function handleStatusChange(ctx: HookContext): Promise { if (!ctx.previous || !ctx.result) return; - const statusChanged = ctx.previous.Status !== ctx.result.Status; + const prev = ctx.previous as Record; + const result = ctx.result as Record; + const statusChanged = prev.Status !== result.Status; if (!statusChanged) return; - const pricebook = ctx.result; + const pricebook = result; try { - console.log(`🔄 Pricebook status changed from "${ctx.previous.Status}" to "${pricebook.Status}"`); + console.log(`🔄 Pricebook status changed from "${prev.Status}" to "${pricebook.Status}"`); // Handle activation if (pricebook.Status === 'active') { @@ -203,7 +207,7 @@ async function handleStatusChange(ctx: any): Promise { // Set effective date if not already set if (!pricebook.EffectiveDate) { - await ctx.ql.doc.update('pricebook', pricebook.Id, { + await (ctx.ql as any).doc.update('pricebook', pricebook.Id, { EffectiveDate: new Date().toISOString().split('T')[0] }); console.log('📅 Effective date set to today'); @@ -212,7 +216,7 @@ async function handleStatusChange(ctx: any): Promise { // If this is a standard pricebook, deactivate other standard pricebooks if (pricebook.IsStandard) { try { - const otherStandard = await ctx.ql.find('pricebook', { + const otherStandard = await (ctx.ql as any).find('pricebook', { filters: [ ['IsStandard', '=', true], ['Status', '=', 'active'], @@ -224,7 +228,7 @@ async function handleStatusChange(ctx: any): Promise { if (otherPricebooks.length > 0) { console.log(`⚠️ Deactivating ${otherPricebooks.length} other standard pricebook(s)`); for (const pb of otherPricebooks) { - await ctx.ql.doc.update('pricebook', pb.Id, { + await (ctx.ql as any).doc.update('pricebook', pb.Id, { Status: 'inactive', ExpirationDate: new Date().toISOString().split('T')[0] }); @@ -244,7 +248,7 @@ async function handleStatusChange(ctx: any): Promise { // Set expiration date if not already set if (!pricebook.ExpirationDate) { - await ctx.ql.doc.update('pricebook', pricebook.Id, { + await (ctx.ql as any).doc.update('pricebook', pricebook.Id, { ExpirationDate: new Date().toISOString().split('T')[0] }); } @@ -253,15 +257,15 @@ async function handleStatusChange(ctx: any): Promise { } // Log activity for status change - await ctx.ql.doc.create('activity', { - Subject: `Pricebook Status Changed: ${ctx.previous.Status} → ${pricebook.Status}`, + await (ctx.ql as any).doc.create('activity', { + Subject: `Pricebook Status Changed: ${prev.Status} → ${pricebook.Status}`, Type: 'Status Change', Status: 'completed', Priority: 'normal', WhatId: pricebook.Id, OwnerId: ctx.user.id, ActivityDate: new Date().toISOString().split('T')[0], - Description: `Pricebook "${pricebook.Name}" status changed from "${ctx.previous.Status}" to "${pricebook.Status}"` + Description: `Pricebook "${pricebook.Name}" status changed from "${prev.Status}" to "${pricebook.Status}"` }); } catch (error) { console.error(`[pricebook.hook] handleStatusChange failed:`, error); @@ -271,25 +275,27 @@ async function handleStatusChange(ctx: any): Promise { /** * Handle currency or exchange rate changes */ -async function handleCurrencyChange(ctx: any): Promise { +async function handleCurrencyChange(ctx: HookContext): Promise { if (!ctx.previous || !ctx.result) return; - const currencyChanged = ctx.previous.CurrencyCode !== ctx.result.CurrencyCode; - const rateChanged = ctx.previous.ExchangeRate !== ctx.result.ExchangeRate; + const prev = ctx.previous as Record; + const result = ctx.result as Record; + const currencyChanged = prev.CurrencyCode !== result.CurrencyCode; + const rateChanged = prev.ExchangeRate !== result.ExchangeRate; if (!currencyChanged && !rateChanged) return; - const pricebook = ctx.result; + const pricebook = result; try { if (currencyChanged) { - console.log(`💱 Currency changed from ${ctx.previous.CurrencyCode} to ${pricebook.CurrencyCode}`); + console.log(`💱 Currency changed from ${prev.CurrencyCode} to ${pricebook.CurrencyCode}`); - console.debug(`[pricebook.hook] Currency update pending: update pricebook entries and notify users about currency change from ${ctx.previous.CurrencyCode} to ${pricebook.CurrencyCode} for pricebook "${pricebook.Name}"`); + console.debug(`[pricebook.hook] Currency update pending: update pricebook entries and notify users about currency change from ${prev.CurrencyCode} to ${pricebook.CurrencyCode} for pricebook "${pricebook.Name}"`); } if (rateChanged) { - console.log(`💱 Exchange rate changed from ${ctx.previous.ExchangeRate} to ${pricebook.ExchangeRate}`); + console.log(`💱 Exchange rate changed from ${prev.ExchangeRate} to ${pricebook.ExchangeRate}`); // Recalculate all prices based on new exchange rate // This would update all PricebookEntry records @@ -300,13 +306,13 @@ async function handleCurrencyChange(ctx: any): Promise { if (currencyChanged || rateChanged) { const changes = []; if (currencyChanged) { - changes.push(`Currency: ${ctx.previous.CurrencyCode} → ${pricebook.CurrencyCode}`); + changes.push(`Currency: ${prev.CurrencyCode} → ${pricebook.CurrencyCode}`); } if (rateChanged) { - changes.push(`Exchange Rate: ${ctx.previous.ExchangeRate} → ${pricebook.ExchangeRate}`); + changes.push(`Exchange Rate: ${prev.ExchangeRate} → ${pricebook.ExchangeRate}`); } - await ctx.ql.doc.create('activity', { + await (ctx.ql as any).doc.create('activity', { Subject: `Pricebook Currency Updated: ${pricebook.Name}`, Type: 'Currency Update', Status: 'completed', diff --git a/packages/products/src/hooks/product.hook.ts b/packages/products/src/hooks/product.hook.ts index b732fc29c..daa6f50a9 100644 --- a/packages/products/src/hooks/product.hook.ts +++ b/packages/products/src/hooks/product.hook.ts @@ -44,13 +44,13 @@ const ProductHook: Hook = { /** * Validate product configuration */ -async function validateProductConfiguration(ctx: any): Promise { - const product = ctx.input?.doc || ctx.result; +async function validateProductConfiguration(ctx: HookContext): Promise { + const product = (ctx.input?.doc || ctx.result) as Record; try { // Ensure product code is unique if (product.ProductCode) { - const existing = await ctx.ql.find('product', { + const existing = await (ctx.ql as any).find('product', { filters: [ ['ProductCode', '=', product.ProductCode], ['Id', '!=', product.Id || ''] @@ -79,8 +79,8 @@ async function validateProductConfiguration(ctx: any): Promise { /** * Validate bundle configuration and dependencies */ -async function validateBundleConfiguration(ctx: any): Promise { - const product = ctx.input?.doc || ctx.result; +async function validateBundleConfiguration(ctx: HookContext): Promise { + const product = (ctx.input?.doc || ctx.result) as Record; try { // If this is a bundle product, validate bundle items @@ -108,16 +108,18 @@ async function validateBundleConfiguration(ctx: any): Promise { /** * Handle stock level changes */ -async function handleStockLevelChange(ctx: any): Promise { +async function handleStockLevelChange(ctx: HookContext): Promise { if (!ctx.previous || !ctx.result) return; - const stockChanged = ctx.previous.StockLevel !== ctx.result.StockLevel; + const prev = ctx.previous as Record; + const result = ctx.result as Record; + const stockChanged = prev.StockLevel !== result.StockLevel; if (!stockChanged) return; - const product = ctx.result; + const product = result; try { - console.log(`📊 Stock level changed from ${ctx.previous.StockLevel} to ${product.StockLevel}`); + console.log(`📊 Stock level changed from ${prev.StockLevel} to ${product.StockLevel}`); // Check if stock is low if (product.LowStockThreshold && product.StockLevel <= product.LowStockThreshold) { @@ -133,7 +135,7 @@ async function handleStockLevelChange(ctx: any): Promise { // Update product status to Out of Stock if (product.Status === 'active') { - await ctx.ql.doc.update('product', product.Id, { + await (ctx.ql as any).doc.update('product', product.Id, { Status: 'Out of Stock' }); console.log('🚫 Product status updated to Out of Stock'); @@ -141,8 +143,8 @@ async function handleStockLevelChange(ctx: any): Promise { } // If stock was replenished, reactivate product - if (ctx.previous.StockLevel === 0 && product.StockLevel > 0 && product.Status === 'Out of Stock') { - await ctx.ql.doc.update('product', product.Id, { + if (prev.StockLevel === 0 && product.StockLevel > 0 && product.Status === 'Out of Stock') { + await (ctx.ql as any).doc.update('product', product.Id, { Status: 'active' }); console.log('✅ Product reactivated after stock replenishment'); @@ -155,22 +157,24 @@ async function handleStockLevelChange(ctx: any): Promise { /** * Handle price changes */ -async function handlePriceChange(ctx: any): Promise { +async function handlePriceChange(ctx: HookContext): Promise { if (!ctx.previous || !ctx.result) return; - const listPriceChanged = ctx.previous.ListPrice !== ctx.result.ListPrice; - const costPriceChanged = ctx.previous.CostPrice !== ctx.result.CostPrice; + const prev = ctx.previous as Record; + const result = ctx.result as Record; + const listPriceChanged = prev.ListPrice !== result.ListPrice; + const costPriceChanged = prev.CostPrice !== result.CostPrice; if (!listPriceChanged && !costPriceChanged) return; - const product = ctx.result; + const product = result; try { if (listPriceChanged) { - console.log(`💰 List price changed from ${ctx.previous.ListPrice} to ${product.ListPrice}`); + console.log(`💰 List price changed from ${prev.ListPrice} to ${product.ListPrice}`); // Log price change activity - await ctx.ql.doc.create('activity', { + await (ctx.ql as any).doc.create('activity', { Subject: `Price Change: ${product.Name}`, Type: 'Price Update', Status: 'completed', @@ -178,17 +182,17 @@ async function handlePriceChange(ctx: any): Promise { WhatId: product.Id, OwnerId: ctx.user.id, ActivityDate: new Date().toISOString().split('T')[0], - Description: `List price changed from ${ctx.previous.ListPrice} to ${product.ListPrice}` + Description: `List price changed from ${prev.ListPrice} to ${product.ListPrice}` }); console.log('✅ Price change activity logged'); console.debug(`[product.hook] Pricebook update pending: update active pricebook entries for product ${product.Name} with new list price ${product.ListPrice}`); - console.debug(`[product.hook] Sales team notification pending: price change for product ${product.Name} from ${ctx.previous.ListPrice} to ${product.ListPrice}`); + console.debug(`[product.hook] Sales team notification pending: price change for product ${product.Name} from ${prev.ListPrice} to ${product.ListPrice}`); } if (costPriceChanged) { - console.log(`💵 Cost price changed from ${ctx.previous.CostPrice} to ${product.CostPrice}`); + console.log(`💵 Cost price changed from ${prev.CostPrice} to ${product.CostPrice}`); // Recalculate margin if (product.ListPrice && product.CostPrice) { @@ -208,16 +212,18 @@ async function handlePriceChange(ctx: any): Promise { /** * Handle product status changes */ -async function handleStatusChange(ctx: any): Promise { +async function handleStatusChange(ctx: HookContext): Promise { if (!ctx.previous || !ctx.result) return; - const statusChanged = ctx.previous.Status !== ctx.result.Status; + const prev = ctx.previous as Record; + const result = ctx.result as Record; + const statusChanged = prev.Status !== result.Status; if (!statusChanged) return; - const product = ctx.result; + const product = result; try { - console.log(`🔄 Product status changed from "${ctx.previous.Status}" to "${product.Status}"`); + console.log(`🔄 Product status changed from "${prev.Status}" to "${product.Status}"`); // Handle deactivation if (product.Status === 'inactive' || product.Status === 'discontinued') { @@ -227,7 +233,7 @@ async function handleStatusChange(ctx: any): Promise { } // Handle reactivation - if ((ctx.previous.Status === 'inactive' || ctx.previous.Status === 'discontinued') && + if ((prev.Status === 'inactive' || prev.Status === 'discontinued') && product.Status === 'active') { console.log(`✅ Product reactivated: ${product.Name}`); @@ -235,15 +241,15 @@ async function handleStatusChange(ctx: any): Promise { } // Log activity for status change - await ctx.ql.doc.create('activity', { - Subject: `Product Status Changed: ${ctx.previous.Status} → ${product.Status}`, + await (ctx.ql as any).doc.create('activity', { + Subject: `Product Status Changed: ${prev.Status} → ${product.Status}`, Type: 'Status Change', Status: 'completed', Priority: 'normal', WhatId: product.Id, OwnerId: ctx.user.id, ActivityDate: new Date().toISOString().split('T')[0], - Description: `Product "${product.Name}" status changed from "${ctx.previous.Status}" to "${product.Status}"` + Description: `Product "${product.Name}" status changed from "${prev.Status}" to "${product.Status}"` }); } catch (error) { console.error(`[product.hook] handleStatusChange failed:`, error); diff --git a/packages/products/src/hooks/quote.hook.ts b/packages/products/src/hooks/quote.hook.ts index 769d0c58e..bcee7a080 100644 --- a/packages/products/src/hooks/quote.hook.ts +++ b/packages/products/src/hooks/quote.hook.ts @@ -53,8 +53,8 @@ const QuotePricingHook: Hook = { /** * Calculate quote pricing based on line items and discounts */ -async function calculateQuotePricing(ctx: any): Promise { - const quote = ctx.input?.doc || ctx.result; +async function calculateQuotePricing(ctx: HookContext): Promise { + const quote = (ctx.input?.doc || ctx.result) as Record; try { // Calculate Discount Amount from Percent or vice versa @@ -88,8 +88,8 @@ async function calculateQuotePricing(ctx: any): Promise { /** * Determine approval requirements based on discount percentage */ -async function determineApprovalRequirements(ctx: any): Promise { - const quote = ctx.input?.doc || ctx.result; +async function determineApprovalRequirements(ctx: HookContext): Promise { + const quote = (ctx.input?.doc || ctx.result) as Record; const discountPercent = quote.DiscountPercent || 0; try { @@ -118,7 +118,7 @@ async function determineApprovalRequirements(ctx: any): Promise { // Set approval status to Pending if discount increased if (ctx.previous) { - const oldDiscount = ctx.previous.DiscountPercent || 0; + const oldDiscount = (ctx.previous as Record).DiscountPercent || 0; if (discountPercent > oldDiscount && quote.ApprovalStatus !== 'approved' && quote.ApprovalStatus !== 'pending') { quote.ApprovalStatus = 'pending'; console.log(`🔄 Quote requires Level ${approvalLevel} approval (${(discountPercent * 100).toFixed(1)}% discount)`); @@ -136,8 +136,8 @@ async function determineApprovalRequirements(ctx: any): Promise { /** * Validate margin protection rules */ -async function validateMarginProtection(ctx: any): Promise { - const quote = ctx.input?.doc || ctx.result; +async function validateMarginProtection(ctx: HookContext): Promise { + const quote = (ctx.input?.doc || ctx.result) as Record; try { // Calculate margin if we have cost information @@ -156,13 +156,13 @@ async function validateMarginProtection(ctx: any): Promise { /** * Initialize quote after creation */ -async function initializeQuote(ctx: any): Promise { - const quote = ctx.result; +async function initializeQuote(ctx: HookContext): Promise { + const quote = ctx.result as Record; try { // Log activity for new quote if (quote.AccountId) { - await ctx.ql.doc.create('activity', { + await (ctx.ql as any).doc.create('activity', { Subject: `New Quote Created: ${quote.Name}`, Type: 'Quote', Status: 'completed', @@ -182,7 +182,7 @@ async function initializeQuote(ctx: any): Promise { const quoteDate = new Date(quote.QuoteDate); const expirationDate = new Date(quoteDate.getTime() + validityDays * 24 * 60 * 60 * 1000); - await ctx.ql.doc.update('quote', quote.Id, { + await (ctx.ql as any).doc.update('quote', quote.Id, { ExpirationDate: expirationDate.toISOString().split('T')[0] }); console.log(`📅 Expiration date set to: ${expirationDate.toISOString().split('T')[0]}`); @@ -195,16 +195,18 @@ async function initializeQuote(ctx: any): Promise { /** * Handle quote status changes */ -async function handleQuoteStatusChange(ctx: any): Promise { +async function handleQuoteStatusChange(ctx: HookContext): Promise { if (!ctx.previous || !ctx.result) return; - const statusChanged = ctx.previous.Status !== ctx.result.Status; + const prev = ctx.previous as Record; + const result = ctx.result as Record; + const statusChanged = prev.Status !== result.Status; if (!statusChanged) return; - const quote = ctx.result; + const quote = result; try { - console.log(`🔄 Quote status changed from "${ctx.previous.Status}" to "${quote.Status}"`); + console.log(`🔄 Quote status changed from "${prev.Status}" to "${quote.Status}"`); // Handle Accepted status if (quote.Status === 'accepted') { @@ -223,8 +225,8 @@ async function handleQuoteStatusChange(ctx: any): Promise { // Log activity for status change if (quote.AccountId) { - await ctx.ql.doc.create('activity', { - Subject: `Quote Status Changed: ${ctx.previous.Status} → ${quote.Status}`, + await (ctx.ql as any).doc.create('activity', { + Subject: `Quote Status Changed: ${prev.Status} → ${quote.Status}`, Type: 'Quote Status Change', Status: 'completed', Priority: 'normal', @@ -232,7 +234,7 @@ async function handleQuoteStatusChange(ctx: any): Promise { WhatId: quote.Id, OwnerId: ctx.user.id, ActivityDate: new Date().toISOString().split('T')[0], - Description: `Quote ${quote.QuoteNumber} status changed from "${ctx.previous.Status}" to "${quote.Status}"` + Description: `Quote ${quote.QuoteNumber} status changed from "${prev.Status}" to "${quote.Status}"` }); } } catch (error) { @@ -243,21 +245,21 @@ async function handleQuoteStatusChange(ctx: any): Promise { /** * Handle quote accepted */ -async function handleQuoteAccepted(ctx: any): Promise { - const quote = ctx.result; +async function handleQuoteAccepted(ctx: HookContext): Promise { + const quote = ctx.result as Record; try { console.log('✅ Processing quote acceptance...'); // Update accepted date and timestamp - await ctx.ql.doc.update('quote', quote.Id, { + await (ctx.ql as any).doc.update('quote', quote.Id, { AcceptedDate: new Date().toISOString() }); // Update opportunity stage if linked if (quote.OpportunityId) { try { - await ctx.ql.doc.update('opportunity', quote.OpportunityId, { + await (ctx.ql as any).doc.update('opportunity', quote.OpportunityId, { Stage: 'closed_won', CloseDate: new Date().toISOString().split('T')[0] }); @@ -286,11 +288,11 @@ async function handleQuoteAccepted(ctx: any): Promise { }; try { - const contract = await ctx.ql.doc.create('contract', contractData); + const contract = await (ctx.ql as any).doc.create('contract', contractData); console.log(`📝 Contract created successfully: ${contract._id}`); // Link Contract back to Quote - await ctx.ql.doc.update('quote', quote.Id, { + await (ctx.ql as any).doc.update('quote', quote.Id, { Description: `${quote.Description || ''}\n Contract Created: ${contract.contract_number}` }); } catch (err) { @@ -305,8 +307,8 @@ async function handleQuoteAccepted(ctx: any): Promise { /** * Handle quote sent to customer */ -async function handleQuoteSent(ctx: any): Promise { - const quote = ctx.result; +async function handleQuoteSent(ctx: HookContext): Promise { + const quote = ctx.result as Record; try { console.log('📧 Quote sent to customer'); @@ -320,8 +322,8 @@ async function handleQuoteSent(ctx: any): Promise { /** * Handle quote expiration */ -async function handleQuoteExpired(ctx: any): Promise { - const quote = ctx.result; +async function handleQuoteExpired(ctx: HookContext): Promise { + const quote = ctx.result as Record; try { console.log('⏰ Quote expired'); @@ -335,20 +337,22 @@ async function handleQuoteExpired(ctx: any): Promise { /** * Handle approval status changes */ -async function handleApprovalStatusChange(ctx: any): Promise { +async function handleApprovalStatusChange(ctx: HookContext): Promise { if (!ctx.previous || !ctx.result) return; - const approvalStatusChanged = ctx.previous.ApprovalStatus !== ctx.result.ApprovalStatus; + const prev = ctx.previous as Record; + const result = ctx.result as Record; + const approvalStatusChanged = prev.ApprovalStatus !== result.ApprovalStatus; if (!approvalStatusChanged) return; - const quote = ctx.result; + const quote = result; try { - console.log(`🔄 Approval status changed from "${ctx.previous.ApprovalStatus}" to "${quote.ApprovalStatus}"`); + console.log(`🔄 Approval status changed from "${prev.ApprovalStatus}" to "${quote.ApprovalStatus}"`); // Handle approved if (quote.ApprovalStatus === 'approved') { - await ctx.ql.doc.update('quote', quote.Id, { + await (ctx.ql as any).doc.update('quote', quote.Id, { ApprovedDate: new Date().toISOString(), ApprovedById: ctx.user.id, Status: quote.Status === 'in_review' ? 'approved' : quote.Status @@ -358,7 +362,7 @@ async function handleApprovalStatusChange(ctx: any): Promise { // Handle rejected if (quote.ApprovalStatus === 'rejected') { - await ctx.ql.doc.update('quote', quote.Id, { + await (ctx.ql as any).doc.update('quote', quote.Id, { RejectedDate: new Date().toISOString(), RejectedById: ctx.user.id, Status: 'rejected' diff --git a/packages/support/src/hooks/knowledge.hook.ts b/packages/support/src/hooks/knowledge.hook.ts index e21106f17..ecb10c4f1 100644 --- a/packages/support/src/hooks/knowledge.hook.ts +++ b/packages/support/src/hooks/knowledge.hook.ts @@ -200,7 +200,7 @@ const KnowledgeArticleSearchAnalytics: Hook = { // Helper Functions -function calculateQualityScore(article: any): number { +function calculateQualityScore(article: Record): number { let score = 0; const maxScore = 100; @@ -269,7 +269,7 @@ function calculateQualityScore(article: any): number { return Math.min(score, maxScore); } -async function calculatePopularityScore(article: any): Promise { +async function calculatePopularityScore(article: Record): Promise { let score = 0; const maxScore = 100; @@ -329,7 +329,7 @@ async function calculatePopularityScore(article: any): Promise { return Math.min(score, maxScore); } -async function performAIArticleClassification(article: any): Promise { +async function performAIArticleClassification(article: Record): Promise { const text = `${article.Title} ${article.Content}`.toLowerCase(); if (text.includes('how to') || text.includes('step by step') || text.includes('tutorial')) { @@ -351,7 +351,7 @@ async function performAIArticleClassification(article: any): Promise { return 'Other'; } -async function extractArticleTags(article: any): Promise { +async function extractArticleTags(article: Record): Promise { const tags: string[] = []; const text = `${article.Title} ${article.Content}`.toLowerCase(); @@ -422,13 +422,13 @@ async function extractArticleKeywords(content: string): Promise { return sortedWords.join(', '); } -async function findRelatedArticles(article: any, ctx: any): Promise { +async function findRelatedArticles(article: Record, ctx: HookContext): Promise { const keywords = article.AIKeywords?.split(',').map((k: string) => k.trim()) || []; if (keywords.length === 0) return []; // Find articles with similar keywords or category - const relatedArticles = await ctx.ql.find('knowledge_article', { + const relatedArticles = await (ctx.ql as any).find('knowledge_article', { filters: [ ['Status', '=', 'Published'], ['id', '!=', article.id] @@ -439,7 +439,7 @@ async function findRelatedArticles(article: any, ctx: any): Promise { return relatedArticles; } -async function trackStatusChange(article: any, oldArticle: any, ctx: any): Promise { +async function trackStatusChange(article: Record, oldArticle: Record, ctx: HookContext): Promise { const statusChange = { ArticleId: article.id, OldStatus: oldArticle.Status, @@ -463,7 +463,7 @@ async function trackStatusChange(article: any, oldArticle: any, ctx: any): Promi } } -async function checkForAutoArchive(article: any, ctx: any): Promise { +async function checkForAutoArchive(article: Record, ctx: HookContext): Promise { // Auto-archive if: // 1. Article is older than 2 years // 2. Low views in last 6 months @@ -482,7 +482,7 @@ async function checkForAutoArchive(article: any, ctx: any): Promise { return recentViewCount < 10 && helpfulRating < 50; } -function calculateNextReviewDate(article: any): Date { +function calculateNextReviewDate(article: Record): Date { // Schedule next review based on category and criticality const now = new Date(); let monthsUntilReview = 12; // Default: annual review @@ -501,16 +501,16 @@ function calculateNextReviewDate(article: any): Date { return reviewDate; } -async function incrementArticleUsage(articleId: string, ctx: any): Promise { +async function incrementArticleUsage(articleId: string, ctx: HookContext): Promise { try { - const article = await ctx.ql.find('knowledge_article', { + const article = await (ctx.ql as any).find('knowledge_article', { filters: [['id', '=', articleId]], limit: 1 }); if (article && article.length > 0) { const currentCount = article[0].CaseResolutionCount || 0; - await ctx.ql.doc.update('knowledge_article', articleId, { + await (ctx.ql as any).doc.update('knowledge_article', articleId, { CaseResolutionCount: currentCount + 1, LastUsedInCaseDate: new Date() }); From 21d997531b418cf78fa9578c624bff46105d0507 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 21 Feb 2026 11:44:47 +0000 Subject: [PATCH 3/6] Replace ctx: any with ctx: HookContext and data record params with Record in hook helper functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update type annotations across 18 hook files: - ctx: any → ctx: HookContext in helper functions - campaign: any, employee: any, etc. → Record for data record params - Keep db: any and ql: any as-is for non-HookContext parameters Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/crm/src/hooks/account.hook.ts | 6 +++--- packages/crm/src/hooks/activity.hook.ts | 6 +++--- packages/hr/src/hooks/application.hook.ts | 14 +++++++------- packages/hr/src/hooks/candidate.hook.ts | 16 ++++++++-------- packages/hr/src/hooks/interview.hook.ts | 8 ++++---- packages/hr/src/hooks/onboarding.hook.ts | 2 +- packages/hr/src/hooks/time_off.hook.ts | 10 +++++----- .../marketing/src/hooks/campaign_member.hook.ts | 4 ++-- 8 files changed, 33 insertions(+), 33 deletions(-) diff --git a/packages/crm/src/hooks/account.hook.ts b/packages/crm/src/hooks/account.hook.ts index ee9aeac70..c700cb429 100644 --- a/packages/crm/src/hooks/account.hook.ts +++ b/packages/crm/src/hooks/account.hook.ts @@ -41,7 +41,7 @@ const AccountHealthScoreTrigger: Hook = { /** * Calculate account health score (0-100) */ -async function calculateHealthScore(account: Record, ctx: any): Promise { +async function calculateHealthScore(account: Record, ctx: HookContext): Promise { let score = 0; // Base score for active customer status (20 points) @@ -125,7 +125,7 @@ const AccountHierarchyTrigger: Hook = { /** * Update parent account metrics by aggregating child account data */ -async function updateParentAccountMetrics(parentId: string, ctx: any): Promise { +async function updateParentAccountMetrics(parentId: string, ctx: HookContext): Promise { console.log(`🔄 Updating parent account metrics: ${parentId}`); // In real implementation, would query all child accounts and aggregate @@ -146,7 +146,7 @@ async function updateParentAccountMetrics(parentId: string, ctx: any): Promise { +async function cascadeOwnershipChange(accountId: string, newOwnerId: string, ctx: HookContext): Promise { console.log(`🔄 Cascading ownership change to child accounts of ${accountId}`); // In real implementation, would query and update all child accounts diff --git a/packages/crm/src/hooks/activity.hook.ts b/packages/crm/src/hooks/activity.hook.ts index 1ac59e4c5..f06365351 100644 --- a/packages/crm/src/hooks/activity.hook.ts +++ b/packages/crm/src/hooks/activity.hook.ts @@ -81,7 +81,7 @@ const ActivityRelatedObjectUpdatesTrigger: Hook = { /** * Update Contact's last activity date */ -async function updateContactLastActivityDate(whoId: string, activityDate: string, ctx: any): Promise { +async function updateContactLastActivityDate(whoId: string, activityDate: string, ctx: HookContext): Promise { console.log(`🔄 Updating LastContactDate for contact: ${whoId}`); // In real implementation: @@ -98,7 +98,7 @@ async function updateContactLastActivityDate(whoId: string, activityDate: string /** * Update related object's last activity date */ -async function updateWhatObjectLastActivityDate(whatId: string, activityDate: string, ctx: any): Promise { +async function updateWhatObjectLastActivityDate(whatId: string, activityDate: string, ctx: HookContext): Promise { console.log(`🔄 Updating LastActivityDate for related object: ${whatId}`); // In real implementation, would need to determine object type from WhatId @@ -154,7 +154,7 @@ const ActivityCompletionTrigger: Hook = { /** * Create next recurrence of a recurring activity */ -async function createNextRecurrence(activity: Record, ctx: any): Promise { +async function createNextRecurrence(activity: Record, ctx: HookContext): Promise { console.log(`🔄 Creating next recurrence for: ${activity.Subject}`); // Calculate next occurrence date based on pattern diff --git a/packages/hr/src/hooks/application.hook.ts b/packages/hr/src/hooks/application.hook.ts index 15da97d7f..b5cd00948 100644 --- a/packages/hr/src/hooks/application.hook.ts +++ b/packages/hr/src/hooks/application.hook.ts @@ -64,7 +64,7 @@ const ApplicationStatusWorkflowTrigger: Hook = { /** * Create an interview record when application moves to interview stage */ -async function createInterviewRecord(application: any, ctx: any): Promise { +async function createInterviewRecord(application: Record, ctx: HookContext): Promise { try { const scheduledDate = new Date(); scheduledDate.setDate(scheduledDate.getDate() + 3); // Default: 3 days from now @@ -91,7 +91,7 @@ async function createInterviewRecord(application: any, ctx: any): Promise /** * Update candidate status to 'hired' when application is hired */ -async function updateCandidateToHired(application: any, ctx: any): Promise { +async function updateCandidateToHired(application: Record, ctx: HookContext): Promise { try { if (!application.candidate_id) { console.warn('⚠️ No candidate linked to application, skipping candidate update'); @@ -112,10 +112,10 @@ async function updateCandidateToHired(application: any, ctx: any): Promise * Log application status change */ async function logApplicationStatusChange( - application: any, + application: Record, oldStatus: string, newStatus: string, - ctx: any + ctx: HookContext ): Promise { try { console.log(`📝 Logging application status change: ${oldStatus} → ${newStatus} for ${application.application_number}`); @@ -179,7 +179,7 @@ const ApplicationScreeningTrigger: Hook = { /** * Fetch candidate record */ -async function fetchCandidate(candidateId: string, ctx: any): Promise { +async function fetchCandidate(candidateId: string, ctx: HookContext): Promise { try { const candidates = await (ctx.ql as any).find('candidate', { filters: [['id', '=', candidateId]], @@ -195,7 +195,7 @@ async function fetchCandidate(candidateId: string, ctx: any): Promise { /** * Fetch recruitment record */ -async function fetchRecruitment(recruitmentId: string, ctx: any): Promise { +async function fetchRecruitment(recruitmentId: string, ctx: HookContext): Promise { try { if (!recruitmentId) return null; const recruitments = await (ctx.ql as any).find('recruitment', { @@ -212,7 +212,7 @@ async function fetchRecruitment(recruitmentId: string, ctx: any): Promise { /** * Calculate screening score based on candidate qualifications */ -function calculateScreeningScore(candidate: any, recruitment: any): number { +function calculateScreeningScore(candidate: Record, recruitment: Record): number { let score = 0; // Experience score (30 points) diff --git a/packages/hr/src/hooks/candidate.hook.ts b/packages/hr/src/hooks/candidate.hook.ts index 951644669..9d89d7dc3 100644 --- a/packages/hr/src/hooks/candidate.hook.ts +++ b/packages/hr/src/hooks/candidate.hook.ts @@ -59,7 +59,7 @@ const CandidateScoringTrigger: Hook = { /** * Calculate candidate score (0-100) based on qualifications */ -function calculateCandidateScore(candidate: any): number { +function calculateCandidateScore(candidate: Record): number { let score = 0; // Education score (25 points) @@ -122,7 +122,7 @@ function calculateCandidateScore(candidate: any): number { /** * Auto-screen candidate against minimum requirements */ -async function autoScreen(candidate: any, ctx: any): Promise { +async function autoScreen(candidate: Record, ctx: HookContext): Promise { // Basic validation if (!candidate.email || !candidate.mobile_phone) { return false; @@ -192,7 +192,7 @@ const CandidateStatusChangeTrigger: Hook = { /** * Handle transition to Interviewing status */ -async function handleInterviewingStatus(candidate: any, ctx: any): Promise { +async function handleInterviewingStatus(candidate: Record, ctx: HookContext): Promise { console.log(`📅 Candidate ${candidate.first_name} ${candidate.last_name} moved to interviewing stage`); console.debug(`[candidate.hook] Interview process pending for ${candidate.first_name} ${candidate.last_name}: schedule first interview, notify hiring manager, and create interview records`); @@ -201,7 +201,7 @@ async function handleInterviewingStatus(candidate: any, ctx: any): Promise /** * Handle transition to Hired status */ -async function handleHiredStatus(candidate: any, ctx: any): Promise { +async function handleHiredStatus(candidate: Record, ctx: HookContext): Promise { console.log(`🎉 Candidate ${candidate.first_name} ${candidate.last_name} has been hired`); console.debug(`[candidate.hook] Hiring process pending for ${candidate.first_name} ${candidate.last_name}: create offer record, initiate onboarding, send welcome email, and notify HR team`); @@ -210,7 +210,7 @@ async function handleHiredStatus(candidate: any, ctx: any): Promise { /** * Handle transition to Rejected status */ -async function handleRejectedStatus(candidate: any, ctx: any): Promise { +async function handleRejectedStatus(candidate: Record, ctx: HookContext): Promise { console.log(`❌ Candidate ${candidate.first_name} ${candidate.last_name} has been rejected`); console.debug(`[candidate.hook] Rejection process pending for ${candidate.first_name} ${candidate.last_name}: send rejection email and archive candidate data`); @@ -219,7 +219,7 @@ async function handleRejectedStatus(candidate: any, ctx: any): Promise { /** * Handle transition to Withdrawn status */ -async function handleWithdrawnStatus(candidate: any, ctx: any): Promise { +async function handleWithdrawnStatus(candidate: Record, ctx: HookContext): Promise { console.log(`🚪 Candidate ${candidate.first_name} ${candidate.last_name} has withdrawn`); console.debug(`[candidate.hook] Withdrawal process pending for ${candidate.first_name} ${candidate.last_name}: log withdrawal reason and update recruitment metrics`); @@ -229,10 +229,10 @@ async function handleWithdrawnStatus(candidate: any, ctx: any): Promise { * Log status change activity */ async function logStatusChange( - candidate: any, + candidate: Record, oldStatus: string, newStatus: string, - ctx: any + ctx: HookContext ): Promise { try { // Note: Activity object might need to be adapted for HR module diff --git a/packages/hr/src/hooks/interview.hook.ts b/packages/hr/src/hooks/interview.hook.ts index 7f8128611..a687eb0e2 100644 --- a/packages/hr/src/hooks/interview.hook.ts +++ b/packages/hr/src/hooks/interview.hook.ts @@ -68,7 +68,7 @@ async function checkInterviewerAvailability( interviewerId: string, scheduledDate: string, duration: number, - ctx: any + ctx: HookContext ): Promise { try { const dateOnly = scheduledDate.split('T')[0]; @@ -137,9 +137,9 @@ const InterviewFeedbackTrigger: Hook = { * Update application status based on interview result */ async function updateApplicationFromResult( - interview: any, + interview: Record, result: string, - ctx: any + ctx: HookContext ): Promise { try { const statusMap: Record = { @@ -174,7 +174,7 @@ async function updateApplicationFromResult( */ async function calculateAverageInterviewScore( applicationId: string, - ctx: any + ctx: HookContext ): Promise { try { const interviews = await (ctx.ql as any).find('interview', { diff --git a/packages/hr/src/hooks/onboarding.hook.ts b/packages/hr/src/hooks/onboarding.hook.ts index 2b96d65ab..5522cfe32 100644 --- a/packages/hr/src/hooks/onboarding.hook.ts +++ b/packages/hr/src/hooks/onboarding.hook.ts @@ -40,7 +40,7 @@ const OnboardingChecklistTrigger: Hook = { /** * Create onboarding record from accepted offer */ -async function createOnboardingFromOffer(offer: any, ctx: any): Promise { +async function createOnboardingFromOffer(offer: Record, ctx: HookContext): Promise { try { const startDate = offer.start_date || new Date().toISOString().split('T')[0]; const start = new Date(startDate); diff --git a/packages/hr/src/hooks/time_off.hook.ts b/packages/hr/src/hooks/time_off.hook.ts index 112b795a3..08b603128 100644 --- a/packages/hr/src/hooks/time_off.hook.ts +++ b/packages/hr/src/hooks/time_off.hook.ts @@ -92,7 +92,7 @@ async function checkLeaveBalance( employeeId: string, leaveType: string, requestedDays: number, - ctx: any + ctx: HookContext ): Promise { try { // Find all approved time-off records for this employee and leave type in current year @@ -188,7 +188,7 @@ const TimeOffApprovalTrigger: Hook = { /** * Handle time-off approval: set approval date */ -async function handleTimeOffApproved(timeOff: any, ctx: any): Promise { +async function handleTimeOffApproved(timeOff: Record, ctx: HookContext): Promise { try { await (ctx.ql as any).doc.update('time_off', timeOff.id, { approval_date: new Date().toISOString().split('T')[0] @@ -205,7 +205,7 @@ async function handleTimeOffApproved(timeOff: any, ctx: any): Promise { /** * Handle time-off cancellation after approval: restore balance */ -async function handleTimeOffCancelled(timeOff: any, ctx: any): Promise { +async function handleTimeOffCancelled(timeOff: Record, ctx: HookContext): Promise { try { console.log(`🔄 Time-off request ${timeOff.request_number} cancelled after approval: ${timeOff.total_days} day(s) of ${timeOff.leave_type} restored`); @@ -219,10 +219,10 @@ async function handleTimeOffCancelled(timeOff: any, ctx: any): Promise { * Log balance change for audit trail */ async function logBalanceChange( - timeOff: any, + timeOff: Record, oldStatus: string, newStatus: string, - ctx: any + ctx: HookContext ): Promise { try { const action = newStatus === 'approved' ? 'DEDUCTED' : newStatus === 'cancelled' ? 'RESTORED' : 'STATUS_CHANGE'; diff --git a/packages/marketing/src/hooks/campaign_member.hook.ts b/packages/marketing/src/hooks/campaign_member.hook.ts index a013585b8..9882d6792 100644 --- a/packages/marketing/src/hooks/campaign_member.hook.ts +++ b/packages/marketing/src/hooks/campaign_member.hook.ts @@ -223,7 +223,7 @@ const CampaignMemberBounceHandlerTrigger: Hook = { /** * Calculate member engagement score (0-100) */ -export function calculateEngagementScore(member: any): number { +export function calculateEngagementScore(member: Record): number { let score = 0; // Base score for being in campaign @@ -270,7 +270,7 @@ export function calculateEngagementScore(member: any): number { export async function trackMemberEngagementTimeline( memberId: string, engagementType: 'opened' | 'clicked' | 'responded' | 'unsubscribed', - ctx: any + ctx: HookContext ): Promise { try { console.log(`🔄 Tracking engagement timeline for member: ${memberId}`); From cf45e518ad56783069a7157ffe7699fea76dff5f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 21 Feb 2026 11:50:35 +0000 Subject: [PATCH 4/6] docs: update ROADMAP, CONTRIBUTING, and DEVELOPMENT_WORKFLOW - ROADMAP.md: fix last-updated date, add Phase 15 section, add version entry - CONTRIBUTING.md: add typecheck/ROADMAP to pre-submission checklist - DEVELOPMENT_WORKFLOW.md: update hook examples to use HookContext Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- CONTRIBUTING.md | 6 ++++-- DEVELOPMENT_WORKFLOW.md | 16 ++++++++++------ ROADMAP.md | 32 +++++++++++++++++++++++++++++++- 3 files changed, 45 insertions(+), 9 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b0b84734d..1ddc925ae 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -143,10 +143,12 @@ Only the compiled `dist/` folder is included in published packages (controlled b ## 🔄 Pull Request Process 1. **Before Creating a PR** - - Ensure all tests pass - - Run linting and fix any issues + - Ensure all tests pass (`pnpm test`) + - Run type checking (`pnpm typecheck`) + - Run linting and fix any issues (`pnpm lint`) - Update documentation if needed - Add tests for new features + - Update ROADMAP.md if the change completes a roadmap item - **Add a changeset** if your changes affect package behavior 2. **Creating a PR** diff --git a/DEVELOPMENT_WORKFLOW.md b/DEVELOPMENT_WORKFLOW.md index 79c9ba34f..3940355bf 100644 --- a/DEVELOPMENT_WORKFLOW.md +++ b/DEVELOPMENT_WORKFLOW.md @@ -86,16 +86,18 @@ export default ObjectSchema.create({ ```typescript // packages/{pkg}/src/my_entity.hook.ts -export function beforeInsert(context: any) { - const { doc, broker } = context; +import type { HookContext } from '@objectstack/spec/data'; + +export function beforeInsert(context: HookContext) { + const doc = context.input.doc as Record; // Validation and business logic if (!doc.name) { throw new Error('Name is required'); } } -export function afterInsert(context: any) { - const { doc, broker } = context; +export function afterInsert(context: HookContext) { + const doc = context.result as Record; // Post-creation side effects (notifications, related records, etc.) } ``` @@ -204,8 +206,10 @@ If the field needs validation or side effects, add logic to the hook file: ```typescript // packages/crm/src/account.hook.ts -export function beforeInsert(context: any) { - const { doc } = context; +import type { HookContext } from '@objectstack/spec/data'; + +export function beforeInsert(context: HookContext) { + const doc = context.input.doc as Record; // Validate URL format if provided if (doc.website_url && !doc.website_url.startsWith('http')) { doc.website_url = 'https://' + doc.website_url; diff --git a/ROADMAP.md b/ROADMAP.md index 2448ce122..e462db006 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,7 +1,7 @@ # HotCRM Development Roadmap > Comprehensive development plan for HotCRM — the world's first AI-Native CRM. -> Protocol: @objectstack/spec v3.0.8 | Last Updated: February 21, 2027 +> Protocol: @objectstack/spec v3.0.8 | Last Updated: February 2026 ## Strategic Direction @@ -15,6 +15,7 @@ 2027 Q1-Q2 ████████████████████████████████ Phase 13: Module Optimization & Seed Data ✅ COMPLETE 2027 Q2-Q3 ████████████████████████████████ Phase 14: v3.0.8 Feed & Interface Builder Adoption ✅ COMPLETE 2027+ ████████████████████████████████ Phase 12E: Advanced AI & Enterprise Features ✅ COMPLETE +2026 Q1 ████████████████████████████████ Phase 15: Repository Quality & DX Improvements 🔄 IN PROGRESS ``` ## Current State Summary @@ -171,10 +172,39 @@ These items were deferred during Phase 13 and have been completed: --- +### Phase 15: Repository Quality & DX Improvements (2026 Q1) + +> Goal: Improve code quality, CI/CD, documentation accuracy, and developer experience. + +#### High Priority + +- [x] **DEVELOPMENT_WORKFLOW.md test count** — Remove hardcoded test count; use dynamic description +- [x] **Hook `any` type migration** — Replace `ctx: any` with `ctx: HookContext` in 18 helper functions across hook files +- [x] **pnpm-workspace.yaml documentation** — Add comment explaining core/server exclusion reason +- [x] **CI workflow typecheck** — Add `pnpm typecheck` step to CI workflow +- [x] **Code quality workflow paths** — Fix `src/` references to `packages/` in code-quality workflow + +#### Medium Priority + +- [x] **ESLint version alignment** — Align `@typescript-eslint/parser` (^6 → ^7) with `@typescript-eslint/eslint-plugin` (^7) +- [x] **CONTRIBUTING.md checklist** — Add pre-submission development checklist (typecheck, test, ROADMAP) +- [x] **Documentation hook examples** — Update DEVELOPMENT_WORKFLOW.md code examples to use `HookContext` instead of `any` +- [x] **ROADMAP.md date sync** — Update last-updated date and version timeline to match current state + +#### Deferred / Future + +- [ ] **Structured logging migration** — Replace `console.log` with pino logger across hook files +- [ ] **Legacy API cleanup** — Remove `db.ts` references and migrate to broker/ObjectQL +- [ ] **E2E test coverage** — Build API-layer end-to-end tests for Feed API / MCP API +- [ ] **FormView `as any` cleanup** — Remove `as any` casts once `@objectstack/spec` aligns FormView column types + +--- + ## Version Upgrade History | Date | From | To | Breaking Changes | Tests | |------|------|----|-----------------|-------| +| 2026-02-21 | v3.0.8 | v3.0.8 | None (Phase 15: Repository quality improvements — hook `any` type migration, CI typecheck, ESLint version alignment, documentation updates, code-quality workflow fixes) | 3799 ✅ | | 2027-02-21 | v3.0.8 | v3.0.8 | None (Phase 14 complete: Activity Feed/Chatter across 6 clouds, 4 Interface Builder blank pages, Dashboard headers & global filters on 6 clouds, ViewTabs on 18 views, Feed API 8 endpoints, Feed service contract, System metadata, Studio builder configs, Navigation areas, 327 new tests) | 3707 ✅ | | 2026-02-21 | v3.0.6 | v3.0.8 | None (New: Activity Feed/Chatter system, Interface Builder/Blank Pages, Dashboard headers & global filters, Feed API contracts, Studio builder configs, Oclif CLI plugin, Package conventions; Phase 14 roadmap added) | 3318 ✅ | | 2026-02-16 | v3.0.0 | v3.0.0 | None (Phase 13 roadmap: Module-by-module deep optimization, seed data foundation, vertical package UI enhancement, core cloud metadata equalization) | 3318 ✅ | From b033afcc8b6ce7f4367129fb8771516e54ecac36 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 21 Feb 2026 11:54:26 +0000 Subject: [PATCH 5/6] fix: add typecheck to CI, fix code-quality paths, align ESLint versions, document workspace exclusions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CI workflow: add pnpm typecheck step - code-quality workflow: fix src/ → packages/ references, use typecheck instead of build - package.json: align @typescript-eslint/parser ^6.21.0 → ^7.18.0 with plugin - pnpm-workspace.yaml: add comment explaining core/server exclusion Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- .github/workflows/ci.yml | 3 +++ .github/workflows/code-quality.yml | 8 ++++---- package.json | 2 +- pnpm-workspace.yaml | 3 +++ 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 865e607c6..dfe03ede5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,6 +39,9 @@ jobs: - name: Run linter run: pnpm run lint + - name: TypeScript type checking + run: pnpm run typecheck + - name: Build project run: pnpm run build diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index b7ddb7ef8..2aff53969 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -40,7 +40,7 @@ jobs: - name: Check for console.log statements run: | - if grep -r "console\.log" src/ --include="*.ts" --exclude-dir=node_modules; then + if grep -r "console\.log" packages/ --include="*.ts" --exclude-dir=node_modules --exclude-dir=__tests__; then echo "Warning: console.log statements found in source code" echo "Please use proper logging instead" # This is just a warning, not failing the build @@ -50,7 +50,7 @@ jobs: continue-on-error: true - name: Check TypeScript compilation - run: pnpm run build + run: pnpm run typecheck - name: Run ESLint run: pnpm run lint @@ -58,11 +58,11 @@ jobs: - name: Check for TODO comments run: | echo "Checking for TODO/FIXME comments..." - grep -r "TODO\|FIXME" src/ --include="*.ts" || echo "No TODO/FIXME found" + grep -r "TODO\|FIXME" packages/ --include="*.ts" --exclude-dir=node_modules || echo "No TODO/FIXME found" continue-on-error: true - name: Check file sizes run: | echo "Checking for large files..." - find src/ -type f -size +100k -exec ls -lh {} \; || echo "No large files found" + find packages/ -type f -size +100k -exec ls -lh {} \; || echo "No large files found" continue-on-error: true diff --git a/package.json b/package.json index ebf296a83..af9c1eb01 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "@objectstack/studio": "^3.0.8", "@types/node": "^25.2.3", "@typescript-eslint/eslint-plugin": "^7.18.0", - "@typescript-eslint/parser": "^6.21.0", + "@typescript-eslint/parser": "^7.18.0", "@vitest/coverage-v8": "^1.6.1", "@vitest/ui": "^1.6.1", "eslint": "^8.57.1", diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d9a21e3b6..95f83a8ad 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,5 +1,8 @@ packages: - 'apps/*' - 'packages/*' + # core and server are internal-only packages (private: true) excluded from + # workspace linking to avoid circular dependency issues during builds. + # They are consumed via tsconfig path aliases instead. - '!packages/core' - '!packages/server' From 2bd0e9956007f1540702b03ae4ab1f36084916a9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 21 Feb 2026 12:04:32 +0000 Subject: [PATCH 6/6] fix: update pnpm-lock.yaml after @typescript-eslint/parser version bump Regenerate lockfile to match @typescript-eslint/parser ^7.18.0 in package.json. Fixes Vercel build failure: ERR_PNPM_OUTDATED_LOCKFILE. Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- pnpm-lock.yaml | 84 +++++++++----------------------------------------- 1 file changed, 14 insertions(+), 70 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5ae311878..ee58e2ae0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,10 +50,10 @@ importers: version: 25.2.3 '@typescript-eslint/eslint-plugin': specifier: ^7.18.0 - version: 7.18.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) '@typescript-eslint/parser': - specifier: ^6.21.0 - version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + specifier: ^7.18.0 + version: 7.18.0(eslint@8.57.1)(typescript@5.9.3) '@vitest/coverage-v8': specifier: ^1.6.1 version: 1.6.1(vitest@1.6.1) @@ -2037,20 +2037,16 @@ packages: typescript: optional: true - '@typescript-eslint/parser@6.21.0': - resolution: {integrity: sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==} - engines: {node: ^16.0.0 || >=18.0.0} + '@typescript-eslint/parser@7.18.0': + resolution: {integrity: sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==} + engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: - eslint: ^7.0.0 || ^8.0.0 + eslint: ^8.56.0 typescript: '*' peerDependenciesMeta: typescript: optional: true - '@typescript-eslint/scope-manager@6.21.0': - resolution: {integrity: sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==} - engines: {node: ^16.0.0 || >=18.0.0} - '@typescript-eslint/scope-manager@7.18.0': resolution: {integrity: sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==} engines: {node: ^18.18.0 || >=20.0.0} @@ -2065,23 +2061,10 @@ packages: typescript: optional: true - '@typescript-eslint/types@6.21.0': - resolution: {integrity: sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==} - engines: {node: ^16.0.0 || >=18.0.0} - '@typescript-eslint/types@7.18.0': resolution: {integrity: sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==} engines: {node: ^18.18.0 || >=20.0.0} - '@typescript-eslint/typescript-estree@6.21.0': - resolution: {integrity: sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - '@typescript-eslint/typescript-estree@7.18.0': resolution: {integrity: sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==} engines: {node: ^18.18.0 || >=20.0.0} @@ -2097,10 +2080,6 @@ packages: peerDependencies: eslint: ^8.56.0 - '@typescript-eslint/visitor-keys@6.21.0': - resolution: {integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==} - engines: {node: ^16.0.0 || >=18.0.0} - '@typescript-eslint/visitor-keys@7.18.0': resolution: {integrity: sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==} engines: {node: ^18.18.0 || >=20.0.0} @@ -3403,10 +3382,6 @@ packages: resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} engines: {node: '>=10'} - minimatch@9.0.3: - resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} - engines: {node: '>=16 || 14 >=14.17'} - minimatch@9.0.5: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} @@ -5926,10 +5901,10 @@ snapshots: '@types/unist@3.0.3': {} - '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.9.3) '@typescript-eslint/scope-manager': 7.18.0 '@typescript-eslint/type-utils': 7.18.0(eslint@8.57.1)(typescript@5.9.3) '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.9.3) @@ -5944,12 +5919,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3)': + '@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 6.21.0 - '@typescript-eslint/types': 6.21.0 - '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 6.21.0 + '@typescript-eslint/scope-manager': 7.18.0 + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 7.18.0 debug: 4.4.3(supports-color@8.1.1) eslint: 8.57.1 optionalDependencies: @@ -5957,11 +5932,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@6.21.0': - dependencies: - '@typescript-eslint/types': 6.21.0 - '@typescript-eslint/visitor-keys': 6.21.0 - '@typescript-eslint/scope-manager@7.18.0': dependencies: '@typescript-eslint/types': 7.18.0 @@ -5979,25 +5949,8 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@6.21.0': {} - '@typescript-eslint/types@7.18.0': {} - '@typescript-eslint/typescript-estree@6.21.0(typescript@5.9.3)': - dependencies: - '@typescript-eslint/types': 6.21.0 - '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.3(supports-color@8.1.1) - globby: 11.1.0 - is-glob: 4.0.3 - minimatch: 9.0.3 - semver: 7.7.4 - ts-api-utils: 1.4.3(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/typescript-estree@7.18.0(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 7.18.0 @@ -6024,11 +5977,6 @@ snapshots: - supports-color - typescript - '@typescript-eslint/visitor-keys@6.21.0': - dependencies: - '@typescript-eslint/types': 6.21.0 - eslint-visitor-keys: 3.4.3 - '@typescript-eslint/visitor-keys@7.18.0': dependencies: '@typescript-eslint/types': 7.18.0 @@ -7642,10 +7590,6 @@ snapshots: dependencies: brace-expansion: 2.0.2 - minimatch@9.0.3: - dependencies: - brace-expansion: 2.0.2 - minimatch@9.0.5: dependencies: brace-expansion: 2.0.2