parent
da5147a1b8
commit
64cb351271
14003
API_REQUEST_LOG.txt
14003
API_REQUEST_LOG.txt
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,397 @@
|
||||||
|
[2026-06-01T06:59:45.525Z] 500 ERROR on PATCH /followups/ca96fb96-a274-452b-b850-c9e20c0c57bd
|
||||||
|
Error:
|
||||||
|
Invalid `this.prisma.followup.update()` invocation in
|
||||||
|
C:\ignosidev\Igcrm\apps\api\src\followups\followups.service.ts:174:53
|
||||||
|
|
||||||
|
171 // Fetch current state before update to detect reassignment
|
||||||
|
172 const existing = await this.prisma.followup.findUnique({ where: { id } });
|
||||||
|
173
|
||||||
|
→ 174 const followup = await this.prisma.followup.update({
|
||||||
|
where: {
|
||||||
|
id: "ca96fb96-a274-452b-b850-c9e20c0c57bd"
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
status: "DONE",
|
||||||
|
demoPersonName: "Binu",
|
||||||
|
demoContactDetails: "8568595623",
|
||||||
|
keyQueries: "",
|
||||||
|
competitorMention: "",
|
||||||
|
customerFeedback: "no remarks",
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
requirementDetails: "nothing much",
|
||||||
|
suggestions: "no",
|
||||||
|
budget: "55000",
|
||||||
|
expectedClosingTimeline: "2",
|
||||||
|
competitorInfo: "no",
|
||||||
|
staffRemarks: "friendly customer",
|
||||||
|
customerCommitments: "nothing",
|
||||||
|
caCsDetails: "no",
|
||||||
|
? id?: String | StringFieldUpdateOperationsInput,
|
||||||
|
? date?: DateTime | DateTimeFieldUpdateOperationsInput,
|
||||||
|
? notes?: String | NullableStringFieldUpdateOperationsInput | Null,
|
||||||
|
? createdAt?: DateTime | DateTimeFieldUpdateOperationsInput,
|
||||||
|
? updatedAt?: DateTime | DateTimeFieldUpdateOperationsInput,
|
||||||
|
? contentType?: String | NullableStringFieldUpdateOperationsInput | Null,
|
||||||
|
? stage?: followup_stage | Enumfollowup_stageFieldUpdateOperationsInput,
|
||||||
|
? objections?: String | NullableStringFieldUpdateOperationsInput | Null,
|
||||||
|
? type?: activity_type | Enumactivity_typeFieldUpdateOperationsInput,
|
||||||
|
? client?: ClientUpdateOneRequiredWithoutFollowupsNestedInput,
|
||||||
|
? enquiry?: EnquiryUpdateOneWithoutFollowupsNestedInput,
|
||||||
|
? opportunity?: OpportunityUpdateOneWithoutActivitiesNestedInput,
|
||||||
|
? user?: UserUpdateOneRequiredWithoutFollowupsNestedInput
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
user: true,
|
||||||
|
client: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Unknown argument `customerFeedback`. Available options are marked with ?.
|
||||||
|
Stack: PrismaClientValidationError:
|
||||||
|
Invalid `this.prisma.followup.update()` invocation in
|
||||||
|
C:\ignosidev\Igcrm\apps\api\src\followups\followups.service.ts:174:53
|
||||||
|
|
||||||
|
171 // Fetch current state before update to detect reassignment
|
||||||
|
172 const existing = await this.prisma.followup.findUnique({ where: { id } });
|
||||||
|
173
|
||||||
|
→ 174 const followup = await this.prisma.followup.update({
|
||||||
|
where: {
|
||||||
|
id: "ca96fb96-a274-452b-b850-c9e20c0c57bd"
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
status: "DONE",
|
||||||
|
demoPersonName: "Binu",
|
||||||
|
demoContactDetails: "8568595623",
|
||||||
|
keyQueries: "",
|
||||||
|
competitorMention: "",
|
||||||
|
customerFeedback: "no remarks",
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
requirementDetails: "nothing much",
|
||||||
|
suggestions: "no",
|
||||||
|
budget: "55000",
|
||||||
|
expectedClosingTimeline: "2",
|
||||||
|
competitorInfo: "no",
|
||||||
|
staffRemarks: "friendly customer",
|
||||||
|
customerCommitments: "nothing",
|
||||||
|
caCsDetails: "no",
|
||||||
|
? id?: String | StringFieldUpdateOperationsInput,
|
||||||
|
? date?: DateTime | DateTimeFieldUpdateOperationsInput,
|
||||||
|
? notes?: String | NullableStringFieldUpdateOperationsInput | Null,
|
||||||
|
? createdAt?: DateTime | DateTimeFieldUpdateOperationsInput,
|
||||||
|
? updatedAt?: DateTime | DateTimeFieldUpdateOperationsInput,
|
||||||
|
? contentType?: String | NullableStringFieldUpdateOperationsInput | Null,
|
||||||
|
? stage?: followup_stage | Enumfollowup_stageFieldUpdateOperationsInput,
|
||||||
|
? objections?: String | NullableStringFieldUpdateOperationsInput | Null,
|
||||||
|
? type?: activity_type | Enumactivity_typeFieldUpdateOperationsInput,
|
||||||
|
? client?: ClientUpdateOneRequiredWithoutFollowupsNestedInput,
|
||||||
|
? enquiry?: EnquiryUpdateOneWithoutFollowupsNestedInput,
|
||||||
|
? opportunity?: OpportunityUpdateOneWithoutActivitiesNestedInput,
|
||||||
|
? user?: UserUpdateOneRequiredWithoutFollowupsNestedInput
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
user: true,
|
||||||
|
client: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Unknown argument `customerFeedback`. Available options are marked with ?.
|
||||||
|
at wn (C:\ignosidev\Igcrm\apps\api\node_modules\@prisma\client\runtime\library.js:29:1363)
|
||||||
|
at $n.handleRequestError (C:\ignosidev\Igcrm\apps\api\node_modules\@prisma\client\runtime\library.js:121:6958)
|
||||||
|
at $n.handleAndLogRequestError (C:\ignosidev\Igcrm\apps\api\node_modules\@prisma\client\runtime\library.js:121:6623)
|
||||||
|
at $n.request (C:\ignosidev\Igcrm\apps\api\node_modules\@prisma\client\runtime\library.js:121:6307)
|
||||||
|
at async l (C:\ignosidev\Igcrm\apps\api\node_modules\@prisma\client\runtime\library.js:130:9633)
|
||||||
|
at async FollowupsService.update (C:\ignosidev\Igcrm\apps\api\src\followups\followups.service.ts:174:26)
|
||||||
|
|
||||||
|
[2026-06-01T07:00:03.225Z] 500 ERROR on PATCH /followups/ca96fb96-a274-452b-b850-c9e20c0c57bd
|
||||||
|
Error:
|
||||||
|
Invalid `this.prisma.followup.update()` invocation in
|
||||||
|
C:\ignosidev\Igcrm\apps\api\src\followups\followups.service.ts:174:53
|
||||||
|
|
||||||
|
171 // Fetch current state before update to detect reassignment
|
||||||
|
172 const existing = await this.prisma.followup.findUnique({ where: { id } });
|
||||||
|
173
|
||||||
|
→ 174 const followup = await this.prisma.followup.update({
|
||||||
|
where: {
|
||||||
|
id: "ca96fb96-a274-452b-b850-c9e20c0c57bd"
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
status: "DONE",
|
||||||
|
demoPersonName: "Binu",
|
||||||
|
demoContactDetails: "8568595623",
|
||||||
|
keyQueries: "",
|
||||||
|
competitorMention: "",
|
||||||
|
customerFeedback: "no remarks",
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
requirementDetails: "nothing much",
|
||||||
|
suggestions: "no",
|
||||||
|
budget: "55000",
|
||||||
|
expectedClosingTimeline: "2",
|
||||||
|
competitorInfo: "no",
|
||||||
|
staffRemarks: "friendly customer",
|
||||||
|
customerCommitments: "nothing",
|
||||||
|
caCsDetails: "no",
|
||||||
|
? id?: String | StringFieldUpdateOperationsInput,
|
||||||
|
? date?: DateTime | DateTimeFieldUpdateOperationsInput,
|
||||||
|
? notes?: String | NullableStringFieldUpdateOperationsInput | Null,
|
||||||
|
? createdAt?: DateTime | DateTimeFieldUpdateOperationsInput,
|
||||||
|
? updatedAt?: DateTime | DateTimeFieldUpdateOperationsInput,
|
||||||
|
? contentType?: String | NullableStringFieldUpdateOperationsInput | Null,
|
||||||
|
? stage?: followup_stage | Enumfollowup_stageFieldUpdateOperationsInput,
|
||||||
|
? objections?: String | NullableStringFieldUpdateOperationsInput | Null,
|
||||||
|
? type?: activity_type | Enumactivity_typeFieldUpdateOperationsInput,
|
||||||
|
? client?: ClientUpdateOneRequiredWithoutFollowupsNestedInput,
|
||||||
|
? enquiry?: EnquiryUpdateOneWithoutFollowupsNestedInput,
|
||||||
|
? opportunity?: OpportunityUpdateOneWithoutActivitiesNestedInput,
|
||||||
|
? user?: UserUpdateOneRequiredWithoutFollowupsNestedInput
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
user: true,
|
||||||
|
client: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Unknown argument `customerFeedback`. Available options are marked with ?.
|
||||||
|
Stack: PrismaClientValidationError:
|
||||||
|
Invalid `this.prisma.followup.update()` invocation in
|
||||||
|
C:\ignosidev\Igcrm\apps\api\src\followups\followups.service.ts:174:53
|
||||||
|
|
||||||
|
171 // Fetch current state before update to detect reassignment
|
||||||
|
172 const existing = await this.prisma.followup.findUnique({ where: { id } });
|
||||||
|
173
|
||||||
|
→ 174 const followup = await this.prisma.followup.update({
|
||||||
|
where: {
|
||||||
|
id: "ca96fb96-a274-452b-b850-c9e20c0c57bd"
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
status: "DONE",
|
||||||
|
demoPersonName: "Binu",
|
||||||
|
demoContactDetails: "8568595623",
|
||||||
|
keyQueries: "",
|
||||||
|
competitorMention: "",
|
||||||
|
customerFeedback: "no remarks",
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
requirementDetails: "nothing much",
|
||||||
|
suggestions: "no",
|
||||||
|
budget: "55000",
|
||||||
|
expectedClosingTimeline: "2",
|
||||||
|
competitorInfo: "no",
|
||||||
|
staffRemarks: "friendly customer",
|
||||||
|
customerCommitments: "nothing",
|
||||||
|
caCsDetails: "no",
|
||||||
|
? id?: String | StringFieldUpdateOperationsInput,
|
||||||
|
? date?: DateTime | DateTimeFieldUpdateOperationsInput,
|
||||||
|
? notes?: String | NullableStringFieldUpdateOperationsInput | Null,
|
||||||
|
? createdAt?: DateTime | DateTimeFieldUpdateOperationsInput,
|
||||||
|
? updatedAt?: DateTime | DateTimeFieldUpdateOperationsInput,
|
||||||
|
? contentType?: String | NullableStringFieldUpdateOperationsInput | Null,
|
||||||
|
? stage?: followup_stage | Enumfollowup_stageFieldUpdateOperationsInput,
|
||||||
|
? objections?: String | NullableStringFieldUpdateOperationsInput | Null,
|
||||||
|
? type?: activity_type | Enumactivity_typeFieldUpdateOperationsInput,
|
||||||
|
? client?: ClientUpdateOneRequiredWithoutFollowupsNestedInput,
|
||||||
|
? enquiry?: EnquiryUpdateOneWithoutFollowupsNestedInput,
|
||||||
|
? opportunity?: OpportunityUpdateOneWithoutActivitiesNestedInput,
|
||||||
|
? user?: UserUpdateOneRequiredWithoutFollowupsNestedInput
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
user: true,
|
||||||
|
client: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Unknown argument `customerFeedback`. Available options are marked with ?.
|
||||||
|
at wn (C:\ignosidev\Igcrm\apps\api\node_modules\@prisma\client\runtime\library.js:29:1363)
|
||||||
|
at $n.handleRequestError (C:\ignosidev\Igcrm\apps\api\node_modules\@prisma\client\runtime\library.js:121:6958)
|
||||||
|
at $n.handleAndLogRequestError (C:\ignosidev\Igcrm\apps\api\node_modules\@prisma\client\runtime\library.js:121:6623)
|
||||||
|
at $n.request (C:\ignosidev\Igcrm\apps\api\node_modules\@prisma\client\runtime\library.js:121:6307)
|
||||||
|
at async l (C:\ignosidev\Igcrm\apps\api\node_modules\@prisma\client\runtime\library.js:130:9633)
|
||||||
|
at async FollowupsService.update (C:\ignosidev\Igcrm\apps\api\src\followups\followups.service.ts:174:26)
|
||||||
|
|
||||||
|
[2026-06-01T07:01:05.920Z] 500 ERROR on PATCH /followups/ca96fb96-a274-452b-b850-c9e20c0c57bd
|
||||||
|
Error:
|
||||||
|
Invalid `this.prisma.followup.update()` invocation in
|
||||||
|
C:\ignosidev\Igcrm\apps\api\src\followups\followups.service.ts:174:53
|
||||||
|
|
||||||
|
171 // Fetch current state before update to detect reassignment
|
||||||
|
172 const existing = await this.prisma.followup.findUnique({ where: { id } });
|
||||||
|
173
|
||||||
|
→ 174 const followup = await this.prisma.followup.update({
|
||||||
|
where: {
|
||||||
|
id: "ca96fb96-a274-452b-b850-c9e20c0c57bd"
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
status: "DONE",
|
||||||
|
demoPersonName: "Binu",
|
||||||
|
demoContactDetails: "8568595623",
|
||||||
|
keyQueries: "",
|
||||||
|
competitorMention: "",
|
||||||
|
customerFeedback: "no remarks",
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
requirementDetails: "nothing much",
|
||||||
|
suggestions: "no",
|
||||||
|
budget: "55000",
|
||||||
|
expectedClosingTimeline: "2",
|
||||||
|
competitorInfo: "no",
|
||||||
|
staffRemarks: "friendly customer",
|
||||||
|
customerCommitments: "nothing",
|
||||||
|
caCsDetails: "no",
|
||||||
|
? id?: String | StringFieldUpdateOperationsInput,
|
||||||
|
? date?: DateTime | DateTimeFieldUpdateOperationsInput,
|
||||||
|
? notes?: String | NullableStringFieldUpdateOperationsInput | Null,
|
||||||
|
? createdAt?: DateTime | DateTimeFieldUpdateOperationsInput,
|
||||||
|
? updatedAt?: DateTime | DateTimeFieldUpdateOperationsInput,
|
||||||
|
? contentType?: String | NullableStringFieldUpdateOperationsInput | Null,
|
||||||
|
? stage?: followup_stage | Enumfollowup_stageFieldUpdateOperationsInput,
|
||||||
|
? objections?: String | NullableStringFieldUpdateOperationsInput | Null,
|
||||||
|
? type?: activity_type | Enumactivity_typeFieldUpdateOperationsInput,
|
||||||
|
? client?: ClientUpdateOneRequiredWithoutFollowupsNestedInput,
|
||||||
|
? enquiry?: EnquiryUpdateOneWithoutFollowupsNestedInput,
|
||||||
|
? opportunity?: OpportunityUpdateOneWithoutActivitiesNestedInput,
|
||||||
|
? user?: UserUpdateOneRequiredWithoutFollowupsNestedInput
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
user: true,
|
||||||
|
client: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Unknown argument `customerFeedback`. Available options are marked with ?.
|
||||||
|
Stack: PrismaClientValidationError:
|
||||||
|
Invalid `this.prisma.followup.update()` invocation in
|
||||||
|
C:\ignosidev\Igcrm\apps\api\src\followups\followups.service.ts:174:53
|
||||||
|
|
||||||
|
171 // Fetch current state before update to detect reassignment
|
||||||
|
172 const existing = await this.prisma.followup.findUnique({ where: { id } });
|
||||||
|
173
|
||||||
|
→ 174 const followup = await this.prisma.followup.update({
|
||||||
|
where: {
|
||||||
|
id: "ca96fb96-a274-452b-b850-c9e20c0c57bd"
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
status: "DONE",
|
||||||
|
demoPersonName: "Binu",
|
||||||
|
demoContactDetails: "8568595623",
|
||||||
|
keyQueries: "",
|
||||||
|
competitorMention: "",
|
||||||
|
customerFeedback: "no remarks",
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
requirementDetails: "nothing much",
|
||||||
|
suggestions: "no",
|
||||||
|
budget: "55000",
|
||||||
|
expectedClosingTimeline: "2",
|
||||||
|
competitorInfo: "no",
|
||||||
|
staffRemarks: "friendly customer",
|
||||||
|
customerCommitments: "nothing",
|
||||||
|
caCsDetails: "no",
|
||||||
|
? id?: String | StringFieldUpdateOperationsInput,
|
||||||
|
? date?: DateTime | DateTimeFieldUpdateOperationsInput,
|
||||||
|
? notes?: String | NullableStringFieldUpdateOperationsInput | Null,
|
||||||
|
? createdAt?: DateTime | DateTimeFieldUpdateOperationsInput,
|
||||||
|
? updatedAt?: DateTime | DateTimeFieldUpdateOperationsInput,
|
||||||
|
? contentType?: String | NullableStringFieldUpdateOperationsInput | Null,
|
||||||
|
? stage?: followup_stage | Enumfollowup_stageFieldUpdateOperationsInput,
|
||||||
|
? objections?: String | NullableStringFieldUpdateOperationsInput | Null,
|
||||||
|
? type?: activity_type | Enumactivity_typeFieldUpdateOperationsInput,
|
||||||
|
? client?: ClientUpdateOneRequiredWithoutFollowupsNestedInput,
|
||||||
|
? enquiry?: EnquiryUpdateOneWithoutFollowupsNestedInput,
|
||||||
|
? opportunity?: OpportunityUpdateOneWithoutActivitiesNestedInput,
|
||||||
|
? user?: UserUpdateOneRequiredWithoutFollowupsNestedInput
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
user: true,
|
||||||
|
client: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Unknown argument `customerFeedback`. Available options are marked with ?.
|
||||||
|
at wn (C:\ignosidev\Igcrm\apps\api\node_modules\@prisma\client\runtime\library.js:29:1363)
|
||||||
|
at $n.handleRequestError (C:\ignosidev\Igcrm\apps\api\node_modules\@prisma\client\runtime\library.js:121:6958)
|
||||||
|
at $n.handleAndLogRequestError (C:\ignosidev\Igcrm\apps\api\node_modules\@prisma\client\runtime\library.js:121:6623)
|
||||||
|
at $n.request (C:\ignosidev\Igcrm\apps\api\node_modules\@prisma\client\runtime\library.js:121:6307)
|
||||||
|
at async l (C:\ignosidev\Igcrm\apps\api\node_modules\@prisma\client\runtime\library.js:130:9633)
|
||||||
|
at async FollowupsService.update (C:\ignosidev\Igcrm\apps\api\src\followups\followups.service.ts:174:26)
|
||||||
|
|
||||||
|
[2026-06-09T06:21:07.186Z] 500 ERROR on PATCH /products/02cb5871-795c-419f-99f8-6710f77274e9
|
||||||
|
Error:
|
||||||
|
Invalid `this.prisma.product.update()` invocation in
|
||||||
|
C:\ignosidev\Igcrm\apps\api\src\products\products.service.ts:45:32
|
||||||
|
|
||||||
|
42 await this.prisma.productAddon.deleteMany({ where: { productId: id } });
|
||||||
|
43 }
|
||||||
|
44
|
||||||
|
→ 45 return this.prisma.product.update({
|
||||||
|
where: {
|
||||||
|
id: "02cb5871-795c-419f-99f8-6710f77274e9"
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
name: "Nidhi",
|
||||||
|
description: "Nidhi is a software for Nidhi companies",
|
||||||
|
price: 35000,
|
||||||
|
maxDiscountPercentage: 15,
|
||||||
|
schemes: {
|
||||||
|
create: [
|
||||||
|
{
|
||||||
|
name: "with branch",
|
||||||
|
price: 45000,
|
||||||
|
features: "",
|
||||||
|
brochureUrl: null,
|
||||||
|
maxDiscountPercentage: 100,
|
||||||
|
productId: "02cb5871-795c-419f-99f8-6710f77274e9",
|
||||||
|
createdAt: "2026-06-04T05:31:55.630Z",
|
||||||
|
updatedAt: "2026-06-04T05:31:55.630Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
addons: undefined
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
schemes: true,
|
||||||
|
addons: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Unknown argument `productId`. Available options are marked with ?.
|
||||||
|
Stack: PrismaClientValidationError:
|
||||||
|
Invalid `this.prisma.product.update()` invocation in
|
||||||
|
C:\ignosidev\Igcrm\apps\api\src\products\products.service.ts:45:32
|
||||||
|
|
||||||
|
42 await this.prisma.productAddon.deleteMany({ where: { productId: id } });
|
||||||
|
43 }
|
||||||
|
44
|
||||||
|
→ 45 return this.prisma.product.update({
|
||||||
|
where: {
|
||||||
|
id: "02cb5871-795c-419f-99f8-6710f77274e9"
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
name: "Nidhi",
|
||||||
|
description: "Nidhi is a software for Nidhi companies",
|
||||||
|
price: 35000,
|
||||||
|
maxDiscountPercentage: 15,
|
||||||
|
schemes: {
|
||||||
|
create: [
|
||||||
|
{
|
||||||
|
name: "with branch",
|
||||||
|
price: 45000,
|
||||||
|
features: "",
|
||||||
|
brochureUrl: null,
|
||||||
|
maxDiscountPercentage: 100,
|
||||||
|
productId: "02cb5871-795c-419f-99f8-6710f77274e9",
|
||||||
|
createdAt: "2026-06-04T05:31:55.630Z",
|
||||||
|
updatedAt: "2026-06-04T05:31:55.630Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
addons: undefined
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
schemes: true,
|
||||||
|
addons: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Unknown argument `productId`. Available options are marked with ?.
|
||||||
|
at wn (C:\ignosidev\Igcrm\apps\api\node_modules\@prisma\client\runtime\library.js:29:1363)
|
||||||
|
at $n.handleRequestError (C:\ignosidev\Igcrm\apps\api\node_modules\@prisma\client\runtime\library.js:121:6958)
|
||||||
|
at $n.handleAndLogRequestError (C:\ignosidev\Igcrm\apps\api\node_modules\@prisma\client\runtime\library.js:121:6623)
|
||||||
|
at $n.request (C:\ignosidev\Igcrm\apps\api\node_modules\@prisma\client\runtime\library.js:121:6307)
|
||||||
|
at async l (C:\ignosidev\Igcrm\apps\api\node_modules\@prisma\client\runtime\library.js:130:9633)
|
||||||
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
const { PrismaClient } = require('@prisma/client');
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const followupId = "ca96fb96-a274-452b-b850-c9e20c0c57bd";
|
||||||
|
const followup = await prisma.followup.findUnique({
|
||||||
|
where: { id: followupId },
|
||||||
|
include: { opportunity: true, client: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("FOLLOWUP:", JSON.stringify(followup, null, 2));
|
||||||
|
|
||||||
|
if (followup && followup.clientId) {
|
||||||
|
const opps = await prisma.opportunity.findMany({
|
||||||
|
where: { clientId: followup.clientId }
|
||||||
|
});
|
||||||
|
console.log("CLIENT OPPORTUNITIES:", JSON.stringify(opps, null, 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.catch(console.error)
|
||||||
|
.finally(() => prisma.$disconnect());
|
||||||
|
|
@ -121,6 +121,7 @@ model Followup {
|
||||||
demoPersonName String?
|
demoPersonName String?
|
||||||
keyQueries String?
|
keyQueries String?
|
||||||
objections String?
|
objections String?
|
||||||
|
suggestions String?
|
||||||
opportunityId String?
|
opportunityId String?
|
||||||
type activity_type @default(FOLLOWUP)
|
type activity_type @default(FOLLOWUP)
|
||||||
client Client @relation(fields: [clientId], references: [id], map: "Followup_clientId_fkey")
|
client Client @relation(fields: [clientId], references: [id], map: "Followup_clientId_fkey")
|
||||||
|
|
@ -189,6 +190,8 @@ model Opportunity {
|
||||||
clientId String
|
clientId String
|
||||||
assignedTo String
|
assignedTo String
|
||||||
expectedCloseDate DateTime?
|
expectedCloseDate DateTime?
|
||||||
|
closingProbability Float?
|
||||||
|
expectedClosingTimeframe String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
competitorMention String?
|
competitorMention String?
|
||||||
|
|
@ -200,6 +203,12 @@ model Opportunity {
|
||||||
objections String?
|
objections String?
|
||||||
paymentMode String?
|
paymentMode String?
|
||||||
specialRate Float?
|
specialRate Float?
|
||||||
|
selectedSchemeId String?
|
||||||
|
addonsSelected String? @db.LongText
|
||||||
|
discountValue Float? @default(0)
|
||||||
|
isAmcMarked Boolean? @default(false)
|
||||||
|
amcPercentage Float? @default(0)
|
||||||
|
extraCharges Float? @default(0)
|
||||||
creatorId String?
|
creatorId String?
|
||||||
closingOwnerId String?
|
closingOwnerId String?
|
||||||
demoOwnerId String?
|
demoOwnerId String?
|
||||||
|
|
@ -259,13 +268,68 @@ model Product {
|
||||||
description String?
|
description String?
|
||||||
price Float
|
price Float
|
||||||
imageUrl String?
|
imageUrl String?
|
||||||
|
brochureUrl String?
|
||||||
|
maxDiscountPercentage Float? @default(100)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
enquiries Enquiry[] @relation("enquiry_products")
|
enquiries Enquiry[] @relation("enquiry_products")
|
||||||
|
schemes ProductScheme[]
|
||||||
|
addons ProductAddon[]
|
||||||
|
userDiscounts UserProductDiscount[]
|
||||||
|
|
||||||
@@map("product")
|
@@map("product")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model ProductScheme {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
productId String
|
||||||
|
name String
|
||||||
|
price Float
|
||||||
|
features String? @db.Text
|
||||||
|
brochureUrl String?
|
||||||
|
maxDiscountPercentage Float? @default(100)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
|
||||||
|
userDiscounts UserProductDiscount[]
|
||||||
|
|
||||||
|
@@index([productId])
|
||||||
|
@@map("product_scheme")
|
||||||
|
}
|
||||||
|
|
||||||
|
model UserProductDiscount {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
userId String
|
||||||
|
productId String
|
||||||
|
schemeId String? // null = product-level cap; set = scheme-specific cap
|
||||||
|
maxDiscountPercentage Float @default(100)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
|
||||||
|
scheme ProductScheme? @relation(fields: [schemeId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@unique([userId, productId, schemeId])
|
||||||
|
@@index([userId])
|
||||||
|
@@index([productId])
|
||||||
|
@@index([schemeId])
|
||||||
|
@@map("user_product_discount")
|
||||||
|
}
|
||||||
|
|
||||||
|
model ProductAddon {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
productId String
|
||||||
|
name String // e.g. "Cloud Backup", "Custom SMTP"
|
||||||
|
price Float // price of this addon
|
||||||
|
description String? @db.Text
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@index([productId])
|
||||||
|
@@map("product_addon")
|
||||||
|
}
|
||||||
|
|
||||||
model Quote {
|
model Quote {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
enquiryId String?
|
enquiryId String?
|
||||||
|
|
@ -382,6 +446,7 @@ model User {
|
||||||
manager User? @relation("userTouser", fields: [managerId], references: [id], map: "User_managerId_fkey")
|
manager User? @relation("userTouser", fields: [managerId], references: [id], map: "User_managerId_fkey")
|
||||||
subordinates User[] @relation("userTouser")
|
subordinates User[] @relation("userTouser")
|
||||||
permissions String? @db.LongText
|
permissions String? @db.LongText
|
||||||
|
productDiscounts UserProductDiscount[]
|
||||||
|
|
||||||
@@index([managerId], map: "User_managerId_fkey")
|
@@index([managerId], map: "User_managerId_fkey")
|
||||||
@@map("user")
|
@@map("user")
|
||||||
|
|
@ -495,4 +560,9 @@ enum activity_type {
|
||||||
FOLLOWUP
|
FOLLOWUP
|
||||||
DEMO
|
DEMO
|
||||||
QUOTE
|
QUOTE
|
||||||
|
SECOND_DEMO
|
||||||
|
SECOND_QUOTE
|
||||||
|
MANAGER_HELP
|
||||||
|
PROBABILITY_UPDATE
|
||||||
|
TIMEFRAME_UPDATE
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,133 @@ export class FollowupsService {
|
||||||
private whatsappService: WhatsappService
|
private whatsappService: WhatsappService
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
|
// Auto-transition pipeline opportunity stage forward
|
||||||
|
private async autoTransitionStage(opportunityId: string, newStage: 'QUALIFIED' | 'POTENTIAL' | 'SALES') {
|
||||||
|
try {
|
||||||
|
const current = await this.prisma.opportunity.findUnique({
|
||||||
|
where: { id: opportunityId },
|
||||||
|
include: { client: true }
|
||||||
|
});
|
||||||
|
if (!current) return;
|
||||||
|
|
||||||
|
const stagesOrder = { LEAD: 1, QUALIFIED: 2, POTENTIAL: 3, SALES: 4, LOST: 0 };
|
||||||
|
const currentPriority = stagesOrder[current.stage] || 0;
|
||||||
|
const newPriority = stagesOrder[newStage] || 0;
|
||||||
|
|
||||||
|
// Only transition forward
|
||||||
|
if (newPriority > currentPriority) {
|
||||||
|
const updated = await this.prisma.opportunity.update({
|
||||||
|
where: { id: opportunityId },
|
||||||
|
data: { stage: newStage, updatedAt: new Date() },
|
||||||
|
include: { client: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[AUTO-STAGE] Transitioned opportunity ${opportunityId} from ${current.stage} to ${newStage}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.whatsappService.sendPipelineUpdate(
|
||||||
|
updated.client.phone,
|
||||||
|
updated.client.name,
|
||||||
|
current.stage,
|
||||||
|
updated.stage
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to send pipeline WhatsApp on auto-transition:', err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error during auto-transition of opportunity stage:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async evaluateStageTransitions(followup: any) {
|
||||||
|
let opportunityIds: string[] = [];
|
||||||
|
|
||||||
|
if (followup.opportunityId) {
|
||||||
|
opportunityIds.push(followup.opportunityId);
|
||||||
|
} else if (followup.clientId) {
|
||||||
|
// Fallback: If opportunityId is null, resolve all open opportunities of the client
|
||||||
|
try {
|
||||||
|
const clientOpps = await this.prisma.opportunity.findMany({
|
||||||
|
where: {
|
||||||
|
clientId: followup.clientId,
|
||||||
|
stage: { in: ['LEAD', 'QUALIFIED', 'POTENTIAL'] }
|
||||||
|
},
|
||||||
|
select: { id: true }
|
||||||
|
});
|
||||||
|
opportunityIds = clientOpps.map(o => o.id);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to look up client opportunities for stage transition fallback:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opportunityIds.length === 0) return;
|
||||||
|
|
||||||
|
const type = followup.type;
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const opportunityId of opportunityIds) {
|
||||||
|
if (['QUOTE_SEND', 'QUOTE', 'QUOTE_REQUEST', 'SECOND_QUOTE'].includes(type)) {
|
||||||
|
// Count previous quotes and activities of this type
|
||||||
|
const prevQuotesCount = await this.prisma.quote.count({
|
||||||
|
where: { opportunityId }
|
||||||
|
});
|
||||||
|
const prevQuoteActivities = await this.prisma.followup.count({
|
||||||
|
where: {
|
||||||
|
opportunityId,
|
||||||
|
id: { not: followup.id },
|
||||||
|
type: { in: ['QUOTE_SEND', 'QUOTE', 'QUOTE_REQUEST', 'SECOND_QUOTE'] }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (prevQuotesCount + prevQuoteActivities >= 1) {
|
||||||
|
await this.autoTransitionStage(opportunityId, 'SALES');
|
||||||
|
} else {
|
||||||
|
await this.autoTransitionStage(opportunityId, 'QUALIFIED');
|
||||||
|
}
|
||||||
|
} else if (['DEMO_SCHEDULED', 'DEMO_COMPLETED', 'DEMO', 'SECOND_DEMO'].includes(type)) {
|
||||||
|
// Count previous demos
|
||||||
|
const prevDemoActivities = await this.prisma.followup.count({
|
||||||
|
where: {
|
||||||
|
opportunityId,
|
||||||
|
id: { not: followup.id },
|
||||||
|
type: { in: ['DEMO_SCHEDULED', 'DEMO_COMPLETED', 'DEMO', 'SECOND_DEMO'] }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (prevDemoActivities >= 1) {
|
||||||
|
await this.autoTransitionStage(opportunityId, 'SALES');
|
||||||
|
} else {
|
||||||
|
await this.autoTransitionStage(opportunityId, 'POTENTIAL');
|
||||||
|
}
|
||||||
|
} else if (['VISIT_SCHEDULED', 'VISIT_COMPLETED'].includes(type)) {
|
||||||
|
await this.autoTransitionStage(opportunityId, 'SALES');
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's a mutation activity marked as DONE
|
||||||
|
if (followup.status === 'DONE') {
|
||||||
|
if (type === 'TIMEFRAME_UPDATE' && followup.expectedClosingTimeframe) {
|
||||||
|
await this.prisma.opportunity.update({
|
||||||
|
where: { id: opportunityId },
|
||||||
|
data: { expectedClosingTimeframe: followup.expectedClosingTimeframe }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (type === 'PROBABILITY_UPDATE' && followup.notes) {
|
||||||
|
const parsedProb = parseFloat(followup.notes);
|
||||||
|
if (!isNaN(parsedProb)) {
|
||||||
|
await this.prisma.opportunity.update({
|
||||||
|
where: { id: opportunityId },
|
||||||
|
data: { closingProbability: parsedProb }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in evaluateStageTransitions:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async create(createFollowupDto: CreateFollowupDto) {
|
async create(createFollowupDto: CreateFollowupDto) {
|
||||||
// Map description to notes if provided, and exclude 'time' which is only used by the frontend
|
// Map description to notes if provided, and exclude 'time' which is only used by the frontend
|
||||||
const { description, time, ...rest } = createFollowupDto as any;
|
const { description, time, ...rest } = createFollowupDto as any;
|
||||||
|
|
@ -25,29 +152,30 @@ export class FollowupsService {
|
||||||
include: { client: true }
|
include: { client: true }
|
||||||
});
|
});
|
||||||
|
|
||||||
// Trigger WhatsApp followup reminder to client
|
// Trigger WhatsApp followup reminder — fire-and-forget (non-blocking)
|
||||||
if (followup.client?.phone) {
|
if (followup.client?.phone) {
|
||||||
try {
|
this.whatsappService.sendFollowupReminder(
|
||||||
await this.whatsappService.sendFollowupReminder(
|
|
||||||
followup.client.phone,
|
followup.client.phone,
|
||||||
followup.client.name,
|
followup.client.name,
|
||||||
new Date(followup.date).toLocaleString()
|
new Date(followup.date).toLocaleString()
|
||||||
);
|
).catch(err => console.error('Failed to send followup WhatsApp:', err.message));
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to send followup WhatsApp:', err.message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send Alert to Assigned User if different from creator
|
// Send Alert to Assigned User — non-blocking
|
||||||
if (data.userId) {
|
if (data.userId) {
|
||||||
await this.notifications.create(
|
this.notifications.create(
|
||||||
data.userId,
|
data.userId,
|
||||||
'New Follow-up Assigned 📅',
|
'New Follow-up Assigned 📅',
|
||||||
`You have been assigned a new follow-up task for client. Deadline: ${new Date(data.date).toLocaleString()}`,
|
`You have been assigned a new follow-up task for client. Deadline: ${new Date(data.date).toLocaleString()}`,
|
||||||
'FOLLOWUP_ASSIGNED'
|
'FOLLOWUP_ASSIGNED'
|
||||||
);
|
).catch(err => console.error('Failed to send notification:', err.message));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Evaluate auto-stage transitions — fire-and-forget (non-blocking)
|
||||||
|
this.evaluateStageTransitions(followup).catch(err =>
|
||||||
|
console.error('Error in evaluateStageTransitions (non-blocking):', err)
|
||||||
|
);
|
||||||
|
|
||||||
return followup;
|
return followup;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -81,9 +209,48 @@ export class FollowupsService {
|
||||||
// Fetch current state before update to detect reassignment
|
// Fetch current state before update to detect reassignment
|
||||||
const existing = await this.prisma.followup.findUnique({ where: { id } });
|
const existing = await this.prisma.followup.findUnique({ where: { id } });
|
||||||
|
|
||||||
|
// Extract extra fields that don't exist in Prisma schema to avoid database errors
|
||||||
|
const schemaFields = [
|
||||||
|
'id', 'clientId', 'enquiryId', 'userId', 'date', 'notes', 'status',
|
||||||
|
'createdAt', 'updatedAt', 'contentType', 'stage', 'competitorMention',
|
||||||
|
'demoContactDetails', 'demoPersonName', 'keyQueries', 'objections',
|
||||||
|
'opportunityId', 'type'
|
||||||
|
];
|
||||||
|
|
||||||
|
const extraData: Record<string, any> = {};
|
||||||
|
const prismaData: Record<string, any> = {};
|
||||||
|
|
||||||
|
Object.keys(updateFollowupDto).forEach(key => {
|
||||||
|
if (schemaFields.includes(key)) {
|
||||||
|
prismaData[key] = updateFollowupDto[key];
|
||||||
|
} else {
|
||||||
|
extraData[key] = updateFollowupDto[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// If there are extra feedback fields, format them nicely and append to 'notes'
|
||||||
|
if (Object.keys(extraData).length > 0) {
|
||||||
|
const feedbackSummary = Object.entries(extraData)
|
||||||
|
.filter(([_, val]) => val !== undefined && val !== null && val !== '')
|
||||||
|
.map(([key, val]) => {
|
||||||
|
const label = key
|
||||||
|
.replace(/([A-Z])/g, ' $1')
|
||||||
|
.replace(/^./, str => str.toUpperCase());
|
||||||
|
return `• ${label}: ${val}`;
|
||||||
|
})
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
if (feedbackSummary) {
|
||||||
|
const originalNotes = existing?.notes || '';
|
||||||
|
prismaData.notes = originalNotes
|
||||||
|
? `${originalNotes}\n\n[DEMO FEEDBACK LOG]\n${feedbackSummary}`
|
||||||
|
: `[DEMO FEEDBACK LOG]\n${feedbackSummary}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const followup = await this.prisma.followup.update({
|
const followup = await this.prisma.followup.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: updateFollowupDto,
|
data: prismaData,
|
||||||
include: { user: true, client: true }
|
include: { user: true, client: true }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -131,6 +298,11 @@ export class FollowupsService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Evaluate auto-stage transitions — only when explicitly marking DONE
|
||||||
|
if (updateFollowupDto.status === 'DONE') {
|
||||||
|
await this.evaluateStageTransitions(followup);
|
||||||
|
}
|
||||||
|
|
||||||
return followup;
|
return followup;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -81,4 +81,28 @@ export class CreateOpportunityDto {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
expectedClosingTimeframe?: string;
|
expectedClosingTimeframe?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
selectedSchemeId?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
addonsSelected?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
discountValue?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
isAmcMarked?: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
amcPercentage?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
extraCharges?: number;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,96 @@ export class OpportunitiesService {
|
||||||
private readonly whatsappService: WhatsappService
|
private readonly whatsappService: WhatsappService
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
|
private async validateDiscount(
|
||||||
|
title: string,
|
||||||
|
assignedTo: string,
|
||||||
|
discountValue: number,
|
||||||
|
selectedSchemeId?: string | null
|
||||||
|
) {
|
||||||
|
if (!discountValue || discountValue <= 0) return;
|
||||||
|
|
||||||
|
// 1. Find product by name
|
||||||
|
const product = await this.prisma.product.findFirst({
|
||||||
|
where: { name: title },
|
||||||
|
include: { schemes: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!product) return;
|
||||||
|
|
||||||
|
// 2. Determine base price and scheme ID
|
||||||
|
let basePrice = product.price;
|
||||||
|
let schemeId: string | null = null;
|
||||||
|
let scheme: any = null;
|
||||||
|
|
||||||
|
if (selectedSchemeId && selectedSchemeId !== 'base') {
|
||||||
|
scheme = product.schemes.find(s => s.id === selectedSchemeId);
|
||||||
|
if (scheme) {
|
||||||
|
basePrice = scheme.price;
|
||||||
|
schemeId = scheme.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (basePrice <= 0) return;
|
||||||
|
|
||||||
|
// 3. Find applicable cap
|
||||||
|
let allowedPercentage: number | null = null;
|
||||||
|
|
||||||
|
// A. User-Scheme cap
|
||||||
|
if (schemeId && assignedTo) {
|
||||||
|
const userSchemeCap = await this.prisma.userProductDiscount.findFirst({
|
||||||
|
where: { userId: assignedTo, productId: product.id, schemeId }
|
||||||
|
});
|
||||||
|
if (userSchemeCap) {
|
||||||
|
allowedPercentage = userSchemeCap.maxDiscountPercentage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// B. User-Product cap
|
||||||
|
if (allowedPercentage === null && assignedTo) {
|
||||||
|
const userProductCap = await this.prisma.userProductDiscount.findFirst({
|
||||||
|
where: { userId: assignedTo, productId: product.id, schemeId: null }
|
||||||
|
});
|
||||||
|
if (userProductCap) {
|
||||||
|
allowedPercentage = userProductCap.maxDiscountPercentage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// C. Scheme fallback cap
|
||||||
|
if (allowedPercentage === null && scheme) {
|
||||||
|
allowedPercentage = scheme.maxDiscountPercentage;
|
||||||
|
}
|
||||||
|
|
||||||
|
// D. Product fallback cap
|
||||||
|
if (allowedPercentage === null) {
|
||||||
|
allowedPercentage = product.maxDiscountPercentage;
|
||||||
|
}
|
||||||
|
|
||||||
|
// E. Ultimate default if everything is null
|
||||||
|
if (allowedPercentage === null) {
|
||||||
|
allowedPercentage = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Calculate applied discount percentage
|
||||||
|
const appliedPercentage = (discountValue / basePrice) * 100;
|
||||||
|
|
||||||
|
// Check if limit exceeded (adding a small epsilon for floating point issues)
|
||||||
|
if (appliedPercentage > allowedPercentage + 0.001) {
|
||||||
|
const maxAllowedValue = (basePrice * allowedPercentage) / 100;
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Applied discount of ₹${discountValue.toLocaleString()} (${appliedPercentage.toFixed(1)}%) exceeds the maximum allowed discount of ${allowedPercentage}% (₹${maxAllowedValue.toLocaleString()}) for this product.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async create(createOpportunityDto: CreateOpportunityDto) {
|
async create(createOpportunityDto: CreateOpportunityDto) {
|
||||||
try {
|
try {
|
||||||
|
await this.validateDiscount(
|
||||||
|
createOpportunityDto.title,
|
||||||
|
createOpportunityDto.assignedTo,
|
||||||
|
createOpportunityDto.discountValue ?? 0,
|
||||||
|
createOpportunityDto.selectedSchemeId
|
||||||
|
);
|
||||||
|
|
||||||
return await this.prisma.opportunity.create({
|
return await this.prisma.opportunity.create({
|
||||||
data: {
|
data: {
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
|
|
@ -32,6 +120,12 @@ export class OpportunitiesService {
|
||||||
creatorId: createOpportunityDto.creatorId,
|
creatorId: createOpportunityDto.creatorId,
|
||||||
closingProbability: createOpportunityDto.closingProbability ? Number(createOpportunityDto.closingProbability) : 0,
|
closingProbability: createOpportunityDto.closingProbability ? Number(createOpportunityDto.closingProbability) : 0,
|
||||||
expectedClosingTimeframe: createOpportunityDto.expectedClosingTimeframe || null,
|
expectedClosingTimeframe: createOpportunityDto.expectedClosingTimeframe || null,
|
||||||
|
selectedSchemeId: createOpportunityDto.selectedSchemeId || null,
|
||||||
|
addonsSelected: createOpportunityDto.addonsSelected || null,
|
||||||
|
discountValue: createOpportunityDto.discountValue !== undefined ? Number(createOpportunityDto.discountValue) : 0,
|
||||||
|
isAmcMarked: !!createOpportunityDto.isAmcMarked,
|
||||||
|
amcPercentage: createOpportunityDto.amcPercentage !== undefined ? Number(createOpportunityDto.amcPercentage) : 0,
|
||||||
|
extraCharges: createOpportunityDto.extraCharges !== undefined ? Number(createOpportunityDto.extraCharges) : 0,
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -48,6 +142,7 @@ export class OpportunitiesService {
|
||||||
include: {
|
include: {
|
||||||
client: true,
|
client: true,
|
||||||
user: true,
|
user: true,
|
||||||
|
activities: true,
|
||||||
},
|
},
|
||||||
orderBy: { updatedAt: 'desc' }
|
orderBy: { updatedAt: 'desc' }
|
||||||
});
|
});
|
||||||
|
|
@ -63,6 +158,7 @@ export class OpportunitiesService {
|
||||||
include: {
|
include: {
|
||||||
client: true,
|
client: true,
|
||||||
user: true,
|
user: true,
|
||||||
|
activities: true,
|
||||||
},
|
},
|
||||||
orderBy: { updatedAt: 'desc' }
|
orderBy: { updatedAt: 'desc' }
|
||||||
});
|
});
|
||||||
|
|
@ -74,6 +170,7 @@ export class OpportunitiesService {
|
||||||
include: {
|
include: {
|
||||||
client: true,
|
client: true,
|
||||||
user: true,
|
user: true,
|
||||||
|
activities: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!opp) throw new NotFoundException('Opportunity not found');
|
if (!opp) throw new NotFoundException('Opportunity not found');
|
||||||
|
|
@ -86,6 +183,13 @@ export class OpportunitiesService {
|
||||||
const current = await this.prisma.opportunity.findUnique({ where: { id } });
|
const current = await this.prisma.opportunity.findUnique({ where: { id } });
|
||||||
if (!current) throw new NotFoundException('Opportunity not found');
|
if (!current) throw new NotFoundException('Opportunity not found');
|
||||||
|
|
||||||
|
const title = updateOpportunityDto.title !== undefined ? updateOpportunityDto.title : current.title;
|
||||||
|
const assignedTo = updateOpportunityDto.assignedTo !== undefined ? updateOpportunityDto.assignedTo : current.assignedTo;
|
||||||
|
const discountValue = updateOpportunityDto.discountValue !== undefined ? Number(updateOpportunityDto.discountValue) : Number(current.discountValue || 0);
|
||||||
|
const selectedSchemeId = updateOpportunityDto.selectedSchemeId !== undefined ? updateOpportunityDto.selectedSchemeId : current.selectedSchemeId;
|
||||||
|
|
||||||
|
await this.validateDiscount(title, assignedTo, discountValue, selectedSchemeId);
|
||||||
|
|
||||||
const newStage = updateOpportunityDto.stage || current.stage;
|
const newStage = updateOpportunityDto.stage || current.stage;
|
||||||
|
|
||||||
// Validation Logic for Demo
|
// Validation Logic for Demo
|
||||||
|
|
@ -126,6 +230,10 @@ export class OpportunitiesService {
|
||||||
|
|
||||||
if (data.value) data.value = Number(data.value);
|
if (data.value) data.value = Number(data.value);
|
||||||
if (data.specialRate) data.specialRate = Number(data.specialRate);
|
if (data.specialRate) data.specialRate = Number(data.specialRate);
|
||||||
|
if (data.discountValue !== undefined) data.discountValue = Number(data.discountValue);
|
||||||
|
if (data.amcPercentage !== undefined) data.amcPercentage = Number(data.amcPercentage);
|
||||||
|
if (data.extraCharges !== undefined) data.extraCharges = Number(data.extraCharges);
|
||||||
|
if (data.isAmcMarked !== undefined) data.isAmcMarked = !!data.isAmcMarked;
|
||||||
|
|
||||||
// Fix foreign key constraint errors for empty strings
|
// Fix foreign key constraint errors for empty strings
|
||||||
if (data.creatorId === '') data.creatorId = null;
|
if (data.creatorId === '') data.creatorId = null;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,41 @@
|
||||||
import { IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator';
|
import { IsNotEmpty, IsNumber, IsOptional, IsString, IsArray, ValidateNested } from 'class-validator';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
|
||||||
|
export class CreateProductSchemeDto {
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
@IsNotEmpty()
|
||||||
|
price: number;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
features?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
brochureUrl?: string;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
@IsOptional()
|
||||||
|
maxDiscountPercentage?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CreateProductAddonDto {
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
@IsNotEmpty()
|
||||||
|
price: number;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class CreateProductDto {
|
export class CreateProductDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
|
|
@ -16,4 +53,24 @@ export class CreateProductDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
imageUrl?: string;
|
imageUrl?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
brochureUrl?: string;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
@IsOptional()
|
||||||
|
maxDiscountPercentage?: number;
|
||||||
|
|
||||||
|
@IsArray()
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => CreateProductSchemeDto)
|
||||||
|
schemes?: CreateProductSchemeDto[];
|
||||||
|
|
||||||
|
@IsArray()
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => CreateProductAddonDto)
|
||||||
|
addons?: CreateProductAddonDto[];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { IsNotEmpty, IsNumber, IsOptional, IsString, Max, Min } from 'class-validator';
|
||||||
|
|
||||||
|
export class UpsertUserProductDiscountDto {
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
userId: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
schemeId?: string; // null / omit = product-level cap
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
@Min(0)
|
||||||
|
@Max(100)
|
||||||
|
maxDiscountPercentage: number;
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,23 @@
|
||||||
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Body,
|
||||||
|
Patch,
|
||||||
|
Param,
|
||||||
|
Delete,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { ProductsService } from './products.service';
|
import { ProductsService } from './products.service';
|
||||||
import { CreateProductDto } from './dto/create-product.dto';
|
import { CreateProductDto } from './dto/create-product.dto';
|
||||||
import { UpdateProductDto } from './dto/update-product.dto';
|
import { UpdateProductDto } from './dto/update-product.dto';
|
||||||
|
import { UpsertUserProductDiscountDto } from './dto/user-product-discount.dto';
|
||||||
|
|
||||||
@Controller('products')
|
@Controller('products')
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
export class ProductsController {
|
export class ProductsController {
|
||||||
constructor(private readonly productsService: ProductsService) { }
|
constructor(private readonly productsService: ProductsService) {}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
create(@Body() createProductDto: CreateProductDto) {
|
create(@Body() createProductDto: CreateProductDto) {
|
||||||
|
|
@ -31,4 +43,32 @@ export class ProductsController {
|
||||||
remove(@Param('id') id: string) {
|
remove(@Param('id') id: string) {
|
||||||
return this.productsService.remove(id);
|
return this.productsService.remove(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Per-User Discount Cap Endpoints ─────────────────────────────────────
|
||||||
|
|
||||||
|
/** GET /products/:productId/user-discounts — list all user caps for this product */
|
||||||
|
@Get(':productId/user-discounts')
|
||||||
|
getUserDiscounts(@Param('productId') productId: string) {
|
||||||
|
return this.productsService.getUserDiscounts(productId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** POST /products/:productId/user-discounts — upsert a user cap */
|
||||||
|
@Post(':productId/user-discounts')
|
||||||
|
upsertUserDiscount(
|
||||||
|
@Param('productId') productId: string,
|
||||||
|
@Body() dto: UpsertUserProductDiscountDto,
|
||||||
|
) {
|
||||||
|
return this.productsService.upsertUserDiscount(
|
||||||
|
productId,
|
||||||
|
dto.userId,
|
||||||
|
dto.maxDiscountPercentage,
|
||||||
|
dto.schemeId ?? null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** DELETE /products/:productId/user-discounts/:discountId — remove a user cap */
|
||||||
|
@Delete(':productId/user-discounts/:discountId')
|
||||||
|
removeUserDiscount(@Param('discountId') discountId: string) {
|
||||||
|
return this.productsService.removeUserDiscount(discountId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,34 +5,114 @@ import { UpdateProductDto } from './dto/update-product.dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ProductsService {
|
export class ProductsService {
|
||||||
constructor(private readonly prisma: PrismaService) { }
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
create(createProductDto: CreateProductDto) {
|
async create(createProductDto: CreateProductDto) {
|
||||||
|
const { schemes, addons, ...productData } = createProductDto;
|
||||||
return this.prisma.product.create({
|
return this.prisma.product.create({
|
||||||
data: createProductDto,
|
data: {
|
||||||
|
...productData,
|
||||||
|
schemes: schemes && schemes.length > 0 ? { create: schemes } : undefined,
|
||||||
|
addons: addons && addons.length > 0 ? { create: addons } : undefined,
|
||||||
|
},
|
||||||
|
include: { schemes: true, addons: true },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
findAll() {
|
findAll() {
|
||||||
return this.prisma.product.findMany();
|
return this.prisma.product.findMany({
|
||||||
|
include: { schemes: true, addons: true },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
findOne(id: string) {
|
findOne(id: string) {
|
||||||
return this.prisma.product.findUnique({
|
return this.prisma.product.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
|
include: { schemes: true, addons: true },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
update(id: string, updateProductDto: UpdateProductDto) {
|
async update(id: string, updateProductDto: UpdateProductDto) {
|
||||||
|
const { schemes, addons, ...productData } = updateProductDto;
|
||||||
|
|
||||||
|
if (schemes) {
|
||||||
|
await this.prisma.productScheme.deleteMany({ where: { productId: id } });
|
||||||
|
}
|
||||||
|
if (addons) {
|
||||||
|
await this.prisma.productAddon.deleteMany({ where: { productId: id } });
|
||||||
|
}
|
||||||
|
|
||||||
return this.prisma.product.update({
|
return this.prisma.product.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: updateProductDto,
|
data: {
|
||||||
|
...productData,
|
||||||
|
schemes: schemes && schemes.length > 0 ? { create: schemes } : undefined,
|
||||||
|
addons: addons && addons.length > 0 ? { create: addons } : undefined,
|
||||||
|
},
|
||||||
|
include: { schemes: true, addons: true },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
remove(id: string) {
|
remove(id: string) {
|
||||||
return this.prisma.product.delete({
|
return this.prisma.product.delete({ where: { id } });
|
||||||
where: { id },
|
}
|
||||||
|
|
||||||
|
// ─── Per-User Discount Cap Methods ───────────────────────────────────────
|
||||||
|
|
||||||
|
/** Get all user discount caps for a product (includes scheme-level caps too) */
|
||||||
|
async getUserDiscounts(productId: string) {
|
||||||
|
return this.prisma.userProductDiscount.findMany({
|
||||||
|
where: { productId },
|
||||||
|
include: {
|
||||||
|
user: { select: { id: true, name: true, email: true, role: true } },
|
||||||
|
scheme: { select: { id: true, name: true, price: true } },
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: 'asc' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create or update a discount cap for a specific user on this product.
|
||||||
|
* Pass schemeId to cap at the scheme/variation level; omit/null for product-level cap.
|
||||||
|
*/
|
||||||
|
async upsertUserDiscount(
|
||||||
|
productId: string,
|
||||||
|
userId: string,
|
||||||
|
maxDiscountPercentage: number,
|
||||||
|
schemeId: string | null,
|
||||||
|
) {
|
||||||
|
// Find existing record manually to avoid Prisma null composite-key issue
|
||||||
|
const existing = await this.prisma.userProductDiscount.findFirst({
|
||||||
|
where: { userId, productId, schemeId: schemeId ?? null },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
return this.prisma.userProductDiscount.update({
|
||||||
|
where: { id: existing.id },
|
||||||
|
data: { maxDiscountPercentage },
|
||||||
|
include: {
|
||||||
|
user: { select: { id: true, name: true, email: true, role: true } },
|
||||||
|
scheme: { select: { id: true, name: true, price: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.prisma.userProductDiscount.create({
|
||||||
|
data: {
|
||||||
|
userId,
|
||||||
|
productId,
|
||||||
|
schemeId: schemeId ?? null,
|
||||||
|
maxDiscountPercentage,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
user: { select: { id: true, name: true, email: true, role: true } },
|
||||||
|
scheme: { select: { id: true, name: true, price: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Delete a specific user discount cap by its record ID */
|
||||||
|
async removeUserDiscount(discountId: string) {
|
||||||
|
return this.prisma.userProductDiscount.delete({ where: { id: discountId } });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,45 @@ export class QuotesService {
|
||||||
private readonly whatsappService: WhatsappService
|
private readonly whatsappService: WhatsappService
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
|
// Auto-transition pipeline opportunity stage forward
|
||||||
|
private async autoTransitionStage(opportunityId: string, newStage: 'QUALIFIED' | 'POTENTIAL' | 'SALES') {
|
||||||
|
try {
|
||||||
|
const current = await this.prisma.opportunity.findUnique({
|
||||||
|
where: { id: opportunityId },
|
||||||
|
include: { client: true }
|
||||||
|
});
|
||||||
|
if (!current) return;
|
||||||
|
|
||||||
|
const stagesOrder = { LEAD: 1, QUALIFIED: 2, POTENTIAL: 3, SALES: 4, LOST: 0 };
|
||||||
|
const currentPriority = stagesOrder[current.stage] || 0;
|
||||||
|
const newPriority = stagesOrder[newStage] || 0;
|
||||||
|
|
||||||
|
// Only transition forward
|
||||||
|
if (newPriority > currentPriority) {
|
||||||
|
const updated = await this.prisma.opportunity.update({
|
||||||
|
where: { id: opportunityId },
|
||||||
|
data: { stage: newStage, updatedAt: new Date() },
|
||||||
|
include: { client: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[AUTO-STAGE] Transitioned opportunity ${opportunityId} from ${current.stage} to ${newStage}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.whatsappService.sendPipelineUpdate(
|
||||||
|
updated.client.phone,
|
||||||
|
updated.client.name,
|
||||||
|
current.stage,
|
||||||
|
updated.stage
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to send pipeline WhatsApp on auto-transition:', err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error during auto-transition of opportunity stage:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async create(createQuoteDto: CreateQuoteDto) {
|
async create(createQuoteDto: CreateQuoteDto) {
|
||||||
const quote = await this.prisma.quote.create({
|
const quote = await this.prisma.quote.create({
|
||||||
data: {
|
data: {
|
||||||
|
|
@ -49,6 +88,29 @@ export class QuotesService {
|
||||||
console.error('Failed to send quote creation WhatsApp:', err.message);
|
console.error('Failed to send quote creation WhatsApp:', err.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Trigger auto-stage transition if linked to an opportunity
|
||||||
|
if (quote.opportunityId) {
|
||||||
|
try {
|
||||||
|
// Count previous quotes for this opportunity (excluding this one)
|
||||||
|
const previousQuotesCount = await this.prisma.quote.count({
|
||||||
|
where: {
|
||||||
|
opportunityId: quote.opportunityId,
|
||||||
|
id: { not: quote.id }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (previousQuotesCount >= 1) {
|
||||||
|
// This is a second/subsequent quote
|
||||||
|
await this.autoTransitionStage(quote.opportunityId, 'SALES');
|
||||||
|
} else {
|
||||||
|
// First quote
|
||||||
|
await this.autoTransitionStage(quote.opportunityId, 'QUALIFIED');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed auto-stage transition on quote creation:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return quote;
|
return quote;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -142,6 +204,11 @@ export class QuotesService {
|
||||||
throw new BadRequestException((result as any).error || 'Failed to send WhatsApp message');
|
throw new BadRequestException((result as any).error || 'Failed to send WhatsApp message');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If manual send succeeds and opportunity exists, ensure it is at least QUALIFIED
|
||||||
|
if (quote.opportunityId) {
|
||||||
|
await this.autoTransitionStage(quote.opportunityId, 'QUALIFIED');
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,8 @@ export class WhatsappService {
|
||||||
const cid = process.env.WHATSAPP_CID;
|
const cid = process.env.WHATSAPP_CID;
|
||||||
|
|
||||||
if (!url) {
|
if (!url) {
|
||||||
throw new Error('WHATSAPP_API_URL is not configured in .env');
|
this.logger.warn('WHATSAPP_API_URL is not configured. Skipping WhatsApp send.');
|
||||||
|
return { success: false, error: 'WHATSAPP_API_URL not configured' };
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
|
|
@ -98,10 +99,16 @@ export class WhatsappService {
|
||||||
this.logger.log(`Attempting to send WhatsApp via POST: ${url}`);
|
this.logger.log(`Attempting to send WhatsApp via POST: ${url}`);
|
||||||
this.logger.log(`Payload: ${JSON.stringify(payload)}`);
|
this.logger.log(`Payload: ${JSON.stringify(payload)}`);
|
||||||
|
|
||||||
|
// Add 8-second timeout via AbortController to prevent indefinite hanging
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), 8000);
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData
|
body: formData,
|
||||||
|
signal: controller.signal
|
||||||
});
|
});
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
const resultText = await response.text();
|
const resultText = await response.text();
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue