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,6 +241,7 @@ export const attivaServizio_SaltaPagamentoAcconto = async (
), ),
); );
if (user.comms_enabled) {
await NewMail({ await NewMail({
template: { template: {
mailType: "servizioAttivato", mailType: "servizioAttivato",
@ -277,8 +280,11 @@ export const attivaServizio_SaltaPagamentoAcconto = async (
userId: ordine.userId, userId: ordine.userId,
}, },
}, },
lock_expires: add(new Date(), { days: servizio.interruzioneDays - 5 }), 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,6 +320,7 @@ export const AdminAttivaServizio_Ufficio = async (
.where("servizio_id", "=", servizioId) .where("servizio_id", "=", servizioId)
.execute(); .execute();
if (data.comms_enabled) {
await addEventToQueue({ await addEventToQueue({
data: { data: {
tipo: "email", tipo: "email",
@ -330,6 +337,7 @@ export const AdminAttivaServizio_Ufficio = async (
}, },
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,6 +937,7 @@ 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 });
if (utente.comms_enabled) {
await NewMail({ await NewMail({
template: { template: {
mailType: "interruzione", mailType: "interruzione",
@ -938,7 +948,7 @@ export const interruzioneServizio = async (servizioId: ServizioServizioId) => {
}, },
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,7 +124,7 @@ 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: { data: {
tipo: "email", tipo: "email",
@ -136,7 +137,7 @@ export const whIntentSucceededHandler = async ({
to: utente.email, to: utente.email,
attachments: [ attachments: [
{ {
filename: `condizioni_contrattuali_${ordine.created_at.toISOString()}.pdf`, filename: condizioniFilename,
generate: { generate: {
type: "condizioni", type: "condizioni",
stringId: (condizioni?.testo_condizioni || stringId: (condizioni?.testo_condizioni ||
@ -146,7 +147,7 @@ export const whIntentSucceededHandler = async ({
contentType: "application/pdf", contentType: "application/pdf",
}, },
{ {
filename: `ricevuta_cortesia_${ordine.ordine_id}.pdf`, filename: ricevutaFilename,
generate: { generate: {
type: "ricevuta_cortesia", type: "ricevuta_cortesia",
ordineId: ordine.ordine_id, ordineId: ordine.ordine_id,
@ -180,6 +181,7 @@ export const whIntentSucceededHandler = async ({
days: servizio.interruzioneDays - 5, days: servizio.interruzioneDays - 5,
}), }),
}); });
}
//SERVIZIO ATTIVATO //SERVIZIO ATTIVATO
break; break;
@ -210,6 +212,7 @@ export const whIntentSucceededHandler = async ({
isOkSaldo: true, isOkSaldo: true,
}) })
.execute(); .execute();
if (utente.comms_enabled) {
await addEventToQueue({ await addEventToQueue({
data: { data: {
tipo: "email", tipo: "email",
@ -222,7 +225,7 @@ export const whIntentSucceededHandler = async ({
to: utente.email, to: utente.email,
attachments: [ attachments: [
{ {
filename: `ricevuta_cortesia_${ordine.ordine_id}.pdf`, filename: ricevutaFilename,
generate: { generate: {
type: "ricevuta_cortesia", type: "ricevuta_cortesia",
ordineId: ordine.ordine_id, ordineId: ordine.ordine_id,
@ -237,10 +240,12 @@ export const whIntentSucceededHandler = async ({
}, },
lock_expires, lock_expires,
}); });
}
break; break;
} }
case OrderTypeEnum.Rinnovo: { case OrderTypeEnum.Rinnovo: {
// nessuna azione aggiuntiva richiesta // nessuna azione aggiuntiva richiesta
if (utente.comms_enabled) {
await addEventToQueue({ await addEventToQueue({
data: { data: {
tipo: "email", tipo: "email",
@ -253,7 +258,7 @@ export const whIntentSucceededHandler = async ({
to: utente.email, to: utente.email,
attachments: [ attachments: [
{ {
filename: `ricevuta_cortesia_${ordine.ordine_id}.pdf`, filename: ricevutaFilename,
generate: { generate: {
type: "ricevuta_cortesia", type: "ricevuta_cortesia",
ordineId: ordine.ordine_id, ordineId: ordine.ordine_id,
@ -268,6 +273,7 @@ export const whIntentSucceededHandler = async ({
}, },
lock_expires, lock_expires,
}); });
}
break; break;
} }
case OrderTypeEnum.Consulenza: { case OrderTypeEnum.Consulenza: {
@ -284,6 +290,7 @@ export const whIntentSucceededHandler = async ({
isOkConsulenza: true, isOkConsulenza: true,
}) })
.execute(); .execute();
if (utente.comms_enabled) {
await addEventToQueue({ await addEventToQueue({
data: { data: {
tipo: "email", tipo: "email",
@ -296,7 +303,7 @@ export const whIntentSucceededHandler = async ({
to: utente.email, to: utente.email,
attachments: [ attachments: [
{ {
filename: `ricevuta_cortesia_${ordine.ordine_id}.pdf`, filename: ricevutaFilename,
generate: { generate: {
type: "ricevuta_cortesia", type: "ricevuta_cortesia",
ordineId: ordine.ordine_id, ordineId: ordine.ordine_id,
@ -311,6 +318,7 @@ export const whIntentSucceededHandler = async ({
}, },
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",
},
], ],
}, },
}, },