Skip to main content

Shared Package

The packages/shared directory contains shared code used across all ForgeX microservices (bids, projects, field). This ensures consistency in types, constants, and utility functions while avoiding code duplication.

info

The shared package is referenced directly via relative paths. No build step or npm publish required — changes are immediately available to all services.


Package Structure

packages/shared/
├── README.md
├── package.json
├── types/
│ └── index.ts # TypeScript type definitions & enums
├── constants/
│ ├── bidStatuses.js # Bid status definitions & transitions
│ ├── defaults.js # Default values (rates, prices, settings)
│ ├── exportOptions.js # PDF export configuration
│ ├── features.js # Feature flags & API endpoints
│ └── validation.js # Validation rules & constraints
└── utils/
├── calculations.js # Cost/quantity calculation utilities
├── formatters.js # Formatting utilities (JS version)
├── formatters.ts # Formatting utilities (TS version)
└── normalize.js # Data normalization utilities

Types (types/index.ts)

Central TypeScript type definitions and enums used across all services.

Enums

Defines user roles and permissions across the platform:

export enum UserRole {
ADMIN = 'ADMIN', // Phase 1 - Full access
ESTIMATOR = 'ESTIMATOR', // Phase 1 - Bid creation/editing
PM = 'PM', // Phase 1 - Read-only bids, Phase 2 - Project management
OPS = 'OPS', // Phase 2 - Purchase orders
ACCOUNTING = 'ACCOUNTING', // Phase 2 - Cost reconciliation
FOREMAN = 'FOREMAN' // Phase 3 - Field operations
}

Usage:

import { UserRole } from '../../../../packages/shared/types';

if (user.role === UserRole.ADMIN) {
// Allow deletion
}

Interfaces

User
export interface User {
id: string;
email: string;
googleId?: string;
name?: string;
role: UserRole;
createdAt: Date;
updatedAt: Date;
}
Client
export interface Client {
id: string;
name: string;
email: string;
createdBy: string;
createdAt: Date;
}
Bid
export interface Bid {
id: string;
clientId: string;
jobName: string;
location: string;
status: BidStatus;
createdBy: string;
createdAt: Date;
lastUpdated: Date;
lastUpdatedBy?: string;

// Cost rollup fields
concreteCost: number;
laborCost: number;
equipmentCost: number;
materialCost: number;
subletCost: number;
miscCost: number;
subtotalCost: number;
overheadAmount: number;
profitAmount: number;
totalCost: number;
}
Scope
export interface Scope {
id: string;
bidId: string;
name: string;
shape: string;
length?: number;
width?: number;
height?: number;
diameter?: number;
cubicYards?: number;
rebarWasteOverride?: number;
createdBy: string;
createdAt: Date;

// Cost rollup fields
concreteCost: number;
laborCost: number;
equipmentCost: number;
materialCost: number;
subletCost: number;
miscCost: number;
totalCost: number;
}
BidWithDetails (Extended)

Extended Bid interface with project details:

export interface BidWithDetails extends Bid {
taxExempt?: boolean;
perDiemEnabled?: boolean;
notes?: string | null;
insuranceType?: InsuranceType;
certifiedPayroll?: boolean;
c3Required?: boolean;
drugTestingRequired?: boolean;
attachments?: BidAttachment[];
exclusions?: BidExclusion[];
}

InsuranceType:

export type InsuranceType = 'CCIP' | 'OCIP' | 'RCIP' | 'NONE' | null;

Constants

features.js - Feature Flags

Controls module and role visibility across all services:

module.exports = {
// Module availability flags
ENABLED_MODULES: {
BID_MANAGER: true, // Phase 1 - LIVE
PROJECT_MANAGEMENT: false, // Phase 2 - Coming soon
FIELD_OPERATIONS: false, // Phase 3 - Coming soon
},

// Role visibility flags
ENABLED_ROLES: {
ADMIN: true, // Phase 1
ESTIMATOR: true, // Phase 1
PM: true, // Phase 1 (read-only)
OPS: false, // Phase 2
ACCOUNTING: false,// Phase 2
FOREMAN: false, // Phase 3
},

// Service API endpoints
API_ENDPOINTS: {
BIDS: process.env.BIDS_API_URL || 'http://localhost:5001',
PROJECTS: process.env.PROJECTS_API_URL || 'http://localhost:5002',
FIELD: process.env.FIELD_API_URL || 'http://localhost:5003',
},

// Pub/Sub topic names
PUBSUB_TOPICS: {
BID_AWARDED: 'bid-awarded',
PROJECT_UPDATED: 'project-updated',
TIMESHEET_SUBMITTED: 'timesheet-submitted',
}
};

Usage:

const { ENABLED_MODULES } = require('../../../../packages/shared/constants/features');

