const express = require('express'); const crypto = require('crypto'); const db = require('../db/models'); const ReviewFlowService = require('../services/reviewflow'); const SubscriptionService = require('../services/subscription'); const wrapAsync = require('../helpers').wrapAsync; const router = express.Router(); const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; function normalizeString(value) { return typeof value === 'string' ? value.trim() : ''; } function requireField(value, message) { if (!normalizeString(value)) { const error = new Error(message); error.code = 400; throw error; } } function validateUrl(value, message) { try { const parsed = new URL(value); if (!['http:', 'https:'].includes(parsed.protocol)) { throw new Error(message); } } catch { const validationError = new Error(message); validationError.code = 400; throw validationError; } } const REVIEW_LINK_FIELDS = { google: 'google_review_link', yelp: 'yelp_review_link', facebook: 'facebook_review_link', trustpilot: 'trustpilot_review_link', angi: 'angi_review_link', opentable: 'opentable_review_link', custom: 'custom_review_link', }; function normalizeReviewDestination(value) { const destination = normalizeString(value).toLowerCase(); if (ReviewFlowService.REVIEW_CHANNELS[destination]) { return destination; } return 'google'; } function getReviewLinkField(reviewDestination) { return REVIEW_LINK_FIELDS[reviewDestination] || null; } function buildEmailBody(customerName, businessName, reviewLink) { const greetingName = customerName || 'there'; return [ `Hi ${greetingName},`, '', `Thank you for choosing ${businessName}. We would love to hear about your experience.`, '', `Leave a review: ${reviewLink}`, '', `Thank you,`, businessName, ].join('\n'); } router.get('/review-channels', wrapAsync(async (req, res) => { res.status(200).send({ channels: ReviewFlowService.serializeReviewChannels() }); })); router.get('/connectors', wrapAsync(async (req, res) => { const businesses = await ReviewFlowService.listConnectorBusinesses(req.currentUser, req); res.status(200).send({ businesses }); })); router.post('/connectors', wrapAsync(async (req, res) => { const business = await ReviewFlowService.connectProvider(req.currentUser, req.body || {}, req); res.status(200).send({ business }); })); router.post('/connectors/:businessId/:provider/rotate', wrapAsync(async (req, res) => { const business = await ReviewFlowService.rotateWebhookToken( req.currentUser, req.params.businessId, req.params.provider, req, ); res.status(200).send({ business }); })); router.get('/summary', wrapAsync(async (req, res) => { const currentUser = req.currentUser; const limit = Math.min(Number(req.query.limit) || 8, 25); const requests = await db.review_requests.findAll({ where: { createdById: currentUser.id }, include: [ { model: db.businesses, as: 'business' }, { model: db.customers, as: 'customer' }, { model: db.transactions, as: 'transaction' }, ], order: [['createdAt', 'DESC']], limit, }); const [ pending, sent, clicked, reviewed, customers, transactions, paymentEvents, recentTransactions, recentEvents, ] = await Promise.all([ db.review_requests.count({ where: { createdById: currentUser.id, status: 'pending' } }), db.review_requests.count({ where: { createdById: currentUser.id, status: 'sent' } }), db.review_requests.count({ where: { createdById: currentUser.id, status: 'clicked' } }), db.review_requests.count({ where: { createdById: currentUser.id, status: 'reviewed' } }), db.customers.count({ where: { createdById: currentUser.id } }), db.transactions.count({ where: { createdById: currentUser.id } }), db.stripe_events.count({ where: { createdById: currentUser.id } }), db.transactions.findAll({ where: { createdById: currentUser.id }, include: [ { model: db.businesses, as: 'business' }, { model: db.customers, as: 'customer' }, ], order: [['createdAt', 'DESC']], limit: 6, }), db.stripe_events.findAll({ where: { createdById: currentUser.id }, include: [ { model: db.businesses, as: 'business' }, ], order: [['createdAt', 'DESC']], limit: 6, }), ]); res.status(200).send({ stats: { pending, sent, clicked, reviewed, customers, transactions, paymentEvents }, requests, recentTransactions, recentEvents, }); })); router.post('/request', wrapAsync(async (req, res) => { const currentUser = req.currentUser; const body = req.body || {}; const businessName = normalizeString(body.businessName); const reviewLink = normalizeString(body.reviewLink); const reviewDestination = normalizeReviewDestination(body.reviewDestination || body.reviewPlatform || 'google'); const isHostedReviewDestination = reviewDestination === 'shopify_hosted'; const reviewLinkField = getReviewLinkField(reviewDestination); const customerEmail = normalizeString(body.customerEmail).toLowerCase(); const customerName = normalizeString(body.customerName); const phone = normalizeString(body.phone); const delayDays = Math.max(0, Math.min(Number(body.delayDays) || 0, 30)); requireField(businessName, 'Business name is required.'); if (!isHostedReviewDestination) { requireField(reviewLink, 'Review link is required.'); } requireField(customerEmail, 'Customer email is required.'); if (!EMAIL_PATTERN.test(customerEmail)) { const error = new Error('Enter a valid customer email address.'); error.code = 400; throw error; } if (reviewLink) { validateUrl(reviewLink, 'Enter a valid review destination URL.'); } await SubscriptionService.assertCanCreateReviewRequests(currentUser, 1); const existingBusiness = await db.businesses.findOne({ where: { name: businessName, createdById: currentUser.id }, }); if (!existingBusiness) { await SubscriptionService.assertCanCreateBusinesses(currentUser, 1); } const scheduledFor = new Date(Date.now() + delayDays * 24 * 60 * 60 * 1000); const trackingToken = crypto.randomBytes(18).toString('hex'); const effectiveReviewLink = isHostedReviewDestination ? ReviewFlowService.getHostedReviewUrl(req, trackingToken) : reviewLink; const transaction = await db.sequelize.transaction(); try { const businessDefaults = { name: businessName, review_destination: reviewDestination, shopify_hosted_reviews_enabled: isHostedReviewDestination, delay_days: delayDays, email_subject_template: `How was your experience with ${businessName}?`, email_body_template: buildEmailBody('{customerName}', businessName, '{reviewLink}'), is_active: true, createdById: currentUser.id, updatedById: currentUser.id, }; if (reviewLink && reviewLinkField) { businessDefaults[reviewLinkField] = reviewLink; } const [business] = await db.businesses.findOrCreate({ where: { name: businessName, createdById: currentUser.id }, defaults: businessDefaults, transaction, }); const businessUpdates = { review_destination: reviewDestination, shopify_hosted_reviews_enabled: business.shopify_hosted_reviews_enabled || isHostedReviewDestination, delay_days: delayDays, is_active: true, updatedById: currentUser.id, }; if (reviewLink && reviewLinkField) { businessUpdates[reviewLinkField] = reviewLink; } await business.update(businessUpdates, { transaction }); const [customer] = await db.customers.findOrCreate({ where: { email: customerEmail, createdById: currentUser.id }, defaults: { email: customerEmail, name: customerName || null, phone: phone || null, contact_status: 'active', businessId: business.id, createdById: currentUser.id, updatedById: currentUser.id, }, transaction, }); await customer.update({ name: customerName || customer.name, phone: phone || customer.phone, contact_status: customer.contact_status || 'active', businessId: business.id, updatedById: currentUser.id, }, { transaction }); const emailSubject = `How was your experience with ${businessName}?`; const reviewRequest = await db.review_requests.create({ status: 'pending', scheduled_for: scheduledFor, email_subject: emailSubject, email_body: buildEmailBody(customerName, businessName, effectiveReviewLink), review_link: effectiveReviewLink, tracking_token: trackingToken, review_platform: reviewDestination, businessId: business.id, customerId: customer.id, createdById: currentUser.id, updatedById: currentUser.id, }, { transaction }); await transaction.commit(); const createdRequest = await db.review_requests.findByPk(reviewRequest.id, { include: [ { model: db.businesses, as: 'business' }, { model: db.customers, as: 'customer' }, ], }); res.status(201).send({ request: createdRequest }); } catch (error) { await transaction.rollback(); throw error; } })); router.use('/', require('../helpers').commonErrorHandler); module.exports = router;