feat: add comms_enabled flag to users and update email notification logic

This commit is contained in:
Marco Pedone 2026-03-30 18:44:51 +02:00
parent 73b1997c15
commit a11fc1241c
7 changed files with 266 additions and 194 deletions

View file

@ -0,0 +1,2 @@
ALTER TABLE IF EXISTS public.users
ADD COLUMN IF NOT EXISTS comms_enabled boolean NOT NULL DEFAULT true;

View file

@ -1,10 +1,6 @@
TODOS: TODOS:
- Log email fallite e retry - Log email fallite e retry
conferma presa visione deve rimandare a servizio giusto non /ricerrca
AFTER MVP: AFTER MVP:
- TODO migrazione app router https://nextjs.org/docs/pages/building-your-application/upgrading/app-router-migration#migrating-from-pages-to-app - TODO migrazione app router https://nextjs.org/docs/pages/building-your-application/upgrading/app-router-migration#migrating-from-pages-to-app
- Sezione "La mia locazione" con i dati della locazione attuale dell'utente - Sezione "La mia locazione" con i dati della locazione attuale dell'utente

View file

@ -210,6 +210,19 @@ const ServizioPrezzo = () => {
</div> </div>
)} )}
</div> </div>
{packs.acconto.success && (
<Link
aria-label="Leggi le condizioni"
className="block w-fit"
href={`/servizio/condizioni/${packs.acconto.pack.testo_condizioni}`}
target="_blank"
>
<Button className="w-full text-sm">
<span>{t.acquisto.leggi_condizioni}</span>
<ExternalLink />
</Button>
</Link>
)}
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>

View file

@ -5,11 +5,13 @@ import { EmailAccordion } from "~/components/area-riservata/comunicazioni";
import { Confirm } from "~/components/confirm"; import { Confirm } from "~/components/confirm";
import { AreaRiservataLayoutUserView } from "~/components/Layout"; import { AreaRiservataLayoutUserView } from "~/components/Layout";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import { Label } from "~/components/ui/label";
import { import {
Popover, Popover,
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
} from "~/components/ui/popover"; } from "~/components/ui/popover";
import { Switch } from "~/components/ui/switch";
import type { NextPageWithLayout } from "~/pages/_app"; import type { NextPageWithLayout } from "~/pages/_app";
import { useEnforcedSession } from "~/providers/SessionProvider"; import { useEnforcedSession } from "~/providers/SessionProvider";
import type { UsersId } from "~/schemas/public/Users"; import type { UsersId } from "~/schemas/public/Users";
@ -74,7 +76,7 @@ const ComunicazioniUser: NextPageWithLayout<ComunicazioniUserProps> = ({
</PopoverContent> </PopoverContent>
</Popover> </Popover>
</div> </div>
<Queue userId={userId} /> <CommsSwitch userId={userId} />
<EmailAccordion isAdmin={user.isAdmin} userId={userId} /> <EmailAccordion isAdmin={user.isAdmin} userId={userId} />
</main> </main>
); );
@ -120,12 +122,35 @@ ComunicazioniUser.getLayout = function getLayout(page) {
return <AreaRiservataLayoutUserView>{page}</AreaRiservataLayoutUserView>; return <AreaRiservataLayoutUserView>{page}</AreaRiservataLayoutUserView>;
}; };
const Queue = ({ userId }: { userId: UsersId }) => { const CommsSwitch = ({ userId }: { userId: UsersId }) => {
const { data } = api.comunicazioni.getUserQueuedEmails.useQuery({ userId }); const { data, isLoading } = api.users.getUser.useQuery({ id: userId });
console.log("Queue data", data); if (isLoading || !data) {
return <div>Loading...</div>;
}
const utils = api.useUtils();
const { mutate } = api.users.editUser.useMutation({
onSuccess: async () => {
toast.success("Impostazione modificata");
await utils.users.getUser.invalidate({ id: userId });
},
onError: (error) => {
console.error(error);
toast.error("Errore durante la modifica dell'impostazione");
},
});
return ( return (
<div> <div className="flex items-center gap-2">
<h2>Eventi email in coda per user {userId}</h2> <Label htmlFor="comms">Abilita comunicazioni</Label>
<Switch
className="data-[state=checked]:bg-neutral-700"
defaultChecked={data.comms_enabled}
id="comms"
onCheckedChange={(checked) => {
mutate({ id: userId, data: { comms_enabled: checked } });
}}
/>
</div> </div>
); );
}; };

View file

@ -39,6 +39,8 @@ export default interface UsersTable {
mustChangePassword: ColumnType<boolean, boolean | undefined, boolean>; mustChangePassword: ColumnType<boolean, boolean | undefined, boolean>;
note: ColumnType<string | null, string | null, string | null>; note: ColumnType<string | null, string | null, string | null>;
comms_enabled: ColumnType<boolean, boolean | undefined, boolean>;
} }
export type Users = Selectable<UsersTable>; export type Users = Selectable<UsersTable>;

