Skip to content

feat/invoice automation notifications docs#1

Open
Adjanour wants to merge 3 commits intomasterfrom
feat/invoice-automation-notifications-docs
Open

feat/invoice automation notifications docs#1
Adjanour wants to merge 3 commits intomasterfrom
feat/invoice-automation-notifications-docs

Conversation

@Adjanour
Copy link
Copy Markdown
Contributor

  • Add invoice automation and notification system
  • Add backup and recovery runbook
  • Document architecture and operational flows

@Adjanour Adjanour marked this pull request as ready for review March 23, 2026 16:03
Copilot AI review requested due to automatic review settings March 23, 2026 16:03
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds invoice automation + email delivery, in-app operational notifications, and accompanying documentation/runbooks to support production operations and recovery.

Changes:

  • Introduces a backend-worker service to generate weekly invoices, send automatic invoice emails, and report automation health/status.
  • Adds notification persistence + API routes and a frontend notifications UI (badge + dropdown/sheet).
  • Adds operational docs (architecture, API reference, role guide, ops runbook) plus Postgres backup/restore scripts and new env knobs.

Reviewed changes

Copilot reviewed 48 out of 49 changed files in this pull request and generated 14 comments.

Show a summary per file
File Description
docs/role-guide.md New role-by-role product usage guide and operational expectations.
docs/ops-runbook.md New backup/restore and incident-response runbook for VPS operations.
docs/architecture.md New high-level architecture + flow documentation (runtime, data model, automation).
docs/api-reference.md New API reference including auth model, core resources, invoices, notifications.
deploy/restore-postgres.sh Adds scripted Postgres restore helper.
deploy/docker-compose.yml Adds backend-worker service and healthcheck to production compose.
deploy/docker-compose.local.yml Adds backend-worker service to local compose stack.
deploy/backup-postgres.sh Adds scripted Postgres backup helper with checksum generation.
deploy/backend.local.env.example Adds SMTP + invoice automation env variables for local runs.
deploy/backend.env.example Adds SMTP + invoice automation env variables for production runs.
deploy/README.md Documents worker service, backups/recovery docs, and new env knobs.
bun.lock Adds nodemailer + types to lockfile.
apps/frontend/src/styles.css Adds styles for notification trigger/badge/dropdown/sheet UI.
apps/frontend/src/pages/InvoicesPage.tsx Adds automation monitor panel and “send/retry invoice email” action + email status fields.
apps/frontend/src/lib/types.ts Adds types for invoice source/email status, automation status, and app notifications.
apps/frontend/src/lib/api.ts Adds API client methods for automation status, invoice email send, and notifications.
apps/frontend/src/components/AppLayout.tsx Adds notification polling, dropdown/sheet UI, and “mark read” flows.
apps/frontend/src/components/AppLayout.test.tsx Adds jsdom tests for notifications badge, open, read, and mark-all-read.
apps/frontend/README.md Replaces template README with repo-specific frontend overview/commands.
apps/backend/src/worker.ts Adds the invoice automation worker loop orchestrating sweeps + email delivery + status updates.
apps/backend/src/worker-health.ts Adds worker healthcheck entrypoint based on automation status evaluation.
apps/backend/src/routes/waybills.ts Adds failed-delivery notification emission hook (currently placed in assign route).
apps/backend/src/routes/shifts.ts Adds shift handover notification for incoming rider.
apps/backend/src/routes/notifications.ts Adds notifications API routes (list/read/read-all).
apps/backend/src/routes/notifications.test.ts Adds route tests for notifications endpoints.
apps/backend/src/routes/invoices.ts Refactors invoice creation into lib, adds automation status endpoint + send-email endpoint, expands invoice list fields.
apps/backend/src/lib/notifications.ts Implements notification creation, dedupe via eventKey, list/unread count, mark read APIs.
apps/backend/src/lib/mailer.ts Implements SMTP invoice email sending with PDF attachment.
apps/backend/src/lib/mailer.test.ts Adds unit test for invoice email content builder.
apps/backend/src/lib/invoices.ts Extracts invoice creation/detail serialization logic and adds email-related fields.
apps/backend/src/lib/invoice-email.ts Implements send-email flow + worker sweep for pending automatic invoice emails.
apps/backend/src/lib/invoice-automation.ts Implements delivery scanning + weekly invoice generation + notifications for invoice-ready.
apps/backend/src/lib/invoice-automation.test.ts Adds unit tests for invoice window grouping logic.
apps/backend/src/lib/automation-status.ts Adds persistence + serialization for automation monitor status.
apps/backend/src/lib/automation-health.ts Adds health evaluation logic (disabled/running/stale/failure).
apps/backend/src/lib/automation-health.test.ts Adds unit tests for automation health evaluation.
apps/backend/src/index.ts Registers notifications routes in the backend router.
apps/backend/src/db/schema.ts Adds invoice source/email status fields, notifications table, automation job statuses table, and new enums/indexes.
apps/backend/src/config.ts Adds SMTP + invoice automation configuration parsing and mail config assertion.
apps/backend/package.json Updates build output to include worker entrypoints + adds nodemailer deps.
apps/backend/drizzle/meta/_journal.json Adds migration journal entries for new schema changes.
apps/backend/drizzle/meta/0002_snapshot.json Adds schema snapshot including new invoice fields (and related enums).
apps/backend/drizzle/0004_broad_joshua_kane.sql Migration to create automation_job_statuses table + index.
apps/backend/drizzle/0003_gigantic_kylun.sql Migration to create notifications table + enum + indexes.
apps/backend/drizzle/0002_fuzzy_silhouette.sql Migration to add invoice source/email status fields + unique billing window index.
apps/backend/README.md Replaces template README with repo-specific backend overview/commands.
README.md Adds repo-level README describing system, architecture, and common commands.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +93 to +106
const actor = await db.query.users.findFirst({
where: eq(users.active, true),
orderBy: asc(users.createdAt),
})

