agmission/Development/server/model/job.js

241 lines
8.2 KiB
JavaScript

'use strict';
const
debug = require('debug')('agm:job-model'),
mongoose = require('mongoose'),
Schema = mongoose.Schema,
App = require('./application'),
JobLog = require('./job_log'),
JobAssign = require('./job_assign'),
jobUtil = require('../helpers/job_util'),
utils = require('../helpers/utils'),
{ Fields, UserTypes, CostingItemType, Units } = require('../helpers/constants'),
{ JobInvoiceStatus } = require('../helpers/job_constants'),
mongoUtil = require('../helpers/mongo'),
Currencies = require('../helpers/currencies'),
AutoIncrement = require('mongoose-sequence')(mongoose);
const schema = new Schema({
_id: Schema.Types.Number,
name: { type: String, required: false },
orderNumber: { type: String, required: false },
measureUnit: { type: Boolean, required: true, default: false },
swathWidth: { type: Number, required: true },
products: [
{
product: { type: Schema.Types.ObjectId, ref: 'Product' },
rate: Number,
unit: Number
}
],
appRate: { type: Number, required: false },
/**
* US: 0: oz/ac, 1: gal/ac, 2: lb/ac
* Metric: 3: l/ha, kg/ ha
* @type {Number}
*/
appRateUnit: { type: Number, required: false },
startDate: { type: Date, required: false, default: Date.now },
endDate: { type: Date, required: false, default: Date.now },
operator: { type: Schema.Types.ObjectId, ref: UserTypes.PILOT },
client: { type: Schema.Types.ObjectId, ref: UserTypes.CLIENT, required: true },
vehicle: { type: Schema.Types.ObjectId, ref: UserTypes.DEVICE },
flightNumber: { type: String, required: false },
crop: { type: Schema.Types.ObjectId, ref: 'Crop', required: false },
farm: { type: String, required: false },
remark: { type: String, required: false },
sysPsi: { type: Number, required: false }, // Spray system pressure (in psi)
missionTime: { type: Number, required: false }, // Total mission time (in hours)
appType: { type: String, required: false }, // Application type
sprayAreas: [{
properties: {
name: { type: String },
type: { type: Number, default: 0 },
appRate: { type: Number },
color: { type: String },
area: { type: Number },
crop: { type: Schema.Types.ObjectId, ref: 'Crop', required: false },
radius: { type: Number, required: false }, // in meters, store the original radius of the pivot area
},
geometry: {
type: { type: String, required: true },
coordinates: { type: [[[Number]]] }
}
}],
excludedAreas: [{
properties: { type: Schema.Types.Mixed },
geometry: {
type: { type: String, required: true },
coordinates: { type: [[[Number]]] }
}
}],
bufs: [{
properties: { type: Schema.Types.Mixed },
geometry: {
type: { type: String, required: true },
coordinates: { type: [[Number]], index: '2dsphere' }
}
}],
waypoints: [{
properties: { type: Schema.Types.Mixed },
geometry: {
type: { type: String, required: true },
coordinates: { type: [Number], index: '2dsphere' }
}
}],
places: [{
properties: { type: Schema.Types.Mixed },
geometry: {
type: { type: String, required: true },
coordinates: { type: [Number], index: '2dsphere' }
}
}],
status: { type: Number, default: 0 },
dlOp: {
type: { type: Number, default: 0 },
mapOp: { type: Schema.Types.Mixed }
},
rptOp: {
printArea: { type: Boolean }, // Whether to also print AreaSize within the report
areaSize: { type: Number }, // in ha
coverage: { type: Number }, // in ha
appRate: { type: Number }, // Manual entered actual applied appRate
useActualVol: { type: Boolean, default: false },
actualVol: { type: Number } // Manual entered total actual applied material volume. Unit is in LPH or KGPH
},
loadOp: {
date: { type: Date, default: Date.now },
area: { type: Number, default: 0 }, // in ha or acre
capacity: { type: Number, default: 0 }, // depends on measureUnit and appRateUnit
loadType: { type: Number, default: 0 },
loads: { type: Number, default: 0 }
},
useCustWI: { type: Boolean, default: false },
weatherInfo: {
windSpd: { type: Number }, // knots
windDir: { type: String }, // one of the 4-direction-each-quarter bearing compass string
temp: { type: Number }, // temperature C or F degree depend on the UM
humid: { type: Number } // humidity in percents
},
// Applicator userId. This is for performance optimization when querying jobs
byPuid: {
type: Schema.Types.ObjectId, ref: 'User',
},
ttSprArea: { type: Number, default: 0 }, // Total sprayable (excluded the non-spray) areas in ha
markedDelete: { type: Boolean, default: false },
invoiceStatus: {
type: String, enum: Object.values(JobInvoiceStatus), require: true, default: JobInvoiceStatus.NONE,
get: (value) => value ?? JobInvoiceStatus.NONE
},
invoiceId: { type: Schema.Types.ObjectId, ref: 'Invoice', require: false },
costings: {
billableArea: { type: Number, min: 0 },
billableAmount: { type: Number, min: 0, require: true },
currency: { type: String, enum: Object.keys(Currencies), require: false },
items: [{
item: { type: Schema.Types.ObjectId, ref: 'costing_items', require: true },
name: { type: String, require: true },
price: { type: Number, min: 0, require: true },
quantity: { type: Number, min: 0, require: true },
type: { type: Number, enum: Object.values(CostingItemType) },
unit: { type: Number, enum: Object.values(Units) },
}],
}
}, { timestamps: true, toJSON: { getters: true }, toObject: { getters: true } });
schema.plugin(AutoIncrement, { inc_field: '_id' });
async function markDeleteJob(session) {
session && (session.startTransaction(mongoUtil.getTranOps()));
try {
this.markedDelete = true;
await this.save({ session });
} catch (error) {
session && (session.abortTransaction());
throw error;
}
await mongoUtil.commitWithRetry(session);
}
async function deleteRelatedData(session) {
session && (session.startTransaction(mongoUtil.getTranOps()));
try {
const sprIds = !utils.isEmptyArray(this.sprayAreas) ? this.sprayAreas.map(a => a._id) : null;
if (!utils.isEmptyArray(sprIds))
await jobUtil.deleteAreaLines(sprIds, session);
await JobLog.deleteMany({ job: this._id }).session(session);
await JobAssign.deleteMany({ job: this._id }).session(session);
} catch (error) {
session && (session.abortTransaction());
throw error;
}
await mongoUtil.commitWithRetry(session);
}
async function deleteApps(session, markDeletedOnly) {
const apps = await App.find({ jobId: this._id });
for (let i = 0; i < apps.length; i++) {
await apps[i].removeFull(session, markDeletedOnly);
}
}
async function deleteJob(session) {
session && (session.startTransaction(mongoUtil.getTranOps()));
try {
await this.remove({ session });
} catch (error) {
session && (session.abortTransaction());
throw error;
}
await mongoUtil.commitWithRetry(session);
}
/**
* Remove the job with all related data in atomic db session's transactions
* @param {ClientSession} ses the db session. If ses 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 job as deleted
if (!this[Fields.MARKED_DELETE])
await mongoUtil.runTransactionWithRetry(markDeleteJob.bind(this), session);
if (!markDeletedOnly) {
// 2. Delete related non application data
await mongoUtil.runTransactionWithRetry(deleteRelatedData.bind(this), session);
// 3. Delete apps and records
await deleteApps.call(this, session, markDeletedOnly);
// 4. Delete the job
await mongoUtil.runTransactionWithRetry(deleteJob.bind(this), session);
}
} catch (error) {
throw error;
} finally {
if (endSesDone && session) await session.endSession();
}
}
module.exports = mongoose.model('Job', schema);