View file

@ -224,6 +224,8 @@ export const attivaServizio_SaltaPagamentoAcconto = async (
() => new Error(`Ordine not found - ordineId: ${ordineId}`), () => new Error(`Ordine not found - ordineId: ${ordineId}`),
); );
const user = await getUser({ db: tx, userId: ordine.userId });
const servizio = await tx const servizio = await tx
.updateTable("servizio") .updateTable("servizio")
.set({ .set({
@ -239,46 +241,50 @@ export const attivaServizio_SaltaPagamentoAcconto = async (
), ),
); );
await NewMail({ if (user.comms_enabled) {
template: { await NewMail({
mailType: "servizioAttivato", template: {
}, mailType: "servizioAttivato",
mail: {
subject: "Pagamento Confermato - Acconto Infoalloggi.it",
to: ordine.email,
attachments: ordine.testo_condizioni
? [
{
filename: `condizioni_contrattuali_${ordine.created_at.toISOString()}.pdf`,
content: await genPdfCondizioniBase64(
ordine.testo_condizioni as TestiEStringheStingaId,
),
encoding: "base64",
contentType: "application/pdf",
},
]
: [],
},
userId: ordine.userId,
});
await addEventToQueue({
data: {
tipo: "email",
data: {
template: {
mailType: "avvisoInterruzione",
},
mail: {
subject: "Avviso Interruzione",
to: ordine.email,
},
userId: ordine.userId,
}, },
}, mail: {
lock_expires: add(new Date(), { days: servizio.interruzioneDays - 5 }), subject: "Pagamento Confermato - Acconto Infoalloggi.it",
}); to: ordine.email,
attachments: ordine.testo_condizioni
? [
{
filename: `condizioni_contrattuali_${ordine.created_at.toISOString()}.pdf`,
content: await genPdfCondizioniBase64(
ordine.testo_condizioni as TestiEStringheStingaId,
),
encoding: "base64",
contentType: "application/pdf",
},
]
: [],
},
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: servizio.interruzioneDays - 5,
}),
});
}
return true; return true;
}); });
} catch (e) { } catch (e) {
@ -298,7 +304,7 @@ export const AdminAttivaServizio_Ufficio = async (
.selectFrom("servizio") .selectFrom("servizio")
.select(["servizio.user_id", "servizio.interruzioneDays"]) .select(["servizio.user_id", "servizio.interruzioneDays"])
.innerJoin("users", "users.id", "servizio.user_id") .innerJoin("users", "users.id", "servizio.user_id")
.select("users.email") .select(["users.email", "users.comms_enabled"])
.where("servizio_id", "=", servizioId) .where("servizio_id", "=", servizioId)
.executeTakeFirstOrThrow( .executeTakeFirstOrThrow(
() => new Error(`Servizio not found - servizioId: ${servizioId}`), () => new Error(`Servizio not found - servizioId: ${servizioId}`),
@ -314,22 +320,24 @@ export const AdminAttivaServizio_Ufficio = async (
.where("servizio_id", "=", servizioId) .where("servizio_id", "=", servizioId)
.execute(); .execute();
await addEventToQueue({ if (data.comms_enabled) {
data: { await addEventToQueue({
tipo: "email",
data: { data: {
template: { tipo: "email",
mailType: "avvisoInterruzione", data: {
template: {
mailType: "avvisoInterruzione",
},
mail: {
subject: "Avviso Interruzione",
to: data.email,
},
userId: data.user_id,
}, },
mail: {
subject: "Avviso Interruzione",
to: data.email,
},
userId: data.user_id,
}, },
}, lock_expires: add(today, { days: data.interruzioneDays - 5 }), //avviso 5 giorni prima della scadenza del periodo di ripensamento
lock_expires: add(today, { days: data.interruzioneDays - 5 }), //avviso 5 giorni prima della scadenza del periodo di ripensamento });
}); }
return true; return true;
}); });
} catch (e) { } catch (e) {
@ -779,7 +787,7 @@ export const SbloccaContatti = async ({
const user = await tx const user = await tx
.selectFrom("users") .selectFrom("users")
.select(["id", "username", "email", "telefono"]) .select(["id", "username", "email", "telefono", "comms_enabled"])
.where("id", "=", servizio.user_id) .where("id", "=", servizio.user_id)
.executeTakeFirstOrThrow(() => new Error("User not found")); .executeTakeFirstOrThrow(() => new Error("User not found"));
@ -812,6 +820,7 @@ export const SbloccaContatti = async ({
user, user,
}; };
}); });
if (!data.user.comms_enabled) return true;
const EmailProprietario: NewMailProps | null = data.annuncio.email const EmailProprietario: NewMailProps | null = data.annuncio.email
? { ? {
@ -928,17 +937,18 @@ export const interruzioneServizio = async (servizioId: ServizioServizioId) => {
// NOTIFICHE EMAIL INTERRUZIONE SERVIZIO // NOTIFICHE EMAIL INTERRUZIONE SERVIZIO
const utente = await getUser({ db, userId: servizio.user_id }); const utente = await getUser({ db, userId: servizio.user_id });
await NewMail({ if (utente.comms_enabled) {
template: { await NewMail({
mailType: "interruzione", template: {
}, mailType: "interruzione",
mail: { },
subject: `Servizio Ricerca Affitto ${servizio.tipologia} interrotto.`, mail: {
to: utente.email, subject: `Servizio Ricerca Affitto ${servizio.tipologia} interrotto.`,
}, to: utente.email,
userId: utente.id, },
}); userId: utente.id,
});
}
await NewMail({ await NewMail({
template: { template: {
mailType: "generic", mailType: "generic",
@ -1051,6 +1061,9 @@ export const userSendConfermaIntent = async ({
// NOTIFICHE EMAIL CONFERMA IMMOBILE // NOTIFICHE EMAIL CONFERMA IMMOBILE
const utente = await getUser({ db, userId: servizio.user_id }); const utente = await getUser({ db, userId: servizio.user_id });
if (!utente.comms_enabled) {
return true;
}
const annuncio_codice = await db const annuncio_codice = await db
.selectFrom("annunci") .selectFrom("annunci")
@ -1148,6 +1161,9 @@ export const adminUpdateConfermaStatus = async ({
} }
//ADMIN CONFERMA LAVORATA EMAIL //ADMIN CONFERMA LAVORATA EMAIL
if (!utente.comms_enabled) {
return true;
}
if (status) { if (status) {
await NewMail({ await NewMail({
template: { template: {

View file

@ -87,7 +87,8 @@ export const whIntentSucceededHandler = async ({
`Payment not found - intent_id: ${pIntentId}, ordineId: ${ordineId}`, `Payment not found - intent_id: ${pIntentId}, ordineId: ${ordineId}`,
); );
}); });
const condizioniFilename = `condizioni_contrattuali_${(ordine.paid_at || ordine.created_at).toISOString()}.pdf`;
const ricevutaFilename = `ricevuta_cortesia_${(ordine.paid_at || ordine.created_at).toISOString()}.pdf`;
const condizioni = await trx const condizioni = await trx
.selectFrom("prezziario") .selectFrom("prezziario")
.select("testo_condizioni") .select("testo_condizioni")
@ -123,63 +124,64 @@ export const whIntentSucceededHandler = async ({
`Failed to update servizio - servizioId: ${ordine.servizio_id}`, `Failed to update servizio - servizioId: ${ordine.servizio_id}`,
), ),
); );
if (utente.comms_enabled) {
await addEventToQueue({ await addEventToQueue({
data: {
tipo: "email",
data: { data: {
template: { tipo: "email",
mailType: "servizioAttivato", data: {
}, template: {
mail: { mailType: "servizioAttivato",
subject: "Pagamento Confermato - Acconto Infoalloggi.it", },
to: utente.email, mail: {
attachments: [ subject: "Pagamento Confermato - Acconto Infoalloggi.it",
{ to: utente.email,
filename: `condizioni_contrattuali_${ordine.created_at.toISOString()}.pdf`, attachments: [
generate: { {
type: "condizioni", filename: condizioniFilename,
stringId: (condizioni?.testo_condizioni || generate: {
CONDIZIONI_DEFAULT) as TestiEStringheStingaId, type: "condizioni",
stringId: (condizioni?.testo_condizioni ||
CONDIZIONI_DEFAULT) as TestiEStringheStingaId,
},
encoding: "base64",
contentType: "application/pdf",
}, },
encoding: "base64", {
contentType: "application/pdf", filename: ricevutaFilename,
}, generate: {
{ type: "ricevuta_cortesia",
filename: `ricevuta_cortesia_${ordine.ordine_id}.pdf`, ordineId: ordine.ordine_id,
generate: { },
type: "ricevuta_cortesia", encoding: "base64",
ordineId: ordine.ordine_id, contentType: "application/pdf",
}, },
encoding: "base64", ],
contentType: "application/pdf", },
}, userId: utente.id,
],
}, },
userId: utente.id,
}, },
}, lock_expires,
lock_expires, });
});
await addEventToQueue({ await addEventToQueue({
data: {
tipo: "email",
data: { data: {
template: { tipo: "email",
mailType: "avvisoInterruzione", data: {
template: {
mailType: "avvisoInterruzione",
},
mail: {
subject: "Promemoria durata servizio - Infoalloggi.it",
to: utente.email,
},
userId: utente.id,
}, },
mail: {
subject: "Promemoria durata servizio - Infoalloggi.it",
to: utente.email,
},
userId: utente.id,
}, },
}, lock_expires: add(new Date(), {
lock_expires: add(new Date(), { days: servizio.interruzioneDays - 5,
days: servizio.interruzioneDays - 5, }),
}), });
}); }
//SERVIZIO ATTIVATO //SERVIZIO ATTIVATO
break; break;
@ -210,64 +212,68 @@ export const whIntentSucceededHandler = async ({
isOkSaldo: true, isOkSaldo: true,
}) })
.execute(); .execute();
await addEventToQueue({ if (utente.comms_enabled) {
data: { await addEventToQueue({
tipo: "email",
data: { data: {
template: { tipo: "email",
mailType: "pagamentoConferma", data: {
}, template: {
mail: { mailType: "pagamentoConferma",
subject: "Pagamento Confermato - Saldo Infoalloggi.it", },
to: utente.email, mail: {
attachments: [ subject: "Pagamento Confermato - Saldo Infoalloggi.it",
{ to: utente.email,
filename: `ricevuta_cortesia_${ordine.ordine_id}.pdf`, attachments: [
generate: { {
type: "ricevuta_cortesia", filename: ricevutaFilename,
ordineId: ordine.ordine_id, generate: {
type: "ricevuta_cortesia",
ordineId: ordine.ordine_id,
},
encoding: "base64",
contentType: "application/pdf",
}, },
encoding: "base64", ],
contentType: "application/pdf", },
}, userId: utente.id,
],
}, },
userId: utente.id,
}, },
}, lock_expires,
lock_expires, });
}); }
break; break;
} }
case OrderTypeEnum.Rinnovo: { case OrderTypeEnum.Rinnovo: {
// nessuna azione aggiuntiva richiesta // nessuna azione aggiuntiva richiesta
await addEventToQueue({ if (utente.comms_enabled) {
data: { await addEventToQueue({
tipo: "email",
data: { data: {
template: { tipo: "email",
mailType: "pagamentoConferma", data: {
}, template: {
mail: { mailType: "pagamentoConferma",
subject: "Pagamento Confermato - Rinnovo Infoalloggi.it", },
to: utente.email, mail: {
attachments: [ subject: "Pagamento Confermato - Rinnovo Infoalloggi.it",
{ to: utente.email,
filename: `ricevuta_cortesia_${ordine.ordine_id}.pdf`, attachments: [
generate: { {
type: "ricevuta_cortesia", filename: ricevutaFilename,
ordineId: ordine.ordine_id, generate: {
type: "ricevuta_cortesia",
ordineId: ordine.ordine_id,
},
encoding: "base64",
contentType: "application/pdf",
}, },
encoding: "base64", ],
contentType: "application/pdf", },
}, userId: utente.id,
],
}, },
userId: utente.id,
}, },
}, lock_expires,
lock_expires, });
}); }
break; break;
} }
case OrderTypeEnum.Consulenza: { case OrderTypeEnum.Consulenza: {
@ -284,33 +290,35 @@ export const whIntentSucceededHandler = async ({
isOkConsulenza: true, isOkConsulenza: true,
}) })
.execute(); .execute();
await addEventToQueue({ if (utente.comms_enabled) {
data: { await addEventToQueue({
tipo: "email",
data: { data: {
template: { tipo: "email",
mailType: "pagamentoConferma", data: {
}, template: {
mail: { mailType: "pagamentoConferma",
subject: "Pagamento Confermato - Consulenza Infoalloggi.it", },
to: utente.email, mail: {
attachments: [ subject: "Pagamento Confermato - Consulenza Infoalloggi.it",
{ to: utente.email,
filename: `ricevuta_cortesia_${ordine.ordine_id}.pdf`, attachments: [
generate: { {
type: "ricevuta_cortesia", filename: ricevutaFilename,
ordineId: ordine.ordine_id, generate: {
type: "ricevuta_cortesia",
ordineId: ordine.ordine_id,
},
encoding: "base64",
contentType: "application/pdf",
}, },
encoding: "base64", ],
contentType: "application/pdf", },
}, userId: utente.id,
],
}, },
userId: utente.id,
}, },
}, lock_expires,
lock_expires, });
}); }
break; break;
} }
case OrderTypeEnum.Altro: case OrderTypeEnum.Altro:
@ -341,6 +349,16 @@ export const whIntentSucceededHandler = async ({
encoding: "base64", encoding: "base64",
contentType: "application/pdf", contentType: "application/pdf",
}, },
{
filename: condizioniFilename,
generate: {
type: "condizioni",
stringId: (condizioni?.testo_condizioni ||
CONDIZIONI_DEFAULT) as TestiEStringheStingaId,
},
encoding: "base64",
contentType: "application/pdf",
},
], ],
}, },
}, },