business-as-code
Version:
Primitives for expressing business logic and processes as code
272 lines (245 loc) • 7.17 kB
text/typescript
/**
* Business process definition and management
*/
import type { ProcessDefinition, ProcessStep, ProcessMetric } from './types.js'
/**
* Define a business process with steps, inputs, outputs, and metrics
*
* @example
* ```ts
* const process = Process({
* name: 'Customer Onboarding',
* description: 'Process for onboarding new customers',
* category: 'core',
* owner: 'Customer Success Team',
* steps: [
* {
* order: 1,
* name: 'Welcome Email',
* description: 'Send personalized welcome email',
* responsible: 'CS Manager',
* duration: '5 minutes',
* automationLevel: 'automated',
* },
* {
* order: 2,
* name: 'Initial Setup Call',
* description: 'Schedule and conduct setup call',
* responsible: 'CS Rep',
* duration: '30 minutes',
* automationLevel: 'manual',
* },
* {
* order: 3,
* name: 'Account Configuration',
* description: 'Configure customer account settings',
* responsible: 'CS Rep',
* duration: '15 minutes',
* automationLevel: 'semi-automated',
* },
* ],
* inputs: ['Customer Information', 'Subscription Plan'],
* outputs: ['Configured Account', 'Training Materials'],
* metrics: [
* {
* name: 'Time to First Value',
* description: 'Time from signup to first successful use',
* target: 24,
* unit: 'hours',
* },
* {
* name: 'Onboarding Completion Rate',
* description: 'Percentage of customers completing onboarding',
* target: 90,
* unit: 'percent',
* },
* ],
* })
* ```
*/
export function Process(definition: ProcessDefinition): ProcessDefinition {
if (!definition.name) {
throw new Error('Process name is required')
}
return {
...definition,
category: definition.category || 'support',
steps: definition.steps || [],
inputs: definition.inputs || [],
outputs: definition.outputs || [],
metrics: definition.metrics || [],
metadata: definition.metadata || {},
}
}
/**
* Get process steps in order
*/
export function getStepsInOrder(process: ProcessDefinition): ProcessStep[] {
return [...(process.steps || [])].sort((a, b) => a.order - b.order)
}
/**
* Get steps by automation level
*/
export function getStepsByAutomationLevel(
process: ProcessDefinition,
level: ProcessStep['automationLevel']
): ProcessStep[] {
return process.steps?.filter((step) => step.automationLevel === level) || []
}
/**
* Calculate total process duration in minutes
*/
export function calculateTotalDuration(process: ProcessDefinition): number {
return (
process.steps?.reduce((total, step) => {
return total + parseDurationToMinutes(step.duration)
}, 0) || 0
)
}
/**
* Parse duration string to minutes
*/
function parseDurationToMinutes(duration?: string): number {
if (!duration) return 0
const lower = duration.toLowerCase()
const match = lower.match(/(\d+)\s*(minute|minutes|min|hour|hours|hr|day|days|week|weeks)/)
if (!match) return 0
const value = parseInt(match[1] || '0', 10)
const unit = match[2]
switch (unit) {
case 'minute':
case 'minutes':
case 'min':
return value
case 'hour':
case 'hours':
case 'hr':
return value * 60
case 'day':
case 'days':
return value * 60 * 24
case 'week':
case 'weeks':
return value * 60 * 24 * 7
default:
return 0
}
}
/**
* Format minutes to human-readable duration
*/
export function formatDuration(minutes: number): string {
if (minutes < 60) {
return `${minutes} minutes`
} else if (minutes < 60 * 24) {
const hours = Math.floor(minutes / 60)
const mins = minutes % 60
return mins > 0 ? `${hours} hours ${mins} minutes` : `${hours} hours`
} else {
const days = Math.floor(minutes / (60 * 24))
const hours = Math.floor((minutes % (60 * 24)) / 60)
return hours > 0 ? `${days} days ${hours} hours` : `${days} days`
}
}
/**
* Calculate automation percentage
*/
export function calculateAutomationPercentage(process: ProcessDefinition): number {
if (!process.steps || process.steps.length === 0) return 0
const automatedSteps = process.steps.filter(
(step) => step.automationLevel === 'automated' || step.automationLevel === 'semi-automated'
).length
return (automatedSteps / process.steps.length) * 100
}
/**
* Get metric by name
*/
export function getMetric(process: ProcessDefinition, name: string): ProcessMetric | undefined {
return process.metrics?.find((m) => m.name === name)
}
/**
* Check if metric meets target
*/
export function meetsTarget(metric: ProcessMetric): boolean {
if (metric.target === undefined || metric.current === undefined) return false
return metric.current >= metric.target
}
/**
* Calculate metric achievement percentage
*/
export function calculateMetricAchievement(metric: ProcessMetric): number {
if (metric.target === undefined || metric.current === undefined) return 0
if (metric.target === 0) return 100
return (metric.current / metric.target) * 100
}
/**
* Update metric current value
*/
export function updateMetric(
process: ProcessDefinition,
metricName: string,
currentValue: number
): ProcessDefinition {
const metrics = process.metrics?.map((m) =>
m.name === metricName ? { ...m, current: currentValue } : m
)
const result: ProcessDefinition = { ...process }
if (metrics !== undefined) result.metrics = metrics
return result
}
/**
* Add step to process
*/
export function addStep(process: ProcessDefinition, step: ProcessStep): ProcessDefinition {
return {
...process,
steps: [...(process.steps || []), step],
}
}
/**
* Remove step from process
*/
export function removeStep(process: ProcessDefinition, stepOrder: number): ProcessDefinition {
const steps = process.steps?.filter((s) => s.order !== stepOrder)
const result: ProcessDefinition = { ...process }
if (steps !== undefined) result.steps = steps
return result
}
/**
* Validate process definition
*/
export function validateProcess(process: ProcessDefinition): { valid: boolean; errors: string[] } {
const errors: string[] = []
if (!process.name) {
errors.push('Process name is required')
}
if (process.steps) {
const orders = new Set<number>()
for (const step of process.steps) {
if (!step.name) {
errors.push(`Step at order ${step.order} must have a name`)
}
if (orders.has(step.order)) {
errors.push(`Duplicate step order: ${step.order}`)
}
orders.add(step.order)
}
}
if (process.metrics) {
for (const metric of process.metrics) {
if (!metric.name) {
errors.push('Metric must have a name')
}
if (metric.target !== undefined && metric.target < 0) {
errors.push(`Metric ${metric.name} target cannot be negative`)
}
if (metric.current !== undefined && metric.current < 0) {
errors.push(`Metric ${metric.name} current value cannot be negative`)
}
}
}
return {
valid: errors.length === 0,
errors,
}
}