@@ -4,6 +4,7 @@ const FollowUpJourney = require('../model/FollowUpJourney');
44const WhatsappActivity = require ( '../model/WhatsappActivity' ) ;
55const FollowUpFlowConfig = require ( '../model/FollowUpFlowConfig' ) ;
66const MembersModel = require ( '../model/members' ) ;
7+ const { default : axios } = require ( 'axios' ) ;
78
89class FollowUpService {
910 _defaultFlowStages ( ) {
@@ -319,18 +320,51 @@ class FollowUpService {
319320 }
320321
321322 _normalizePhone ( phone ) {
323+ return String ( phone || '' ) . replace ( / \D / g, '' ) ;
324+ }
325+
326+ async _resolveRealPhoneFromJid ( jid ) {
322327 if ( ! jid ) return null ;
323328
324- // Remove @c .us or @lid
325- const cleaned = jid . split ( '@' ) [ 0 ] ;
329+ const id = jid . split ( '@' ) [ 0 ] ;
330+
331+ // If already normal phone
332+ if ( id . startsWith ( '234' ) && id . length === 13 ) {
333+ return id . slice ( 3 ) ;
334+ }
335+
336+ if ( id . startsWith ( '0' ) && id . length === 11 ) {
337+ return id . slice ( 1 ) ;
338+ }
339+
340+ // If LID → resolve via WAHA
341+ if ( jid . endsWith ( '@lid' ) ) {
342+ try {
343+ const response = await axios . get (
344+ `${ process . env . WAHA_API_URL } /api/contacts` ,
345+ {
346+ params : {
347+ contactId : jid ,
348+ session : 'default'
349+ }
350+ }
351+ ) ;
352+
353+ const contact = response . data ;
354+
355+ if ( contact ?. number ) {
356+ const full = contact . number ;
357+ return full . startsWith ( '234' ) ? full . slice ( 3 ) : full ;
358+ }
326359
327- // If it starts with 0 (Nigeria local format)
328- if ( cleaned . startsWith ( '0' ) ) {
329- return '234' + cleaned . substring ( 1 ) ;
360+ return null ;
361+ } catch ( err ) {
362+ console . error ( 'Failed to resolve LID:' , err . message ) ;
363+ return null ;
364+ }
330365 }
331366
332- return cleaned ;
333- // return String(phone || '').replace(/\D/g, '');
367+ return null ;
334368 }
335369
336370 _isOptOut ( text ) {
@@ -545,134 +579,135 @@ class FollowUpService {
545579 }
546580
547581 async handleReply ( phone , messageBody ) {
548- const cleanedPhone = this . _normalizePhone ( phone ) ;
582+ console . log ( `Handling reply from ${ phone } : ${ messageBody } ` ) ;
583+ const cleanedPhone = await _resolveRealPhoneFromJid ( phone )
584+ console . log ( `Cleaned phone: ${ cleanedPhone } ` ) ;
549585 const reply = this . _normalizeInboundText ( messageBody ) ;
550586
551- console . log ( cleanedPhone , reply , 'normalize' )
552-
553- const memberByPhone = await MembersModel . findOne ( {
554- phone : { $regex : cleanedPhone } ,
555- } ) ;
556-
557- // Global commands should work even when there is no active journey.
558- if ( this . _isOptOut ( reply ) && memberByPhone ) {
559- memberByPhone . whatsappOptIn = false ;
560- memberByPhone . whatsappOptOutDate = new Date ( ) ;
561- await memberByPhone . save ( ) ;
562-
563- await FollowUpJourney . updateMany (
564- { memberId : memberByPhone . _id , status : { $in : [ 'active' , 'escalated' ] } } ,
565- { $set : { status : 'opted_out' , nextMessageAt : null } }
566- ) ;
567-
568- await wahaService . sendText (
569- cleanedPhone ,
570- 'You have been unsubscribed from WhatsApp follow-up messages. Reply START to opt in again.'
571- ) ;
572-
573- return { action : 'opted_out' , journey : null } ;
574- }
575-
576- if ( this . _isOptIn ( reply ) && memberByPhone ) {
577- memberByPhone . whatsappOptIn = true ;
578- memberByPhone . whatsappOptInDate = new Date ( ) ;
579- memberByPhone . whatsappOptOutDate = null ;
580- await memberByPhone . save ( ) ;
581-
582- await wahaService . sendText (
583- cleanedPhone ,
584- 'You are now subscribed again. Thank you.'
585- ) ;
586- return { action : 'opted_in' , journey : null } ;
587- }
588-
589- if ( this . _isHelp ( reply ) ) {
590- await wahaService . sendText (
591- cleanedPhone ,
592- 'Reply 1, 2, or 3 to choose an option. Reply STOP to opt out, START to opt in.'
593- ) ;
594- return { action : 'help' , journey : null } ;
595- }
596-
597- const journey = await FollowUpJourney . findOne ( {
598- phone : { $regex : cleanedPhone } ,
599- status : { $in : [ 'active' , 'escalated' ] } ,
600- } ) . populate ( 'memberId' ) ;
601-
602- if ( ! journey ) {
603- if ( memberByPhone ) {
604- const absentHandled = await this . _handleAbsentReminderReply (
605- memberByPhone ,
606- cleanedPhone ,
607- reply
608- ) ;
609- if ( absentHandled ) return { action : absentHandled . action , journey : null } ;
610- }
611- return null ;
612- }
613-
614- const member = journey . memberId ;
615- const firstName = member ?. firstName || 'Friend' ;
616- const flowStages = await this . getActiveFlowStages ( ) ;
617- const stageConfig = this . _findStageConfig ( flowStages , journey . currentStage ) ;
618-
619- await WhatsappActivity . create ( {
620- memberId : member ?. _id ,
621- phone : cleanedPhone ,
622- direction : 'inbound' ,
623- messageType : 'reply' ,
624- content : reply ,
625- followUpStage : journey . currentStage ,
626- conversationStage : 'awaiting_reply' ,
627- status : 'read' ,
628- } ) ;
629-
630- let action = 'unknown' ;
631- const configuredOption = this . _findConfiguredResponseOption (
632- reply ,
633- stageConfig ?. responseOptions || [ ]
634- ) ;
635-
636- if ( configuredOption ) {
637- action = await this . _applyConfiguredResponseOption ( {
638- option : configuredOption ,
639- journey,
640- member,
641- phone : cleanedPhone ,
642- } ) ;
643- } else {
644- const option = this . _detectOption ( reply , journey . currentStage ) ;
645- if ( option === 1 ) action = await this . _handleOption1 ( journey , firstName , cleanedPhone ) ;
646- else if ( option === 2 ) action = await this . _handleOption2 ( journey , firstName , cleanedPhone ) ;
647- else if ( option === 3 ) action = await this . _handleOption3 ( journey , firstName , cleanedPhone ) ;
648- else if (
649- journey . currentStage === 2 ||
650- member ?. whatsappConversationStage === 'prayer_requested'
651- ) {
652- await this . _handlePrayerRequest ( member , cleanedPhone , reply ) ;
653- action = 'prayer_submitted' ;
654- } else {
655- action = 'free_text' ;
656- }
657- }
658-
659- journey . replies . push ( {
660- content : reply ,
661- receivedAt : new Date ( ) ,
662- stage : journey . currentStage ,
663- action,
664- } ) ;
665- journey . engagementScore = this . _calculateEngagement ( journey ) ;
666- await journey . save ( ) ;
667-
668- if ( member ) {
669- member . lastWhatsappReply = new Date ( ) ;
670- member . totalReplies = ( member . totalReplies || 0 ) + 1 ;
671- member . whatsappEngagementStatus = 'active' ;
672- await member . save ( ) ;
673- }
674-
675- return { action, journey } ;
587+ // const memberByPhone = await MembersModel.findOne({
588+ // phone: { $regex: cleanedPhone },
589+ // });
590+ return { }
591+
592+ // // Global commands should work even when there is no active journey.
593+ // if (this._isOptOut(reply) && memberByPhone) {
594+ // memberByPhone.whatsappOptIn = false;
595+ // memberByPhone.whatsappOptOutDate = new Date();
596+ // await memberByPhone.save();
597+
598+ // await FollowUpJourney.updateMany(
599+ // { memberId: memberByPhone._id, status: { $in: ['active', 'escalated'] } },
600+ // { $set: { status: 'opted_out', nextMessageAt: null } }
601+ // );
602+
603+ // await wahaService.sendText(
604+ // cleanedPhone,
605+ // 'You have been unsubscribed from WhatsApp follow-up messages. Reply START to opt in again.'
606+ // );
607+
608+ // return { action: 'opted_out', journey: null };
609+ // }
610+
611+ // if (this._isOptIn(reply) && memberByPhone) {
612+ // memberByPhone.whatsappOptIn = true;
613+ // memberByPhone.whatsappOptInDate = new Date();
614+ // memberByPhone.whatsappOptOutDate = null;
615+ // await memberByPhone.save();
616+
617+ // await wahaService.sendText(
618+ // cleanedPhone,
619+ // 'You are now subscribed again. Thank you.'
620+ // );
621+ // return { action: 'opted_in', journey: null };
622+ // }
623+
624+ // if (this._isHelp(reply)) {
625+ // await wahaService.sendText(
626+ // cleanedPhone,
627+ // 'Reply 1, 2, or 3 to choose an option. Reply STOP to opt out, START to opt in.'
628+ // );
629+ // return { action: 'help', journey: null };
630+ // }
631+
632+ // const journey = await FollowUpJourney.findOne({
633+ // phone: { $regex: cleanedPhone },
634+ // status: { $in: ['active', 'escalated'] },
635+ // }).populate('memberId');
636+
637+ // if (!journey) {
638+ // if (memberByPhone) {
639+ // const absentHandled = await this._handleAbsentReminderReply(
640+ // memberByPhone,
641+ // cleanedPhone,
642+ // reply
643+ // );
644+ // if (absentHandled) return { action: absentHandled.action, journey: null };
645+ // }
646+ // return null;
647+ // }
648+
649+ // const member = journey.memberId;
650+ // const firstName = member?.firstName || 'Friend';
651+ // const flowStages = await this.getActiveFlowStages();
652+ // const stageConfig = this._findStageConfig(flowStages, journey.currentStage);
653+
654+ // await WhatsappActivity.create({
655+ // memberId: member?._id,
656+ // phone: cleanedPhone,
657+ // direction: 'inbound',
658+ // messageType: 'reply',
659+ // content: reply,
660+ // followUpStage: journey.currentStage,
661+ // conversationStage: 'awaiting_reply',
662+ // status: 'read',
663+ // });
664+
665+ // let action = 'unknown';
666+ // const configuredOption = this._findConfiguredResponseOption(
667+ // reply,
668+ // stageConfig?.responseOptions || []
669+ // );
670+
671+ // if (configuredOption) {
672+ // action = await this._applyConfiguredResponseOption({
673+ // option: configuredOption,
674+ // journey,
675+ // member,
676+ // phone: cleanedPhone,
677+ // });
678+ // } else {
679+ // const option = this._detectOption(reply, journey.currentStage);
680+ // if (option === 1) action = await this._handleOption1(journey, firstName, cleanedPhone);
681+ // else if (option === 2) action = await this._handleOption2(journey, firstName, cleanedPhone);
682+ // else if (option === 3) action = await this._handleOption3(journey, firstName, cleanedPhone);
683+ // else if (
684+ // journey.currentStage === 2 ||
685+ // member?.whatsappConversationStage === 'prayer_requested'
686+ // ) {
687+ // await this._handlePrayerRequest(member, cleanedPhone, reply);
688+ // action = 'prayer_submitted';
689+ // } else {
690+ // action = 'free_text';
691+ // }
692+ // }
693+
694+ // journey.replies.push({
695+ // content: reply,
696+ // receivedAt: new Date(),
697+ // stage: journey.currentStage,
698+ // action,
699+ // });
700+ // journey.engagementScore = this._calculateEngagement(journey);
701+ // await journey.save();
702+
703+ // if (member) {
704+ // member.lastWhatsappReply = new Date();
705+ // member.totalReplies = (member.totalReplies || 0) + 1;
706+ // member.whatsappEngagementStatus = 'active';
707+ // await member.save();
708+ // }
709+
710+ // return { action, journey };
676711 }
677712
678713 async _handleOption1 ( journey , firstName , phone ) {
0 commit comments