feat: send statements through Gmail SMTP

Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
SJCoetz 2026-08-03 01:05:45 +02:00
parent 312f061ad7
commit b92a5b2817
2 changed files with 5 additions and 64 deletions

View file

@ -3,67 +3,20 @@
import { revalidatePath } from "next/cache";
import { z } from "zod";
import PocketBase from "pocketbase";
import { Resend } from "resend";
import { pbCreate, pbList, pbUpdate } from "@/lib/pocketbase";
import { sendEmail } from "@/lib/email";
import { getAuth } from "@/lib/auth";
import { pbId } from "@/lib/pb";
import { PB_URL } from "@/lib/pocketbase";
export type ActionResult = { ok: true } | { ok: false; error: string };
const emailSettingsSchema = z.object({
statement_from_email: z.string().email("Valid email").or(z.literal("")).optional(),
statement_cc_email: z.string().email("Valid email").or(z.literal("")).optional(),
statement_app_email: z.string().email("Valid email").or(z.literal("")).optional(),
statement_email_password: z.string().optional(),
});
function clean(obj: any) {
const out: any = {};
for (const [k, v] of Object.entries(obj)) {
if (v === "" || v === null || v === undefined) continue;
out[k] = v;
}
return out;
}
export async function updateEmailSettings(_: unknown, formData: FormData): Promise<ActionResult> {
const { token } = await getAuth();
if (!token) return { ok: false, error: "Not signed in" };
const parsed = emailSettingsSchema.safeParse({
statement_from_email: formData.get("statement_from_email") || "",
statement_cc_email: formData.get("statement_cc_email") || "",
statement_app_email: formData.get("statement_app_email") || "",
statement_email_password: formData.get("statement_email_password") || "",
});
if (!parsed.success) return { ok: false, error: parsed.error.issues[0]?.message ?? "Invalid input" };
try {
const settings = await pbList<any>("fuel_settings", { perPage: 1 }, token);
if (settings.items.length === 0) return { ok: false, error: "Settings not configured" };
await pbUpdate("fuel_settings", settings.items[0].id, clean(parsed.data), token);
} catch (err: any) {
return { ok: false, error: err?.message || "Failed to update" };
}
revalidatePath("/settings");
return { ok: true };
}
export async function sendTestEmail(to: string): Promise<ActionResult> {
if (!to) return { ok: false, error: "Recipient email required" };
const apiKey = process.env.RESEND_API_KEY;
if (!apiKey) return { ok: false, error: "RESEND_API_KEY not configured" };
const fromAddr = process.env.RESEND_FROM || "statements@rjbulk.co.za";
try {
const resend = new Resend(apiKey);
const result = await resend.emails.send({
from: fromAddr,
await sendEmail({
to,
subject: "RJ Bulk test email",
html: "<p>This is a test email from your RJ Bulk back-office. If you got this, email is working correctly.</p>",
});
if (result.error) {
return { ok: false, error: result.error.message || "Send failed" };
}
} catch (err: any) {
return { ok: false, error: err?.message || "Send failed" };
}

View file

@ -2,7 +2,7 @@
import { revalidatePath } from "next/cache";
import { z } from "zod";
import { Resend } from "resend";
import { sendEmail } from "@/lib/email";
import { pbCreate, pbList, pbUpdate } from "@/lib/pocketbase";
import { getAuth } from "@/lib/auth";
import { pbId } from "@/lib/pb";
@ -105,20 +105,12 @@ export async function sendStatement(_: unknown, formData: FormData): Promise<Act
companySettings: settingsRow,
});
// Email via Resend
const apiKey = process.env.RESEND_API_KEY;
if (!apiKey) return { ok: false, error: "RESEND_API_KEY not configured" };
const fromAddr = process.env.RESEND_FROM || settingsRow?.statement_from_email || "statements@rjbulk.co.za";
const resend = new Resend(apiKey);
const cc = settingsRow?.statement_cc_email ? [settingsRow.statement_cc_email] : undefined;
// Email via Gmail SMTP
const totalQty = notes.items.reduce((a, n) => a + n.qty_litres_supplied, 0);
const totalValue = notes.items.reduce((a, n) => a + (n.total_value || 0), 0);
const result = await resend.emails.send({
from: fromAddr,
await sendEmail({
to: parsed.data.recipient_email,
cc,
subject: `Delivery Statement — ${cust.name} (${start} to ${end})`,
html: `<p>Hi ${cust.name},</p>
<p>Please find your delivery statement attached for the period <strong>${start}</strong> to <strong>${end}</strong>.</p>
@ -134,10 +126,6 @@ Total value: <strong>R ${totalValue.toFixed(2)}</strong></p>
],
});
if (result.error) {
return { ok: false, error: result.error.message || "Email send failed" };
}
// Notes stay "Pending" — the customer pays separately (EFT / CASH / CARD).
// We don't auto-flip the status here; the owner marks notes as paid manually.