@ideal-photography/shared
Version:
Shared MongoDB and utility logic for Ideal Photography PWAs: users, products, services, bookings, orders/cart, galleries, reviews, notifications, campaigns, settings, audit logs, minimart items/orders, and push notification subscriptions.
445 lines (383 loc) • 14.6 kB
JavaScript
import mongoose from 'mongoose';
const bookingWorkflowSchema = new mongoose.Schema({
// Workflow Template Information
name: {
type: String,
required: [true, 'Workflow name is required'],
trim: true
},
description: String,
// Booking Type this workflow applies to
bookingType: {
type: String,
enum: ['studio_session', 'makeover_session', 'equipment_rental', 'event_coverage', ],
required: [true, 'Booking type is required']
},
// Workflow Configuration
version: { type: String, default: '1.0' },
isActive: { type: Boolean, default: true },
isDefault: { type: Boolean, default: false }, // default workflow for this booking type
// Workflow Stages Definition
stages: [{
// Stage identification
id: { type: String, required: true }, // unique within workflow
name: { type: String, required: true },
description: String,
order: { type: Number, required: true },
// Stage configuration
isRequired: { type: Boolean, default: true },
canSkip: { type: Boolean, default: false },
autoComplete: { type: Boolean, default: false }, // auto-complete based on conditions
// Time constraints
estimatedDuration: Number, // minutes
maxDuration: Number, // minutes
// Dependencies
dependencies: [String], // stage IDs that must be completed first
// Conditions for stage activation/completion
activationConditions: [{
field: String, // booking field to check
operator: { type: String, enum: ['equals', 'not_equals', 'greater_than', 'less_than', 'contains'] },
value: mongoose.Schema.Types.Mixed,
required: { type: Boolean, default: true }
}],
completionConditions: [{
field: String,
operator: { type: String, enum: ['equals', 'not_equals', 'greater_than', 'less_than', 'contains'] },
value: mongoose.Schema.Types.Mixed,
required: { type: Boolean, default: true }
}],
// Actions to perform when stage starts/completes
actions: {
onStart: [{
type: { type: String, enum: ['notification', 'email', 'status_update', 'assignment', 'reminder'] },
config: mongoose.Schema.Types.Mixed
}],
onComplete: [{
type: { type: String, enum: ['notification', 'email', 'status_update', 'assignment', 'reminder'] },
config: mongoose.Schema.Types.Mixed
}]
},
// Checklist items for this stage
checklist: [{
id: String,
item: { type: String, required: true },
description: String,
isRequired: { type: Boolean, default: true },
category: String, // grouping checklist items
estimatedTime: Number, // minutes
assignedRole: String, // which role should complete this
order: Number
}],
// Required roles/permissions for this stage
requiredRoles: [String],
// Stage-specific data collection
dataFields: [{
name: String,
type: { type: String, enum: ['text', 'number', 'boolean', 'date', 'select', 'multiselect'] },
required: { type: Boolean, default: false },
options: [String], // for select/multiselect
validation: {
min: Number,
max: Number,
pattern: String
}
}]
}],
// State Transitions
transitions: [{
from: String, // stage ID or status
to: String, // stage ID or status
conditions: [{
field: String,
operator: String,
value: mongoose.Schema.Types.Mixed
}],
actions: [{
type: String,
config: mongoose.Schema.Types.Mixed
}]
}],
// Notification Templates
notifications: [{
id: String,
name: String,
trigger: { type: String, enum: ['stage_start', 'stage_complete', 'workflow_start', 'workflow_complete', 'delay', 'error'] },
recipients: [{ type: String, enum: ['client', 'assigned_staff', 'all_admins', 'specific_role'] }],
template: {
subject: String,
body: String,
variables: [String] // available template variables
},
channels: [{ type: String, enum: ['email', 'sms', 'push', 'in_app'] }]
}],
// SLA and Performance Metrics
sla: {
totalDuration: Number, // expected total workflow duration in hours
criticalStages: [String], // stage IDs that are critical for SLA
escalationRules: [{
condition: String, // when to escalate
delayHours: Number,
escalateTo: String, // role or specific user
action: String
}]
},
// Workflow Analytics Configuration
analytics: {
trackMetrics: [String], // which metrics to track
kpis: [{
name: String,
calculation: String, // how to calculate this KPI
target: Number,
unit: String
}]
},
// Customization per booking conditions
conditionalWorkflows: [{
conditions: [{
field: String,
operator: String,
value: mongoose.Schema.Types.Mixed
}],
modifications: {
addStages: [mongoose.Schema.Types.Mixed],
removeStages: [String],
modifyStages: [{
stageId: String,
changes: mongoose.Schema.Types.Mixed
}]
}
}],
// Integration Settings
integrations: [{
service: String, // external service name
trigger: String, // when to call the service
config: mongoose.Schema.Types.Mixed,
errorHandling: String
}],
// Admin and Metadata
createdBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
lastModifiedBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
tags: [String],
notes: String
}, {
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true }
});
// Workflow Instance Schema (for active bookings)
const workflowInstanceSchema = new mongoose.Schema({
// Reference to booking and workflow template
booking: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Booking',
required: true
},
workflowTemplate: {
type: mongoose.Schema.Types.ObjectId,
ref: 'BookingWorkflow',
required: true
},
// Current state
currentStage: String, // current stage ID
status: {
type: String,
enum: ['not_started', 'in_progress', 'completed', 'cancelled', 'on_hold', 'error'],
default: 'not_started'
},
// Stage execution history
stageHistory: [{
stageId: String,
stageName: String,
status: { type: String, enum: ['pending', 'in_progress', 'completed', 'skipped', 'error'] },
startedAt: Date,
completedAt: Date,
completedBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
duration: Number, // actual duration in minutes
notes: String,
// Checklist completion
checklist: [{
itemId: String,
item: String,
completed: { type: Boolean, default: false },
completedBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
completedAt: Date,
notes: String,
timeSpent: Number // minutes
}],
// Stage-specific data collected
data: mongoose.Schema.Types.Mixed,
// Issues or delays
issues: [{
description: String,
reportedBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
reportedAt: { type: Date, default: Date.now },
severity: { type: String, enum: ['low', 'medium', 'high', 'critical'] },
resolved: { type: Boolean, default: false },
resolvedAt: Date,
resolvedBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
resolution: String
}]
}],
// Overall workflow metrics
metrics: {
startedAt: Date,
completedAt: Date,
totalDuration: Number, // actual total duration in minutes
delayedStages: Number,
skippedStages: Number,
issueCount: Number,
slaStatus: { type: String, enum: ['on_track', 'at_risk', 'breached'] }
},
// Escalations
escalations: [{
reason: String,
escalatedAt: { type: Date, default: Date.now },
escalatedTo: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
resolved: { type: Boolean, default: false },
resolvedAt: Date,
resolution: String
}],
// Notifications sent
notificationHistory: [{
notificationId: String,
sentAt: { type: Date, default: Date.now },
channel: String,
recipient: String,
status: { type: String, enum: ['sent', 'delivered', 'failed'] },
error: String
}]
}, {
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true }
});
// Indexes for BookingWorkflow
bookingWorkflowSchema.index({ bookingType: 1, isActive: 1 });
bookingWorkflowSchema.index({ isDefault: 1, bookingType: 1 });
bookingWorkflowSchema.index({ name: 'text', description: 'text' });
// Indexes for WorkflowInstance
workflowInstanceSchema.index({ booking: 1 });
workflowInstanceSchema.index({ status: 1, currentStage: 1 });
workflowInstanceSchema.index({ 'metrics.slaStatus': 1 });
workflowInstanceSchema.index({ createdAt: -1 });
// Virtuals for BookingWorkflow
bookingWorkflowSchema.virtual('stageCount').get(function () {
return this.stages.length;
});
bookingWorkflowSchema.virtual('estimatedDuration').get(function () {
return this.stages.reduce((total, stage) => total + (stage.estimatedDuration || 0), 0);
});
// Virtuals for WorkflowInstance
workflowInstanceSchema.virtual('completionPercentage').get(function () {
if (!this.stageHistory.length) return 0;
const completedStages = this.stageHistory.filter(stage => stage.status === 'completed').length;
return Math.round((completedStages / this.stageHistory.length) * 100);
});
workflowInstanceSchema.virtual('isDelayed').get(function () {
return this.metrics.slaStatus === 'at_risk' || this.metrics.slaStatus === 'breached';
});
// Methods for BookingWorkflow
bookingWorkflowSchema.methods.createInstance = function (bookingId) {
const WorkflowInstance = mongoose.model('WorkflowInstance');
return new WorkflowInstance({
booking: bookingId,
workflowTemplate: this._id,
currentStage: this.stages[0]?.id,
stageHistory: this.stages.map(stage => ({
stageId: stage.id,
stageName: stage.name,
status: 'pending',
checklist: stage.checklist.map(item => ({
itemId: item.id,
item: item.item,
completed: false
}))
}))
});
};
bookingWorkflowSchema.methods.validateStageOrder = function () {
const orders = this.stages.map(stage => stage.order);
const uniqueOrders = [...new Set(orders)];
return orders.length === uniqueOrders.length;
};
// Methods for WorkflowInstance
workflowInstanceSchema.methods.getCurrentStage = function () {
return this.stageHistory.find(stage => stage.stageId === this.currentStage);
};
workflowInstanceSchema.methods.completeStage = function (stageId, completedBy, notes = '', data = {}) {
const stage = this.stageHistory.find(s => s.stageId === stageId);
if (!stage) {
throw new Error('Stage not found');
}
stage.status = 'completed';
stage.completedAt = new Date();
stage.completedBy = completedBy;
stage.notes = notes;
stage.data = data;
if (stage.startedAt) {
stage.duration = Math.round((stage.completedAt - stage.startedAt) / (1000 * 60)); // minutes
}
// Move to next stage
this.moveToNextStage();
return this.save();
};
workflowInstanceSchema.methods.moveToNextStage = function () {
const currentStageIndex = this.stageHistory.findIndex(s => s.stageId === this.currentStage);
const nextStage = this.stageHistory[currentStageIndex + 1];
if (nextStage) {
this.currentStage = nextStage.stageId;
nextStage.status = 'in_progress';
nextStage.startedAt = new Date();
} else {
// Workflow completed
this.status = 'completed';
this.metrics.completedAt = new Date();
if (this.metrics.startedAt) {
this.metrics.totalDuration = Math.round((this.metrics.completedAt - this.metrics.startedAt) / (1000 * 60));
}
}
};
workflowInstanceSchema.methods.skipStage = function (stageId, reason, skippedBy) {
const stage = this.stageHistory.find(s => s.stageId === stageId);
if (!stage) {
throw new Error('Stage not found');
}
stage.status = 'skipped';
stage.completedAt = new Date();
stage.completedBy = skippedBy;
stage.notes = `Skipped: ${reason}`;
this.metrics.skippedStages = (this.metrics.skippedStages || 0) + 1;
this.moveToNextStage();
return this.save();
};
workflowInstanceSchema.methods.reportIssue = function (stageId, description, reportedBy, severity = 'medium') {
const stage = this.stageHistory.find(s => s.stageId === stageId);
if (!stage) {
throw new Error('Stage not found');
}
stage.issues.push({
description,
reportedBy,
severity
});
this.metrics.issueCount = (this.metrics.issueCount || 0) + 1;
// Update SLA status if critical issue
if (severity === 'critical') {
this.metrics.slaStatus = 'breached';
}
return this.save();
};
// Static methods
bookingWorkflowSchema.statics.getDefaultWorkflow = function (bookingType) {
return this.findOne({ bookingType, isDefault: true, isActive: true });
};
workflowInstanceSchema.statics.getDelayedWorkflows = function () {
return this.find({
status: 'in_progress',
'metrics.slaStatus': { $in: ['at_risk', 'breached'] }
}).populate('booking workflowTemplate');
};
// Export both models
export const BookingWorkflow = mongoose.model('BookingWorkflow', bookingWorkflowSchema);
export const WorkflowInstance = mongoose.model('WorkflowInstance', workflowInstanceSchema);
export default BookingWorkflow;