270 lines
9.3 KiB
JavaScript
270 lines
9.3 KiB
JavaScript
'use strict';
|
|
/*
|
|
Database structure for jobs:
|
|
User/Applicator and Client
|
|
├── Job
|
|
|
|
Job
|
|
├── JobLog
|
|
├── JobAssign
|
|
└── Application
|
|
└── AppFile
|
|
└── AppDetail
|
|
*/
|
|
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 loadOpSchema = new Schema({
|
|
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 }
|
|
}, { _id: false, toJSON: { getters: true } });
|
|
|
|
const itemsSchema = new Schema({
|
|
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) },
|
|
}, { _id: false });
|
|
|
|
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, index: 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]]], required: true }
|
|
}
|
|
}],
|
|
excludedAreas: [{
|
|
properties: { type: Schema.Types.Mixed },
|
|
geometry: {
|
|
type: { type: String, required: true },
|
|
coordinates: { type: [[[Number]]], required: true },
|
|
}
|
|
}],
|
|
bufs: [{
|
|
properties: { type: Schema.Types.Mixed },
|
|
geometry: {
|
|
type: { type: String, required: true },
|
|
coordinates: { type: [[Number]], required: true }
|
|
}
|
|
}],
|
|
waypoints: [{
|
|
properties: { type: Schema.Types.Mixed },
|
|
geometry: {
|
|
type: { type: String, required: true },
|
|
coordinates: { type: [Number], required: true }
|
|
}
|
|
}],
|
|
places: [{
|
|
properties: { type: Schema.Types.Mixed },
|
|
geometry: {
|
|
type: { type: String, required: true },
|
|
coordinates: { type: [Number], required: true }
|
|
}
|
|
}],
|
|
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: {
|
|
type: loadOpSchema,
|
|
get: (loadOp) => jobUtil.defLoadOp(loadOp)
|
|
},
|
|
|
|
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', index: true
|
|
},
|
|
|
|
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: [itemsSchema, { _id: false, id: false }],
|
|
}
|
|
}, { timestamps: true, toJSON: { getters: true }, toObject: { getters: true } });
|
|
|
|
schema.plugin(AutoIncrement, { inc_field: '_id' });
|
|
|
|
/*
|
|
// NOTE: Enabling 2dsphere indexed for the benefits of powerfull geo queries for these types of data will REQUIRE standardized polygon (CCW for boundaries, CW for holes or xcls) compliant to GeoJSON.
|
|
// and also no connected multi-polygons. These are required to ensure the data is valid for the 2dsphere index. LATER.
|
|
|
|
schema.index({ 'sprayAreas.geometry': '2dsphere' });
|
|
schema.index({ 'excludedAreas.geometry': '2dsphere' });
|
|
*/
|
|
schema.index({ 'bufs.geometry': '2dsphere' });
|
|
schema.index({ 'waypoints.geometry': '2dsphere' });
|
|
schema.index({ 'places.geometry': '2dsphere' });
|
|
|
|
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);
|