Technical Implementation Notes - Currency Training System
Current Implementation Analysis
The currency system is currently managed through:
1. Database Tables
Main Currency Tables:
channel_currency_instructor- Instructor currency recordschannel_currency_coach- Coach currency recordschannel_currency_trainer- Trainer currency recordschannel_currency_military- Military currency recordsmemberstable fields:currency_instructor,currency_coach,currency_trainer,currency_military
Checkbox Tables (Track Individual Training Components):
channel_currency_instructor_checkboxes- Instructor checkbox completionschannel_currency_coach_checkboxes- Coach checkbox completionschannel_currency_trainer_checkboxes- Trainer checkbox completionschannel_currency_military_checkboxes- Military checkbox completions
Note: Flyers are excluded from this new system - they maintain the existing 6-month activity-based currency model.
2. Key Service Files
api/src/features/shared/currency/service/index.js- Base currency service with renewal date calculationsapi/src/features/account/safety-training/service/*.service.js- Safety training services for each roleapi/src/features/account/safety-training/constant/index.js- Checkbox definitions for each role
3. Current Logic
- Fixed 6-month currency periods (Jan 16 - Jul 15, Jul 16 - Jan 15)
- Currency calculated based on training completion within fixed periods
- When recurrency training starts, system creates checkbox rows for member's current levels
- Checkbox completion determines updated approval levels via
getNewApprovalLevel()method - Expiration triggers deactivation after 6 months
4. Checkbox-Based Training System
Instructor Example (api/src/features/account/safety-training/service/instructor.service.js:221-231):
async getNewApprovalLevel() {
const passedCheckboxes = this.checkboxes.filter(box => box.passed);
const lastPassedLevel = passedCheckboxes.length > 0
? passedCheckboxes[passedCheckboxes.length - 1].level || 0
: 0;
if (!lastPassedLevel) return 0;
const approvalLevel = await this.safetyTrainingRepository.getApprovalLevel(lastPassedLevel);
return approvalLevel?.approval_level_assigned_instructor || 0;
}
Role-Specific Checkboxes:
- Instructors: Level-based training (Level 1-4) + general requirements
- Trainers: Maintain vs Regain checkboxes with supervision requirements
- Coaches: Maintain vs Regain with coaching session requirements
- Military: Maintain vs Regain with military instruction requirements
Required Changes for Relative Currency Periods
1. Database Schema Changes
-- Add recurrency tracking fields to member tables
ALTER TABLE members
ADD COLUMN currency_recurrent_date INT(11) DEFAULT NULL COMMENT 'Unix timestamp of last recurrency training',
ADD COLUMN currency_base_period VARCHAR(20) DEFAULT 'standard' COMMENT 'standard or recurrent',
ADD COLUMN currency_next_expiry INT(11) DEFAULT NULL COMMENT 'Calculated next expiry date';
-- Add recurrency tracking to currency tables
ALTER TABLE channel_currency_instructor
ADD COLUMN is_recurrent TINYINT(1) DEFAULT 0 COMMENT 'Whether this is recurrent training',
ADD COLUMN previous_expiry INT(11) DEFAULT NULL COMMENT 'Previous expiry date when recurrent taken';
-- Note: Flyer tables are NOT modified as flyers are excluded from this system
-- Similar for coach, trainer, military tables
2. Service Layer Changes
File: api/src/features/shared/currency/service/index.js
Add new methods to handle relative currency periods:
// New method to calculate currency based on recurrent date
calculateRecurrentCurrency(recurrentDate, member) {
const recurrentTimestamp = new Date(recurrentDate).getTime();
const now = Date.now();
const monthsSinceRecurrent = (now - recurrentTimestamp) / (1000 * 60 * 60 * 24 * 30);
// If within 6 months of recurrent, they're current
if (monthsSinceRecurrent <= 6) {
// Calculate next expiry relative to recurrent date
const nextExpiry = new Date(recurrentTimestamp);
nextExpiry.setMonth(nextExpiry.getMonth() + 6);
// Align to next period boundary (Jan 16 or Jul 16)
return this.alignToNextPeriodBoundary(nextExpiry);
}
return 'Expired';
}
// Method to determine if member needs relative currency
getCurrencyMode(member) {
if (member.currency_recurrent_date && member.currency_base_period === 'recurrent') {
return 'recurrent';
}
return 'standard';
}
// Updated renewal date calculation
currencyRenewalDate(lastEntry, member) {
const currencyMode = this.getCurrencyMode(member);
if (currencyMode === 'recurrent') {
return this.calculateRecurrentCurrency(member.currency_recurrent_date, member);
}
// Existing standard logic
return this.standardCurrencyRenewalDate(lastEntry, member);
}
3. Safety Training Service Updates
Files: api/src/features/account/safety-training/service/{instructor,trainer,coach,military}.service.js
Modify each service to track recurrent training (excluding flyer):
// Example for InstructorSafetyTrainingService
async saveInstructorMember() {
const isNewEntry = !this.checkboxes.some(box => box.row_id);
// Check if this is recurrent training (member was expired)
const wasExpired = await this.checkMemberExpiredStatus(this.req.body.member_id);
if (wasExpired) {
// Mark as recurrent training
await this.updateMemberField('currency_recurrent_date', Date.now() / 1000, this.req.body.member_id);
await this.updateMemberField('currency_base_period', 'recurrent', this.req.body.member_id);
// Add recurrent flag to params
this.params.is_recurrent = 1;
this.params.previous_expiry = wasExpired.expiry_date;
}
// Existing checkbox processing and approval level updates
await this.updateMemberApprovalLevel();
// Rest of existing logic...
}
// New method to check if member was expired
async checkMemberExpiredStatus(memberId) {
const lastEntry = await this.safetyTrainingRepository.getLastCurrencyEntry(memberId);
if (!lastEntry) return null;
const expiryDate = this.currencyService.currencyRenewalDate(lastEntry, this.memberDetails);
if (expiryDate === 'Expired') {
return { expired: true, expiry_date: lastEntry.entry_date };
}
return null;
}
// Enhanced approval level update for recurrent training
async updateMemberApprovalLevel() {
const newApprovalLevel = await this.getNewApprovalLevel();
// For recurrent training, level may decrease based on checkbox completion
if (this.memberDetails.currency_base_period === 'recurrent') {
// Always update to the level achieved in recurrent training
await this.updateMemberField('approval_level_instructor', newApprovalLevel, this.req.body.member_id);
} else {
// Standard logic - only increase levels
if (newApprovalLevel > this.memberDetails.approval_level_instructor) {
await this.updateMemberField('approval_level_instructor', newApprovalLevel, this.req.body.member_id);
}
}
return { status: true };
}
4. Cron Job Updates
Files: api/src/features/shared/currency/service/{instructor,trainer,coach,military}.service.js
Update cron jobs to respect relative currency (excluding flyers):
// Example for InstructorService
async getCurrencyMembers(options = {}) {
// Modified query to include recurrent currency members
const members = await this.currencyRepository.getInstructorCurrencyMembersWithRecurrent();
// Filter based on currency mode
return members.filter(member => {
if (member.currency_base_period === 'recurrent') {
// Check relative to recurrent date
return this.isExpiredRelativeToRecurrent(member);
}
// Standard 6-month check
return this.isExpiredStandard(member);
});
}
isExpiredRelativeToRecurrent(member) {
const monthsSinceRecurrent =
(Date.now() - member.currency_recurrent_date * 1000) / (1000 * 60 * 60 * 24 * 30);
return monthsSinceRecurrent > 6;
}
// Update deactivation logic
async makeMembersInactive(members) {
for (const memberId of members) {
// Standard deactivation
await this.currencyRepository.deactivateMember(this.membersInactive.query, {member: memberId});
// Clear recurrent status when deactivated
await this.currencyRepository.updateMemberField('currency_base_period', 'standard', memberId);
await this.currencyRepository.updateMemberField('currency_recurrent_date', null, memberId);
}
}
5. Repository/Model Updates
File: api/src/features/shared/currency/model/index.js
Add queries to support recurrent tracking (for instructors, trainers, coaches, military):
static getInstructorCurrencyMembersWithRecurrent(options, membersId, table) {
const reportAdditionalQuery = `${this.reportAdditionalQuery(table)} last_currency_training`;
return `
SELECT
M.member_id,
M.screen_name,
M.currency_instructor,
M.currency_recurrent_date,
M.currency_base_period,
M.currency_next_expiry,
M.approval_level_instructor,
-- existing fields...
FROM members AS M
-- existing joins...
WHERE M.role_id IN (8,9,10,12)
AND M.level_instructor NOT IN (${catLevels.level0}, ${catLevels.afc})
AND (
-- Standard currency check
(M.currency_base_period = 'standard' OR M.currency_base_period IS NULL)
OR
-- Recurrent currency check - expires 6 months from recurrent date
(M.currency_base_period = 'recurrent'
AND M.currency_recurrent_date IS NOT NULL
AND M.currency_recurrent_date + (6 * 30 * 24 * 60 * 60) < UNIX_TIMESTAMP())
)
`;
}
// Similar methods for trainer, coach, military with role-specific conditions
File: api/src/features/account/safety-training/model/index.js
Update checkbox queries to include recurrent tracking:
static getCurrencyCheckboxes(type, dateRange, includeTimezone = false) {
const tableMap = {
instructor: {
checkbox: 'channel_currency_instructor_checkboxes',
currency: 'channel_currency_instructor'
},
trainer: {
checkbox: 'channel_currency_trainer_checkboxes',
currency: 'channel_currency_trainer'
},
coach: {
checkbox: 'channel_currency_coach_checkboxes',
currency: 'channel_currency_coach'
},
military: {
checkbox: 'channel_currency_military_checkboxes',
currency: 'channel_currency_military'
}
};
const tables = tableMap[type];
if (!tables) throw new Error(`Invalid currency type: ${type}`);
return `
SELECT
CCC.row_id,
CCC.entry_id,
CCC.entry_date,
CCC.instructor,
CCC.label,
CCC.tunnel,
CCC.passed,
CCC.level,
CC.is_recurrent,
CC.previous_expiry
FROM ${tables.checkbox} CCC
JOIN ${tables.currency} CC ON CC.entry_id = CCC.entry_id
WHERE CC.author_id = :member_id
${dateRange}
`;
}
6. Checkbox Management for Recurrent Training
Default Checkbox Creation Logic: When a member starts recurrency, the system creates checkboxes based on their current level:
// In InstructorSafetyTrainingService
async getInstructorDefaultCheckboxes() {
const { level_instructor, currency_instructor } = this.member;
const instructorCheckboxData = _.cloneDeep(instructorCheckbox);
const checkboxMap = {
'status-active': currency_instructor === 1,
'status-inactive': currency_instructor !== 1,
'meeting': true,
'level1': this._isLevelAllowed(level_instructor, [catLevels.instructorLevel1, ...]),
'level2': this._isLevelAllowed(level_instructor, [catLevels.instructorLevel2, ...]),
'level3': this._isLevelAllowed(level_instructor, [catLevels.instructorLevel3, ...]),
'level4': this._isLevelAllowed(level_instructor, [catLevels.instructorLevel4])
};
return Object.entries(checkboxMap)
.filter(([_, shouldInclude]) => shouldInclude)
.map(([id]) => _.find(instructorCheckboxData, { id }))
.filter(Boolean);
}
7. Migration Strategy
- Phase 1: Deploy database schema changes
- Phase 2: Deploy code changes with backward compatibility
- Phase 3: Run migration script to identify and mark recurrent members
- Phase 4: Monitor and adjust based on edge cases
- Phase 5: Verify checkbox-based approval level calculations work correctly
8. Testing Requirements
- Unit tests for new currency calculation methods
- Integration tests for recurrent training flow with checkbox completion
- Test approval level updates based on checkbox completion
- Edge case testing for period boundaries
- Test checkbox creation for different member levels
- Migration testing on staging environment
- Performance testing for modified queries
9. Monitoring & Rollback Plan
- Add logging for currency mode decisions
- Track metrics on recurrent vs standard currency members
- Monitor approval level changes during recurrent training
- Prepare rollback scripts to revert database changes
- Document manual override procedures for edge cases
Key Implementation Considerations
Period Alignment Logic
When a member takes recurrent training, their new currency period should:
- Start from the recurrent training date
- Extend for 6 months from that date
- Align to the next standard period boundary (Jan 16 or Jul 16) if within 30 days
Note: This does NOT apply to flyers - they maintain the existing activity-based currency system.
Checkbox-Based Level Determination
For instructors, trainers, coaches, and military members:
- When recurrency starts, checkboxes are created based on current certification level
- Completed checkboxes determine the new approval level via
getNewApprovalLevel() - Member's approval level may decrease if they don't complete higher-level checkboxes
- This ensures only competent members retain higher-level privileges
Edge Cases to Handle
- Member takes recurrent training just before a standard period boundary
- Member has multiple expired currencies (instructor + coach + trainer)
- Member transitions from recurrent back to standard currency
- Member fails to complete expected level checkboxes (level downgrade)
- Historical data migration for existing expired members
- Checkbox validation for different approval levels (instructor.service.js:92-120)
API Changes
New endpoints or modifications needed:
GET /api/member/{id}/currency-status- Include recurrent status and approval levelsPOST /api/safety-training/{type}/recurrent- Mark training as recurrent for specific roleGET /api/reports/currency- Filter by standard vs recurrent, show approval level changesGET /api/safety-training/{type}/checkboxes- Enhanced to show recurrent vs maintain checkboxes
UI/UX Considerations
- Display different expiry dates for standard vs recurrent members
- Show "Recurrent Currency" badge or indicator
- Display current vs new approval levels during recurrency training
- Checkbox interface showing which levels member is eligible for
- Clear messaging about potential level downgrades
- Admin tools to manually adjust currency mode and approval levels if needed
Role-Specific Considerations
Instructors:
- Level-based checkboxes (1-4) determine approval level
- Validation rules prevent unauthorized level assignments
- May require reactivation of flyer currency if inactive
Trainers:
- Maintain vs Regain checkbox sets
- Supervision requirements for regain
- Integration with instructor currency requirements
Coaches:
- Coach spotter skill requirements
- Maintain vs Regain based on activity
- Session count requirements
Military:
- Military-specific skills validation
- Military flag requirement
- Separate checkbox set for military instruction
Flyers:
- EXCLUDED from this system
- Continue using existing activity-based 6-month currency
- No checkbox-based training requirements