assert(
actor && (actor.role === 'admin' || actor.role === 'ops'),
new AppError(
500,
'automation_actor_missing',
'Create an admin or ops account, or set AUTOMATION_ACTOR_PHONE before enabling invoice automation.',
),
)

Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolveAutomationActor() falls back to the earliest active user without filtering by role, then asserts the role is admin/ops. If the first active account is a rider, invoice automation will hard-fail even if an active admin/ops exists. Change the fallback query to filter to active users with role in (admin,ops) (and keep the createdAt ordering).

Copilot uses AI. Check for mistakes.
Comment on lines +162 to +176
const existing = await findInvoiceByWindow(input.clientId, input.periodStart, input.periodEnd)
if (existing) {
if (input.onExisting === 'reuse') {
return {
created: false as const,
invoice: await getInvoiceDetail(existing.id),
}
}

throw new AppError(
409,
'invoice_exists',
'An invoice already exists for that client and billing period.',
)
}
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createInvoiceForWindow() has a race between findInvoiceByWindow(...) and the later insert. With the new worker and manual creation, concurrent callers can both miss the pre-check and then hit the unique index on (clientId, periodStart, periodEnd). For onExisting: 'reuse', handle unique-constraint conflicts by fetching and returning the existing invoice, or do the insert with an UPSERT/transactional approach so the function is safely idempotent under concurrency.

Copilot uses AI. Check for mistakes.
Comment on lines +178 to +190
Checklist:

1. Check `backend-worker` logs.
2. Confirm `INVOICE_AUTOMATION_ENABLED=true`.
3. Confirm `AUTOMATION_ACTOR_PHONE` is set to an active `admin` or `ops` user, or that at least one active `admin/ops` user exists.
4. Restart the worker:

```bash
docker compose --env-file deploy/compose.env -f deploy/docker-compose.yml restart backend-worker
```

