Add daily document expiry cron + update VAC fees to 2025-26
Document expiry notifications (NEW): - Cloudflare Cron Trigger: runs daily at 9am AEST (23:00 UTC) - Queries ALL users' documents expiring within 30 days - Sends HTML email to Cgabijendra@gmail.com grouped by urgency: 🔴 Already expired / 🟠 Within 7 days / 🟡 Within 30 days - Includes client name, email, phone, expiry date in each row - Also logs to Google Sheet for record VAC Fees updated to 2025-26 (effective 1 July 2025): - 189/190/491/186/187/494: $4,990 / $2,495 / $1,250 - 482 TSS: $3,270 / $1,635 / $1,635 - 500 Student: $750 / $375 / $375 - 485 Graduate: $1,985 / $995 / $500 - 820/309 Partner: $9,265 / $4,635 - 600 Visitor: $195 - Added "as at 1 July 2025" disclaimer with link to verify at DHA Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
adc1e2716c
commit
ce771edec5
4 changed files with 108 additions and 18 deletions
81
src/index.js
81
src/index.js
|
|
@ -426,4 +426,83 @@ app.notFound(c => {
|
|||
return c.html(`<!DOCTYPE html><html><head><title>404 Not Found</title><meta name="viewport" content="width=device-width,initial-scale=1"></head><body style="font-family:Arial;text-align:center;padding:80px 24px;color:#1a2744"><h1 style="font-size:4rem">404</h1><p style="color:#64748b;margin:12px 0 24px">Page not found</p><a href="/" style="background:#1a5bb8;color:#fff;padding:10px 24px;border-radius:8px;text-decoration:none;font-weight:700">Go Home</a></body></html>`, 404);
|
||||
});
|
||||
|
||||
export default app;
|
||||
// ── SCHEDULED: daily document expiry notifications ───────────────────────────
|
||||
async function scheduled(event, env, ctx) {
|
||||
const today = new Date();
|
||||
const in30 = new Date(today); in30.setDate(in30.getDate() + 30);
|
||||
const in7 = new Date(today); in7.setDate(in7.getDate() + 7);
|
||||
|
||||
// Get all expiring documents with user details
|
||||
const { results } = await env.DB.prepare(`
|
||||
SELECT d.id, d.doc_label, d.expiry_date, d.user_id,
|
||||
u.full_name, u.email, u.phone
|
||||
FROM document_expiries d
|
||||
JOIN users u ON u.id = d.user_id
|
||||
WHERE d.expiry_date <= ?
|
||||
ORDER BY d.expiry_date ASC
|
||||
`).bind(in30.toISOString().split('T')[0]).all();
|
||||
|
||||
if (!results || results.length === 0) return;
|
||||
|
||||
// Group by urgency
|
||||
const expired = results.filter(d => new Date(d.expiry_date) < today);
|
||||
const urgent = results.filter(d => { const dt = new Date(d.expiry_date); return dt >= today && dt <= in7; });
|
||||
const upcoming = results.filter(d => { const dt = new Date(d.expiry_date); return dt > in7 && dt <= in30; });
|
||||
|
||||
const fmt = d => new Date(d.expiry_date).toLocaleDateString('en-AU', { day: 'numeric', month: 'short', year: 'numeric' });
|
||||
const row = d => `<tr><td style="padding:8px 12px;border-bottom:1px solid #f1f5f9">${d.doc_label}</td><td style="padding:8px 12px;border-bottom:1px solid #f1f5f9">${d.full_name}</td><td style="padding:8px 12px;border-bottom:1px solid #f1f5f9"><a href="mailto:${d.email}">${d.email}</a></td><td style="padding:8px 12px;border-bottom:1px solid #f1f5f9">${d.phone||'—'}</td><td style="padding:8px 12px;border-bottom:1px solid #f1f5f9">${fmt(d)}</td></tr>`;
|
||||
|
||||
const section = (title, color, rows) => rows.length === 0 ? '' : `
|
||||
<h3 style="color:${color};margin:20px 0 8px;font-size:1rem">${title} (${rows.length})</h3>
|
||||
<table style="width:100%;border-collapse:collapse;font-size:.88rem;margin-bottom:16px">
|
||||
<tr style="background:#f8faff"><th style="padding:8px 12px;text-align:left">Document</th><th style="padding:8px 12px;text-align:left">Client</th><th style="padding:8px 12px;text-align:left">Email</th><th style="padding:8px 12px;text-align:left">Phone</th><th style="padding:8px 12px;text-align:left">Expiry</th></tr>
|
||||
${rows.map(row).join('')}
|
||||
</table>`;
|
||||
|
||||
const html = `
|
||||
<div style="font-family:Arial,sans-serif;max-width:700px;color:#1a2744">
|
||||
<div style="background:#1a5bb8;padding:20px 24px;border-radius:8px 8px 0 0">
|
||||
<h2 style="color:#fff;margin:0;font-size:1rem">📄 Daily Document Expiry Report — Careers Gateway</h2>
|
||||
<div style="color:rgba(255,255,255,.8);font-size:.85rem;margin-top:4px">${today.toLocaleDateString('en-AU',{weekday:'long',day:'numeric',month:'long',year:'numeric'})}</div>
|
||||
</div>
|
||||
<div style="padding:20px 24px;background:#f8faff;border:1px solid #e8f0fe;border-top:none;border-radius:0 0 8px 8px">
|
||||
${section('🔴 Already Expired', '#dc2626', expired)}
|
||||
${section('🟠 Expiring within 7 days', '#d97706', urgent)}
|
||||
${section('🟡 Expiring within 30 days', '#ca8a04', upcoming)}
|
||||
<div style="margin-top:16px;padding:12px;background:#dbeafe;border-radius:6px;font-size:.85rem;color:#1d4ed8">
|
||||
${results.length} document${results.length!==1?'s':''} across ${new Set(results.map(r=>r.user_id)).size} client${new Set(results.map(r=>r.user_id)).size!==1?'s':''} require attention.
|
||||
<a href="https://careers.bored.investments/dashboard">View Dashboard →</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
await fetch('https://api.mailchannels.net/tx/v1/send', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
personalizations: [{ to: [{ email: env.CONTACT_EMAIL, name: 'Careers Gateway Team' }] }],
|
||||
from: { email: 'contact@careersgateway.com.au', name: 'Careers Gateway Portal' },
|
||||
subject: `📄 ${results.length} document${results.length!==1?'s':''} expiring — Daily Expiry Report`,
|
||||
content: [{ type: 'text/html', value: html }],
|
||||
}),
|
||||
}).catch(() => {});
|
||||
|
||||
// Also push to Google Sheet for record
|
||||
if (env.GOOGLE_SHEET_WEBHOOK) {
|
||||
await fetch(env.GOOGLE_SHEET_WEBHOOK, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
fullName: 'SYSTEM - Daily Expiry Report',
|
||||
email: env.CONTACT_EMAIL,
|
||||
phone: '',
|
||||
service: `📄 Document Expiry Alert: ${results.length} documents`,
|
||||
message: results.map(d => `${d.doc_label} | ${d.full_name} | ${d.email} | ${fmt(d)}`).join('\n'),
|
||||
source: 'cron-daily-expiry',
|
||||
submittedAt: today.toLocaleString('en-AU', { timeZone: 'Australia/Sydney' }),
|
||||
}),
|
||||
}).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
export default { fetch: app.fetch, scheduled };
|
||||
|
|
|
|||
|
|
@ -93,23 +93,25 @@ export function calcPoints(profile) {
|
|||
return { total: pts, breakdown };
|
||||
}
|
||||
|
||||
// ── VAC FEES ─────────────────────────────────────────────────────────────────
|
||||
// ── VAC FEES — updated 1 July 2025 (2025-26 financial year) ──────────────────
|
||||
// Source: Department of Home Affairs — immi.homeaffairs.gov.au/visas/fees
|
||||
// Fees increase annually on 1 July. Always verify before lodging.
|
||||
export const VAC_FEES = [
|
||||
{ subclass: '189', name: 'Skilled Independent', primary: 4770, secondary: 2385, child: 1195 },
|
||||
{ subclass: '190', name: 'Skilled Nominated', primary: 4770, secondary: 2385, child: 1195 },
|
||||
{ subclass: '491', name: 'Skilled Work Regional', primary: 4770, secondary: 2385, child: 1195 },
|
||||
{ subclass: '186', name: 'Employer Nomination (ENS)', primary: 4770, secondary: 2385, child: 1195 },
|
||||
{ subclass: '187', name: 'Regional Sponsored (RSMS)', primary: 4770, secondary: 2385, child: 1195 },
|
||||
{ subclass: '482', name: 'Temporary Skill Shortage', primary: 3115, secondary: 1560, child: 1560 },
|
||||
{ subclass: '494', name: 'Skilled Employer Sponsored Regional', primary: 4770, secondary: 2385, child: 1195 },
|
||||
{ subclass: '500', name: 'Student Visa', primary: 710, secondary: 355, child: 355 },
|
||||
{ subclass: '485', name: 'Graduate Temporary', primary: 1895, secondary: 950, child: 475 },
|
||||
{ subclass: '820', name: 'Partner (onshore)', primary: 8850, secondary: 4425, child: 0 },
|
||||
{ subclass: '801', name: 'Partner (permanent onshore)',primary: 0, secondary: 0, child: 0, note: 'No additional fee if applying with 820' },
|
||||
{ subclass: '309', name: 'Partner (offshore)', primary: 8850, secondary: 4425, child: 0 },
|
||||
{ subclass: '100', name: 'Partner (permanent offshore)',primary: 0, secondary: 0, child: 0, note: 'No additional fee if applying with 309' },
|
||||
{ subclass: '600', name: 'Visitor Visa', primary: 190, secondary: 190, child: 190 },
|
||||
{ subclass: '407', name: 'Training Visa', primary: 335, secondary: 170, child: 170 },
|
||||
{ subclass: '189', name: 'Skilled Independent', primary: 4990, secondary: 2495, child: 1250 },
|
||||
{ subclass: '190', name: 'Skilled Nominated', primary: 4990, secondary: 2495, child: 1250 },
|
||||
{ subclass: '491', name: 'Skilled Work Regional', primary: 4990, secondary: 2495, child: 1250 },
|
||||
{ subclass: '186', name: 'Employer Nomination (ENS)', primary: 4990, secondary: 2495, child: 1250 },
|
||||
{ subclass: '187', name: 'Regional Sponsored (RSMS)', primary: 4990, secondary: 2495, child: 1250 },
|
||||
{ subclass: '482', name: 'Temporary Skill Shortage (TSS)', primary: 3270, secondary: 1635, child: 1635 },
|
||||
{ subclass: '494', name: 'Skilled Employer Sponsored Regional', primary: 4990, secondary: 2495, child: 1250 },
|
||||
{ subclass: '500', name: 'Student Visa', primary: 750, secondary: 375, child: 375 },
|
||||
{ subclass: '485', name: 'Graduate Temporary', primary: 1985, secondary: 995, child: 500 },
|
||||
{ subclass: '820', name: 'Partner (onshore)', primary: 9265, secondary: 4635, child: 0, note: 'Includes 801 permanent component' },
|
||||
{ subclass: '801', name: 'Partner (permanent onshore)', primary: 0, secondary: 0, child: 0, note: 'No additional fee if applying with 820' },
|
||||
{ subclass: '309', name: 'Partner (offshore)', primary: 9265, secondary: 4635, child: 0 },
|
||||
{ subclass: '100', name: 'Partner (permanent offshore)', primary: 0, secondary: 0, child: 0, note: 'No additional fee if applying with 309' },
|
||||
{ subclass: '600', name: 'Visitor Visa', primary: 195, secondary: 195, child: 195 },
|
||||
{ subclass: '407', name: 'Training Visa', primary: 350, secondary: 175, child: 175 },
|
||||
];
|
||||
|
||||
// ── PROCESSING TIMES ──────────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -350,7 +350,12 @@ export function feesPage(user) {
|
|||
<div style="border-left:2px solid #d1d9e8"><div style="font-size:.8rem;color:#64748b">Total VAC</div><div class="fee-total" id="vac-total">—</div></div>
|
||||
</div>
|
||||
<div id="vac-note" style="font-size:.82rem;color:#64748b"></div>
|
||||
<div style="font-size:.78rem;color:#94a3b8;margin-top:8px">Figures in AUD. VAC fees are subject to change; verify at homeaffairs.gov.au before lodging.</div>
|
||||
<div style="font-size:.78rem;color:#94a3b8;margin-top:8px">
|
||||
Figures in AUD. Fees current as at <strong>1 July 2025</strong> (2025–26 financial year).
|
||||
Fees increase annually on 1 July — always
|
||||
<a href="https://immi.homeaffairs.gov.au/visas/getting-a-visa/fees-and-charges/current-visa-pricing" target="_blank" style="color:#1a5bb8">verify at homeaffairs.gov.au</a>
|
||||
before lodging.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@ main = "src/index.js"
|
|||
compatibility_date = "2024-11-01"
|
||||
compatibility_flags = ["nodejs_compat"]
|
||||
|
||||
[triggers]
|
||||
# Run daily at 9am AEST (23:00 UTC previous day)
|
||||
crons = ["0 23 * * *"]
|
||||
|
||||
[[custom_domains]]
|
||||
pattern = "careers.bored.investments"
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue