202 lines
6.8 KiB
JavaScript
202 lines
6.8 KiB
JavaScript
const mongoose = require('mongoose'),
|
|
Schema = mongoose.Schema,
|
|
mongoUtil = require('../helpers/mongo'),
|
|
{ Fields, UserTypes, APTypes, TrialTypes } = require('../helpers/constants'),
|
|
{ stripe } = require('../helpers/subscription_util'),
|
|
User = require('../model/user'),
|
|
BillPeriod = require('../model/bill_period'),
|
|
Client = require('../model/client'),
|
|
Product = require('../model/product'),
|
|
Vehicle = require('../model/vehicle'),
|
|
Crop = require('../model/crop'),
|
|
AddressSchema = require('./common').addressSchema,
|
|
SubscriptionSchema = require('./subscription').subscriptionSchema,
|
|
uniqid = require('uniqid'),
|
|
validator = require('validator');
|
|
|
|
const schema = new Schema({
|
|
contact: { type: String, required: false },
|
|
fax: { type: String, required: false },
|
|
premium: { type: Number, default: 0 },
|
|
billable: { type: Boolean, default: false },
|
|
country: {
|
|
type: String,
|
|
validate: [validator.isISO31661Alpha2, 'invalid_country_code'],
|
|
trim: true,
|
|
uppercase: true,
|
|
required: [true, 'Country is required']
|
|
},
|
|
|
|
membership: {
|
|
custId: String, // The Stripe Customer ID of this customer
|
|
subscriptions: [SubscriptionSchema],
|
|
trials: {
|
|
type: {
|
|
type: String,
|
|
enum: {
|
|
values: Object.values(TrialTypes),
|
|
message: '{VALUE} for \'trials.type\' is not supported'
|
|
}
|
|
},
|
|
trialDays: { type: Number, default: 0 },
|
|
byDate: { type: Date, default: null },
|
|
startDate: { type: Date }, // The date the customer is offered a trial
|
|
lastStartDate: { type: Date }, // Last trial start date
|
|
lastEndDate: { type: Date }, // Last trial end date
|
|
}
|
|
},
|
|
billAddress: { type: AddressSchema, required: false },
|
|
|
|
/** Dealer that managge the account */
|
|
// dealerCode: { type: String, required: false }
|
|
|
|
}, { strictQuery: false });
|
|
|
|
async function prepareDefault(cust, ses) {
|
|
let defProd = new Product({ name: 'Water', type: APTypes.CARRIER, desc: 'Default carrier product', byPuid: cust._id });
|
|
await defProd.save({ session: ses });
|
|
}
|
|
|
|
/**
|
|
* Create a new customer (applicator) with all default data in atomic db session's transactions
|
|
* @param {*} ses the db session. If ses is null, create a session and end when done
|
|
* @param {boolean} endSesDone whether to end the session
|
|
* @returns the new customer instance otherwise thrown exceptions
|
|
*/
|
|
schema.methods.createOne = async function (ses = null, endSesDone = false) {
|
|
let _ses = ses;
|
|
if (!ses) {
|
|
_ses = await this.db.startSession();
|
|
endSesDone = true;
|
|
}
|
|
try {
|
|
if (_ses.transaction && _ses.transaction.isActive) {
|
|
await prepareDefault(this, _ses);
|
|
await this.save({ session: _ses });
|
|
} else {
|
|
await _ses.withTransaction(async () => {
|
|
await prepareDefault(this, _ses);
|
|
return this.save({ session: _ses });
|
|
});
|
|
}
|
|
} finally {
|
|
(endSesDone && _ses) && (await _ses.endSession());
|
|
}
|
|
return this;
|
|
}
|
|
|
|
async function freeRelatedUserNames(session) {
|
|
const users = await User.find({ parent: this._id }, '_id username', { lean: true });
|
|
if (!users.length) return;
|
|
|
|
session && (session.startTransaction(mongoUtil.getTranOps()));
|
|
try {
|
|
// De-activate all related user in one batch
|
|
const bulkDelOps = []
|
|
let updateDoc;
|
|
for (let user of users) {
|
|
updateDoc = { active: false };
|
|
if (user.username)
|
|
updateDoc.username = user.username + '#' + uniqid();
|
|
|
|
bulkDelOps.push({
|
|
'updateOne': {
|
|
'filter': { _id: user._id },
|
|
'update': updateDoc
|
|
}
|
|
});
|
|
}
|
|
await User.bulkWrite(bulkDelOps, { session });
|
|
} catch (error) {
|
|
session && (session.abortTransaction());
|
|
throw error;
|
|
}
|
|
await mongoUtil.commitWithRetry(session);
|
|
}
|
|
|
|
async function deleteRelatedData(session) {
|
|
const custUid = this._id;
|
|
session && (session.startTransaction(mongoUtil.getTranOps()));
|
|
try {
|
|
// Entities
|
|
await Product.deleteMany({ byPuid: custUid }).session(session);
|
|
await Crop.deleteMany({ byPuid: custUid }).session(session);
|
|
|
|
// Other accounts, related configs
|
|
const users = await User.find({ parent: custUid, kind: { $nin: [UserTypes.CLIENT, UserTypes.DEVICE] } });
|
|
for (let j = 0; j < users.length; j++) {
|
|
await users[j].remove({ session });
|
|
}
|
|
} catch (error) {
|
|
session && (session.abortTransaction());
|
|
throw error;
|
|
}
|
|
await mongoUtil.commitWithRetry(session);
|
|
}
|
|
|
|
async function deleteCustomerClients(session, markDeletedOnly) {
|
|
const clients = await Client.find({ parent: this._id });
|
|
for (let i = 0; i < clients.length; i++) {
|
|
await clients[i].removeFull(session, markDeletedOnly);
|
|
}
|
|
}
|
|
|
|
async function deleteCustomerVehicles(session, markDeletedOnly) {
|
|
const vehicles = await Vehicle.find({ parent: this._id });
|
|
for (let i = 0; i < vehicles.length; i++) {
|
|
await vehicles[i].removeFull(session, markDeletedOnly);
|
|
}
|
|
}
|
|
|
|
async function deleteSubscriptionInfo(session) {
|
|
if (this.membership && this.membership?.custId) {
|
|
stripe && (await stripe.customers.del(this.membership.custId));
|
|
await BillPeriod.deleteMany({ custId: this.membership.custId }, { session });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove the Customer (applicator) with all related data in one atomic db session's transaction
|
|
* @param {ClientSession} ses the db session. If sess is null, create a session and end when done
|
|
* @param {boolean} markDeletedOnly Determine whether to just mark the item as deleted only. Default is true.
|
|
* @param {boolean} endSesDone whether to end the session
|
|
*/
|
|
schema.methods.removeFull = async function (ses = null, markDeletedOnly = true, endSesDone = false) {
|
|
let session = ses;
|
|
if (!ses) {
|
|
session = await this.db.startSession({ readPreference: { mode: "primary" } });
|
|
endSesDone = true;
|
|
}
|
|
try {
|
|
// 1. Mark the customer as deleted
|
|
if (!this[Fields.MARKED_DELETE])
|
|
await mongoUtil.runTransactionWithRetry(this.markAsDeleted.bind(this), session);
|
|
|
|
if (markDeletedOnly) {
|
|
// 1.1 Free related users
|
|
await mongoUtil.runTransactionWithRetry(freeRelatedUserNames.bind(this), session);
|
|
} else {
|
|
// 2. Delete related common data: i.e.: entities
|
|
await mongoUtil.runTransactionWithRetry(deleteRelatedData.bind(this), session);
|
|
|
|
// 3. Delete related clients and jobs and job data.
|
|
await deleteCustomerClients.call(this, session, markDeletedOnly);
|
|
|
|
// 4. Delete related vehicles and data
|
|
await deleteCustomerVehicles.call(this, session, markDeletedOnly);
|
|
|
|
// 5. Delete subscriptions relalted info such as: Stripe Billing Period info
|
|
await deleteSubscriptionInfo.call(this, session);
|
|
|
|
// 5. Delete the customer
|
|
await mongoUtil.runTransactionWithRetry(this.deleteMarked.bind(this), session);
|
|
}
|
|
} catch (error) {
|
|
throw error;
|
|
} finally {
|
|
if (endSesDone && session) await session.endSession();
|
|
}
|
|
}
|
|
|
|
module.exports = User.discriminator(UserTypes.APP, schema, { clone: false });
|