feat: implement AvvisoInterruzione email template and integrate into mailer service

This commit is contained in:
Marco Pedone 2026-03-23 12:01:29 +01:00
parent 70cc0e4536
commit 1d1051f25a
7 changed files with 186 additions and 6 deletions

View file

@ -0,0 +1,48 @@
import { Heading, Link, Row, Section, Text } from "@react-email/components";
import Base from "./base";
export type AvvisoInterruzione_NewMail = {
mailType: "avvisoInterruzione";
};
const AvvisoInterruzione = () => {
return (
<Base noreply preview="Promemoria durata servizio">
<>
<Heading className="text-center text-2xl leading-8">
Promemoria durata servizio
</Heading>
<Section className="px-5 text-left text-base md:px-12">
<Row>
<Text>Gentile Cliente,</Text>
<Text>
Il periodo per interrompere il servizio (entro 14 giorni dalla
decorrenza) sta per scadere tra pochi giorni.
</Text>
<Text>
Ricordati che <b> se non sei interessato a confermare</b> nessun
immobile puoi interrompere la ricerca:
</Text>
<Text className="font-semibold">
- Dalla tua area riservata, tramite il pulsante dedicato
</Text>
<Text className="font-semibold">
- Inviando una mail a:{" "}
<Link href="mailto:interruzione@infoalloggi.it">
interruzione@infoalloggi.it
</Link>
</Text>
<Text>
Inviando l'interruzione non dovrai pagare il saldo del servizio
acquistato.
</Text>
<Text>Restiamo a disposizione, Cordiali saluti</Text>
</Row>
</Section>
</>
</Base>
);
};
export default AvvisoInterruzione;

View file

