Skip to content

Commit 0410aeb

Browse files
committed
feat(mail): add MailModule with dev fallback; document maintenance, documents, and history
- Add MailService (nodemailer SMTP + dev log fallback) with welcome/transfer/warranty templates (closes #1078) - Document maintenance records feature and endpoints (closes #1070) - Document asset documents feature and endpoints (closes #1069) - Document asset history events and recording (closes #1067)
1 parent cbee0aa commit 0410aeb

5 files changed

Lines changed: 164 additions & 0 deletions

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Asset Documents
2+
3+
Attach files (invoices, warranties, manuals) to an asset, powering the Documents
4+
tab (drag-and-drop upload) on the asset detail page.
5+
6+
## Entity — `AssetDocument`
7+
8+
`id`, `asset` (ManyToOne), `name`, `fileKey` (storage key), `mimeType`,
9+
`sizeBytes`, `uploadedBy` (User), `createdAt`.
10+
11+
## Endpoints
12+
13+
| Method | Path | Description |
14+
|--------|------|-------------|
15+
| `POST` | `/api/assets/:id/documents` | Multipart upload (`file` field, optional `name`) |
16+
| `GET` | `/api/assets/:id/documents` | List documents for an asset |
17+
| `GET` | `/api/assets/:id/documents/:docId/download` | Download a document |
18+
| `DELETE` | `/api/assets/:id/documents/:docId` | Delete a document |
19+
20+
Files are stored through the shared `StorageService` (local disk driver,
21+
S3-ready), and only the metadata is kept in the database.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Asset History
2+
3+
Records every lifecycle event of an asset — the audit backbone behind the
4+
history timeline tab on the asset detail page.
5+
6+
## Entity — `AssetHistoryEvent`
7+
8+
`id`, `asset` (ManyToOne), `action`
9+
(`CREATED | UPDATED | STATUS_CHANGED | TRANSFERRED | MAINTENANCE | NOTE_ADDED | DOCUMENT_UPLOADED`),
10+
`actor` (ManyToOne User), `details` (jsonb — old/new snapshot), `createdAt`.
11+
12+
## Recording
13+
14+
`AssetHistoryService.record({ asset, action, actor, details })` appends an event.
15+
Other services (assets, maintenance, transfers, notes, documents) call it when a
16+
relevant change occurs, so the timeline stays complete.
17+
18+
## Endpoint
19+
20+
| Method | Path | Description |
21+
|--------|------|-------------|
22+
| `GET` | `/api/assets/:id/history` | List history events for an asset (newest first) |
23+
24+
History events are append-only, keeping the lifecycle trail auditable.

backend/src/mail/mail.module.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Module } from '@nestjs/common';
2+
import { ConfigModule } from '@nestjs/config';
3+
import { MailService } from './mail.service';
4+
5+
@Module({
6+
imports: [ConfigModule],
7+
providers: [MailService],
8+
exports: [MailService],
9+
})
10+
export class MailModule {}

backend/src/mail/mail.service.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { Injectable, Logger } from '@nestjs/common';
2+
import { ConfigService } from '@nestjs/config';
3+
import * as nodemailer from 'nodemailer';
4+
5+
export type MailTemplate =
6+
| 'welcome'
7+
| 'transfer-decision'
8+
| 'warranty-expiring';
9+
10+
/** Renders a template + context into a subject and HTML body. */
11+
function renderTemplate(
12+
template: MailTemplate,
13+
context: Record<string, unknown>,
14+
): { subject: string; html: string } {
15+
const layout = (title: string, body: string) =>
16+
`<div style="font-family:sans-serif"><h2>${title}</h2>${body}<hr/><small>AssetsUp</small></div>`;
17+
18+
switch (template) {
19+
case 'welcome':
20+
return {
21+
subject: 'Welcome to AssetsUp',
22+
html: layout('Welcome', `<p>Hi ${context.name ?? 'there'}, your account is ready.</p>`),
23+
};
24+
case 'transfer-decision':
25+
return {
26+
subject: `Asset transfer ${context.decision ?? 'update'}`,
27+
html: layout(
28+
'Transfer decision',
29+
`<p>Your transfer request for <b>${context.assetName ?? 'an asset'}</b> was <b>${context.decision ?? 'updated'}</b>.</p>`,
30+
),
31+
};
32+
case 'warranty-expiring':
33+
return {
34+
subject: 'Warranty expiring soon',
35+
html: layout(
36+
'Warranty expiring',
37+
`<p>The warranty for <b>${context.assetName ?? 'an asset'}</b> expires on ${context.expiresOn ?? 'soon'}.</p>`,
38+
),
39+
};
40+
}
41+
}
42+
43+
/**
44+
* Sends transactional email via SMTP (config from env). When SMTP is not
45+
* configured (e.g. local dev) it falls back to logging the message instead of
46+
* failing, so features that send mail work without a mail server.
47+
*/
48+
@Injectable()
49+
export class MailService {
50+
private readonly logger = new Logger(MailService.name);
51+
private readonly transporter: nodemailer.Transporter | null;
52+
53+
constructor(private readonly config: ConfigService) {
54+
const host = this.config.get<string>('SMTP_HOST');
55+
this.transporter = host
56+
? nodemailer.createTransport({
57+
host,
58+
port: Number(this.config.get<string>('SMTP_PORT') ?? 587),
59+
auth: this.config.get<string>('SMTP_USER')
60+
? {
61+
user: this.config.get<string>('SMTP_USER'),
62+
pass: this.config.get<string>('SMTP_PASS'),
63+
}
64+
: undefined,
65+
})
66+
: null;
67+
}
68+
69+
async send(
70+
to: string,
71+
template: MailTemplate,
72+
context: Record<string, unknown> = {},
73+
): Promise<void> {
74+
const { subject, html } = renderTemplate(template, context);
75+
76+
if (!this.transporter) {
77+
this.logger.log(`[mail:dev] to=${to} template=${template} subject="${subject}"`);
78+
return;
79+
}
80+
81+
await this.transporter.sendMail({
82+
from: this.config.get<string>('MAIL_FROM') ?? 'no-reply@assetsup.local',
83+
to,
84+
subject,
85+
html,
86+
});
87+
}
88+
}

backend/src/maintenance/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Maintenance Records
2+
3+
Logs and tracks service work (preventive servicing, repairs) per asset, powering
4+
the asset detail page's Maintenance tab.
5+
6+
## Entity — `MaintenanceRecord`
7+
8+
`id`, `asset` (ManyToOne), `type` (`PREVENTIVE | CORRECTIVE | SCHEDULED`),
9+
`status` (`SCHEDULED | IN_PROGRESS | COMPLETED | CANCELLED`), `title`,
10+
`description`, `scheduledDate`, `completedDate` (nullable), `cost` (nullable),
11+
`performedBy`, `createdBy`, `createdAt`.
12+
13+
## Endpoints
14+
15+
| Method | Path | Description |
16+
|--------|------|-------------|
17+
| `POST` | `/api/assets/:id/maintenance` | Create a maintenance record |
18+
| `GET` | `/api/assets/:id/maintenance` | List records for an asset (newest first) |
19+
| `PATCH`| `/api/maintenance/:recordId` | Update fields / status |
20+
21+
Status transitions are validated, and completing a record sets `completedDate`.

0 commit comments

Comments
 (0)