1. Recheck the automation monitor.

Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The checklist numbering jumps from step 4 to step 1 (“Recheck the automation monitor”), which is confusing in an ops runbook. Renumber that last step to continue the sequence (or switch the whole block to unordered bullets).

Copilot uses AI. Check for mistakes.
Comment on lines +60 to +65
Primary code lives in:

- [apps/frontend/src/pages](/home/bernard/Work/way-bills/apps/frontend/src/pages)
- [apps/frontend/src/components](/home/bernard/Work/way-bills/apps/frontend/src/components)
- [apps/frontend/src/lib](/home/bernard/Work/way-bills/apps/frontend/src/lib)

Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doc uses absolute local filesystem paths in markdown links (e.g. /home/bernard/Work/way-bills/...), which will be broken for anyone reading the docs in GitHub. Convert these to repo-relative links (e.g. apps/frontend/src/pages) so they work for all readers.

Copilot uses AI. Check for mistakes.
Comment on lines +7 to +10
- [apps/frontend](/home/bernard/Work/way-bills/apps/frontend): React/Vite operations UI
- [apps/backend](/home/bernard/Work/way-bills/apps/backend): Hono API, worker, migrations, PDF generation, email delivery
- [deploy](/home/bernard/Work/way-bills/deploy): Docker, VPS, Caddy, backup, and deployment helpers
- [docs](/home/bernard/Work/way-bills/docs): product, architecture, and operations documentation
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This README uses absolute local filesystem paths in links (e.g. /home/bernard/Work/way-bills/...), which will be broken for anyone not on that machine. Please convert these to repo-relative links (e.g. /apps/frontend, /docs/architecture.md) so they render correctly on GitHub.

Copilot uses AI. Check for mistakes.
Comment on lines +265 to +276
const [summary] = await db.select({ count: count() }).from(invoices)
const now = new Date()
const dueAt =
input.dueAt ?? new Date(now.getTime() + client.paymentTermsDays * 86400000)
const subtotalCents = pricedWaybills.reduce(
(total, item) => total + item.amountCents,
0,
)

const invoiceId = crypto.randomUUID()
const invoiceNumber = createInvoiceNumber(now, Number(summary?.count ?? 0) + 1)

Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invoice numbers are generated from count(invoices)+1. Under concurrent invoice creation (now more likely with an automation worker), two inserts can compute the same sequence and violate the invoice_number unique index. Consider a database-backed sequence/serial, or retry-on-conflict generation (or using the invoice id/timestamp-based suffix) to guarantee uniqueness without relying on a non-atomic count.

Copilot uses AI. Check for mistakes.
Comment on lines +39 to +44
html: [
`<p>Hello ${salutation},</p>`,
`<p>Attached is invoice <strong>${input.invoiceNumber}</strong> for deliveries completed between <strong>${input.periodStart}</strong> and <strong>${input.periodEnd}</strong>.</p>`,
`<p>Total due: <strong>${input.subtotalLabel}</strong><br/>Payment due by: <strong>${input.dueAt}</strong></p>`,
'<p>Please contact operations if anything needs review.</p>',
].join(''),
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildInvoiceEmailContent() interpolates untrusted values (client/contact names, dates, amounts) directly into the HTML string. If any of these contain <, &, etc., the email HTML can break or be abused for HTML injection. Escape dynamic fields before inserting into the HTML body (or use a templating approach that auto-escapes).

Copilot uses AI. Check for mistakes.
Comment on lines +17 to +20
- app bootstrap: [src/main.tsx](/home/bernard/Work/way-bills/apps/frontend/src/main.tsx)
- shared shell: [src/components/AppLayout.tsx](/home/bernard/Work/way-bills/apps/frontend/src/components/AppLayout.tsx)
- main pages: [src/pages](/home/bernard/Work/way-bills/apps/frontend/src/pages)
- shared API/types: [src/lib](/home/bernard/Work/way-bills/apps/frontend/src/lib)
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These links point to an absolute local path (/home/bernard/Work/way-bills/...), which won’t resolve in the repository. Use repo-relative links (e.g. src/main.tsx, src/components/AppLayout.tsx) so the README works on GitHub.

