Files
accounted/lib/push/web-push.ts
T
Jakob Wennberg 0aecf5b41c Refactor to generic ERP base
Remove influencer-specific features (campaigns, TikTok, gifts, shadow ledger,
contracts, briefings) and consolidate into a clean ERP foundation with core
bookkeeping, invoicing, receipts, tax reporting, and calendar functionality.
Reorganize database migrations into a clean numbered sequence.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 15:39:13 +01:00

204 lines
4.8 KiB
TypeScript

/**
* Web Push configuration and sending utilities
*/
import webpush from 'web-push'
// Configure web-push with VAPID keys
const vapidPublicKey = process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY
const vapidPrivateKey = process.env.VAPID_PRIVATE_KEY
const vapidSubject = process.env.VAPID_SUBJECT || 'mailto:support@erp-base.se'
if (vapidPublicKey && vapidPrivateKey) {
webpush.setVapidDetails(vapidSubject, vapidPublicKey, vapidPrivateKey)
}
/**
* Push subscription keys from the browser
*/
export interface PushSubscriptionKeys {
endpoint: string
p256dh: string
auth: string
}
/**
* Notification payload
*/
export interface NotificationPayload {
title: string
body: string
icon?: string
badge?: string
tag?: string
data?: {
url?: string
type?: string
id?: string
}
actions?: Array<{
action: string
title: string
icon?: string
}>
}
/**
* Send a push notification to a subscription
*/
export async function sendPushNotification(
subscription: PushSubscriptionKeys,
payload: NotificationPayload
): Promise<{ success: boolean; error?: string }> {
if (!vapidPublicKey || !vapidPrivateKey) {
console.error('VAPID keys not configured')
return { success: false, error: 'VAPID keys not configured' }
}
const pushSubscription = {
endpoint: subscription.endpoint,
keys: {
p256dh: subscription.p256dh,
auth: subscription.auth,
},
}
try {
await webpush.sendNotification(
pushSubscription,
JSON.stringify(payload),
{
TTL: 86400, // 24 hours
urgency: 'normal',
}
)
return { success: true }
} catch (error: unknown) {
const err = error as { statusCode?: number; message?: string }
// Handle subscription gone (user unsubscribed)
if (err.statusCode === 410) {
return { success: false, error: 'subscription_gone' }
}
console.error('Push notification error:', err)
return { success: false, error: err.message || 'Unknown error' }
}
}
/**
* Send push notifications to multiple subscriptions
*/
export async function sendPushToMultiple(
subscriptions: PushSubscriptionKeys[],
payload: NotificationPayload
): Promise<{
successful: number
failed: number
goneSubscriptions: string[]
}> {
let successful = 0
let failed = 0
const goneSubscriptions: string[] = []
const results = await Promise.allSettled(
subscriptions.map((sub) => sendPushNotification(sub, payload))
)
results.forEach((result, index) => {
if (result.status === 'fulfilled' && result.value.success) {
successful++
} else {
failed++
if (result.status === 'fulfilled' && result.value.error === 'subscription_gone') {
goneSubscriptions.push(subscriptions[index].endpoint)
}
}
})
return { successful, failed, goneSubscriptions }
}
/**
* Get the public VAPID key for client-side subscription
*/
export function getVapidPublicKey(): string | null {
return vapidPublicKey || null
}
/**
* Create notification payload for tax deadline
*/
export function createTaxDeadlinePayload(
title: string,
dueDate: string,
daysUntil: number,
deadlineId: string
): NotificationPayload {
const urgencyText = daysUntil === 0 ? 'IDAG!' : daysUntil === 1 ? 'imorgon' : `om ${daysUntil} dagar`
return {
title: `📋 ${title}`,
body: `Deadline ${urgencyText} (${formatDate(dueDate)})`,
icon: '/icons/icon-192.png',
badge: '/icons/badge-72.png',
tag: `tax-deadline-${deadlineId}`,
data: {
url: '/calendar',
type: 'tax_deadline',
id: deadlineId,
},
actions: [
{ action: 'view', title: 'Visa' },
{ action: 'dismiss', title: 'Avfärda' },
],
}
}
/**
* Create notification payload for invoice reminder
*/
export function createInvoicePayload(
invoiceNumber: string,
customerName: string,
amount: number,
dueDate: string,
isOverdue: boolean,
invoiceId: string
): NotificationPayload {
const title = isOverdue
? `⚠️ Obetald faktura #${invoiceNumber}`
: `💰 Faktura #${invoiceNumber} förfaller`
const body = isOverdue
? `${customerName} - ${amount.toLocaleString('sv-SE')} kr (förföll ${formatDate(dueDate)})`
: `${customerName} - ${amount.toLocaleString('sv-SE')} kr (${formatDate(dueDate)})`
return {
title,
body,
icon: '/icons/icon-192.png',
badge: '/icons/badge-72.png',
tag: `invoice-${invoiceId}`,
data: {
url: `/invoices/${invoiceId}`,
type: isOverdue ? 'invoice_overdue' : 'invoice_due',
id: invoiceId,
},
actions: [
{ action: 'view', title: 'Visa faktura' },
],
}
}
/**
* Format date for notifications
*/
function formatDate(dateStr: string): string {
const date = new Date(dateStr)
return date.toLocaleDateString('sv-SE', {
day: 'numeric',
month: 'short',
})
}