@ -21,8 +21,8 @@ const PagamentoConferma = () => {
Puoi controllare lo stato del tuo account nella sezione dedicata. Puoi controllare lo stato del tuo account nella sezione dedicata.
</Text> </Text>
<Text> <Text>
In allegato troverai una ricevuta di cortesia, la fattura eletnica In allegato troverai una ricevuta di cortesia, la fattura
arriverà separatamente. elettronica arriverà separatamente.
</Text> </Text>
</Row> </Row>
</Section> </Section>

View file

@ -1,6 +1,7 @@
import { add, isBefore } from "date-fns"; import { add, isBefore } from "date-fns";
import { import {
Bug, Bug,
Check,
ChevronDown, ChevronDown,
ClockAlert, ClockAlert,
Cog, Cog,
@ -97,6 +98,7 @@ export const AdminServizioActions = () => {
if (!isAdmin) return null; if (!isAdmin) return null;
return ( return (
<div className="flex flex-wrap items-center gap-3"> <div className="flex flex-wrap items-center gap-3">
<AdminActivationUfficio />
<AddAnnuncio servizioId={servizio.servizio_id} userId={userId} /> <AddAnnuncio servizioId={servizio.servizio_id} userId={userId} />
<EditServizioAdmin /> <EditServizioAdmin />
</div> </div>
@ -116,7 +118,7 @@ const UserActions = () => {
if (isAdmin || canUserEdit) { if (isAdmin || canUserEdit) {
return ( return (
<> <>
<EditParametri /> <EditParametri />
<InterruzioneServizio /> <InterruzioneServizio />
</> </>
); );
@ -442,6 +444,35 @@ export const ServizioPacksInfos = () => {
); );
}; };
const AdminActivationUfficio = () => {
const { servizio } = useServizio();
const utils = api.useUtils();
const { mutate } = api.servizio.AdminAttivaServizio_Ufficio.useMutation({
onError: (error) => {
toast.error(error.message);
},
onSuccess: async () => {
toast.success("Servizio attivato con successo");
await utils.servizio.invalidate();
},
});
if (servizio.onboardOk) return null;
if (!servizio.ordini.some((o) => o.isActive)) return null;
return (
<Confirm
description="Sei sicuro di voler attivare il servizio manualmente?"
onConfirm={() => mutate({ servizioId: servizio.servizio_id })}
title="Attivazione Servizio"
>
<Button variant="destructive">
<Check />
<span>Attivazione Ufficio</span>
</Button>
</Confirm>
);
};
const AddAnnuncio = ({ const AddAnnuncio = ({
servizioId, servizioId,
}: { }: {
@ -699,7 +730,6 @@ const EditParametri = () => {
); );
}; };
const EditServizioAdmin = () => { const EditServizioAdmin = () => {
const { servizioId, userId, servizio } = useServizio(); const { servizioId, userId, servizio } = useServizio();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);

View file

@ -17,6 +17,7 @@ import {
} from "~/server/api/trpc"; } from "~/server/api/trpc";
import { getPacksPerServizio } from "~/server/controllers/pagamenti.controller"; import { getPacksPerServizio } from "~/server/controllers/pagamenti.controller";
import { import {
AdminAttivaServizio_Ufficio,
addServizioAnnunci, addServizioAnnunci,
adminUpdateConfermaStatus, adminUpdateConfermaStatus,
attivaServizio_SaltaPagamentoAcconto, attivaServizio_SaltaPagamentoAcconto,
@ -275,6 +276,15 @@ export const servizioRouter = createTRPCRouter({
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
return await attivaServizio_SaltaPagamentoAcconto(input.ordineId); return await attivaServizio_SaltaPagamentoAcconto(input.ordineId);
}), }),
AdminAttivaServizio_Ufficio: protectedProcedure
.input(
z.object({
servizioId: zServizioId,
}),
)
.mutation(async ({ input }) => {
return await AdminAttivaServizio_Ufficio(input.servizioId);
}),
removeAnnuncioServizio: protectedProcedure removeAnnuncioServizio: protectedProcedure
.input( .input(

View file

@ -257,6 +257,22 @@ export const attivaServizio_SaltaPagamentoAcconto = async (
}, },
userId: ordine.userId, userId: ordine.userId,
}); });
await addEventToQueue({
data: {
tipo: "email",
data: {
template: {
mailType: "avvisoInterruzione",
},
mail: {
subject: "Avviso Interruzione",
to: ordine.email,
},
userId: ordine.userId,
},
},
lock_expires: add(new Date(), { days: 10 }),
});
return true; return true;
}); });
} catch (e) { } catch (e) {
@ -267,6 +283,57 @@ export const attivaServizio_SaltaPagamentoAcconto = async (
} }
}; };
export const AdminAttivaServizio_Ufficio = async (
servizioId: ServizioServizioId,
) => {
try {
return await db.transaction().execute(async (tx) => {
const data = await tx
.selectFrom("servizio")
.select("servizio.user_id")
.innerJoin("users", "users.id", "servizio.user_id")
.select("users.email")
.where("servizio_id", "=", servizioId)
.executeTakeFirstOrThrow(
() => new Error(`Servizio not found - servizioId: ${servizioId}`),
);
const today = new Date();
await tx
.updateTable("servizio")
.set({
decorrenza: today,
isOkAcconto: true,
onboardOk: true,
})
.where("servizio_id", "=", servizioId)
.execute();
await addEventToQueue({
data: {
tipo: "email",
data: {
template: {
mailType: "avvisoInterruzione",
},
mail: {
subject: "Avviso Interruzione",
to: data.email,
},
userId: data.user_id,
},
},
lock_expires: add(today, { days: 10 }),
});
return true;
});
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Error activating servizio from office: ${(e as Error).message}`,
});
}
};
type DocRef = { type DocRef = {
storageId: string; storageId: string;
ref: UsersStorageUserStorageId | null; ref: UsersStorageUserStorageId | null;

View file

@ -1,5 +1,5 @@
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { format } from "date-fns"; import { add, format } from "date-fns";
import { env } from "~/env"; import { env } from "~/env";
import stripe from "~/lib/stripe"; import stripe from "~/lib/stripe";
import OrderTypeEnum from "~/schemas/public/OrderTypeEnum"; import OrderTypeEnum from "~/schemas/public/OrderTypeEnum";
@ -154,6 +154,23 @@ export const whIntentSucceededHandler = async ({
}, },
lock_expires, lock_expires,
}); });
await addEventToQueue({
data: {
tipo: "email",
data: {
template: {
mailType: "avvisoInterruzione",
},
mail: {
subject: "Avviso Interruzione",
to: utente.email,
},
userId: utente.id,
},
},
lock_expires: add(new Date(), { days: 10 }),
});
//SERVIZIO ATTIVATO //SERVIZIO ATTIVATO
break; break;

View file

@ -1,5 +1,7 @@
import { render } from "@react-email/render"; import { render } from "@react-email/render";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import type { AvvisoInterruzione_NewMail } from "emails/avviso-interruzione";
import AvvisoInterruzione from "emails/avviso-interruzione";
import ContattoAdminEmail, { import ContattoAdminEmail, {
type ContattoAdmin_NewMail, type ContattoAdmin_NewMail,
} from "emails/contatto"; } from "emails/contatto";
@ -91,7 +93,8 @@ export type MailsTemplates =
| Generic_NewMail | Generic_NewMail
| SchedaContatto_NewMail | SchedaContatto_NewMail
| Invito_NewMail | Invito_NewMail
| ServizioAttivato_NewMail; | ServizioAttivato_NewMail
| AvvisoInterruzione_NewMail;
export const genMail = async ({ ...mail }: MailsTemplates) => { export const genMail = async ({ ...mail }: MailsTemplates) => {
let mailComponent: JSX.Element | null = null; let mailComponent: JSX.Element | null = null;
@ -179,6 +182,11 @@ export const genMail = async ({ ...mail }: MailsTemplates) => {
mailComponent = ServizioAttivato(); mailComponent = ServizioAttivato();
title = "Servizio Attivato"; title = "Servizio Attivato";
break; break;
case "avvisoInterruzione":
//TODO da fare
mailComponent = AvvisoInterruzione();
title = "Avviso Interruzione";
break;
default: default:
throw new TRPCError({ throw new TRPCError({
code: "INTERNAL_SERVER_ERROR", code: "INTERNAL_SERVER_ERROR",