import moment from 'moment' import config from '../../config' import Constants from '../../constants/general' import Fragments from '../../graphql/Fragments' import * as Email from '../../utils/email' import GeneralUtils from '../../utils/general' import { logger } from '../../utils/logger' import * as Notification from '../../utils/notification' import * as Slack from '../../utils/slack' import * as Twilio from '../../utils/twilio' import WalterService from '../../utils/walterService' import { prismaClient as prisma } from '../../prismaClient' import { Order, User, Option, LineItem, Booking, Address } from '@prisma/client' const ORDER_STATUS = [ 'CANCELED', 'PENDING', 'CONFIRMED', 'IN_PROGRESS', 'ARRIVING', 'ARRIVED', 'COMPLETED', ] const getOrderPriceDetails = ({ promoCodes, lineItems }) => { if (!lineItems || !promoCodes) { throw new Error('missing lineitems or promocodes') } const subTotal = lineItems.reduce( (total, lineItem) => (total += lineItem.optionJson.price * (lineItem.quantity || 1) || 0), 0 ) const totalDiscounts = promoCodes.reduce( (total, promoCode) => (total += promoCode.isPercentage ? subTotal * promoCode.value : promoCode.value), 0 ) const totalTax = (subTotal - totalDiscounts) * Constants.QUEBEC_TAXES_RATE return { subtotalPrice: parseFloat((subTotal - totalDiscounts).toFixed(2)), totalTax: parseFloat(totalTax.toFixed(2)), totalShipping: 0, totalPrice: parseFloat((subTotal - totalDiscounts + totalTax).toFixed(2)), totalDiscounts: parseFloat(totalDiscounts.toFixed(2)), } } const orderIsAtLeastConfirmed = (orderStatus) => ORDER_STATUS.findIndex((status) => status === orderStatus) > 1 const getOrderConfirmationTemplateDynamicDataForOrder = ( order: Order & { customer: User booking: Booking & { address: Address } lineItems: (LineItem & { option: Option })[] } ) => { return { name: GeneralUtils.getUserName(order.customer), orderNumber: order.number, bookingDate: moment(order.booking.date).format('LLL'), bookingAddress: GeneralUtils.getFormattedAddress(order.booking.address), lineItems: order.lineItems.map((lineItem) => ({ title: lineItem.option.title, description: GeneralUtils.truncate({ str: lineItem.option.description, maxChars: 50 }), quantity: lineItem.quantity, price: lineItem.option.price, })), tax: order.totalTax.toFixed(2), subTotal: order.subtotalPrice, totalDiscounts: order.totalDiscounts, total: order.totalPrice, } } // Make sure we have order number like: 1000, 1001, 1002, etc... let currentOrderNumber const getCurrentOrderNumber = async () => { if (!currentOrderNumber) { const lastOrder = await prisma.order.findMany({ where: { number: { gte: 1000, }, }, take: -1, select: { number: true, }, }) if (lastOrder[0]) { currentOrderNumber = lastOrder[0].number } else { currentOrderNumber = 999 } } currentOrderNumber++ return Promise.resolve(currentOrderNumber) } const getOrderSMSInformation = (order) => { let message = 'ORDER INFO:' if (order.number) { message += `\n\nOrder #${order.number}` } if (order.booking && order.booking.date) { message += `\n\nBooking date: ${moment(order.booking.date).format('LLL')}` } if (order.lineItems) { message += `\n\nItems: ${GeneralUtils.getOrderLineItemsInlineString(order.lineItems)}` } if (order.customer) { message += '\n\n\nCUSTOMER INFO:' message += `\n\nName: ${GeneralUtils.getUserName(order.customer)}` if (order.customer.phone && order.customer.phone.number) { message += `\n\nPhone Number: ${order.customer.phone.number}` } } if (order.booking && order.booking.address) { message += `\n\nAddress: ${GeneralUtils.getFormattedAddress(order.booking.address)}` } return message } const sendOrderRequestReminderSMS = ({ customerPhoneNumber, bookingDate }) => { return Twilio.sendSMS({ to: customerPhoneNumber, body: `You have an order for ${moment(bookingDate).format('LLL')} that's still not confirmed.`, }) } const sendOrderConfirmationTemplate = async ({ to, orderId, subject, additionalFields = {}, isForPartner = false, }): Promise => { const order = await prisma.order.findOne({ where: { id: orderId }, include: { booking: { include: { address: true, }, }, lineItems: { include: { option: true, }, }, customer: true, serviceProject: { include: { service: { include: { users: true, }, }, }, }, }, }) Promise.all( order.serviceProject.service.users.map((user) => Email.send({ template: 'ORDER_CONFIRMATION', to, ...getOrderConfirmationTemplateDynamicDataForOrder(order), subject, ...(isForPartner ? { name: GeneralUtils.getUserName(order.customer), title: 'Order Request Summary', isProvider: true, serviceProjectId: order.serviceProject.id, partnerEmail: user.email, partnerPassword: '', } : null), ...additionalFields, }) ) ) } const createOrder = async ({ data }) => { const orderNumber = await getCurrentOrderNumber() let createdOrder = await prisma.order.create({ data: { ...data, number: orderNumber }, include: Fragments.ORDER_FIELDS, }) // Protection to make sure the price are good if (!createdOrder.subtotalPrice) { createdOrder = await prisma.order .findOne({ where: { id: createdOrder.id, }, include: { lineItems: { include: { option: true, }, }, promoCodes: true, }, }) .then((order) => { return prisma.order.update({ where: { id: createdOrder.id }, data: getOrderPriceDetails({ lineItems: order.lineItems, promoCodes: order.promoCodes, }), include: Fragments.ORDER_FIELDS, }) }) } // It means that the use that made this request is a customer himself Notification.send({ users: [createdOrder.customer], title: "We've received your new order", noEmail: true, noSMS: true, type: 'NEW_ORDER', data: createdOrder, serviceProjectId: createdOrder.serviceProject.id, }).catch(logger.error) // Also send notification to Partner Notification.send({ users: createdOrder.serviceProject.service.users, title: 'New order', message: `On ${moment(createdOrder.booking.date).format( 'LLLL' )}. View more details on the Walter http://services.usewalter.com/`, type: 'NEW_ORDER', data: createdOrder, forceSMS: true, forceEmail: true, serviceProjectId: createdOrder.serviceProject.id, }) // For Slack webhook Slack.send({ channel: 'order', subject: `🚀 New order #${createdOrder.number} on ${moment(createdOrder.booking.date).format( 'LLLL' )}`, text: `*CUSTOMER:*\n* ${GeneralUtils.getUserNameAndProject( createdOrder.customer, createdOrder.serviceProject.project )}\n* ${GeneralUtils.getFormattedAddress(createdOrder.booking.address)}\n* ${ createdOrder.customer.email || 'No email' }\n* ${ createdOrder.customer.phone?.nationalFormat || createdOrder.customer.phone?.number || 'No phone number' }\n\n*SERVICE:*\n* ${WalterService.getDataForServiceType( createdOrder.serviceProject.service.type, 'TITLE' )}\n* ${GeneralUtils.getOrderLineItemsInlineString(createdOrder.lineItems)}`, }).catch(logger.error) return Promise.resolve(createdOrder) } const sendOrderToCustomer = ({ order }) => { if (order.customer.email) { sendOrderConfirmationTemplate({ orderId: order.id, to: [{ email: order.customer.email }], // prettier-ignore subject: `Your ${moment().format('dddd')} ${moment().hour() < 12 ? 'morning' : moment().hour() > 17 ? 'evening' : 'afternoon'} service request with Walter`, additionalFields: { title: 'Service Request Summary', type: 'ORDER_REQUEST', isResident: true, }, }).catch(logger.error) } return Promise.resolve() } export default { orderIsAtLeastConfirmed, getOrderConfirmationTemplateDynamicDataForOrder, getCurrentOrderNumber, getOrderSMSInformation, sendOrderRequestReminderSMS, sendOrderConfirmationTemplate, createOrder, sendOrderToCustomer, }