2025-08-04 17:45:44 +02:00
|
|
|
import type { Override } from "TypeHelpers.type";
|
2025-08-28 18:27:07 +02:00
|
|
|
import { TRPCError } from "@trpc/server";
|
|
|
|
|
import { add, set } from "date-fns";
|
2026-03-25 17:30:33 +01:00
|
|
|
import { sql } from "kysely";
|
2026-03-04 20:45:21 +01:00
|
|
|
import type { Attachment } from "nodemailer/lib/mailer";
|
|
|
|
|
import type { AnnunciId } from "~/schemas/public/Annunci";
|
2026-04-03 11:51:55 +02:00
|
|
|
import type {
|
|
|
|
|
EventQueueEventId,
|
|
|
|
|
EventQueueUpdate,
|
|
|
|
|
NewEventQueue,
|
|
|
|
|
} from "~/schemas/public/EventQueue";
|
2026-03-04 20:45:21 +01:00
|
|
|
import type { OrdiniOrdineId } from "~/schemas/public/Ordini";
|
|
|
|
|
import type { TestiEStringheStingaId } from "~/schemas/public/TestiEStringhe";
|
2026-03-25 17:30:33 +01:00
|
|
|
import type { UsersId } from "~/schemas/public/Users";
|
2026-04-03 18:57:39 +02:00
|
|
|
import { getAnnunciSchedaData } from "~/server/controllers/annunci.controller";
|
|
|
|
|
import { getRicevutaData } from "~/server/controllers/pagamenti.controller";
|
2025-08-04 17:45:44 +02:00
|
|
|
import {
|
2025-08-28 18:27:07 +02:00
|
|
|
type MinimalSMSData,
|
|
|
|
|
SendMessage,
|
2025-08-04 17:45:44 +02:00
|
|
|
} from "~/server/controllers/skebby.controller";
|
2025-08-28 18:27:07 +02:00
|
|
|
import { db } from "~/server/db";
|
|
|
|
|
import { NewMail, type NewMailProps } from "~/server/services/mailer";
|
2026-03-04 20:45:21 +01:00
|
|
|
import {
|
2026-04-03 18:57:39 +02:00
|
|
|
convertHtmlToTypst,
|
|
|
|
|
TypstGenerate,
|
|
|
|
|
} from "~/server/services/typst.service";
|
|
|
|
|
import { getKeydbClient } from "~/utils/keydb";
|
|
|
|
|
import {
|
|
|
|
|
CONDIZIONI_CACHE_PREFIX,
|
|
|
|
|
SCHEDA_ANNUNCIO_CACHE_PREFIX,
|
|
|
|
|
} from "../services/cache.service";
|
2026-03-04 20:45:21 +01:00
|
|
|
|
|
|
|
|
type GeneratedAttachmentTypes =
|
|
|
|
|
| { type: "scheda_annuncio"; annuncioId: AnnunciId }
|
|
|
|
|
| { type: "ricevuta_cortesia"; ordineId: OrdiniOrdineId }
|
|
|
|
|
| { type: "condizioni"; stringId: TestiEStringheStingaId };
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2026-03-04 20:45:21 +01:00
|
|
|
type CustomAttachment = Attachment & { generate?: GeneratedAttachmentTypes };
|
2026-04-03 11:51:55 +02:00
|
|
|
export type Email_EventData = {
|
2025-08-28 18:27:07 +02:00
|
|
|
tipo: "email";
|
2026-03-04 20:45:21 +01:00
|
|
|
data: NewMailProps & {
|
|
|
|
|
mail: {
|
|
|
|
|
attachments?: CustomAttachment[];
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const generatePdfContent = async (params: GeneratedAttachmentTypes) => {
|
|
|
|
|
switch (params.type) {
|
2026-04-03 18:57:39 +02:00
|
|
|
case "ricevuta_cortesia": {
|
|
|
|
|
const ricData = await getRicevutaData(params.ordineId);
|
|
|
|
|
const pdf = await TypstGenerate({
|
|
|
|
|
templateId: "receipt.typ",
|
|
|
|
|
data: ricData.data,
|
|
|
|
|
});
|
|
|
|
|
return pdf.toString("base64");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case "condizioni": {
|
|
|
|
|
const keybd = getKeydbClient();
|
|
|
|
|
const cacheKey = `${CONDIZIONI_CACHE_PREFIX}${params.stringId}`;
|
|
|
|
|
if (keybd) {
|
|
|
|
|
const cached = await keybd.get(cacheKey);
|
|
|
|
|
if (cached) {
|
|
|
|
|
return cached;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const data = await db
|
|
|
|
|
.selectFrom("testi_e_stringhe")
|
|
|
|
|
.selectAll()
|
|
|
|
|
.where("stinga_id", "=", params.stringId)
|
|
|
|
|
.executeTakeFirst();
|
2026-04-13 10:28:34 +02:00
|
|
|
if (!data?.stringa_value) {
|
2026-04-03 18:57:39 +02:00
|
|
|
throw new TRPCError({
|
|
|
|
|
code: "NOT_FOUND",
|
|
|
|
|
message: "Stringa non trovata",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const stringa = convertHtmlToTypst(data.stringa_value);
|
|
|
|
|
const pdf = await TypstGenerate({
|
|
|
|
|
templateId: "testi.typ",
|
|
|
|
|
data: { stringa },
|
|
|
|
|
});
|
|
|
|
|
const pdfBase64 = pdf.toString("base64");
|
|
|
|
|
if (keybd) {
|
|
|
|
|
await keybd.set(cacheKey, pdfBase64, "EX", 3600 * 24 * 5);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return pdfBase64;
|
|
|
|
|
}
|
|
|
|
|
case "scheda_annuncio": {
|
|
|
|
|
const keybd = getKeydbClient();
|
|
|
|
|
const cacheKey = `${SCHEDA_ANNUNCIO_CACHE_PREFIX}${params.annuncioId}`;
|
|
|
|
|
if (keybd) {
|
|
|
|
|
const cached = await keybd.get(cacheKey);
|
|
|
|
|
if (cached) {
|
|
|
|
|
return cached;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const data = await getAnnunciSchedaData(params.annuncioId);
|
|
|
|
|
const pdf = await TypstGenerate({
|
|
|
|
|
templateId: "annuncio.typ",
|
|
|
|
|
data,
|
|
|
|
|
});
|
|
|
|
|
const pdfBase64 = pdf.toString("base64");
|
|
|
|
|
if (keybd) {
|
|
|
|
|
await keybd.set(cacheKey, pdfBase64, "EX", 3600 * 5);
|
|
|
|
|
}
|
|
|
|
|
return pdfBase64;
|
|
|
|
|
}
|
2026-03-04 20:45:21 +01:00
|
|
|
default:
|
|
|
|
|
throw new Error(`Unknown PDF type: `);
|
|
|
|
|
}
|
2025-08-04 17:45:44 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type SMS_EventData = {
|
2025-08-28 18:27:07 +02:00
|
|
|
tipo: "sms";
|
|
|
|
|
data: MinimalSMSData;
|
2025-08-04 17:45:44 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const canSendNotification = () => {
|
2025-08-28 18:27:07 +02:00
|
|
|
const d = new Date();
|
|
|
|
|
const hour = d.getHours();
|
|
|
|
|
return hour >= 9 && hour <= 20;
|
2025-08-04 17:45:44 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const nextTimeForNotification = () => {
|
2025-08-28 18:27:07 +02:00
|
|
|
const d = new Date();
|
|
|
|
|
const targetDay = d.getHours() < 9 ? d : add(d, { days: 1 });
|
|
|
|
|
const lock_expires = set(targetDay, {
|
|
|
|
|
hours: 9,
|
|
|
|
|
minutes: 0,
|
|
|
|
|
});
|
|
|
|
|
return lock_expires;
|
2025-08-04 17:45:44 +02:00
|
|
|
};
|
|
|
|
|
|
2026-03-05 12:32:03 +01:00
|
|
|
type EventDataTypes = Email_EventData | SMS_EventData;
|
2025-08-04 17:45:44 +02:00
|
|
|
|
|
|
|
|
type NewEventQueueProps = Override<NewEventQueue, { data: EventDataTypes }>;
|
|
|
|
|
|
|
|
|
|
export const addEventToQueue = async (data: NewEventQueueProps) => {
|
2025-08-28 18:27:07 +02:00
|
|
|
try {
|
|
|
|
|
await db.insertInto("event_queue").values(data).execute();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: "INTERNAL_SERVER_ERROR",
|
|
|
|
|
message: `Error while adding event to queue, ${(e as Error).message}`,
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-08-04 17:45:44 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getEventsFromQueue = async () => {
|
2025-08-28 18:27:07 +02:00
|
|
|
try {
|
|
|
|
|
const events = await db
|
|
|
|
|
.selectFrom("event_queue")
|
|
|
|
|
.selectAll()
|
|
|
|
|
.where("status", "=", "pending")
|
|
|
|
|
.where("event_queue.lock_expires", "<=", new Date())
|
|
|
|
|
.execute();
|
|
|
|
|
return events;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: "INTERNAL_SERVER_ERROR",
|
|
|
|
|
message: `Error while getting events from queue, ${(e as Error).message}`,
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-08-04 17:45:44 +02:00
|
|
|
};
|
|
|
|
|
|
2026-03-04 20:45:21 +01:00
|
|
|
const processEmailEvent = async (event: Email_EventData) => {
|
|
|
|
|
const { template, mail, userId } = event.data;
|
|
|
|
|
|
|
|
|
|
// Generate attachments on-demand
|
|
|
|
|
if (mail.attachments && mail.attachments.length > 0) {
|
|
|
|
|
mail.attachments = await Promise.all(
|
|
|
|
|
mail.attachments.map(async (attachment: CustomAttachment) => {
|
|
|
|
|
// If attachment has generate metadata, generate it now
|
|
|
|
|
if (attachment.generate) {
|
|
|
|
|
const content = await generatePdfContent(attachment.generate);
|
|
|
|
|
return {
|
|
|
|
|
filename: attachment.filename,
|
|
|
|
|
content,
|
|
|
|
|
encoding: attachment.encoding,
|
|
|
|
|
contentType: attachment.contentType,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
// If already has content, return as-is
|
|
|
|
|
return attachment;
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await NewMail({
|
|
|
|
|
template,
|
|
|
|
|
mail,
|
|
|
|
|
userId,
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2025-08-04 17:45:44 +02:00
|
|
|
export const processEvents = async () => {
|
2025-08-28 18:27:07 +02:00
|
|
|
const events = await getEventsFromQueue();
|
|
|
|
|
if (events.length === 0) {
|
2026-02-19 12:14:34 +01:00
|
|
|
return { message: "No events to process" };
|
2025-08-28 18:27:07 +02:00
|
|
|
}
|
|
|
|
|
for (const e of events) {
|
|
|
|
|
const data = e.data as EventDataTypes;
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
switch (data.tipo) {
|
2025-11-19 17:18:28 +01:00
|
|
|
case "email": {
|
2026-03-04 20:45:21 +01:00
|
|
|
await processEmailEvent(data);
|
2025-08-28 18:27:07 +02:00
|
|
|
break;
|
2025-11-19 17:18:28 +01:00
|
|
|
}
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
case "sms":
|
|
|
|
|
await SendMessage(data.data);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
// Mark event as processed
|
|
|
|
|
await db
|
|
|
|
|
.deleteFrom("event_queue")
|
|
|
|
|
.where("event_id", "=", e.event_id)
|
|
|
|
|
.execute();
|
|
|
|
|
}
|
2026-02-19 12:14:34 +01:00
|
|
|
return { message: `${events.length} events processed` };
|
2025-08-04 17:45:44 +02:00
|
|
|
};
|
2026-04-29 16:31:57 +02:00
|
|
|
export const getEmailQueue = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const events = await db
|
|
|
|
|
.selectFrom("event_queue")
|
|
|
|
|
.selectAll("event_queue")
|
|
|
|
|
.select((eb) => [
|
|
|
|
|
eb
|
|
|
|
|
.selectFrom("users")
|
|
|
|
|
.select("username")
|
|
|
|
|
.whereRef(
|
|
|
|
|
"users.id",
|
|
|
|
|
"=",
|
|
|
|
|
sql`(event_queue.data->'data'->>'userId')::uuid`,
|
|
|
|
|
)
|
|
|
|
|
.as("username"),
|
|
|
|
|
])
|
|
|
|
|
.where(sql`data->>'tipo'`, "=", "email")
|
|
|
|
|
.execute();
|
|
|
|
|
return events;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: "INTERNAL_SERVER_ERROR",
|
|
|
|
|
message: `Error while getting email events from queue, ${(e as Error).message}`,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-03-25 17:30:33 +01:00
|
|
|
export const userEmailQueue = async (userId: UsersId) => {
|
|
|
|
|
try {
|
|
|
|
|
const events = await db
|
|
|
|
|
.selectFrom("event_queue")
|
|
|
|
|
.selectAll()
|
|
|
|
|
.where(sql`data->>'tipo'`, "=", "email")
|
|
|
|
|
.where(sql`data->'data'->>'userId'`, "=", userId)
|
|
|
|
|
.execute();
|
|
|
|
|
return events;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: "INTERNAL_SERVER_ERROR",
|
|
|
|
|
message: `Error while getting user events from queue, ${(e as Error).message}`,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-04-03 11:51:55 +02:00
|
|
|
|
|
|
|
|
export const removeEventFromQueue = async (eventId: EventQueueEventId) => {
|
|
|
|
|
try {
|
|
|
|
|
await db
|
|
|
|
|
.deleteFrom("event_queue")
|
|
|
|
|
.where("event_id", "=", eventId)
|
|
|
|
|
.execute();
|
|
|
|
|
return true;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: "INTERNAL_SERVER_ERROR",
|
|
|
|
|
message: `Error while removing event from queue, ${(e as Error).message}`,
|
|
|
|
|
cause: e,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-04-29 16:31:57 +02:00
|
|
|
export const clearEmailQueue = async () => {
|
|
|
|
|
try {
|
|
|
|
|
await db
|
|
|
|
|
.deleteFrom("event_queue")
|
|
|
|
|
.where(sql`data->>'tipo'`, "=", "email")
|
|
|
|
|
.execute();
|
|
|
|
|
return true;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: "INTERNAL_SERVER_ERROR",
|
|
|
|
|
message: `Error while clearing email events from queue, ${(e as Error).message}`,
|
|
|
|
|
cause: e,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-04-03 11:51:55 +02:00
|
|
|
export const clearUserEmailQueue = async (userId: UsersId) => {
|
|
|
|
|
try {
|
|
|
|
|
await db
|
|
|
|
|
.deleteFrom("event_queue")
|
|
|
|
|
.where(sql`data->>'tipo'`, "=", "email")
|
|
|
|
|
.where(sql`data->'data'->>'userId'`, "=", userId)
|
|
|
|
|
.execute();
|
|
|
|
|
return true;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: "INTERNAL_SERVER_ERROR",
|
|
|
|
|
message: `Error while clearing user events from queue, ${(e as Error).message}`,
|
|
|
|
|
cause: e,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const updateEvent = async (
|
|
|
|
|
eventId: EventQueueEventId,
|
|
|
|
|
data: EventQueueUpdate,
|
|
|
|
|
) => {
|
|
|
|
|
try {
|
|
|
|
|
await db
|
|
|
|
|
.updateTable("event_queue")
|
|
|
|
|
.set(data)
|
|
|
|
|
.where("event_id", "=", eventId)
|
|
|
|
|
.execute();
|
|
|
|
|
return true;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: "INTERNAL_SERVER_ERROR",
|
|
|
|
|
message: `Error while updating event in queue, ${(e as Error).message}`,
|
|
|
|
|
cause: e,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|