Copilot uses AI. Check for mistakes.
Comment on lines +19 to +22
- API server: [src/index.ts](/home/bernard/Work/way-bills/apps/backend/src/index.ts)
- worker: [src/worker.ts](/home/bernard/Work/way-bills/apps/backend/src/worker.ts)
- worker health: [src/worker-health.ts](/home/bernard/Work/way-bills/apps/backend/src/worker-health.ts)
- schema: [src/db/schema.ts](/home/bernard/Work/way-bills/apps/backend/src/db/schema.ts)
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These links point to an absolute local path (/home/bernard/Work/way-bills/...), which won’t resolve in the repository. Use repo-relative links so the README works on GitHub.

Copilot uses AI. Check for mistakes.
Comment on lines +26 to +120
export async function sendInvoiceEmail(invoiceId: string) {
const detail = await getInvoiceDetail(invoiceId)

assert(detail.status !== 'void', new AppError(409, 'invoice_void', 'Voided invoices cannot be emailed.'))
assert(
detail.client.contactEmail,
new AppError(409, 'missing_client_email', 'This client does not have a billing email address yet.'),
)

await db
.update(invoices)
.set({
emailStatus: 'queued',
emailDeliveryAttempts: detail.emailDeliveryAttempts + 1,
lastEmailError: null,
})
.where(eq(invoices.id, detail.id))

try {
const pdfBytes = await buildInvoicePdf(toInvoicePdfDetail(detail))
const content = buildInvoiceEmailContent({
invoiceNumber: detail.invoiceNumber,
clientName: detail.client.name,
contactName: detail.client.contactName,
recipientEmail: detail.client.contactEmail,
periodStart: formatDateLabel(detail.periodStart.toISOString()),
periodEnd: formatDateLabel(detail.periodEnd.toISOString()),
subtotalLabel: formatMoneyLabel(detail.subtotalCents, detail.currency),
dueAt: formatDateLabel(detail.dueAt.toISOString()),
attachmentBytes: pdfBytes,
})

await sendInvoiceEmailMessage(content)

await db
.update(invoices)
.set({
emailStatus: 'sent',
emailSentAt: new Date(),
lastEmailError: null,
})
.where(eq(invoices.id, detail.id))
} catch (error) {
await db
.update(invoices)
.set({
emailStatus: 'failed',
lastEmailError: error instanceof Error ? error.message : 'Unknown email delivery failure.',
})
.where(eq(invoices.id, detail.id))

await createRoleNotifications(['admin', 'ops'], {
type: 'invoice_email_failed',
title: 'Invoice email failed',
message: `Email delivery failed for ${detail.invoiceNumber}.`,
linkPath: '/ops/invoices',
eventKey: `invoice_email_failed:${detail.id}`,
})

throw error
}

return {
invoice: serializeInvoiceDetail(await getInvoiceDetail(invoiceId)),
}
}

export async function deliverPendingAutomaticInvoiceEmails() {
const pending = await db.query.invoices.findMany({
where: and(
eq(invoices.source, 'automatic'),
eq(invoices.status, 'issued'),
inArray(invoices.emailStatus, ['not_sent', 'failed']),
),
orderBy: asc(invoices.issuedAt),
})

let sentCount = 0
let failedCount = 0

for (const invoice of pending) {
try {
await sendInvoiceEmail(invoice.id)
sentCount += 1
} catch {
failedCount += 1
}
}

return {
scanned: pending.length,
sentCount,
failedCount,
}
}
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sendInvoiceEmail() and deliverPendingAutomaticInvoiceEmails() introduce fairly involved state transitions (queued/sent/failed, attempt counting, lastEmailError, role notifications on failure). There are new unit tests for other automation helpers in this PR, but none covering these email workflows. Adding tests for success/failure cases (including void/missing email guards) would help prevent regressions.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants