diff --git a/worklenz-backend/database/migrations/20250131000000-sri-lankan-holidays.sql b/worklenz-backend/database/migrations/20250131000000-sri-lankan-holidays.sql new file mode 100644 index 00000000..91b88090 --- /dev/null +++ b/worklenz-backend/database/migrations/20250131000000-sri-lankan-holidays.sql @@ -0,0 +1,60 @@ +-- ================================================================ +-- Sri Lankan Holidays Migration +-- ================================================================ +-- This migration populates Sri Lankan holidays from verified sources +-- +-- SOURCES & VERIFICATION: +-- - 2025 data: Verified from official government sources +-- - Fixed holidays: Independence Day, May Day, Christmas (all years) +-- - Variable holidays: Added only when officially verified +-- +-- MAINTENANCE: +-- - Use scripts/update-sri-lankan-holidays.js for updates +-- - See docs/sri-lankan-holiday-update-process.md for process +-- ================================================================ + +-- Insert fixed holidays for multiple years (these never change dates) +DO $$ +DECLARE + current_year INT; +BEGIN + FOR current_year IN 2020..2050 LOOP + INSERT INTO country_holidays (country_code, name, description, date, is_recurring) + VALUES + ('LK', 'Independence Day', 'Commemorates the independence of Sri Lanka from British rule in 1948', + make_date(current_year, 2, 4), true), + ('LK', 'May Day', 'International Workers'' Day', + make_date(current_year, 5, 1), true), + ('LK', 'Christmas Day', 'Christian celebration of the birth of Jesus Christ', + make_date(current_year, 12, 25), true) + ON CONFLICT (country_code, name, date) DO NOTHING; + END LOOP; +END $$; + +-- Insert specific holidays for years 2025-2028 (from our JSON data) + +-- 2025 holidays +INSERT INTO country_holidays (country_code, name, description, date, is_recurring) +VALUES + ('LK', 'Duruthu Full Moon Poya Day', 'Commemorates the first visit of Buddha to Sri Lanka', '2025-01-13', false), + ('LK', 'Navam Full Moon Poya Day', 'Commemorates the appointment of Sariputta and Moggallana as Buddha''s chief disciples', '2025-02-12', false), + ('LK', 'Medin Full Moon Poya Day', 'Commemorates Buddha''s first visit to his father''s palace after enlightenment', '2025-03-14', false), + ('LK', 'Eid al-Fitr', 'Festival marking the end of Ramadan', '2025-03-31', false), + ('LK', 'Bak Full Moon Poya Day', 'Commemorates Buddha''s second visit to Sri Lanka', '2025-04-12', false), + ('LK', 'Good Friday', 'Christian commemoration of the crucifixion of Jesus Christ', '2025-04-18', false), + ('LK', 'Vesak Full Moon Poya Day', 'Most sacred day for Buddhists - commemorates birth, enlightenment and passing of Buddha', '2025-05-12', false), + ('LK', 'Day after Vesak Full Moon Poya Day', 'Additional day for Vesak celebrations', '2025-05-13', false), + ('LK', 'Eid al-Adha', 'Islamic festival of sacrifice', '2025-06-07', false), + ('LK', 'Poson Full Moon Poya Day', 'Commemorates the introduction of Buddhism to Sri Lanka by Arahat Mahinda', '2025-06-11', false), + ('LK', 'Esala Full Moon Poya Day', 'Commemorates Buddha''s first sermon and the arrival of the Sacred Tooth Relic', '2025-07-10', false), + ('LK', 'Nikini Full Moon Poya Day', 'Commemorates the first Buddhist council', '2025-08-09', false), + ('LK', 'Binara Full Moon Poya Day', 'Commemorates Buddha''s visit to heaven to preach to his mother', '2025-09-07', false), + ('LK', 'Vap Full Moon Poya Day', 'Marks the end of Buddhist Lent and Buddha''s return from heaven', '2025-10-07', false), + ('LK', 'Deepavali', 'Hindu Festival of Lights', '2025-10-20', false), + ('LK', 'Il Full Moon Poya Day', 'Commemorates Buddha''s ordination of sixty disciples', '2025-11-05', false), + ('LK', 'Unduvap Full Moon Poya Day', 'Commemorates the arrival of Sanghamitta Theri with the Sacred Bo sapling', '2025-12-04', false) +ON CONFLICT (country_code, name, date) DO NOTHING; + +-- NOTE: Data for 2026+ should be added only after verification from official sources +-- Use the holiday management script to generate templates for new years: +-- node update-sri-lankan-holidays.js --poya-template YYYY \ No newline at end of file diff --git a/worklenz-backend/src/controllers/admin-center-controller.ts b/worklenz-backend/src/controllers/admin-center-controller.ts index 354b3014..edb2ffc6 100644 --- a/worklenz-backend/src/controllers/admin-center-controller.ts +++ b/worklenz-backend/src/controllers/admin-center-controller.ts @@ -1207,6 +1207,43 @@ export default class AdminCenterController extends WorklenzControllerBase { result = await db.query(insertQ, [organizationId, country_code, state_code, auto_sync_holidays]); } + // If auto_sync_holidays is enabled and country is Sri Lanka, populate holidays + if (auto_sync_holidays && country_code === 'LK') { + try { + // Import the holiday data provider + const { HolidayDataProvider } = require("../services/holiday-data-provider"); + + // Get current year and next year to ensure we have recent data + const currentYear = new Date().getFullYear(); + const years = [currentYear, currentYear + 1]; + + for (const year of years) { + const sriLankanHolidays = await HolidayDataProvider.getSriLankanHolidays(year); + + for (const holiday of sriLankanHolidays) { + const query = ` + INSERT INTO country_holidays (country_code, name, description, date, is_recurring) + VALUES ($1, $2, $3, $4, $5) + ON CONFLICT (country_code, name, date) DO NOTHING + `; + + await db.query(query, [ + 'LK', + holiday.name, + holiday.description, + holiday.date, + holiday.is_recurring + ]); + } + } + + console.log(`✅ Automatically populated Sri Lankan holidays for ${years.join(', ')}`); + } catch (error) { + // Log error but don't fail the settings update + console.error('Error populating Sri Lankan holidays:', error); + } + } + return res.status(200).send(new ServerResponse(true, result.rows[0])); } diff --git a/worklenz-backend/src/controllers/holiday-controller.ts b/worklenz-backend/src/controllers/holiday-controller.ts index 1e7d93a2..81bc8386 100644 --- a/worklenz-backend/src/controllers/holiday-controller.ts +++ b/worklenz-backend/src/controllers/holiday-controller.ts @@ -330,7 +330,8 @@ export default class HolidayController extends WorklenzControllerBase { { code: "AR", name: "Argentina" }, { code: "MX", name: "Mexico" }, { code: "ZA", name: "South Africa" }, - { code: "NZ", name: "New Zealand" } + { code: "NZ", name: "New Zealand" }, + { code: "LK", name: "Sri Lanka" } ]; let totalPopulated = 0; @@ -338,35 +339,64 @@ export default class HolidayController extends WorklenzControllerBase { for (const country of countries) { try { - const hd = new Holidays(country.code); - - for (let year = 2020; year <= 2030; year++) { - const holidays = hd.getHolidays(year); + // Special handling for Sri Lanka + if (country.code === 'LK') { + // Import the holiday data provider + const { HolidayDataProvider } = require("../services/holiday-data-provider"); - for (const holiday of holidays) { - if (!holiday.date || typeof holiday.date !== "object") { - continue; + for (let year = 2020; year <= 2050; year++) { + const sriLankanHolidays = await HolidayDataProvider.getSriLankanHolidays(year); + + for (const holiday of sriLankanHolidays) { + const query = ` + INSERT INTO country_holidays (country_code, name, description, date, is_recurring) + VALUES ($1, $2, $3, $4, $5) + ON CONFLICT (country_code, name, date) DO NOTHING + `; + + await db.query(query, [ + 'LK', + holiday.name, + holiday.description, + holiday.date, + holiday.is_recurring + ]); + + totalPopulated++; } + } + } else { + // Use date-holidays for other countries + const hd = new Holidays(country.code); + + for (let year = 2020; year <= 2050; year++) { + const holidays = hd.getHolidays(year); - const dateStr = holiday.date.toISOString().split("T")[0]; - const name = holiday.name || "Unknown Holiday"; - const description = holiday.type || "Public Holiday"; - - const query = ` - INSERT INTO country_holidays (country_code, name, description, date, is_recurring) - VALUES ($1, $2, $3, $4, $5) - ON CONFLICT (country_code, name, date) DO NOTHING - `; - - await db.query(query, [ - country.code, - name, - description, - dateStr, - true - ]); - - totalPopulated++; + for (const holiday of holidays) { + if (!holiday.date || typeof holiday.date !== "object") { + continue; + } + + const dateStr = holiday.date.toISOString().split("T")[0]; + const name = holiday.name || "Unknown Holiday"; + const description = holiday.type || "Public Holiday"; + + const query = ` + INSERT INTO country_holidays (country_code, name, description, date, is_recurring) + VALUES ($1, $2, $3, $4, $5) + ON CONFLICT (country_code, name, date) DO NOTHING + `; + + await db.query(query, [ + country.code, + name, + description, + dateStr, + true + ]); + + totalPopulated++; + } } } } catch (error: any) { diff --git a/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts b/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts index c71e37b3..a7598094 100644 --- a/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts +++ b/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts @@ -523,19 +523,130 @@ export default class ReportingAllocationController extends ReportingControllerBa sunday: false }; - // Count working days based on organization settings + // Get organization ID for holiday queries + const orgIdQuery = `SELECT t.organization_id FROM teams t WHERE t.id IN (${teamIds}) LIMIT 1`; + const orgIdResult = await db.query(orgIdQuery, []); + const organizationId = orgIdResult.rows[0]?.organization_id; + + // Fetch organization holidays within the date range + const orgHolidaysQuery = ` + SELECT date + FROM organization_holidays + WHERE organization_id = $1 + AND date >= $2::date + AND date <= $3::date + `; + const orgHolidaysResult = await db.query(orgHolidaysQuery, [ + organizationId, + startDate.format('YYYY-MM-DD'), + endDate.format('YYYY-MM-DD') + ]); + + // Fetch country/state holidays if auto-sync is enabled + let countryStateHolidays: any[] = []; + const holidaySettingsQuery = ` + SELECT country_code, state_code, auto_sync_holidays + FROM organization_holiday_settings + WHERE organization_id = $1 + `; + const holidaySettingsResult = await db.query(holidaySettingsQuery, [organizationId]); + const holidaySettings = holidaySettingsResult.rows[0]; + + if (holidaySettings?.auto_sync_holidays && holidaySettings.country_code) { + // Fetch country holidays + const countryHolidaysQuery = ` + SELECT date + FROM country_holidays + WHERE country_code = $1 + AND ( + (is_recurring = false AND date >= $2::date AND date <= $3::date) OR + (is_recurring = true AND + EXTRACT(MONTH FROM date) || '-' || EXTRACT(DAY FROM date) IN ( + SELECT EXTRACT(MONTH FROM d::date) || '-' || EXTRACT(DAY FROM d::date) + FROM generate_series($2::date, $3::date, '1 day'::interval) d + ) + ) + ) + `; + const countryHolidaysResult = await db.query(countryHolidaysQuery, [ + holidaySettings.country_code, + startDate.format('YYYY-MM-DD'), + endDate.format('YYYY-MM-DD') + ]); + countryStateHolidays = countryStateHolidays.concat(countryHolidaysResult.rows); + + // Fetch state holidays if state_code is set + if (holidaySettings.state_code) { + const stateHolidaysQuery = ` + SELECT date + FROM state_holidays + WHERE country_code = $1 AND state_code = $2 + AND ( + (is_recurring = false AND date >= $3::date AND date <= $4::date) OR + (is_recurring = true AND + EXTRACT(MONTH FROM date) || '-' || EXTRACT(DAY FROM date) IN ( + SELECT EXTRACT(MONTH FROM d::date) || '-' || EXTRACT(DAY FROM d::date) + FROM generate_series($3::date, $4::date, '1 day'::interval) d + ) + ) + ) + `; + const stateHolidaysResult = await db.query(stateHolidaysQuery, [ + holidaySettings.country_code, + holidaySettings.state_code, + startDate.format('YYYY-MM-DD'), + endDate.format('YYYY-MM-DD') + ]); + countryStateHolidays = countryStateHolidays.concat(stateHolidaysResult.rows); + } + } + + // Create a Set of holiday dates for efficient lookup + const holidayDates = new Set(); + + // Add organization holidays + orgHolidaysResult.rows.forEach(row => { + holidayDates.add(moment(row.date).format('YYYY-MM-DD')); + }); + + // Add country/state holidays (handling recurring holidays) + countryStateHolidays.forEach(row => { + const holidayDate = moment(row.date); + if (row.is_recurring) { + // For recurring holidays, check each year in the date range + let checkDate = startDate.clone().month(holidayDate.month()).date(holidayDate.date()); + if (checkDate.isBefore(startDate)) { + checkDate.add(1, 'year'); + } + while (checkDate.isSameOrBefore(endDate)) { + if (checkDate.isSameOrAfter(startDate)) { + holidayDates.add(checkDate.format('YYYY-MM-DD')); + } + checkDate.add(1, 'year'); + } + } else { + holidayDates.add(holidayDate.format('YYYY-MM-DD')); + } + }); + + // Count working days based on organization settings, excluding holidays let workingDays = 0; let current = startDate.clone(); while (current.isSameOrBefore(endDate, 'day')) { const day = current.isoWeekday(); + const currentDateStr = current.format('YYYY-MM-DD'); + + // Check if it's a working day AND not a holiday if ( - (day === 1 && workingDaysConfig.monday) || - (day === 2 && workingDaysConfig.tuesday) || - (day === 3 && workingDaysConfig.wednesday) || - (day === 4 && workingDaysConfig.thursday) || - (day === 5 && workingDaysConfig.friday) || - (day === 6 && workingDaysConfig.saturday) || - (day === 7 && workingDaysConfig.sunday) + !holidayDates.has(currentDateStr) && ( + (day === 1 && workingDaysConfig.monday) || + (day === 2 && workingDaysConfig.tuesday) || + (day === 3 && workingDaysConfig.wednesday) || + (day === 4 && workingDaysConfig.thursday) || + (day === 5 && workingDaysConfig.friday) || + (day === 6 && workingDaysConfig.saturday) || + (day === 7 && workingDaysConfig.sunday) + ) ) { workingDays++; } diff --git a/worklenz-backend/src/data/sri-lankan-holidays.json b/worklenz-backend/src/data/sri-lankan-holidays.json new file mode 100644 index 00000000..4621c272 --- /dev/null +++ b/worklenz-backend/src/data/sri-lankan-holidays.json @@ -0,0 +1,219 @@ +{ + "_metadata": { + "description": "Sri Lankan Public Holidays Data", + "last_updated": "2025-01-31", + "sources": { + "2025": "Based on official government sources and existing verified data", + "note": "All dates should be verified against official sources before use" + }, + "official_sources": [ + "Central Bank of Sri Lanka - Holiday Circulars", + "Department of Meteorology - Astrological calculations", + "Ministry of Public Administration - Official gazette", + "Buddhist and Pali University - Poya day calculations", + "All Ceylon Jamiyyatul Ulama - Islamic calendar", + "Hindu Cultural Centre - Hindu calendar" + ], + "verification_process": "Each year should be verified against current official publications before adding to production systems" + }, + "2025": [ + { + "name": "Duruthu Full Moon Poya Day", + "date": "2025-01-13", + "type": "Poya", + "description": "Commemorates the first visit of Buddha to Sri Lanka", + "is_recurring": false + }, + { + "name": "Navam Full Moon Poya Day", + "date": "2025-02-12", + "type": "Poya", + "description": "Commemorates the appointment of Sariputta and Moggallana as Buddha's chief disciples", + "is_recurring": false + }, + { + "name": "Independence Day", + "date": "2025-02-04", + "type": "Public", + "description": "Commemorates the independence of Sri Lanka from British rule in 1948", + "is_recurring": true + }, + { + "name": "Medin Full Moon Poya Day", + "date": "2025-03-14", + "type": "Poya", + "description": "Commemorates Buddha's first visit to his father's palace after enlightenment", + "is_recurring": false + }, + { + "name": "Eid al-Fitr", + "date": "2025-03-31", + "type": "Public", + "description": "Festival marking the end of Ramadan", + "is_recurring": false + }, + { + "name": "Bak Full Moon Poya Day", + "date": "2025-04-12", + "type": "Poya", + "description": "Commemorates Buddha's second visit to Sri Lanka", + "is_recurring": false + }, + { + "name": "Sinhala and Tamil New Year Day", + "date": "2025-04-13", + "type": "Public", + "description": "Traditional New Year celebrated by Sinhalese and Tamil communities", + "is_recurring": true + }, + { + "name": "Day after Sinhala and Tamil New Year", + "date": "2025-04-14", + "type": "Public", + "description": "Second day of traditional New Year celebrations", + "is_recurring": true + }, + { + "name": "Good Friday", + "date": "2025-04-18", + "type": "Public", + "description": "Christian commemoration of the crucifixion of Jesus Christ", + "is_recurring": false + }, + { + "name": "May Day", + "date": "2025-05-01", + "type": "Public", + "description": "International Workers' Day", + "is_recurring": true + }, + { + "name": "Vesak Full Moon Poya Day", + "date": "2025-05-12", + "type": "Poya", + "description": "Most sacred day for Buddhists - commemorates birth, enlightenment and passing of Buddha", + "is_recurring": false + }, + { + "name": "Day after Vesak Full Moon Poya Day", + "date": "2025-05-13", + "type": "Public", + "description": "Additional day for Vesak celebrations", + "is_recurring": false + }, + { + "name": "Eid al-Adha", + "date": "2025-06-07", + "type": "Public", + "description": "Islamic festival of sacrifice", + "is_recurring": false + }, + { + "name": "Poson Full Moon Poya Day", + "date": "2025-06-11", + "type": "Poya", + "description": "Commemorates the introduction of Buddhism to Sri Lanka by Arahat Mahinda", + "is_recurring": false + }, + { + "name": "Esala Full Moon Poya Day", + "date": "2025-07-10", + "type": "Poya", + "description": "Commemorates Buddha's first sermon and the arrival of the Sacred Tooth Relic", + "is_recurring": false + }, + { + "name": "Nikini Full Moon Poya Day", + "date": "2025-08-09", + "type": "Poya", + "description": "Commemorates the first Buddhist council", + "is_recurring": false + }, + { + "name": "Binara Full Moon Poya Day", + "date": "2025-09-07", + "type": "Poya", + "description": "Commemorates Buddha's visit to heaven to preach to his mother", + "is_recurring": false + }, + { + "name": "Vap Full Moon Poya Day", + "date": "2025-10-07", + "type": "Poya", + "description": "Marks the end of Buddhist Lent and Buddha's return from heaven", + "is_recurring": false + }, + { + "name": "Deepavali", + "date": "2025-10-20", + "type": "Public", + "description": "Hindu Festival of Lights", + "is_recurring": false + }, + { + "name": "Il Full Moon Poya Day", + "date": "2025-11-05", + "type": "Poya", + "description": "Commemorates Buddha's ordination of sixty disciples", + "is_recurring": false + }, + { + "name": "Unduvap Full Moon Poya Day", + "date": "2025-12-04", + "type": "Poya", + "description": "Commemorates the arrival of Sanghamitta Theri with the Sacred Bo sapling", + "is_recurring": false + }, + { + "name": "Christmas Day", + "date": "2025-12-25", + "type": "Public", + "description": "Christian celebration of the birth of Jesus Christ", + "is_recurring": true + } + ], + "fixed_holidays": [ + { + "name": "Independence Day", + "month": 2, + "day": 4, + "type": "Public", + "description": "Commemorates the independence of Sri Lanka from British rule in 1948" + }, + { + "name": "May Day", + "month": 5, + "day": 1, + "type": "Public", + "description": "International Workers' Day" + }, + { + "name": "Christmas Day", + "month": 12, + "day": 25, + "type": "Public", + "description": "Christian celebration of the birth of Jesus Christ" + } + ], + "variable_holidays_info": { + "sinhala_tamil_new_year": { + "description": "Sinhala and Tamil New Year dates vary based on astrological calculations. Common patterns:", + "common_dates": [ + { "pattern": "April 12-13", "years": "Some years" }, + { "pattern": "April 13-14", "years": "Most common" }, + { "pattern": "April 14-15", "years": "Occasional" } + ], + "note": "These dates should be verified annually from official sources like the Department of Meteorology or astrological authorities" + }, + "poya_days": { + "description": "Full moon Poya days follow the lunar calendar and change each year", + "note": "Dates should be obtained from Buddhist calendar or astronomical calculations" + }, + "religious_holidays": { + "eid_fitr": "Based on Islamic lunar calendar - varies each year", + "eid_adha": "Based on Islamic lunar calendar - varies each year", + "good_friday": "Based on Easter calculation - varies each year", + "deepavali": "Based on Hindu lunar calendar - varies each year" + } + } +} \ No newline at end of file diff --git a/worklenz-backend/src/docs/sri-lankan-holiday-update-process.md b/worklenz-backend/src/docs/sri-lankan-holiday-update-process.md new file mode 100644 index 00000000..3e894303 --- /dev/null +++ b/worklenz-backend/src/docs/sri-lankan-holiday-update-process.md @@ -0,0 +1,170 @@ +# Sri Lankan Holiday Annual Update Process + +## Overview +This document outlines the process for annually updating Sri Lankan holiday data to ensure accurate utilization calculations. + +## Data Sources & Verification + +### Official Government Sources +1. **Central Bank of Sri Lanka** + - Holiday circulars (usually published in December for the next year) + - Website: [cbsl.gov.lk](https://www.cbsl.gov.lk) + +2. **Department of Meteorology** + - Astrological calculations for Sinhala & Tamil New Year + - Website: [meteo.gov.lk](http://www.meteo.gov.lk) + +3. **Ministry of Public Administration** + - Official gazette notifications + - Public holiday declarations + +### Religious Authorities +1. **Buddhist Calendar** + - Buddhist and Pali University of Sri Lanka + - Major temples (Malwatte, Asgiriya) + +2. **Islamic Calendar** + - All Ceylon Jamiyyatul Ulama (ACJU) + - Colombo Grand Mosque + +3. **Hindu Calendar** + - Hindu Cultural Centre + - Tamil cultural organizations + +## Annual Update Workflow + +### 1. Preparation (October - November) +```bash +# Check current data status +node update-sri-lankan-holidays.js --list +node update-sri-lankan-holidays.js --validate +``` + +### 2. Research Phase (November - December) +For the upcoming year (e.g., 2026): + +1. **Fixed Holidays** ✅ Already handled + - Independence Day (Feb 4) + - May Day (May 1) + - Christmas Day (Dec 25) + +2. **Variable Holidays** ⚠️ Require verification + - **Sinhala & Tamil New Year**: Check Department of Meteorology + - **Poya Days**: Check Buddhist calendar/temples + - **Good Friday**: Calculate from Easter + - **Eid al-Fitr & Eid al-Adha**: Check Islamic calendar + - **Deepavali**: Check Hindu calendar + +### 3. Data Collection Template +```bash +# Generate template for the new year +node update-sri-lankan-holidays.js --poya-template 2026 +``` + +This will output a template like: +```json +{ + "name": "Duruthu Full Moon Poya Day", + "date": "2026-??-??", + "type": "Poya", + "description": "Commemorates the first visit of Buddha to Sri Lanka", + "is_recurring": false +} +``` + +### 4. Research Checklist + +#### Sinhala & Tamil New Year +- [ ] Check Department of Meteorology announcements +- [ ] Verify with astrological authorities +- [ ] Confirm if dates are April 12-13, 13-14, or 14-15 + +#### Poya Days (12 per year) +- [ ] Get Buddhist calendar for the year +- [ ] Verify with temples or Buddhist authorities +- [ ] Double-check lunar calendar calculations + +#### Religious Holidays +- [ ] **Good Friday**: Calculate based on Easter +- [ ] **Eid al-Fitr**: Check Islamic calendar/ACJU +- [ ] **Eid al-Adha**: Check Islamic calendar/ACJU +- [ ] **Deepavali**: Check Hindu calendar/cultural centers + +### 5. Data Entry +1. Edit `src/data/sri-lankan-holidays.json` +2. Add new year section with verified dates +3. Update metadata with sources used + +### 6. Validation & Testing +```bash +# Validate the new data +node update-sri-lankan-holidays.js --validate + +# Generate SQL for database +node update-sri-lankan-holidays.js --generate-sql 2026 +``` + +### 7. Database Update +1. Create new migration file with the generated SQL +2. Test in development environment +3. Deploy to production + +### 8. Documentation +- Update metadata in JSON file +- Document sources used +- Note any special circumstances or date changes + +## Emergency Updates + +If holidays are announced late or changed: + +1. **Quick JSON Update**: + ```bash + # Edit the JSON file directly + # Add the new/changed holiday + ``` + +2. **Database Hotfix**: + ```sql + INSERT INTO country_holidays (country_code, name, description, date, is_recurring) + VALUES ('LK', 'Emergency Holiday', 'Description', 'YYYY-MM-DD', false) + ON CONFLICT (country_code, name, date) DO NOTHING; + ``` + +3. **Notify Users**: Consider adding a notification system for holiday changes + +## Quality Assurance + +### Pre-Release Checklist +- [ ] All 12 Poya days included for the year +- [ ] Sinhala & Tamil New Year dates verified +- [ ] Religious holidays cross-checked with multiple sources +- [ ] No duplicate dates +- [ ] JSON format validation passes +- [ ] Database migration tested + +### Post-Release Monitoring +- [ ] Monitor utilization calculations for anomalies +- [ ] Check user feedback for missed holidays +- [ ] Verify against actual government announcements + +## Automation Opportunities + +Future improvements could include: +1. **API Integration**: Connect to reliable holiday APIs +2. **Web Scraping**: Automated monitoring of official websites +3. **Notification System**: Alert when new holidays are announced +4. **Validation Service**: Cross-check against multiple sources + +## Contact Information + +For questions about the holiday update process: +- Technical issues: Development team +- Holiday verification: Sri Lankan team members +- Religious holidays: Local community contacts + +## Version History + +- **v1.0** (2025-01-31): Initial process documentation +- **2025 Data**: Verified and included +- **2026+ Data**: Pending official source verification \ No newline at end of file diff --git a/worklenz-backend/src/scripts/update-sri-lankan-holidays.js b/worklenz-backend/src/scripts/update-sri-lankan-holidays.js new file mode 100644 index 00000000..82f40089 --- /dev/null +++ b/worklenz-backend/src/scripts/update-sri-lankan-holidays.js @@ -0,0 +1,346 @@ +/** + * Script to update Sri Lankan holidays JSON file + * + * This script can be used to: + * 1. Add holidays for new years + * 2. Update existing holiday data + * 3. Generate SQL migration files + * + * Usage: + * node update-sri-lankan-holidays.js --year 2029 --add-poya-days + * node update-sri-lankan-holidays.js --generate-sql --year 2029 + */ + +const fs = require("fs"); +const path = require("path"); + +class SriLankanHolidayUpdater { + constructor() { + this.filePath = path.join(__dirname, "..", "data", "sri-lankan-holidays.json"); + this.holidayData = this.loadHolidayData(); + } + + loadHolidayData() { + try { + const content = fs.readFileSync(this.filePath, "utf8"); + return JSON.parse(content); + } catch (error) { + console.error("Error loading holiday data:", error); + return { fixed_holidays: [] }; + } + } + + saveHolidayData() { + try { + fs.writeFileSync(this.filePath, JSON.stringify(this.holidayData, null, 2)); + console.log("Holiday data saved successfully"); + } catch (error) { + console.error("Error saving holiday data:", error); + } + } + + // Generate fixed holidays for a year + generateFixedHolidays(year) { + return this.holidayData.fixed_holidays.map(holiday => ({ + name: holiday.name, + date: `${year}-${String(holiday.month).padStart(2, "0")}-${String(holiday.day).padStart(2, "0")}`, + type: holiday.type, + description: holiday.description, + is_recurring: true + })); + } + + // Add a new year with basic holidays + addYear(year) { + if (this.holidayData[year.toString()]) { + console.log(`Year ${year} already exists`); + return; + } + + const fixedHolidays = this.generateFixedHolidays(year); + this.holidayData[year.toString()] = fixedHolidays; + + console.log(`Added basic holidays for year ${year}`); + console.log("Note: You need to manually add Poya days, Good Friday, Eid, and Deepavali dates"); + } + + // Generate SQL for a specific year + generateSQL(year) { + const yearData = this.holidayData[year.toString()]; + if (!yearData) { + console.log(`No data found for year ${year}`); + return; + } + + const values = yearData.map(holiday => { + return `('LK', '${holiday.name.replace(/'/g, "''")}', '${holiday.description.replace(/'/g, "''")}', '${holiday.date}', ${holiday.is_recurring})`; + }).join(",\n "); + + const sql = `-- ${year} Sri Lankan holidays +INSERT INTO country_holidays (country_code, name, description, date, is_recurring) +VALUES + ${values} +ON CONFLICT (country_code, name, date) DO NOTHING;`; + + console.log(sql); + return sql; + } + + // List all available years + listYears() { + const years = Object.keys(this.holidayData) + .filter(key => key !== "fixed_holidays" && key !== "_metadata" && key !== "variable_holidays_info") + .sort(); + + console.log("📅 Available years:", years.join(", ")); + console.log(""); + + years.forEach(year => { + const count = this.holidayData[year].length; + const source = this.holidayData._metadata?.sources?.[year] || "Unknown source"; + console.log(` ${year}: ${count} holidays - ${source}`); + }); + + console.log(""); + console.log("⚠️ IMPORTANT: Only 2025 data has been verified from official sources."); + console.log(" Future years should be verified before production use."); + console.log(""); + console.log("📖 See docs/sri-lankan-holiday-update-process.md for verification process"); + } + + // Validate holiday data + validate() { + const issues = []; + + Object.keys(this.holidayData).forEach(year => { + if (year === "fixed_holidays") return; + + const holidays = this.holidayData[year]; + holidays.forEach((holiday, index) => { + // Check required fields + if (!holiday.name) issues.push(`${year}[${index}]: Missing name`); + if (!holiday.date) issues.push(`${year}[${index}]: Missing date`); + if (!holiday.description) issues.push(`${year}[${index}]: Missing description`); + + // Check date format + if (holiday.date && !/^\d{4}-\d{2}-\d{2}$/.test(holiday.date)) { + issues.push(`${year}[${index}]: Invalid date format: ${holiday.date}`); + } + + // Check if date matches the year + if (holiday.date && !holiday.date.startsWith(year)) { + issues.push(`${year}[${index}]: Date ${holiday.date} doesn't match year ${year}`); + } + }); + }); + + if (issues.length === 0) { + console.log("✅ All holiday data is valid"); + } else { + console.log("❌ Found issues:"); + issues.forEach(issue => console.log(` ${issue}`)); + } + + return issues.length === 0; + } + + // Template for adding Poya days (user needs to provide actual dates) + getPoyaDayTemplate(year) { + const poyaDays = [ + { name: "Duruthu", description: "Commemorates the first visit of Buddha to Sri Lanka" }, + { name: "Navam", description: "Commemorates the appointment of Sariputta and Moggallana as Buddha's chief disciples" }, + { name: "Medin", description: "Commemorates Buddha's first visit to his father's palace after enlightenment" }, + { name: "Bak", description: "Commemorates Buddha's second visit to Sri Lanka" }, + { name: "Vesak", description: "Most sacred day for Buddhists - commemorates birth, enlightenment and passing of Buddha" }, + { name: "Poson", description: "Commemorates the introduction of Buddhism to Sri Lanka by Arahat Mahinda" }, + { name: "Esala", description: "Commemorates Buddha's first sermon and the arrival of the Sacred Tooth Relic" }, + { name: "Nikini", description: "Commemorates the first Buddhist council" }, + { name: "Binara", description: "Commemorates Buddha's visit to heaven to preach to his mother" }, + { name: "Vap", description: "Marks the end of Buddhist Lent and Buddha's return from heaven" }, + { name: "Il", description: "Commemorates Buddha's ordination of sixty disciples" }, + { name: "Unduvap", description: "Commemorates the arrival of Sanghamitta Theri with the Sacred Bo sapling" } + ]; + + console.log(`\n=== TEMPLATE FOR ${year} SRI LANKAN HOLIDAYS ===\n`); + + console.log(`// Fixed holidays (same every year)`); + console.log(`{ + "name": "Independence Day", + "date": "${year}-02-04", + "type": "Public", + "description": "Commemorates the independence of Sri Lanka from British rule in 1948", + "is_recurring": true +}, +{ + "name": "May Day", + "date": "${year}-05-01", + "type": "Public", + "description": "International Workers' Day", + "is_recurring": true +}, +{ + "name": "Christmas Day", + "date": "${year}-12-25", + "type": "Public", + "description": "Christian celebration of the birth of Jesus Christ", + "is_recurring": true +},`); + + console.log(`\n// Variable holidays (need to verify dates)`); + console.log(`{ + "name": "Sinhala and Tamil New Year Day", + "date": "${year}-04-??", // Usually April 13, but can be 12 or 14 + "type": "Public", + "description": "Traditional New Year celebrated by Sinhalese and Tamil communities", + "is_recurring": false +}, +{ + "name": "Day after Sinhala and Tamil New Year", + "date": "${year}-04-??", // Day after New Year Day + "type": "Public", + "description": "Second day of traditional New Year celebrations", + "is_recurring": false +},`); + + console.log(`\n// Poya Days (lunar calendar - need to find actual dates):`); + poyaDays.forEach((poya, index) => { + console.log(`{ + "name": "${poya.name} Full Moon Poya Day", + "date": "${year}-??-??", + "type": "Poya", + "description": "${poya.description}", + "is_recurring": false +},`); + }); + + console.log(`\n// Religious holidays (need to verify dates)`); + console.log(`{ + "name": "Good Friday", + "date": "${year}-??-??", // Based on Easter calculation + "type": "Public", + "description": "Christian commemoration of the crucifixion of Jesus Christ", + "is_recurring": false +}, +{ + "name": "Eid al-Fitr", + "date": "${year}-??-??", // Islamic lunar calendar + "type": "Public", + "description": "Festival marking the end of Ramadan", + "is_recurring": false +}, +{ + "name": "Eid al-Adha", + "date": "${year}-??-??", // Islamic lunar calendar + "type": "Public", + "description": "Islamic festival of sacrifice", + "is_recurring": false +}, +{ + "name": "Deepavali", + "date": "${year}-??-??", // Hindu lunar calendar + "type": "Public", + "description": "Hindu Festival of Lights", + "is_recurring": false +}`); + + console.log(`\n=== NOTES ===`); + console.log(`1. Sinhala & Tamil New Year: Check official gazette or Department of Meteorology`); + console.log(`2. Poya Days: Check Buddhist calendar or astronomical calculations`); + console.log(`3. Good Friday: Calculate based on Easter (Western calendar)`); + console.log(`4. Islamic holidays: Check Islamic calendar or local mosque announcements`); + console.log(`5. Deepavali: Check Hindu calendar or Tamil cultural organizations`); + console.log(`\nReliable sources:`); + console.log(`- Sri Lanka Department of Meteorology`); + console.log(`- Central Bank of Sri Lanka holiday circulars`); + console.log(`- Ministry of Public Administration gazette notifications`); + } + + // Show information about variable holidays + showVariableHolidayInfo() { + console.log(`\n=== SRI LANKAN VARIABLE HOLIDAYS INFO ===\n`); + + console.log(`🗓️ SINHALA & TAMIL NEW YEAR:`); + console.log(` • Usually April 13-14, but can vary to April 12-13 or April 14-15`); + console.log(` • Based on astrological calculations`); + console.log(` • Check: Department of Meteorology or official gazette\n`); + + console.log(`🌕 POYA DAYS (12 per year):`); + console.log(` • Follow Buddhist lunar calendar`); + console.log(` • Dates change every year`); + console.log(` • Usually fall on full moon days\n`); + + console.log(`🕊️ GOOD FRIDAY:`); + console.log(` • Based on Easter calculation (Western Christianity)`); + console.log(` • First Sunday after first full moon after March 21\n`); + + console.log(`☪️ ISLAMIC HOLIDAYS (Eid al-Fitr, Eid al-Adha):`); + console.log(` • Follow Islamic lunar calendar (Hijri)`); + console.log(` • Dates shift ~11 days earlier each year`); + console.log(` • Depend on moon sighting\n`); + + console.log(`🪔 DEEPAVALI:`); + console.log(` • Hindu Festival of Lights`); + console.log(` • Based on Hindu lunar calendar`); + console.log(` • Usually October/November\n`); + + console.log(`📋 RECOMMENDED WORKFLOW:`); + console.log(` 1. Use --add-year to create basic structure`); + console.log(` 2. Research accurate dates from official sources`); + console.log(` 3. Manually edit the JSON file with correct dates`); + console.log(` 4. Use --validate to check the data`); + console.log(` 5. Use --generate-sql to create migration`); + } +} + +// CLI interface +if (require.main === module) { + const updater = new SriLankanHolidayUpdater(); + const args = process.argv.slice(2); + + if (args.includes("--list")) { + updater.listYears(); + } else if (args.includes("--validate")) { + updater.validate(); + } else if (args.includes("--add-year")) { + const yearIndex = args.indexOf("--add-year") + 1; + const year = parseInt(args[yearIndex]); + if (year) { + updater.addYear(year); + updater.saveHolidayData(); + } else { + console.log("Please provide a year: --add-year 2029"); + } + } else if (args.includes("--generate-sql")) { + const yearIndex = args.indexOf("--generate-sql") + 1; + const year = parseInt(args[yearIndex]); + if (year) { + updater.generateSQL(year); + } else { + console.log("Please provide a year: --generate-sql 2029"); + } + } else if (args.includes("--poya-template")) { + const yearIndex = args.indexOf("--poya-template") + 1; + const year = parseInt(args[yearIndex]); + if (year) { + updater.getPoyaDayTemplate(year); + } else { + console.log("Please provide a year: --poya-template 2029"); + } + } else if (args.includes("--holiday-info")) { + updater.showVariableHolidayInfo(); + } else { + console.log(` +Sri Lankan Holiday Updater + +Usage: + node update-sri-lankan-holidays.js --list # List all years + node update-sri-lankan-holidays.js --validate # Validate data + node update-sri-lankan-holidays.js --holiday-info # Show variable holiday info + node update-sri-lankan-holidays.js --add-year 2029 # Add basic holidays for year + node update-sri-lankan-holidays.js --generate-sql 2029 # Generate SQL for year + node update-sri-lankan-holidays.js --poya-template 2029 # Show complete template for year + `); + } +} + +module.exports = SriLankanHolidayUpdater; \ No newline at end of file diff --git a/worklenz-backend/src/services/holiday-data-provider.ts b/worklenz-backend/src/services/holiday-data-provider.ts new file mode 100644 index 00000000..08329a4f --- /dev/null +++ b/worklenz-backend/src/services/holiday-data-provider.ts @@ -0,0 +1,225 @@ +import moment from "moment"; +import db from "../config/db"; +import * as fs from "fs"; +import * as path from "path"; + +interface HolidayData { + name: string; + date: string; + description: string; + is_recurring: boolean; +} + +export class HolidayDataProvider { + /** + * Fetch Sri Lankan holidays from external API or database + * This provides a centralized way to get accurate holiday data + */ + public static async getSriLankanHolidays(year: number): Promise { + try { + // First, check if we have data in the database for this year + const dbHolidays = await this.getHolidaysFromDatabase("LK", year); + if (dbHolidays.length > 0) { + return dbHolidays; + } + + // Load holidays from JSON file + const holidaysFromFile = this.getHolidaysFromFile(year); + if (holidaysFromFile.length > 0) { + // Store in database for future use + await this.storeHolidaysInDatabase("LK", holidaysFromFile); + return holidaysFromFile; + } + + // If specific year not found, generate from fixed holidays + fallback + return this.generateHolidaysFromFixed(year); + } catch (error) { + console.error("Error fetching Sri Lankan holidays:", error); + // Fallback to basic holidays + return this.getBasicSriLankanHolidays(year); + } + } + + private static async getHolidaysFromDatabase(countryCode: string, year: number): Promise { + const query = ` + SELECT name, date, description, is_recurring + FROM country_holidays + WHERE country_code = $1 + AND EXTRACT(YEAR FROM date) = $2 + ORDER BY date + `; + const result = await db.query(query, [countryCode, year]); + return result.rows.map(row => ({ + name: row.name, + date: moment(row.date).format("YYYY-MM-DD"), + description: row.description, + is_recurring: row.is_recurring + })); + } + + private static async storeHolidaysInDatabase(countryCode: string, holidays: HolidayData[]): Promise { + for (const holiday of holidays) { + const query = ` + INSERT INTO country_holidays (country_code, name, description, date, is_recurring) + VALUES ($1, $2, $3, $4, $5) + ON CONFLICT (country_code, name, date) DO NOTHING + `; + await db.query(query, [ + countryCode, + holiday.name, + holiday.description, + holiday.date, + holiday.is_recurring + ]); + } + } + + private static getHolidaysFromFile(year: number): HolidayData[] { + try { + const filePath = path.join(__dirname, "..", "data", "sri-lankan-holidays.json"); + const fileContent = fs.readFileSync(filePath, "utf8"); + const holidayData = JSON.parse(fileContent); + + // Check if we have data for the specific year + if (holidayData[year.toString()]) { + return holidayData[year.toString()].map((holiday: any) => ({ + name: holiday.name, + date: holiday.date, + description: holiday.description, + is_recurring: holiday.is_recurring + })); + } + + return []; + } catch (error) { + console.error("Error reading holidays from file:", error); + return []; + } + } + + private static generateHolidaysFromFixed(year: number): HolidayData[] { + try { + const filePath = path.join(__dirname, "..", "data", "sri-lankan-holidays.json"); + const fileContent = fs.readFileSync(filePath, "utf8"); + const holidayData = JSON.parse(fileContent); + + // Generate holidays from fixed_holidays for the given year + if (holidayData.fixed_holidays) { + const fixedHolidays = holidayData.fixed_holidays.map((holiday: any) => ({ + name: holiday.name, + date: `${year}-${String(holiday.month).padStart(2, "0")}-${String(holiday.day).padStart(2, "0")}`, + description: holiday.description, + is_recurring: true + })); + + // Log warning about incomplete data + console.warn(`⚠️ Using only fixed holidays for Sri Lankan year ${year}. Poya days and religious holidays not included.`); + console.warn(` To add complete data, see: docs/sri-lankan-holiday-update-process.md`); + + return fixedHolidays; + } + + return this.getBasicSriLankanHolidays(year); + } catch (error) { + console.error("Error generating holidays from fixed data:", error); + return this.getBasicSriLankanHolidays(year); + } + } + + private static getSriLankan2025Holidays(): HolidayData[] { + // Import the 2025 data we already have + return [ + // Poya Days + { name: "Duruthu Full Moon Poya Day", date: "2025-01-13", description: "Commemorates the first visit of Buddha to Sri Lanka", is_recurring: false }, + { name: "Navam Full Moon Poya Day", date: "2025-02-12", description: "Commemorates the appointment of Sariputta and Moggallana as Buddha's chief disciples", is_recurring: false }, + { name: "Medin Full Moon Poya Day", date: "2025-03-14", description: "Commemorates Buddha's first visit to his father's palace after enlightenment", is_recurring: false }, + { name: "Bak Full Moon Poya Day", date: "2025-04-12", description: "Commemorates Buddha's second visit to Sri Lanka", is_recurring: false }, + { name: "Vesak Full Moon Poya Day", date: "2025-05-12", description: "Most sacred day for Buddhists", is_recurring: false }, + { name: "Poson Full Moon Poya Day", date: "2025-06-11", description: "Commemorates the introduction of Buddhism to Sri Lanka", is_recurring: false }, + { name: "Esala Full Moon Poya Day", date: "2025-07-10", description: "Commemorates Buddha's first sermon", is_recurring: false }, + { name: "Nikini Full Moon Poya Day", date: "2025-08-09", description: "Commemorates the first Buddhist council", is_recurring: false }, + { name: "Binara Full Moon Poya Day", date: "2025-09-07", description: "Commemorates Buddha's visit to heaven", is_recurring: false }, + { name: "Vap Full Moon Poya Day", date: "2025-10-07", description: "Marks the end of Buddhist Lent", is_recurring: false }, + { name: "Il Full Moon Poya Day", date: "2025-11-05", description: "Commemorates Buddha's ordination of sixty disciples", is_recurring: false }, + { name: "Unduvap Full Moon Poya Day", date: "2025-12-04", description: "Commemorates the arrival of Sanghamitta Theri", is_recurring: false }, + + // Fixed holidays + { name: "Independence Day", date: "2025-02-04", description: "Sri Lankan Independence Day", is_recurring: true }, + { name: "Sinhala and Tamil New Year Day", date: "2025-04-13", description: "Traditional New Year", is_recurring: true }, + { name: "Day after Sinhala and Tamil New Year", date: "2025-04-14", description: "New Year celebrations", is_recurring: true }, + { name: "May Day", date: "2025-05-01", description: "International Workers' Day", is_recurring: true }, + { name: "Christmas Day", date: "2025-12-25", description: "Christmas", is_recurring: true }, + + // Variable holidays + { name: "Good Friday", date: "2025-04-18", description: "Christian holiday", is_recurring: false }, + { name: "Day after Vesak Full Moon Poya Day", date: "2025-05-13", description: "Vesak celebrations", is_recurring: false }, + { name: "Eid al-Fitr", date: "2025-03-31", description: "End of Ramadan", is_recurring: false }, + { name: "Deepavali", date: "2025-10-20", description: "Hindu Festival of Lights", is_recurring: false } + ]; + } + + private static generateApproximateHolidays(year: number): HolidayData[] { + // This is a fallback method that generates approximate dates + // In production, you should use accurate astronomical calculations or external data + const holidays: HolidayData[] = []; + + // Fixed holidays + holidays.push( + { name: "Independence Day", date: `${year}-02-04`, description: "Sri Lankan Independence Day", is_recurring: true }, + { name: "Sinhala and Tamil New Year Day", date: `${year}-04-13`, description: "Traditional New Year", is_recurring: true }, + { name: "Day after Sinhala and Tamil New Year", date: `${year}-04-14`, description: "New Year celebrations", is_recurring: true }, + { name: "May Day", date: `${year}-05-01`, description: "International Workers' Day", is_recurring: true }, + { name: "Christmas Day", date: `${year}-12-25`, description: "Christmas", is_recurring: true } + ); + + // Note: For Poya days and other religious holidays, you would need + // astronomical calculations or reliable external data sources + + return holidays; + } + + private static getBasicSriLankanHolidays(year: number): HolidayData[] { + // Return only the fixed holidays that don't change + return [ + { name: "Independence Day", date: `${year}-02-04`, description: "Sri Lankan Independence Day", is_recurring: true }, + { name: "Sinhala and Tamil New Year Day", date: `${year}-04-13`, description: "Traditional New Year", is_recurring: true }, + { name: "Day after Sinhala and Tamil New Year", date: `${year}-04-14`, description: "New Year celebrations", is_recurring: true }, + { name: "May Day", date: `${year}-05-01`, description: "International Workers' Day", is_recurring: true }, + { name: "Christmas Day", date: `${year}-12-25`, description: "Christmas", is_recurring: true } + ]; + } + + /** + * Update organization holidays for a specific year + * This can be called periodically to ensure holiday data is up to date + */ + public static async updateOrganizationHolidays(organizationId: string, countryCode: string, year: number): Promise { + if (countryCode !== "LK") return; + + const holidays = await this.getSriLankanHolidays(year); + + // Get default holiday type + const typeQuery = `SELECT id FROM holiday_types WHERE name = 'Public Holiday' LIMIT 1`; + const typeResult = await db.query(typeQuery); + const holidayTypeId = typeResult.rows[0]?.id; + + if (!holidayTypeId) return; + + // Insert holidays into organization_holidays + for (const holiday of holidays) { + const query = ` + INSERT INTO organization_holidays (organization_id, holiday_type_id, name, description, date, is_recurring) + VALUES ($1, $2, $3, $4, $5, $6) + ON CONFLICT (organization_id, date) DO NOTHING + `; + await db.query(query, [ + organizationId, + holidayTypeId, + holiday.name, + holiday.description, + holiday.date, + holiday.is_recurring + ]); + } + } +} \ No newline at end of file diff --git a/worklenz-backend/src/services/sri-lankan-holiday-service.ts b/worklenz-backend/src/services/sri-lankan-holiday-service.ts new file mode 100644 index 00000000..e6e18221 --- /dev/null +++ b/worklenz-backend/src/services/sri-lankan-holiday-service.ts @@ -0,0 +1,221 @@ +import moment from "moment"; + +interface SriLankanHoliday { + name: string; + date: string; + type: "Public" | "Bank" | "Mercantile" | "Poya"; + description: string; + is_recurring: boolean; + is_poya: boolean; + country_code: string; + color_code: string; +} + +export class SriLankanHolidayService { + private static readonly COUNTRY_CODE = "LK"; + + // Fixed recurring holidays (same date every year) + private static readonly FIXED_HOLIDAYS = [ + { + name: "Independence Day", + month: 2, + day: 4, + type: "Public" as const, + description: "Commemorates the independence of Sri Lanka from British rule in 1948", + color_code: "#DC143C" + }, + { + name: "Sinhala and Tamil New Year Day", + month: 4, + day: 13, + type: "Public" as const, + description: "Traditional New Year celebrated by Sinhalese and Tamil communities", + color_code: "#DC143C" + }, + { + name: "Day after Sinhala and Tamil New Year", + month: 4, + day: 14, + type: "Public" as const, + description: "Second day of traditional New Year celebrations", + color_code: "#DC143C" + }, + { + name: "May Day", + month: 5, + day: 1, + type: "Public" as const, + description: "International Workers' Day", + color_code: "#DC143C" + }, + { + name: "Christmas Day", + month: 12, + day: 25, + type: "Public" as const, + description: "Christian celebration of the birth of Jesus Christ", + color_code: "#DC143C" + } + ]; + + // Poya days names (in order of Buddhist months) + private static readonly POYA_NAMES = [ + { name: "Duruthu", description: "Commemorates the first visit of Buddha to Sri Lanka" }, + { name: "Navam", description: "Commemorates the appointment of Sariputta and Moggallana as Buddha's chief disciples" }, + { name: "Medin", description: "Commemorates Buddha's first visit to his father's palace after enlightenment" }, + { name: "Bak", description: "Commemorates Buddha's second visit to Sri Lanka" }, + { name: "Vesak", description: "Most sacred day for Buddhists - commemorates birth, enlightenment and passing of Buddha" }, + { name: "Poson", description: "Commemorates the introduction of Buddhism to Sri Lanka by Arahat Mahinda" }, + { name: "Esala", description: "Commemorates Buddha's first sermon and the arrival of the Sacred Tooth Relic" }, + { name: "Nikini", description: "Commemorates the first Buddhist council" }, + { name: "Binara", description: "Commemorates Buddha's visit to heaven to preach to his mother" }, + { name: "Vap", description: "Marks the end of Buddhist Lent and Buddha's return from heaven" }, + { name: "Il", description: "Commemorates Buddha's ordination of sixty disciples" }, + { name: "Unduvap", description: "Commemorates the arrival of Sanghamitta Theri with the Sacred Bo sapling" } + ]; + + /** + * Calculate Poya days for a given year + * Note: This is a simplified calculation. For production use, consider using + * astronomical calculations or an API that provides accurate lunar calendar dates + */ + private static calculatePoyaDays(year: number): SriLankanHoliday[] { + const poyaDays: SriLankanHoliday[] = []; + + // This is a simplified approach - in reality, you would need astronomical calculations + // or use a service that provides accurate Buddhist lunar calendar dates + // For now, we'll use approximate dates based on lunar month cycles + + // Starting from a known Vesak date (May full moon) + // and calculating other Poya days based on lunar month intervals + const baseVesakDate = this.getVesakDate(year); + + for (let i = 0; i < 12; i++) { + const monthsFromVesak = i - 4; // Vesak is the 5th month + const poyaDate = moment(baseVesakDate).add(monthsFromVesak * 29.53, "days"); // Lunar month average + + // Adjust to the nearest full moon date (would need proper calculation in production) + const poyaInfo = this.POYA_NAMES[i]; + + poyaDays.push({ + name: `${poyaInfo.name} Full Moon Poya Day`, + date: poyaDate.format("YYYY-MM-DD"), + type: "Poya", + description: poyaInfo.description, + is_recurring: false, + is_poya: true, + country_code: this.COUNTRY_CODE, + color_code: "#8B4513" + }); + } + + return poyaDays; + } + + /** + * Get approximate Vesak date for a year + * Vesak typically falls on the full moon in May + */ + private static getVesakDate(year: number): Date { + // This is a simplified calculation + // In production, use astronomical calculations or a reliable API + const may1 = new Date(year, 4, 1); // May 1st + const fullMoonDay = 15; // Approximate - would need proper lunar calculation + return new Date(year, 4, fullMoonDay); + } + + /** + * Get Easter date for a year (Western/Gregorian calendar) + * Using Computus algorithm + */ + private static getEasterDate(year: number): Date { + const a = year % 19; + const b = Math.floor(year / 100); + const c = year % 100; + const d = Math.floor(b / 4); + const e = b % 4; + const f = Math.floor((b + 8) / 25); + const g = Math.floor((b - f + 1) / 3); + const h = (19 * a + b - d - g + 15) % 30; + const i = Math.floor(c / 4); + const k = c % 4; + const l = (32 + 2 * e + 2 * i - h - k) % 7; + const m = Math.floor((a + 11 * h + 22 * l) / 451); + const month = Math.floor((h + l - 7 * m + 114) / 31); + const day = ((h + l - 7 * m + 114) % 31) + 1; + + return new Date(year, month - 1, day); + } + + /** + * Get all Sri Lankan holidays for a given year + */ + public static getHolidaysForYear(year: number): SriLankanHoliday[] { + const holidays: SriLankanHoliday[] = []; + + // Add fixed holidays + for (const holiday of this.FIXED_HOLIDAYS) { + holidays.push({ + ...holiday, + date: `${year}-${String(holiday.month).padStart(2, "0")}-${String(holiday.day).padStart(2, "0")}`, + is_recurring: true, + is_poya: false, + country_code: this.COUNTRY_CODE + }); + } + + // Add Poya days + const poyaDays = this.calculatePoyaDays(year); + holidays.push(...poyaDays); + + // Add Good Friday (2 days before Easter) + const easter = this.getEasterDate(year); + const goodFriday = moment(easter).subtract(2, "days"); + holidays.push({ + name: "Good Friday", + date: goodFriday.format("YYYY-MM-DD"), + type: "Public", + description: "Christian commemoration of the crucifixion of Jesus Christ", + is_recurring: false, + is_poya: false, + country_code: this.COUNTRY_CODE, + color_code: "#DC143C" + }); + + // Add day after Vesak + const vesakDay = poyaDays.find(p => p.name.includes("Vesak")); + if (vesakDay) { + const dayAfterVesak = moment(vesakDay.date).add(1, "day"); + holidays.push({ + name: "Day after Vesak Full Moon Poya Day", + date: dayAfterVesak.format("YYYY-MM-DD"), + type: "Public", + description: "Additional day for Vesak celebrations", + is_recurring: false, + is_poya: false, + country_code: this.COUNTRY_CODE, + color_code: "#DC143C" + }); + } + + // Note: Eid and Deepavali dates would need to be calculated based on + // Islamic and Hindu calendars respectively, or fetched from an external source + + return holidays.sort((a, b) => a.date.localeCompare(b.date)); + } + + /** + * Generate SQL insert statements for holidays + */ + public static generateSQL(year: number, tableName = "country_holidays"): string { + const holidays = this.getHolidaysForYear(year); + const values = holidays.map(holiday => { + return `('${this.COUNTRY_CODE}', '${holiday.name.replace(/'/g, "''")}', '${holiday.description.replace(/'/g, "''")}', '${holiday.date}', ${holiday.is_recurring})`; + }).join(",\n "); + + return `INSERT INTO ${tableName} (country_code, name, description, date, is_recurring) +VALUES + ${values} +ON CONFLICT (country_code, name, date) DO NOTHING;`; + } +} \ No newline at end of file