221 lines
7.3 KiB
JavaScript
221 lines
7.3 KiB
JavaScript
'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'),
|
|
{ buildDynamicFilter } = require('../helpers/dynamic_filter'),
|
|
debug = require('debug')('agm:controllers-customer');
|
|
|
|
const CUSTOMER_FILTER_SCHEMA = {
|
|
name: 'text',
|
|
username: 'text',
|
|
email: 'text',
|
|
contact: 'text',
|
|
createdAt: 'date-preset',
|
|
};
|
|
|
|
async function getCustomers_get(req, res) {
|
|
const filtersStr = req.query.filters || '';
|
|
|
|
const dynamicFilter = filtersStr ? buildDynamicFilter(filtersStr, CUSTOMER_FILTER_SCHEMA) : {};
|
|
|
|
const customers = await Customer.aggregate(
|
|
[
|
|
{ $match: { kind: UserTypes.APP, markedDelete: { $ne: true }, ...dynamicFilter } },
|
|
{
|
|
$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
|
|
}
|