Repository navigation
Implement CPQ (Configure-Price-Quote) module with multi-level approval workflow - #46
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
| /** | ||
| * Activate pricebook entries based on effective date | ||
| */ | ||
| async function activatePricebookEntries( |
Check notice
Code scanning / CodeQL
Unused variable, import, function or class Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 9 months ago
In general, unused functions should be removed to keep code clear and avoid confusion. If a function is meant for future use, it is better to either (a) actually wire it into current logic, or (b) track it via issue management rather than leaving dead code.
For this specific case, the smallest change that does not alter existing functionality is to delete the unused activatePricebookEntries function definition (lines 338–351 in the provided snippet). No existing code calls it, and it only logs messages and contains a TODO, so removing it has no behavioral impact. We will leave expirePricebookEntries untouched, as CodeQL did not flag it and it may be used elsewhere. No new imports, methods, or type definitions are required beyond removing this block.
| @@ -333,24 +333,6 @@ | ||
| } | ||
|
|
||
| /** | ||
| * Activate pricebook entries based on effective date | ||
| */ | ||
| async function activatePricebookEntries( | ||
| pricebookId: string, | ||
| db: typeof import('@hotcrm/core').db | ||
| ): Promise<void> { | ||
| try { | ||
| console.log(`✅ Activating pricebook entries for pricebook: ${pricebookId}`); | ||
|
|
||
| // TODO: Query and activate all PricebookEntry records for this pricebook | ||
| // This would set their status to Active | ||
| console.log('💰 Pricebook entry activation would be performed here'); | ||
| } catch (error) { | ||
| console.error('❌ Error activating pricebook entries:', error); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Expire pricebook entries | ||
| */ | ||
| async function expirePricebookEntries( |
| /** | ||
| * Expire pricebook entries | ||
| */ | ||
| async function expirePricebookEntries( |
Check notice
Code scanning / CodeQL
Unused variable, import, function or class Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 9 months ago
To fix the issue, remove the unused expirePricebookEntries function entirely from pricebook.hook.ts. This eliminates the unused symbol without changing any existing, actually-executed behavior, because the function is never referenced, exported, or invoked in the shown code.
Concretely:
- In
packages/products/src/hooks/pricebook.hook.ts, delete the block from the JSDoc comment/** Expire pricebook entries */through the closing brace of theexpirePricebookEntriesfunction (lines 353–369 in the snippet). - No additional imports, helpers, or type changes are needed, since the function is self-contained and only uses
consoleand its parameters.
| @@ -350,22 +350,4 @@ | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Expire pricebook entries | ||
| */ | ||
| async function expirePricebookEntries( | ||
| pricebookId: string, | ||
| db: typeof import('@hotcrm/core').db | ||
| ): Promise<void> { | ||
| try { | ||
| console.log(`⏰ Expiring pricebook entries for pricebook: ${pricebookId}`); | ||
|
|
||
| // TODO: Query and expire all PricebookEntry records for this pricebook | ||
| // This would set their status to Expired | ||
| console.log('⏰ Pricebook entry expiration would be performed here'); | ||
| } catch (error) { | ||
| console.error('❌ Error expiring pricebook entries:', error); | ||
| } | ||
| } | ||
|
|
||
| export default PricebookHook; |
| * Validate bundle dependencies | ||
| * Ensures that required products are available when bundle items are configured | ||
| */ | ||
| async function validateBundleDependencies( |
Check notice
Code scanning / CodeQL
Unused variable, import, function or class Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 9 months ago
In general, the correct fix for an unused function is to either remove it or start using it. Since we must avoid changing existing functionality, and the function is not referenced anywhere nor exported, the safest fix is to remove it entirely. This reduces dead code and aligns with the CodeQL recommendation without altering runtime behavior.
Concretely, in packages/products/src/hooks/product.hook.ts, remove the entire validateBundleDependencies function definition (lines 265–282 as shown), including its JSDoc comment and implementation. No additional imports, methods, or type definitions are needed because nothing depends on this function in the provided code. The rest of the file, including checkBundleConstraints, remains unchanged.
| @@ -263,25 +263,6 @@ | ||
| } | ||
|
|
||
| /** | ||
| * Validate bundle dependencies | ||
| * Ensures that required products are available when bundle items are configured | ||
| */ | ||
| async function validateBundleDependencies( | ||
| bundleId: string, | ||
| db: typeof import('@hotcrm/core').db | ||
| ): Promise<boolean> { | ||
| try { | ||
| // TODO: Query ProductBundleItem and ProductBundleDependency | ||
| // Validate that all dependencies are satisfied | ||
| console.log(`🔗 Validating bundle dependencies for ${bundleId}`); | ||
| return true; | ||
| } catch (error) { | ||
| console.error('❌ Error validating bundle dependencies:', error); | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Check bundle constraints | ||
| * Ensures that conflicting products are not included in the same bundle | ||
| */ |
| * Check bundle constraints | ||
| * Ensures that conflicting products are not included in the same bundle | ||
| */ | ||
| async function checkBundleConstraints( |
Check notice
Code scanning / CodeQL
Unused variable, import, function or class Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 9 months ago
In general, to fix an "unused function" issue you either remove the unused function or start using it meaningfully. Using it would change the runtime behavior of the hook (e.g., failing an operation if constraints aren’t met), which goes beyond the goal of eliminating the warning without altering existing behavior.
The best fix here is to delete the unused checkBundleConstraints function definition (lines 288–301) while leaving all other logic intact. This function currently only logs a message and returns true/false and is not referenced elsewhere, so its removal does not alter any executed code paths. No new imports or helper definitions are required; we simply remove that block.
Concretely, in packages/products/src/hooks/product.hook.ts, delete the entire checkBundleConstraints function including its JSDoc comment (lines 284–301 in the snippet). Ensure the surrounding export default ProductHook; remains as the final export.
| @@ -281,23 +281,4 @@ | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Check bundle constraints | ||
| * Ensures that conflicting products are not included in the same bundle | ||
| */ | ||
| async function checkBundleConstraints( | ||
| bundleId: string, | ||
| db: typeof import('@hotcrm/core').db | ||
| ): Promise<boolean> { | ||
| try { | ||
| // TODO: Query ProductBundleConstraint | ||
| // Validate that no constraints are violated | ||
| console.log(`⚠️ Checking bundle constraints for ${bundleId}`); | ||
| return true; | ||
| } catch (error) { | ||
| console.error('❌ Error checking bundle constraints:', error); | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| export default ProductHook; |
| events: ['beforeInsert', 'beforeUpdate', 'afterInsert', 'afterUpdate'], | ||
| handler: async (ctx: TriggerContext) => { | ||
| try { | ||
| const isInsert = !ctx.old; |
Check notice
Code scanning / CodeQL
Unused variable, import, function or class Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 9 months ago
To fix the problem, we should remove the unused variable declaration so the code no longer defines isInsert. This preserves existing functionality because the value of isInsert is not used anywhere.
Concretely, in packages/products/src/hooks/quote.hook.ts, inside the handler of QuotePricingHook, delete the line that declares isInsert. No new imports, methods, or definitions are needed. We should leave isUpdate and quote untouched unless they are also flagged separately, because the current report is specifically about isInsert.
| @@ -26,7 +26,6 @@ | ||
| events: ['beforeInsert', 'beforeUpdate', 'afterInsert', 'afterUpdate'], | ||
| handler: async (ctx: TriggerContext) => { | ||
| try { | ||
| const isInsert = !ctx.old; | ||
| const isUpdate = !!ctx.old; | ||
| const quote = ctx.new; | ||
|
|
| handler: async (ctx: TriggerContext) => { | ||
| try { | ||
| const isInsert = !ctx.old; | ||
| const isUpdate = !!ctx.old; |
Check notice
Code scanning / CodeQL
Unused variable, import, function or class Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 9 months ago
To fix an unused-variable issue, remove the declaration (or its unused part) so that no variable is defined without being read. This avoids confusion and any wasted computation.
In this case, the best minimal fix is to delete the line that declares isUpdate, since the value !!ctx.old has no side effects and the variable is not referenced anywhere else in the shown code. We will leave isInsert and quote untouched to avoid altering any unshown logic in the [...] region. Concretely, in packages/products/src/hooks/quote.hook.ts, inside the handler function of QuotePricingHook, remove line 30 (const isUpdate = !!ctx.old;). No new imports, methods, or definitions are required.
| @@ -27,7 +27,6 @@ | ||
| handler: async (ctx: TriggerContext) => { | ||
| try { | ||
| const isInsert = !ctx.old; | ||
| const isUpdate = !!ctx.old; | ||
| const quote = ctx.new; | ||
|
|
||
| // Before Insert/Update: Calculate pricing |
| try { | ||
| const isInsert = !ctx.old; | ||
| const isUpdate = !!ctx.old; | ||
| const quote = ctx.new; |
Check notice
Code scanning / CodeQL
Unused variable, import, function or class Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 9 months ago
In general, unused local variables should either be removed or used. Here, quote is simply an alias for ctx.new and is never read. The safest, non‑behaviour‑changing fix is to remove the declaration line entirely.
Concretely, in packages/products/src/hooks/quote.hook.ts, within the handler function of QuotePricingHook, delete the line:
const quote = ctx.new;No other code needs to change, because all logic already uses ctx directly and there are no references to quote. No imports, method definitions, or additional variables are required.
| @@ -28,7 +28,6 @@ | ||
| try { | ||
| const isInsert = !ctx.old; | ||
| const isUpdate = !!ctx.old; | ||
| const quote = ctx.new; | ||
|
|
||
| // Before Insert/Update: Calculate pricing | ||
| if (ctx.event === 'beforeInsert' || ctx.event === 'beforeUpdate') { |
| * Handle quote sent to customer | ||
| */ | ||
| async function handleQuoteSent(ctx: TriggerContext): Promise<void> { | ||
| const quote = ctx.new; |
Check notice
Code scanning / CodeQL
Unused variable, import, function or class Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 9 months ago
In general, the correct fix for an unused local variable is to remove the declaration, as long as it has no side effects and is not required for types. Here, quote is a simple alias for ctx.new, and is never referenced, so we can safely remove it.
The single best fix is to delete the line const quote = ctx.new; inside handleQuoteSent. This preserves all existing functionality (only logs and TODO comments remain) and resolves the CodeQL warning. No other parts of the file need to change, and no new imports or helpers are required.
Concretely, in packages/products/src/hooks/quote.hook.ts, within the handleQuoteSent function around line 285–287, remove the variable declaration line and keep the rest of the function unchanged.
| @@ -283,7 +283,6 @@ | ||
| * Handle quote sent to customer | ||
| */ | ||
| async function handleQuoteSent(ctx: TriggerContext): Promise<void> { | ||
| const quote = ctx.new; | ||
|
|
||
| try { | ||
| console.log('📧 Quote sent to customer'); |
| * Handle quote expiration | ||
| */ | ||
| async function handleQuoteExpired(ctx: TriggerContext): Promise<void> { | ||
| const quote = ctx.new; |
Check notice
Code scanning / CodeQL
Unused variable, import, function or class Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 9 months ago
To fix the problem, remove the unused local variable declaration from handleQuoteExpired. In general, unused locals should either be deleted or, if needed only for future development, commented out or given a leading underscore to signal intentional non-use. Here the best fix without changing existing behavior is simply to delete the const quote = ctx.new; line, since the function doesn’t use quote at all.
Concretely, in packages/products/src/hooks/quote.hook.ts, in the handleQuoteExpired function (around lines 302–305), delete the line const quote = ctx.new; and keep the rest of the function unchanged. No new imports, methods, or definitions are needed.
| @@ -300,8 +300,6 @@ | ||
| * Handle quote expiration | ||
| */ | ||
| async function handleQuoteExpired(ctx: TriggerContext): Promise<void> { | ||
| const quote = ctx.new; | ||
|
|
||
| try { | ||
| console.log('⏰ Quote expired'); | ||
|
|
There was a problem hiding this comment.
Pull request overview
This pull request implements a comprehensive CPQ (Configure-Price-Quote) module for HotCRM following the @objectstack/spec v0.6.1 protocol. The implementation adds product configuration capabilities, a sophisticated pricing rules engine with 7 rule types, and a multi-level discount approval workflow spanning 5 approval levels.
Changes:
- Introduces 5 new object definitions (ProductBundle, PriceRule, QuoteLineItem, ApprovalRequest, DiscountSchedule) with 211 total fields
- Implements 3 business logic hooks for automated pricing calculations, approval routing, bundle validation, stock management, and pricebook date management
- Adds comprehensive documentation detailing the technical implementation
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 22 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/products/src/quote_line_item.object.ts | New object for quote line items with pricing, discounts, and AI recommendations (40 fields) |
| packages/products/src/product_bundle.object.ts | New object for product bundles with dependencies and constraints (25 fields) |
| packages/products/src/price_rule.object.ts | New object for pricing rules with 7 rule types and validation (45 fields) |
| packages/products/src/approval_request.object.ts | New object for multi-level approval workflow (48 fields) |
| packages/products/src/discount_schedule.object.ts | New object for time-based discount schedules with margin protection (53 fields) |
| packages/products/src/hooks/quote.hook.ts | Business logic hook for quote pricing calculations and approval routing |
| packages/products/src/hooks/product.hook.ts | Business logic hook for product validation and inventory management |
| packages/products/src/hooks/pricebook.hook.ts | Business logic hook for pricebook date management and currency handling |
| packages/products/src/index.ts | Updated package exports to include new CPQ objects and hooks |
| CPQ_IMPLEMENTATION.md | Comprehensive technical documentation for the CPQ implementation |
| FINAL_SUMMARY.md | Implementation summary with statistics and metrics |
| @@ -0,0 +1,336 @@ | |||
| import type { ServiceObject } from '@objectstack/spec/data'; | |||
|
|
|||
| const ProductBundle = { | |||
There was a problem hiding this comment.
Missing type annotation: The object definition should be typed as ServiceObject to ensure type safety and protocol compliance. Change to: const ProductBundle: ServiceObject = {
| const ProductBundle = { | |
| const ProductBundle: ServiceObject = { |
| @@ -0,0 +1,539 @@ | |||
| import type { ServiceObject } from '@objectstack/spec/data'; | |||
|
|
|||
| const PriceRule = { | |||
There was a problem hiding this comment.
Missing type annotation: The object definition should be typed as ServiceObject to ensure type safety and protocol compliance. Change to: const PriceRule: ServiceObject = {
| const PriceRule = { | |
| const PriceRule: ServiceObject = { |
| @@ -0,0 +1,592 @@ | |||
| import type { ServiceObject } from '@objectstack/spec/data'; | |||
|
|
|||
| const DiscountSchedule = { | |||
There was a problem hiding this comment.
Missing type annotation: The object definition should be typed as ServiceObject to ensure type safety and protocol compliance. Change to: const DiscountSchedule: ServiceObject = {
| const DiscountSchedule = { | |
| const DiscountSchedule: ServiceObject = { |
| }, | ||
| { | ||
| name: 'ApprovalHistory', | ||
| type: 'hasMany', | ||
| object: 'ApprovalHistory', | ||
| foreignKey: 'ApprovalRequestId', | ||
| label: 'Approval History' |
There was a problem hiding this comment.
Reference to undefined object: The relationship references 'ApprovalHistory' object which doesn't appear to exist in the codebase. Ensure this object is defined or remove this relationship if it's not needed yet.
| }, | |
| { | |
| name: 'ApprovalHistory', | |
| type: 'hasMany', | |
| object: 'ApprovalHistory', | |
| foreignKey: 'ApprovalRequestId', | |
| label: 'Approval History' |
| type: 'lookup', | ||
| label: 'Approval Matrix', | ||
| reference: 'ApprovalMatrix', | ||
| description: 'Custom approval matrix for this schedule' |
There was a problem hiding this comment.
Reference to undefined object: The lookup field references 'ApprovalMatrix' which doesn't appear to exist in the codebase. Either define this object or update the reference to point to an existing object.
| type: 'lookup', | |
| label: 'Approval Matrix', | |
| reference: 'ApprovalMatrix', | |
| description: 'Custom approval matrix for this schedule' | |
| type: 'text', | |
| label: 'Approval Matrix', | |
| maxLength: 255, | |
| description: 'Custom approval matrix identifier for this schedule' |
| if (ctx.old.StockLevel === 0 && product.StockLevel > 0 && product.Status === 'Out of Stock') { | ||
| await ctx.db.doc.update('Product', product.Id, { | ||
| Status: 'Active' | ||
| }); | ||
| console.log('✅ Product reactivated after stock replenishment'); |
There was a problem hiding this comment.
Potential infinite loop: Updating the Product record in afterUpdate event will trigger another afterUpdate event, potentially causing infinite recursion. Add guards to prevent this.
| }, | ||
| { | ||
| name: 'BundleConstraints', | ||
| type: 'hasMany', | ||
| object: 'ProductBundleConstraint', | ||
| foreignKey: 'BundleId', | ||
| label: 'Constraints' |
There was a problem hiding this comment.
Reference to undefined object: The relationship references 'ProductBundleConstraint' object which doesn't appear to exist in the codebase. Ensure this object is defined or remove this relationship if it's not needed yet.
| }, | |
| { | |
| name: 'BundleConstraints', | |
| type: 'hasMany', | |
| object: 'ProductBundleConstraint', | |
| foreignKey: 'BundleId', | |
| label: 'Constraints' |
| await ctx.db.doc.update('Quote', quote.Id, { | ||
| ApprovedDate: new Date().toISOString(), | ||
| ApprovedById: ctx.user.id, | ||
| Status: quote.Status === 'In Review' ? 'Approved' : quote.Status | ||
| }); | ||
| console.log('✅ Quote approved'); | ||
| } | ||
|
|
||
| // Handle rejected | ||
| if (quote.ApprovalStatus === 'Rejected') { | ||
| await ctx.db.doc.update('Quote', quote.Id, { | ||
| RejectedDate: new Date().toISOString(), | ||
| RejectedById: ctx.user.id, | ||
| Status: 'Rejected' | ||
| }); |
There was a problem hiding this comment.
Same infinite loop issue: Updating the Quote record in afterUpdate event will trigger another afterUpdate event. These updates should either be moved to beforeUpdate or have guards to prevent recursion.
| if (effectiveDateOnly <= today && pricebook.Status === 'Draft') { | ||
| console.log(`✅ Pricebook effective date reached, activating: ${pricebook.Name}`); | ||
|
|
||
| await ctx.db.doc.update('Pricebook', pricebook.Id, { | ||
| Status: 'Active' | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| // Check if pricebook should be expired | ||
| if (pricebook.ExpirationDate) { | ||
| const expirationDate = new Date(pricebook.ExpirationDate); | ||
| const expirationDateOnly = new Date(expirationDate.getFullYear(), expirationDate.getMonth(), expirationDate.getDate()); | ||
|
|
||
| if (expirationDateOnly < today && pricebook.Status === 'Active') { | ||
| console.log(`⏰ Pricebook expired, deactivating: ${pricebook.Name}`); | ||
|
|
||
| await ctx.db.doc.update('Pricebook', pricebook.Id, { | ||
| Status: 'Expired' | ||
| }); | ||
| } | ||
| } |
There was a problem hiding this comment.
Potential infinite loop in date management: Updating the Pricebook record in afterUpdate event (lines 144-146 and 158-160) will trigger another afterUpdate event. The date comparisons at lines 141 and 155 check the current pricebook state, but after the update, these conditions might still be true, causing repeated updates. Add guards to check if the Status has already been changed to prevent recursion.
| @@ -0,0 +1,459 @@ | |||
| import type { ServiceObject } from '@objectstack/spec/data'; | |||
|
|
|||
| const QuoteLineItem = { | |||
There was a problem hiding this comment.
Missing type annotation: The object definition should be typed as ServiceObject to ensure type safety and protocol compliance. Change to: const QuoteLineItem: ServiceObject = {
| const QuoteLineItem = { | |
| const QuoteLineItem: ServiceObject = { |
Description
Implements Phase 3.1 CPQ module: product configuration engine, pricing rules engine with 7 rule types, multi-level discount approval workflow (5 levels), and quote-to-cash automation. All objects follow @objectstack/spec v0.6.1 protocol with AI-enhanced fields for intelligent recommendations.
Type of Change
Changes Made
Core Objects (211 fields across 5 objects)
Business Logic Hooks
Architecture Patterns
Approval Matrix Example
Testing
npm test)npm run lint)npm run build)Protocol Validation:
Checklist
Additional Notes
Code Review Fixes Applied:
Documentation:
CPQ_IMPLEMENTATION.md: Technical specs for all objects and hooksFINAL_SUMMARY.md: Implementation statistics and metricsNext Phase: UI components (Quote Builder, Product Configurator, Approval Dashboard, PDF generation)
Original prompt
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.