'use strict'; const { User } = require('../model'); const { SubStatus } = require('../model/subscription'); const Customer = require('../model/customer'), cache = require('../helpers/mem_cache'), ObjectId = require('mongodb').ObjectId, utils = require('../helpers/utils'), { UserTypes, TrialTypes } = require('../helpers/constants'), { AppParamError } = require('../helpers/app_error'), { validateTrial } = require('../helpers/subscription_util'), moment = require('moment'), debug = require('debug')('agm:controllers-customer'); async function getCustomers_get(req, res) { const customers = await Customer.aggregate( [ { $match: { kind: UserTypes.APP, markedDelete: { $ne: true } } }, { $lookup: { from: 'jobs', // Reference the Job collection localField: '_id', foreignField: 'byPuid', as: 'cust_jobs' } }, { $unwind: { path: '$cust_jobs', preserveNullAndEmptyArrays: true } }, { $lookup: { from: 'partners', // Reference the Partner collection localField: 'partner', foreignField: '_id', as: 'partner_info', }, }, { $group: { '_id': { _id: '$_id', name: '$name', contact: '$contact', email: '$email', active: '$active', billable: '$billable', premium: '$premium', createdAt: '$createdAt', username: '$username', partner: { $arrayElemAt: [ { $map: { input: '$partner_info', as: 'partner', in: { _id: '$$partner._id', name: '$$partner.name', active: '$$partner.active', }, }, }, 0, ], }, // Include only _id, name, and active fields for partner selfSignup: '$selfSignup' }, totalJobs: { $sum: { $cond: [{ $gte: ['$cust_jobs', null] }, 1, 0] } } } }, { $replaceRoot: { newRoot: { $mergeObjects: ['$$ROOT', '$_id'] } } }, ]).allowDiskUse(true); res.json(customers); } async function createCustomer_post(req, res) { const _customer = req.body; if (!_customer || !_customer.username || !_customer.password) AppParamError.throw(); if (_customer.membership && _customer.membership.trials) { validateTrial(_customer.membership.trials); } delete _customer._id; const newCustomer = new Customer(Customer.toUser(_customer)); const theUser = await newCustomer.createOne(); res.json(theUser); } async function getCustomer_get(req, res) { const cId = req.params.customer_id; if (!ObjectId.isValid(cId)) AppParamError.throw(); const view = req.query.view; let query = Customer.findOne({ _id: ObjectId(cId) }, null, { lean: true }) .populate({ path: 'Country', select: 'code name -_id' }) .populate({ path: 'partner', select: 'name description' }); if (view !== 'edit') { query = query.select('-password'); } const customer = await query; res.json(customer); } async function updateCustomer_put(req, res) { const _customer = req.body; // Note: Never try to update any subscriptions, customer must follow the sub. change process or changed made in Stripe dashboard only. delete _customer?.membership?.subscriptions; if (!_customer || !utils.isObjectId(_customer._id)) AppParamError.throw(); if (utils.isBlank(_customer.username) && !utils.isBlank(_customer.password)) _customer.password = undefined; if (!_customer.hasOwnProperty('active')) _customer.active = true; let customer = await Customer.findOne({ _id: ObjectId(_customer._id) }).lean(); if (customer) { if (!utils.isEmptyObj(_customer.membership)) { // Check whether the customer having any trial subscriptions yet const hasTrialSub = customer.membership && customer.membership.subscriptions && customer.membership.subscriptions.some(sub => sub.status === SubStatus.TRIALING && !sub.canceled); // If having trial subscriptions, do not allow changing trials info for now if (hasTrialSub) { delete _customer.membership.trials; } else { if (!utils.isEmptyObj(_customer.membership.trials)) { const custTrials = customer.membership && customer.membership.trials; const curMoment = moment.utc(); if (custTrials && custTrials.lasStartDate && custTrials.lastEndDate) { if (curMoment.isBetween(moment.utc(custTrials.lastStartDate), moment.utc(custTrials.lastEndDate))) { // Ignore updating trials if the customer is being in the middle of trial period delete _customer.membership.trials; } } // For days trial, if startDate not mentioned (and it is not in the trial period), infer and set the trial startDate from today UTC if (_customer.membership.trials.type === TrialTypes.DAYS && !_customer.membership.trials.startDate) { _customer.membership.trials.startDate = curMoment.toDate(); } validateTrial(_customer.membership.trials); } else { delete _customer.membership.trials; } } } // Build update object with dot notation for membership fields to avoid overwriting subscriptions const updateDoc = { ..._customer }; if (updateDoc.membership) { const membership = updateDoc.membership; delete updateDoc.membership; // Use dot notation for each membership field to preserve subscriptions for (const key in membership) { if (membership.hasOwnProperty(key)) { updateDoc[`membership.${key}`] = membership[key]; } } } customer = await Customer.findOneAndUpdate({ _id: ObjectId(_customer._id) }, { $set: updateDoc }, { runValidators: true, new: true, lean: true }); // Update related cached user info if the applicator premium changed // TODO: Later, may be improving this logic using hash or independant cached when needed. const uiList = cache.filterByKind(customer._id.toHexString()); let uiValue; for (let i = 0; i < uiList.length; i++) { uiValue = uiList[i]; if (uiValue) { if (customer.premium !== uiValue[1]['premium']) uiValue[1].premium = customer.premium; } } } res.json(customer); } async function deleteCustomer(req, res) { const _id = req.params.customer_id; if (!utils.isObjectId(_id)) AppParamError.throw(); const customer = await Customer.findById(ObjectId(_id)); if (customer) await customer.removeFull(); // Remove all related logged user sessions const uiList = cache.filterByKind(_id); if (uiList) { for (const u of uiList) cache.delete(u[0]); } cache.delete(_id); res.json({ message: 'deleted' }); } module.exports = { getCustomers_get, createCustomer_post, getCustomer_get, updateCustomer_put, deleteCustomer }