if (ENABLED_MODULES.PROJECT_MANAGEMENT) {
// Show Projects tile
}

See Feature Flags for details.


defaults.js - Default Values

Centralized default values for rates, prices, and settings:

module.exports = {
REBAR: {
BASE_PRICE: 0.85,
TAX_RATE: 0.0825,
WASTE_PERCENT: 0,
STANDARD_WEIGHTS: {
'#2': 0.167,
'#3': 0.376,
'#4': 0.668,
'#5': 1.043,
'#6': 1.502,
'#7': 2.044,
'#8': 2.670,
'#9': 3.400,
'#10': 4.303,
'#11': 5.310
}
},

LABOR: {
PER_DIEM_RATE: 138,
DEFAULT_HOURS_PER_DAY: 8,
DEFAULT_DAYS: 1
},

EQUIPMENT: {
DEFAULT_DELIVERY_FEE: 300,
DEFAULT_FUEL_CHARGE_PER_DAY: 50,
DEFAULT_RENTAL_TAX_RATE: 0.1225,
FORMWORK_TAX_RATE: 0,
DEFAULT_DURATION_VALUE: 1,
DEFAULT_DURATION_UNIT: 'day'
},

MATERIALS: {
DEFAULT_WASTE_PERCENT: 0,
DEFAULT_TAX_RATE: 0.0825
},

SUBCONTRACTOR: {
DEFAULT_WASTE_PERCENT: 0,
DEFAULT_UNIT: 'LS'
},

BID: {
DEFAULT_LOCATION: 'Texas',
DEFAULT_STATUS: 'DRAFT',
DEFAULT_TAX_EXEMPT: false,
DEFAULT_PER_DIEM_ENABLED: false
}
};

Usage:

const { EQUIPMENT } = require('../../../../packages/shared/constants/defaults');

const deliveryFee = item.deliveryFee ?? EQUIPMENT.DEFAULT_DELIVERY_FEE;

bidStatuses.js - Status Definitions

Bid status transitions and validation:

module.exports = {
STATUSES: {
DRAFT: 'DRAFT',
SUBMITTED: 'SUBMITTED',
NEGOTIATION: 'NEGOTIATION',
AWARDED: 'AWARDED',
COMPLETED: 'COMPLETED',
LOST: 'LOST'
},

VALID_TRANSITIONS: {
DRAFT: ['SUBMITTED'],
SUBMITTED: ['NEGOTIATION', 'AWARDED', 'LOST'],
NEGOTIATION: ['AWARDED', 'LOST', 'SUBMITTED'],
AWARDED: ['COMPLETED'],
COMPLETED: [],
LOST: []
},

// Check if transition is valid
canTransition(from, to) {
return this.VALID_TRANSITIONS[from]?.includes(to) ?? false;
}
};

Usage:

const { canTransition } = require('../../../../packages/shared/constants/bidStatuses');

if (!canTransition(bid.status, newStatus)) {
throw new Error('Invalid status transition');
}

validation.js - Validation Rules

Validation constraints for inputs:

module.exports = {
BID: {
JOB_NAME_MAX_LENGTH: 255,
LOCATION_MAX_LENGTH: 255,
NOTES_MAX_LENGTH: 5000
},

SCOPE: {
NAME_MAX_LENGTH: 255,
MAX_MULTIPLIER: 1000,
MIN_MULTIPLIER: 1
},

ITEM: {
DESCRIPTION_MAX_LENGTH: 500,
MAX_QUANTITY: 1000000,
MIN_QUANTITY: 0
},

CLIENT: {
NAME_MAX_LENGTH: 255,
EMAIL_MAX_LENGTH: 255
}
};

exportOptions.js - Export Configuration

PDF export field definitions and options:

module.exports = {
FIELD_GROUPS: {
BASIC: ['jobName', 'clientName', 'location'],
COMPLIANCE: ['insuranceType', 'certifiedPayroll', 'c3Required'],
COSTS: ['concreteCost', 'laborCost', 'equipmentCost']
},

EXCLUDED_FIELDS: ['createdAt', 'updatedAt', 'id'],

DATE_FORMAT: 'YYYY-MM-DD',
CURRENCY_FORMAT: { style: 'currency', currency: 'USD' }
};

Utilities

formatters.ts / formatters.js - Formatting

Consistent formatting utilities for currency, percentages, and quantities:

export const formatCurrency = (
value: number | string | null | undefined,
options?: Intl.NumberFormatOptions
): string => {
const numeric = Number(value || 0);
if (!Number.isFinite(numeric)) {
return '$0.00';
}

return numeric.toLocaleString('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
...options
});
};

Usage:

import { formatCurrency } from '../../../../packages/shared/utils/formatters';

