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:
King Omar 2026-07-10 14:46:00 +10:00
parent adc1e2716c
commit ce771edec5
4 changed files with 108 additions and 18 deletions

View file

@ -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 };

View file

@ -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 ──────────────────────────────────────────────────────────

View file

@ -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> (202526 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>

View file

@ -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"