feat(sms): send class start date, days, and course time
Expose schedule fields on class and invoice templates so SMS can include Jalali start date, weekdays, and hour range.
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
'use strict';
|
||||
|
||||
const PERSIAN_WEEKDAYS = ['یکشنبه', 'دوشنبه', 'سهشنبه', 'چهارشنبه', 'پنجشنبه', 'جمعه', 'شنبه'];
|
||||
const IRAN_WEEK_ORDER = [6, 0, 1, 2, 3, 4, 5];
|
||||
|
||||
const toDate = (value) => {
|
||||
if (!value) return null;
|
||||
const date = value instanceof Date ? value : new Date(value);
|
||||
return Number.isNaN(date.getTime()) ? null : date;
|
||||
};
|
||||
|
||||
const formatClockPart = (time) => {
|
||||
const raw = String(time || '').trim();
|
||||
if (!raw) return '';
|
||||
const [hoursRaw, minutesRaw] = raw.split(':');
|
||||
const hours = String(parseInt(hoursRaw, 10));
|
||||
if (hours === 'NaN') return '';
|
||||
if (!minutesRaw || minutesRaw === '00') return hours;
|
||||
return `${hours}:${minutesRaw}`;
|
||||
};
|
||||
|
||||
const formatCourseTime = (startTime, endTime) => {
|
||||
const start = formatClockPart(startTime);
|
||||
const end = formatClockPart(endTime);
|
||||
if (!start) return '';
|
||||
if (!end) return start;
|
||||
return `از ${start} تا ${end}`;
|
||||
};
|
||||
|
||||
const weekdayIndex = (value) => {
|
||||
const date = toDate(value);
|
||||
return date ? date.getDay() : null;
|
||||
};
|
||||
|
||||
const joinFaList = (items) => {
|
||||
if (!items.length) return '';
|
||||
if (items.length === 1) return items[0];
|
||||
if (items.length === 2) return `${items[0]} و ${items[1]}`;
|
||||
return `${items.slice(0, -1).join('، ')} و ${items[items.length - 1]}`;
|
||||
};
|
||||
|
||||
const formatClassDays = (sessions = []) => {
|
||||
const indexes = [];
|
||||
const seen = new Set();
|
||||
for (const session of sessions) {
|
||||
const dayIndex = weekdayIndex(session?.day);
|
||||
if (dayIndex == null || seen.has(dayIndex)) continue;
|
||||
seen.add(dayIndex);
|
||||
indexes.push(dayIndex);
|
||||
}
|
||||
indexes.sort((a, b) => IRAN_WEEK_ORDER.indexOf(a) - IRAN_WEEK_ORDER.indexOf(b));
|
||||
return joinFaList(indexes.map((index) => PERSIAN_WEEKDAYS[index]).filter(Boolean));
|
||||
};
|
||||
|
||||
const formatClassStartDate = (value) => {
|
||||
const date = toDate(value);
|
||||
if (!date) return '';
|
||||
try {
|
||||
return date.toLocaleDateString('fa-IR');
|
||||
} catch {
|
||||
return date.toISOString().slice(0, 10);
|
||||
}
|
||||
};
|
||||
|
||||
const buildClassScheduleContext = (classDoc, sessions = [], currentSession = null) => {
|
||||
const sortedSessions = [...sessions].sort((a, b) => {
|
||||
const left = toDate(a?.day)?.getTime() || 0;
|
||||
const right = toDate(b?.day)?.getTime() || 0;
|
||||
return left - right;
|
||||
});
|
||||
const sessionForTime = currentSession || sortedSessions[0] || null;
|
||||
|
||||
return {
|
||||
classStartDate: formatClassStartDate(classDoc?.startDate || sortedSessions[0]?.day),
|
||||
classDays: formatClassDays(sortedSessions),
|
||||
courseTime: formatCourseTime(sessionForTime?.startTime, sessionForTime?.endTime)
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
formatCourseTime,
|
||||
formatClassDays,
|
||||
formatClassStartDate,
|
||||
buildClassScheduleContext
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
'use strict';
|
||||
|
||||
const { describe, it } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const {
|
||||
formatCourseTime,
|
||||
formatClassDays,
|
||||
formatClassStartDate,
|
||||
buildClassScheduleContext
|
||||
} = require('./classSchedule');
|
||||
|
||||
describe('class schedule SMS helpers', () => {
|
||||
it('formats course time as an hour range', () => {
|
||||
assert.equal(formatCourseTime('19:00', '21:00'), 'از 19 تا 21');
|
||||
assert.equal(formatCourseTime('19:30', '21:00'), 'از 19:30 تا 21');
|
||||
});
|
||||
|
||||
it('joins class weekdays in Persian week order', () => {
|
||||
const atNoon = (year, month, day) => new Date(year, month - 1, day, 12, 0, 0);
|
||||
const monday = atNoon(2026, 8, 17);
|
||||
const saturday = atNoon(2026, 8, 15);
|
||||
const wednesday = atNoon(2026, 8, 19);
|
||||
|
||||
assert.equal(
|
||||
formatClassDays([
|
||||
{ day: monday },
|
||||
{ day: saturday },
|
||||
{ day: wednesday },
|
||||
{ day: saturday }
|
||||
]),
|
||||
'شنبه، دوشنبه و چهارشنبه'
|
||||
);
|
||||
});
|
||||
|
||||
it('builds start date, days, and course time from class and sessions', () => {
|
||||
const atNoon = (year, month, day) => new Date(year, month - 1, day, 12, 0, 0);
|
||||
const context = buildClassScheduleContext(
|
||||
{ startDate: atNoon(2026, 8, 15) },
|
||||
[
|
||||
{ day: atNoon(2026, 8, 15), startTime: '19:00', endTime: '21:00' },
|
||||
{ day: atNoon(2026, 8, 17), startTime: '19:00', endTime: '21:00' }
|
||||
]
|
||||
);
|
||||
|
||||
assert.ok(context.classStartDate);
|
||||
assert.equal(context.classDays, 'شنبه و دوشنبه');
|
||||
assert.equal(context.courseTime, 'از 19 تا 21');
|
||||
});
|
||||
|
||||
it('uses the current session for course time when provided', () => {
|
||||
const context = buildClassScheduleContext(
|
||||
{ startDate: new Date('2026-08-15T00:00:00Z') },
|
||||
[{ day: new Date('2026-08-15T00:00:00Z'), startTime: '19:00', endTime: '21:00' }],
|
||||
{ startTime: '10:00', endTime: '12:00' }
|
||||
);
|
||||
|
||||
assert.equal(context.courseTime, 'از 10 تا 12');
|
||||
});
|
||||
});
|
||||
@@ -58,6 +58,9 @@ const sendClassRegisteredSms = async (receiver, className, userId = null, extraC
|
||||
fullName: fullName || 'کارآموز',
|
||||
phoneNumber: receiver,
|
||||
mobile: receiver,
|
||||
classStartDate: extraContext.classStartDate || '',
|
||||
classDays: extraContext.classDays || '',
|
||||
courseTime: extraContext.courseTime || '',
|
||||
...extraContext
|
||||
}))
|
||||
});
|
||||
@@ -85,6 +88,9 @@ const sendClassReminderSms = async (receiver, className, time, place = '', userI
|
||||
fullName: fullName || 'کارآموز',
|
||||
phoneNumber: receiver,
|
||||
mobile: receiver,
|
||||
classStartDate: extraContext.classStartDate || '',
|
||||
classDays: extraContext.classDays || '',
|
||||
courseTime: extraContext.courseTime || extraContext.time || time || '',
|
||||
...extraContext
|
||||
}))
|
||||
});
|
||||
@@ -97,11 +103,17 @@ const sendInvoiceCreatedSms = async (receiver, payload, userId = null) => {
|
||||
let fullName = '';
|
||||
let amount = '';
|
||||
let course = '';
|
||||
let classStartDate = '';
|
||||
let classDays = '';
|
||||
let courseTime = '';
|
||||
|
||||
if (typeof payload === 'object' && payload !== null) {
|
||||
fullName = payload.fullName || '';
|
||||
amount = payload.amount != null ? String(payload.amount) : '';
|
||||
course = payload.course || '';
|
||||
classStartDate = payload.classStartDate || '';
|
||||
classDays = payload.classDays || '';
|
||||
courseTime = payload.courseTime || '';
|
||||
} else {
|
||||
amount = String(payload || '');
|
||||
}
|
||||
@@ -121,7 +133,10 @@ const sendInvoiceCreatedSms = async (receiver, payload, userId = null) => {
|
||||
amount: amountLabel,
|
||||
course: courseLabel,
|
||||
phoneNumber: receiver,
|
||||
mobile: receiver
|
||||
mobile: receiver,
|
||||
classStartDate,
|
||||
classDays,
|
||||
courseTime
|
||||
}))
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user