formatCurrency(1234.56) // "$1,234.56"
formatCurrency(null) // "$0.00"

calculations.js - Calculation Utilities

Common calculation functions:

module.exports = {
/**
* Calculate rebar pounds from cubic yards
*/
calculateRebarPounds(cubicYards, poundsPerCY) {
return cubicYards * poundsPerCY;
},

/**
* Calculate cubic yards from dimensions
*/
calculateCubicYards(length, width, depth) {
return (length * width * depth) / 27; // 27 cubic feet per cubic yard
},

/**
* Apply waste percentage
*/
applyWaste(quantity, wastePercent) {
return quantity * (1 + wastePercent / 100);
},

/**
* Calculate tax amount
*/
calculateTax(amount, taxRate) {
return amount * taxRate;
}
};

normalize.js - Normalization Utilities

Data normalization and validation:

module.exports = {
/**
* Normalize boolean from various formats
*/
normalizeBoolean(value) {
if (typeof value === 'boolean') return value;
if (value === 'true' || value === '1' || value === 1) return true;
if (value === 'false' || value === '0' || value === 0) return false;
return null;
},

/**
* Normalize numeric value
*/
normalizeNumber(value, defaultValue = 0) {
const num = Number(value);
return Number.isFinite(num) ? num : defaultValue;
},

/**
* Normalize email (lowercase, trim)
*/
normalizeEmail(email) {
if (typeof email !== 'string') return null;
return email.toLowerCase().trim();
}
};

How Services Use Shared

Backend (Node.js)

Import via relative paths:

// Types (via JSDoc)
/**
* @typedef {import('../../../../packages/shared/types').Bid} Bid
* @typedef {import('../../../../packages/shared/types').UserRole} UserRole
*/

// Constants
const { ENABLED_MODULES } = require('../../../../packages/shared/constants/features');
const { REBAR } = require('../../../../packages/shared/constants/defaults');

// Utilities
const { formatCurrency } = require('../../../../packages/shared/utils/formatters');
const { normalizeBoolean } = require('../../../../packages/shared/utils/normalize');

Frontend (React + TypeScript)

Import via relative paths:

// Types & Enums
import { UserRole, BidStatus, ModuleType } from '../../../../packages/shared/types';

// Constants
import { ENABLED_MODULES } from '../../../../packages/shared/constants/features';

// Utilities
import { formatCurrency, formatPercent } from '../../../../packages/shared/utils/formatters';

// Usage
const formattedTotal = formatCurrency(bid.totalCost);

if (user.role === UserRole.ADMIN) {
// Show admin features
}

Adding New Shared Code

1
Determine Category

Decide where the new code belongs:

  • Types: New interface or enum
  • Constants: Configuration or default value
  • Utils: Reusable function
2
Add to Appropriate File

Edit the relevant file in packages/shared/:

  • types/index.ts for TypeScript types
  • constants/[category].js for constants
  • utils/[utility].js or .ts for utilities
3
Export from Module

Ensure the new code is exported:

// types/index.ts
export interface NewInterface { ... }

// constants/features.js
module.exports = { NEW_FEATURE_FLAG: true };

// utils/calculations.js
module.exports = { newCalculation: () => { ... } };
4
Update README

Document the new addition in packages/shared/README.md.

5
Use in Services

Import and use in any service:

import { NewInterface } from '../../../../packages/shared/types';

Changes are immediately available (no build step).


Best Practices

🛡️

Type Everything

Use TypeScript types for all shared interfaces. Enforce type safety across services.

Single Source of Truth

Define constants once in shared package. Avoid duplicating values across services.

🔬

Keep It Pure

Shared utilities should be pure functions with no side effects or dependencies.

📖

Document Changes

Update README.md when adding new shared code. Include usage examples.


Versioning & Breaking Changes

warning

Shared package changes affect ALL services. Breaking changes require coordinated updates across services.

Safe Changes

✅ Adding new types, constants, or utilities ✅ Adding optional fields to interfaces ✅ Adding new enum values (append only) ✅ Deprecating (but not removing) old utilities

Breaking Changes

❌ Removing types, constants, or utilities ❌ Changing function signatures ❌ Removing or renaming interface fields ❌ Removing enum values

For breaking changes:

  1. Deprecate old version (add comment)
  2. Add new version alongside old
  3. Update all services to use new version
  4. Remove old version in next major release

Testing Shared Code

Shared code should be tested within each service that uses it:

// Example: Test formatCurrency in bids backend
describe('formatCurrency', () => {
it('formats valid numbers', () => {
expect(formatCurrency(1234.56)).toBe('$1,234.56');
});

it('handles null gracefully', () => {
expect(formatCurrency(null)).toBe('$0.00');
});
});

Next Steps