infoalloggi-monorepo/apps/infoalloggi/src/server/controllers/servizio.controller.ts

1471 lines
38 KiB
TypeScript
Raw Normal View History

import type { Override } from "TypeHelpers.type";
2025-08-04 17:45:44 +02:00
import { TRPCError } from "@trpc/server";
import { add } from "date-fns";
2025-08-28 18:27:07 +02:00
import { jsonArrayFrom, jsonObjectFrom } from "kysely/helpers/postgres";
import { z } from "zod/v4";
import type { PurchaseData } from "~/components/acquisto_receipt";
import { env } from "~/env";
import type { PaymentType } from "~/i18n/stripe";
import { parseDBComune, parseDBNazione } from "~/lib/catasto";
import type { Annunci, AnnunciId } from "~/schemas/public/Annunci";
2025-08-28 18:27:07 +02:00
import OrderTypeEnum from "~/schemas/public/OrderTypeEnum";
import type { Ordini, OrdiniOrdineId } from "~/schemas/public/Ordini";
import type { Prezziario } from "~/schemas/public/Prezziario";
2025-08-28 18:27:07 +02:00
import type {
Servizio,
2025-08-28 18:27:07 +02:00
ServizioServizioId,
ServizioUpdate,
} from "~/schemas/public/Servizio";
import type { ServizioAnnunci } from "~/schemas/public/ServizioAnnunci";
import type { TestiEStringheStingaId } from "~/schemas/public/TestiEStringhe";
import type TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
2025-08-28 18:27:07 +02:00
import type { Users, UsersId } from "~/schemas/public/Users";
import type { UsersAnagraficaUpdate } from "~/schemas/public/UsersAnagrafica";
import type { UsersStorageUserStorageId } from "~/schemas/public/UsersStorage";
2025-08-04 17:45:44 +02:00
import {
2025-08-28 18:27:07 +02:00
addEventToQueue,
canSendNotification,
nextTimeForNotification,
2025-08-04 17:45:44 +02:00
} from "~/server/controllers/event_queue.controller";
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";
import { getUser } from "~/server/services/user.service";
import {
parseNullableDateString,
withImages,
withVideos,
} from "~/utils/kysely-helper";
import { GetUserInterests } from "../services/interests.service";
import { createOrdine } from "../services/ordini.service";
import { genPdfCondizioniBase64 } from "../services/puppeteer.service";
import { getServizioById } from "../services/servizio.service";
import type { AnnuncioRicerca } from "./annunci.controller";
import { getComuni, getNazioni } from "./catasto.controller";
import { SaldoSolver } from "./pagamenti.controller";
2025-08-04 17:45:44 +02:00
export const getServiziByUserId = async (userId: UsersId) => {
2025-08-28 18:27:07 +02:00
try {
return await db
.selectFrom("servizio")
.selectAll("servizio")
.select((eb) => [
jsonArrayFrom(
eb
.selectFrom("servizio_annunci")
.select(["servizio_annunci.annunci_id"])
.innerJoin("annunci", "annunci.id", "servizio_annunci.annunci_id")
.select("annunci.codice")
.whereRef(
"servizio_annunci.servizio_id",
"=",
"servizio.servizio_id",
),
).as("annunci"),
])
.where("user_id", "=", userId)
.orderBy("created_at", "asc")
.execute();
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Error fetching servizi by user ID: ${(e as Error).message}`,
});
}
2025-08-04 17:45:44 +02:00
};
2025-08-28 18:27:07 +02:00
2025-08-04 17:45:44 +02:00
export const getDataPerAcquisto = async (servizioId: ServizioServizioId) => {
2025-08-28 18:27:07 +02:00
try {
const servizio = await getServizioById(servizioId);
if (!servizio) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Servizio not found",
});
}
const userData = await db
.selectFrom("users")
.select([
"users.id",
"users.nome",
"users.cognome",
"users.email",
"users.telefono",
])
.where("id", "=", servizio.user_id)
.executeTakeFirstOrThrow(
() => new Error(`User not found - userId: ${servizio.user_id}`),
);
2025-08-28 18:27:07 +02:00
if (!userData) {
throw new TRPCError({
code: "NOT_FOUND",
message: "User not found",
});
}
const anagrafica = await db
.selectFrom("users_anagrafica")
.selectAll("users_anagrafica")
.where("users_anagrafica.userid", "=", servizio.user_id)
.executeTakeFirst();
return {
2025-08-29 16:18:32 +02:00
anagrafica,
2025-08-28 18:27:07 +02:00
servizio,
userData,
};
} catch (e) {
throw new TRPCError({
code: "NOT_FOUND",
message: `Servizio not found: ${(e as Error).message}`,
});
}
2025-08-04 17:45:44 +02:00
};
export const processServizioOnboard = async ({
2025-08-28 18:27:07 +02:00
servizioId,
userId,
user,
anagrafica,
servizio,
2025-08-04 17:45:44 +02:00
}: {
2025-08-28 18:27:07 +02:00
servizioId: ServizioServizioId;
userId: UsersId;
user: Pick<Users, "nome" | "cognome" | "email" | "telefono" | "username">;
2025-08-28 18:27:07 +02:00
anagrafica: UsersAnagraficaUpdate;
servizio: ServizioUpdate;
}) => {
2025-08-28 18:27:07 +02:00
try {
await db.transaction().execute(async (tx) => {
// Update user and anagrafica data
2025-08-28 18:27:07 +02:00
await tx
.updateTable("users")
.set(user)
.where("id", "=", userId)
.execute();
2025-08-28 18:27:07 +02:00
const { idanagrafica, ...rest } = anagrafica;
if (idanagrafica) {
await tx
.updateTable("users_anagrafica")
.set({
...rest,
userid: userId,
})
.where("idanagrafica", "=", idanagrafica)
.returningAll()
.executeTakeFirstOrThrow(
() =>
new Error(
`Failed to update users_anagrafica - idanagrafica: ${idanagrafica}`,
),
);
2025-08-28 18:27:07 +02:00
} else {
await tx
.insertInto("users_anagrafica")
.values({
...rest,
userid: userId,
})
.returningAll()
.executeTakeFirstOrThrow(
() => new Error("Failed to insert new users_anagrafica"),
);
2025-08-28 18:27:07 +02:00
}
// Update servizio data
2025-08-28 18:27:07 +02:00
return await tx
.updateTable("servizio")
.set({ ...servizio, onboardOk: true })
2025-08-28 18:27:07 +02:00
.where("servizio_id", "=", servizioId)
.returningAll()
.executeTakeFirstOrThrow(
() =>
new Error(`Failed to update servizio - servizioId: ${servizioId}`),
);
2025-08-28 18:27:07 +02:00
});
return true;
2025-08-28 18:27:07 +02:00
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Error updating data for acquisto: ${(e as Error).message}`,
});
}
2025-08-04 17:45:44 +02:00
};
/**
* Attiva il servizio senza pagamento,\
* usato se un acconto già attivo è presente per il servizio,\
* invio delle condizioni contrattuali in allegato.
*/
export const attivaServizio_SaltaPagamentoAcconto = async (
ordineId: OrdiniOrdineId,
) => {
2025-08-28 18:27:07 +02:00
try {
return await db.transaction().execute(async (tx) => {
const ordine = await tx
.selectFrom("ordini")
.innerJoin("users", "users.id", "ordini.userid")
.innerJoin("prezziario", "prezziario.idprezziario", "ordini.packid")
.select([
"ordini.ordine_id",
"ordini.servizio_id",
"ordini.created_at",
"prezziario.testo_condizioni",
"users.email",
"users.id as userId",
])
.where("ordine_id", "=", ordineId)
.executeTakeFirstOrThrow(
() => new Error(`Ordine not found - ordineId: ${ordineId}`),
);
2025-08-28 18:27:07 +02:00
await tx
.updateTable("servizio")
.set({
decorrenza: new Date(),
isOkAcconto: true,
2025-08-28 18:27:07 +02:00
})
.where("servizio_id", "=", ordine.servizio_id)
2025-08-28 18:27:07 +02:00
.execute();
await NewMail({
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,
});
return true;
2025-08-28 18:27:07 +02:00
});
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Error activating servizio without payment: ${(e as Error).message}`,
2025-08-28 18:27:07 +02:00
});
}
2025-08-04 17:45:44 +02:00
};
type DocRef = {
storageId: string;
ref: UsersStorageUserStorageId | null;
};
export type ServizioData = Servizio & {
doc_personale_fronte: DocRef | null;
doc_personale_retro: DocRef | null;
doc_motivazione: DocRef | null;
annunci: (ServizioAnnunci &
Pick<
Annunci,
| "id"
| "codice"
| "comune"
| "provincia"
| "prezzo"
| "consegna"
| "numero_camere"
| "mq"
| "tipo"
| "titolo_it"
| "titolo_en"
| "desc_en"
| "desc_it"
| "modificato_il"
| "stato"
| "external_videos"
| "media_updated_at"
| "homepage"
| "web"
| "disponibile_da"
| "persone"
| "permanenza"
> & {
images: {
img: string;
thumb: string;
}[];
videos: {
thumb: string;
video: string;
}[];
})[];
ordini: (Ordini & Pick<Prezziario, "testo_condizioni">)[];
};
export const getAllServizioAnnunci = async (
userId: UsersId,
): Promise<ServizioData[]> => {
2025-08-28 18:27:07 +02:00
try {
const data = await db
2025-08-28 18:27:07 +02:00
.selectFrom("servizio")
.where("user_id", "=", userId)
.leftJoin(
"users_anagrafica",
"servizio.user_id",
"users_anagrafica.userid",
)
.selectAll("servizio")
2025-08-28 18:27:07 +02:00
.select((eb) => [
jsonObjectFrom(
eb
.selectFrom("users_storage")
.select([
"users_storage.storageId as storageId",
"users_anagrafica.doc_personale_fronte_ref as ref",
])
.whereRef(
"users_storage.user_storage_id",
"=",
"users_anagrafica.doc_personale_fronte_ref",
),
).as("doc_personale_fronte"),
jsonObjectFrom(
eb
.selectFrom("users_storage")
.select([
"users_storage.storageId as storageId",
"users_anagrafica.doc_personale_retro_ref as ref",
])
.whereRef(
"users_storage.user_storage_id",
"=",
"users_anagrafica.doc_personale_retro_ref",
),
).as("doc_personale_retro"),
jsonObjectFrom(
eb
.selectFrom("users_storage")
.select([
"users_storage.storageId as storageId",
"servizio.doc_motivazione_ref as ref",
])
.whereRef(
"users_storage.user_storage_id",
"=",
"servizio.doc_motivazione_ref",
),
).as("doc_motivazione"),
2025-08-28 18:27:07 +02:00
jsonArrayFrom(
eb
.selectFrom("servizio_annunci")
.selectAll("servizio_annunci")
.innerJoin("annunci", "annunci.id", "servizio_annunci.annunci_id")
.select((_eb) => [
2025-08-28 18:27:07 +02:00
"annunci.id",
"annunci.codice",
"annunci.comune",
"annunci.provincia",
2025-08-28 18:27:07 +02:00
"annunci.prezzo",
"annunci.consegna",
"annunci.numero_camere",
"annunci.mq",
"annunci.tipo",
"annunci.titolo_it",
"annunci.titolo_en",
2025-08-28 18:27:07 +02:00
"annunci.desc_en",
"annunci.desc_it",
"annunci.modificato_il",
2025-08-28 18:27:07 +02:00
"annunci.stato",
"annunci.external_videos",
"annunci.media_updated_at",
"annunci.homepage",
"annunci.web",
"annunci.disponibile_da",
"annunci.persone",
"annunci.permanenza",
withImages(),
withVideos(),
2025-08-28 18:27:07 +02:00
])
//ignora annunci che non sono disponibili
//.where("annunci.web", "=", true)
//.where("annunci.stato", "!=", "Sospeso")
.orderBy("servizio_annunci.user_confirmed_at", (ob) =>
ob.asc().nullsLast(),
)
.orderBy("servizio_annunci.open_contatti_at", (ob) =>
ob.asc().nullsLast(),
)
2025-08-28 18:27:07 +02:00
.orderBy("servizio_annunci.created_at", "asc")
.whereRef(
"servizio_annunci.servizio_id",
"=",
"servizio.servizio_id",
),
).as("annunci"),
jsonArrayFrom(
eb
.selectFrom("ordini")
.selectAll("ordini")
.innerJoin("prezziario", "prezziario.idprezziario", "ordini.packid")
.select("prezziario.testo_condizioni")
.whereRef("ordini.servizio_id", "=", "servizio.servizio_id")
.orderBy("ordini.created_at", "desc"),
).as("ordini"),
2025-08-28 18:27:07 +02:00
])
.orderBy("servizio.created_at", "desc")
.execute();
const formattedData = data.map((item) => {
return {
...item,
annunci: item.annunci.map((annuncio) => ({
...annuncio,
created_at: new Date(annuncio.created_at),
open_contatti_at: parseNullableDateString(annuncio.open_contatti_at),
user_confirmed_at: parseNullableDateString(
annuncio.user_confirmed_at,
),
accettato_conferma_at: parseNullableDateString(
annuncio.accettato_conferma_at,
),
contratto_decorrenza: parseNullableDateString(
annuncio.contratto_decorrenza,
),
contratto_scadenza: parseNullableDateString(
annuncio.contratto_scadenza,
),
modificato_il: parseNullableDateString(annuncio.modificato_il),
disponibile_da: parseNullableDateString(annuncio.disponibile_da),
media_updated_at: parseNullableDateString(annuncio.media_updated_at),
})),
ordini: item.ordini.map((ordine) => ({
...ordine,
created_at: new Date(ordine.created_at),
paid_at: ordine.paid_at ? new Date(ordine.paid_at) : null,
})),
};
});
return formattedData;
2025-08-28 18:27:07 +02:00
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Error fetching servizio annunci: ${(e as Error).message}`,
});
}
2025-08-04 17:45:44 +02:00
};
export const getServizioDataById = async (
servizioId: ServizioServizioId,
): Promise<ServizioData> => {
try {
const data = await db
.selectFrom("servizio")
.leftJoin(
"users_anagrafica",
"servizio.user_id",
"users_anagrafica.userid",
)
.selectAll("servizio")
.select((eb) => [
jsonObjectFrom(
eb
.selectFrom("users_storage")
.select([
"users_storage.storageId as storageId",
"users_anagrafica.doc_personale_fronte_ref as ref",
])
.whereRef(
"users_storage.user_storage_id",
"=",
"users_anagrafica.doc_personale_fronte_ref",
),
).as("doc_personale_fronte"),
jsonObjectFrom(
eb
.selectFrom("users_storage")
.select([
"users_storage.storageId as storageId",
"users_anagrafica.doc_personale_retro_ref as ref",
])
.whereRef(
"users_storage.user_storage_id",
"=",
"users_anagrafica.doc_personale_retro_ref",
),
).as("doc_personale_retro"),
jsonObjectFrom(
eb
.selectFrom("users_storage")
.select([
"users_storage.storageId as storageId",
"servizio.doc_motivazione_ref as ref",
])
.whereRef(
"users_storage.user_storage_id",
"=",
"servizio.doc_motivazione_ref",
),
).as("doc_motivazione"),
jsonArrayFrom(
eb
.selectFrom("servizio_annunci")
.selectAll("servizio_annunci")
.innerJoin("annunci", "annunci.id", "servizio_annunci.annunci_id")
.select((_eb) => [
"annunci.id",
"annunci.codice",
"annunci.comune",
"annunci.provincia",
"annunci.prezzo",
"annunci.consegna",
"annunci.numero_camere",
"annunci.mq",
"annunci.tipo",
"annunci.titolo_it",
"annunci.titolo_en",
"annunci.desc_en",
"annunci.desc_it",
"annunci.modificato_il",
"annunci.stato",
"annunci.external_videos",
"annunci.media_updated_at",
"annunci.homepage",
"annunci.web",
"annunci.disponibile_da",
"annunci.persone",
"annunci.permanenza",
withImages(),
withVideos(),
])
//ignora annunci che non sono disponibili
//.where("annunci.web", "=", true)
//.where("annunci.stato", "!=", "Sospeso")
.orderBy("servizio_annunci.user_confirmed_at", (ob) =>
ob.asc().nullsLast(),
)
.orderBy("servizio_annunci.open_contatti_at", (ob) =>
ob.asc().nullsLast(),
)
.orderBy("servizio_annunci.created_at", "asc")
.whereRef(
"servizio_annunci.servizio_id",
"=",
"servizio.servizio_id",
),
).as("annunci"),
jsonArrayFrom(
eb
.selectFrom("ordini")
.selectAll("ordini")
.innerJoin("prezziario", "prezziario.idprezziario", "ordini.packid")
.select("prezziario.testo_condizioni")
.whereRef("ordini.servizio_id", "=", "servizio.servizio_id")
.orderBy("ordini.created_at", "desc"),
).as("ordini"),
])
.orderBy("servizio.created_at", "desc")
.where("servizio.servizio_id", "=", servizioId)
.executeTakeFirst();
if (!data) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Servizio not found",
});
}
const formattedData = {
...data,
annunci: data.annunci.map((annuncio) => ({
...annuncio,
created_at: new Date(annuncio.created_at),
open_contatti_at: parseNullableDateString(annuncio.open_contatti_at),
user_confirmed_at: parseNullableDateString(annuncio.user_confirmed_at),
accettato_conferma_at: parseNullableDateString(
annuncio.accettato_conferma_at,
),
contratto_decorrenza: parseNullableDateString(
annuncio.contratto_decorrenza,
),
contratto_scadenza: parseNullableDateString(
annuncio.contratto_scadenza,
),
modificato_il: parseNullableDateString(annuncio.modificato_il),
disponibile_da: parseNullableDateString(annuncio.disponibile_da),
media_updated_at: parseNullableDateString(annuncio.media_updated_at),
})),
ordini: data.ordini.map((ordine) => ({
...ordine,
created_at: new Date(ordine.created_at),
paid_at: ordine.paid_at ? new Date(ordine.paid_at) : null,
})),
};
return formattedData;
} catch (e) {
throw new TRPCError({
code: "NOT_FOUND",
message: `Servizio annunci not found: ${(e as Error).message}`,
});
}
};
2025-08-04 17:45:44 +02:00
export const addServizioAnnunci = async ({
2025-08-28 18:27:07 +02:00
servizioId,
annunci,
2025-08-04 17:45:44 +02:00
}: {
2025-08-28 18:27:07 +02:00
servizioId: ServizioServizioId;
annunci: AnnunciId[];
2025-08-04 17:45:44 +02:00
}) => {
2025-08-28 18:27:07 +02:00
try {
await db
.insertInto("servizio_annunci")
.values(
annunci.map((annuncio, idx) => ({
annunci_id: annuncio,
created_at: add(new Date(), {
//trick per assicurare che la data sia sempre diversa
seconds: idx,
}),
2025-08-29 16:18:32 +02:00
servizio_id: servizioId,
2025-08-28 18:27:07 +02:00
})),
)
.onConflict((oc) => oc.columns(["servizio_id", "annunci_id"]).doNothing())
.execute();
return true;
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Error adding servizio to annuncio: ${(e as Error).message}`,
});
}
2025-08-04 17:45:44 +02:00
};
export const getAnnunciAvailableToAdd = async (
2025-08-28 18:27:07 +02:00
servizioId: ServizioServizioId,
2025-08-04 17:45:44 +02:00
) => {
2025-08-28 18:27:07 +02:00
try {
return await db.transaction().execute(async (tx) => {
const servizioAnnunci = await tx
.selectFrom("servizio_annunci")
.select("annunci_id")
.where("servizio_id", "=", servizioId)
.execute();
const ids = servizioAnnunci.map((item) => item.annunci_id);
let qry = tx
.selectFrom("annunci")
.select(["id", "codice", "titolo_it"])
2025-08-28 18:27:07 +02:00
.where("annunci.web", "=", true)
.where("stato", "!=", "Sospeso");
if (ids.length > 0) {
qry = qry.where("id", "not in", ids);
}
return await qry.execute();
});
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Error fetching available annunci: ${(e as Error).message}`,
});
}
2025-08-04 17:45:44 +02:00
};
export const SbloccaContatti = async ({
2025-08-28 18:27:07 +02:00
servizioId,
annuncioId,
2025-08-04 17:45:44 +02:00
}: {
2025-08-28 18:27:07 +02:00
servizioId: ServizioServizioId;
annuncioId: AnnunciId;
2025-08-04 17:45:44 +02:00
}) => {
2025-08-28 18:27:07 +02:00
try {
const alreadyUnlocked = await db
.selectFrom("servizio_annunci")
.where("servizio_id", "=", servizioId)
.where("open_contatti_at", "is not", null)
.select((eb) => [eb.fn.count<number>("annunci_id").as("count")])
.executeTakeFirst();
if (alreadyUnlocked && alreadyUnlocked.count >= 10) {
return false;
}
const data = await db.transaction().execute(async (tx) => {
await tx
.updateTable("servizio_annunci")
.set({
open_contatti_at: new Date(),
})
.where("servizio_id", "=", servizioId)
.where("annunci_id", "=", annuncioId)
.execute();
const servizio = await tx
.selectFrom("servizio")
.select("user_id")
.where("servizio_id", "=", servizioId)
.executeTakeFirstOrThrow(() => new Error("Servizio not found"));
const user = await tx
.selectFrom("users")
.select(["id", "username", "email", "telefono"])
.where("id", "=", servizio.user_id)
.executeTakeFirstOrThrow(() => new Error("User not found"));
const anagrafica = await tx
.selectFrom("users_anagrafica")
.select("sesso")
.where("userid", "=", servizio.user_id)
.executeTakeFirstOrThrow(() => new Error("Anagrafica not found"));
const annuncio = await tx
.selectFrom("annunci")
.where("id", "=", annuncioId)
.select([
"codice",
"indirizzo",
"email",
"numero",
"locatore",
"civico",
"comune",
])
.executeTakeFirstOrThrow(() => new Error("Annuncio not found"));
return {
anagrafica,
annuncio,
annuncioId,
2025-08-29 16:18:32 +02:00
servizioId,
user,
2025-08-28 18:27:07 +02:00
};
});
const EmailProprietario: NewMailProps | null = data.annuncio.email
? {
template: {
mailType: "emailContattoInteressato",
props: {
indirizzo: data.annuncio.indirizzo,
nome_cognome: data.annuncio.locatore || "",
nome_cognome_utente: data.user.username,
sesso: data.anagrafica.sesso,
telefono: data.user.telefono,
},
},
mail: {
subject: "Un interessato ha ricevuto i Suoi contatti",
to: data.annuncio.email,
2025-08-28 18:27:07 +02:00
},
}
: null;
const numeroProprietario = data.annuncio.numero
? data.annuncio.numero.replace(/\s+/g, "").split(",")[0] || null
: null;
const SmsProprietario: MinimalSMSData | null = numeroProprietario
? {
message: `${data.anagrafica.sesso ? (data.anagrafica.sesso === "M" ? "Il Sig." : "La Sig.ra") : "Il/La Sig./Sig.ra"} ${data.user.username}, con numero ${data.user.telefono} ha sbloccato i contatti per l'annuncio in ${data.annuncio.indirizzo} ${data.annuncio.civico} a ${data.annuncio.comune}.`,
2025-08-29 16:18:32 +02:00
recipient: [numeroProprietario],
2025-08-28 18:27:07 +02:00
}
: null;
await addEventToQueue({
data: {
data: {
template: {
mailType: "emailContattiConScheda",
props: {
nominativo: data.annuncio.locatore || "",
telefono: data.annuncio.numero || "",
indirizzo: data.annuncio.indirizzo || "",
codice: data.annuncio.codice,
},
},
mail: {
subject: `Contatti Annuncio ${data.annuncio.codice} - InfoAlloggi`,
to: data.user.email,
attachments: [
{
filename: `scheda annuncio ${data.annuncio.codice}.pdf`,
generate: {
type: "scheda_annuncio",
annuncioId: data.annuncioId,
},
encoding: "base64",
contentType: "application/pdf",
},
],
},
userId: data.user.id,
},
tipo: "email",
},
lock_expires: new Date(Date.now() + 3000),
});
2025-08-28 18:27:07 +02:00
if (canSendNotification()) {
if (EmailProprietario) {
await NewMail(EmailProprietario);
}
if (SmsProprietario) {
await SendMessage(SmsProprietario);
}
} else {
const lock_expires = nextTimeForNotification();
if (EmailProprietario) {
await addEventToQueue({
data: {
data: EmailProprietario,
2025-08-29 16:18:32 +02:00
tipo: "email",
2025-08-28 18:27:07 +02:00
},
2025-08-29 16:18:32 +02:00
lock_expires,
2025-08-28 18:27:07 +02:00
});
}
if (SmsProprietario) {
await addEventToQueue({
data: {
data: SmsProprietario,
2025-08-29 16:18:32 +02:00
tipo: "sms",
2025-08-28 18:27:07 +02:00
},
2025-08-29 16:18:32 +02:00
lock_expires,
2025-08-28 18:27:07 +02:00
});
}
}
return true;
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Error unlocking contatti: ${(e as Error).message}`,
});
}
2025-08-04 17:45:44 +02:00
};
export const interruzioneServizio = async (servizioId: ServizioServizioId) => {
2025-08-28 18:27:07 +02:00
try {
const servizio = await getServizioById(servizioId);
if (!servizio) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Servizio not found",
});
}
if (servizio.isInterrotto) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Servizio already interrupted",
});
}
if (servizio.decorrenza == null) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Servizio decorrenza not set",
});
}
const isWithinRipensamento =
new Date() <=
add(servizio.decorrenza, {
days: 14,
});
if (!isWithinRipensamento) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Periodo di ripensamento scaduto",
});
}
await db
.updateTable("servizio")
.set({
isInterrotto: true,
})
.where("servizio_id", "=", servizioId)
.execute();
2025-08-28 18:27:07 +02:00
// NOTIFICHE EMAIL INTERRUZIONE SERVIZIO
const utente = await getUser({ db, userId: servizio.user_id });
await NewMail({
template: {
mailType: "generic",
props: {
noreply: true,
testo: `Abbiamo ricevuto la tua richiesta di interruzione del servizio di ricerca affitto per la tipologia ${servizio.tipologia}. Il servizio è stato interrotto con successo.`,
title: `Servizio Ricerca Affitto ${servizio.tipologia} interrotto`,
},
},
mail: {
subject: `Servizio Ricerca Affitto ${servizio.tipologia} interrotto`,
to: utente.email,
2025-08-28 18:27:07 +02:00
},
2025-08-29 16:18:32 +02:00
userId: utente.id,
2025-08-28 18:27:07 +02:00
});
await NewMail({
template: {
mailType: "generic",
props: {
link: {
href: `${env.BASE_URL}/area-riservata/admin/user-view/ricerca/${servizio.user_id}#${servizioId}`,
label: "Vai al servizio",
},
noreply: true,
testo: `L'utente ha richiesto l'interruzione del servizio di ricerca affitto per la tipologia ${servizio.tipologia}. Puoi procedere con le prossime azioni.`,
title: `Utente ${utente.username} ha richieto l'interruzione del servizio di ricerca affitto`,
2025-08-28 18:27:07 +02:00
},
},
mail: {
subject: `Utente ${utente.username} ha richieto l'interruzione del servizio di ricerca affitto`,
to: "web@infoalloggi.it",
},
2025-08-28 18:27:07 +02:00
});
return true;
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Error interruzione servizio: ${(e as Error).message}`,
});
}
2025-08-04 17:45:44 +02:00
};
export const InteractionLogicTipologia = async ({
2025-08-28 18:27:07 +02:00
tipologia,
userId,
annuncioId,
2025-08-04 17:45:44 +02:00
}: {
2025-08-28 18:27:07 +02:00
tipologia: string;
userId: UsersId;
annuncioId: AnnunciId;
2025-08-04 17:45:44 +02:00
}) => {
2025-08-28 18:27:07 +02:00
const parse = z.custom<TipologiaPosizioneEnum>();
const parsedTipologia = parse.safeParse(tipologia);
if (!parsedTipologia.success) {
return { status: "invalid" as const };
}
const servizi = await db
.selectFrom("servizio")
.select(["tipologia", "servizio_id"])
.where("tipologia", "=", parsedTipologia.data)
.where("isOkAcconto", "=", true)
.where("isInterrotto", "=", false)
.where("decorrenza", "is not", null)
.where("user_id", "=", userId)
.select((eb) => [
jsonObjectFrom(
eb
.selectFrom("servizio_annunci")
.select("annunci_id")
.where("servizio_annunci.annunci_id", "=", annuncioId)
.whereRef("servizio.servizio_id", "=", "servizio_annunci.servizio_id")
.limit(1),
).as("alreadySaved"),
])
.executeTakeFirst();
if (!servizi) {
return { status: "not_found" as const };
}
if (servizi.alreadySaved) {
return {
servizioId: servizi.servizio_id,
2025-08-29 16:18:32 +02:00
status: "already_saved" as const,
2025-08-28 18:27:07 +02:00
};
}
return {
servizioId: servizi.servizio_id,
2025-08-29 16:18:32 +02:00
status: "found" as const,
2025-08-28 18:27:07 +02:00
tipologia: servizi.tipologia,
};
2025-08-04 17:45:44 +02:00
};
export const userSendConfermaIntent = async ({
2025-08-28 18:27:07 +02:00
servizioId,
annuncioId,
2025-08-04 17:45:44 +02:00
}: {
2025-08-28 18:27:07 +02:00
servizioId: ServizioServizioId;
annuncioId: AnnunciId;
2025-08-04 17:45:44 +02:00
}) => {
2025-08-28 18:27:07 +02:00
try {
const servizio = await getServizioById(servizioId);
if (
!servizio ||
servizio.isInterrotto ||
!servizio.isOkAcconto ||
servizio.decorrenza == null
) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Servizio not found",
});
}
await db
.updateTable("servizio_annunci")
.set({
user_confirmed_at: new Date(),
})
.where("servizio_id", "=", servizioId)
.where("annunci_id", "=", annuncioId)
.execute();
2025-08-28 18:27:07 +02:00
// NOTIFICHE EMAIL CONFERMA IMMOBILE
const utente = await getUser({ db, userId: servizio.user_id });
const annuncio_codice = await db
.selectFrom("annunci")
.select("codice")
.where("id", "=", annuncioId)
.executeTakeFirstOrThrow(
() =>
new Error(
`Annuncio not found - servizioId: ${servizioId}, annunciId: ${annuncioId}`,
),
);
2025-08-28 18:27:07 +02:00
await NewMail({
template: {
mailType: "generic",
props: {
link: {
href: `${env.BASE_URL}/area-riservata/admin/user-view/ricerca/${servizio.user_id}#${servizioId}-${annuncioId}`,
label: "Visualizza Annuncio",
},
noreply: true,
testo: `L'utente ${utente.username} ha confermato l'immobile con codice ${annuncio_codice.codice}.`,
title: `Nuova conferma immobile ${annuncio_codice.codice}`,
2025-08-28 18:27:07 +02:00
},
},
mail: {
subject: `Nuova conferma immobile ${annuncio_codice.codice}`,
to: "web@infoalloggi.it",
},
2025-08-28 18:27:07 +02:00
});
await NewMail({
template: {
mailType: "generic",
props: {
noreply: true,
testo: `La tua conferma per l'immobile con codice ${annuncio_codice.codice} è stata inviata con successo. Il nostro team la esaminerà a breve. Riceverai una notifica via email per procedere.`,
title: `Conferma immobile ${annuncio_codice.codice} inviata con successo`,
},
},
mail: {
subject: `Conferma immobile ${annuncio_codice.codice} inviata con successo`,
to: utente.email,
2025-08-28 18:27:07 +02:00
},
2025-08-29 16:18:32 +02:00
userId: utente.id,
2025-08-28 18:27:07 +02:00
});
return true;
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Error confirming immobile: ${(e as Error).message}`,
});
}
2025-08-04 17:45:44 +02:00
};
export const adminUpdateConfermaStatus = async ({
servizioId,
annuncioId,
status,
}: {
servizioId: ServizioServizioId;
annuncioId: AnnunciId;
status: boolean;
}) => {
try {
await db
.updateTable("servizio_annunci")
.set({
hasConfermaAdmin: status,
})
.where("servizio_id", "=", servizioId)
.where("annunci_id", "=", annuncioId)
.execute();
const servizio = await getServizioById(servizioId);
const utente = await getUser({ db, userId: servizio.user_id });
const saldo = await SaldoSolver({
tipologia: servizio.tipologia,
permanenza: servizio.permanenza,
budget: servizio.budget,
});
if (saldo.success) {
await createOrdine({
servizio_id: servizioId,
type: OrderTypeEnum.Saldo,
amount_cent: saldo.pack.prezzo_cent,
packid: saldo.pack.idprezziario,
userid: servizio.user_id,
});
} else {
console.error(
`Errore nel calcolo del saldo per il servizio ${servizioId}: ${saldo.message}, procedura invio conferma`,
);
}
//ADMIN CONFERMA LAVORATA EMAIL
if (status) {
await NewMail({
template: {
mailType: "generic",
props: {
link: {
href: `${env.BASE_URL}/area-riservata/servizio/${servizioId}`,
label: "Vai al servizio",
},
noreply: true,
testo: `La conferma è stata lavorata con successo. Puoi procedere con le prossime azioni.`,
title: "Conferma lavorata per il servizio di ricerca affitto",
},
},
mail: {
subject: "Conferma lavorata per il servizio di ricerca affitto",
to: utente.email,
},
userId: utente.id,
});
}
return true;
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Error updating conferma status from annuncio: ${(e as Error).message}`,
});
}
};
export const userAccettaConferma = async ({
servizioId,
annuncioId,
}: {
servizioId: ServizioServizioId;
annuncioId: AnnunciId;
}) => {
try {
await db
.updateTable("servizio_annunci")
.set({
accettato_conferma_at: new Date(),
})
.where("servizio_id", "=", servizioId)
.where("annunci_id", "=", annuncioId)
.execute();
const servizio = await getServizioById(servizioId);
const utente = await getUser({ db, userId: servizio.user_id });
// USER ACCETTA CONFERMA EMAIL
await NewMail({
template: {
mailType: "generic",
props: {
link: {
href: `${env.BASE_URL}/area-riservata/admin/user-view/ricerca/${servizio.user_id}#${servizioId}-${annuncioId}`,
label: "Vai al servizio",
},
noreply: true,
testo: `L'utente ha accettato le condizioni di conferma per la locazione. Puoi procedere con le prossime azioni.`,
title: `Utente ${utente.username} ha accettato le condizioni di conferma`,
},
},
mail: {
subject: `Utente ${utente.username} ha accettato le condizioni di conferma`,
to: "web@infoalloggi.it",
},
});
return { success: true, userId: utente.id };
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Error accepting conferma from annuncio: ${(e as Error).message}`,
});
}
};
2025-08-04 17:45:44 +02:00
export const SendContattoAnnuncioEmail = async ({
2025-08-28 18:27:07 +02:00
codice,
nome,
email,
telefono,
messaggio,
2025-08-04 17:45:44 +02:00
}: {
2025-08-28 18:27:07 +02:00
codice: string;
nome: string;
email: string;
telefono: string;
messaggio: string;
2025-08-04 17:45:44 +02:00
}) => {
2025-08-28 18:27:07 +02:00
try {
await NewMail({
template: {
mailType: "contattoAnnuncioEmail",
props: {
codice,
email,
messaggio,
nome,
telefono,
},
},
mail: {
subject: "Contatto per annuncio",
to: "web@infoalloggi.it",
2025-08-28 18:27:07 +02:00
},
});
return {
email,
2025-08-29 16:18:32 +02:00
nome,
2025-08-28 18:27:07 +02:00
telefono,
};
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Error sending email: ${(e as Error).message}`,
});
}
2025-08-04 17:45:44 +02:00
};
export const getCompatibileAnnunci = async ({
servizioId,
adminOverride = false,
}: {
servizioId: ServizioServizioId;
adminOverride?: boolean;
}): Promise<(AnnuncioRicerca & { alreadyRequested: boolean })[]> => {
2025-08-28 18:27:07 +02:00
try {
const servizio = await getServizioById(servizioId);
if (!servizio) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Servizio not found",
});
}
const interests = await GetUserInterests(servizio.user_id);
2025-08-28 18:27:07 +02:00
let qry = db
.selectFrom("annunci")
.distinctOn("annunci.id")
.select((eb) => [
"id",
"codice",
"comune",
"provincia",
"mq",
"prezzo",
"desc_en",
"desc_it",
"titolo_en",
"titolo_it",
"tipo",
"consegna",
"stato",
"web",
"media_updated_at",
"homepage",
"numero_camere",
"external_videos",
"modificato_il",
withImages(),
withVideos(),
eb
.case()
.when("id", "in", interests)
.then(true)
.else(false)
.end()
.as("alreadyRequested"),
2025-08-28 18:27:07 +02:00
])
.where("web", "=", true)
.where("stato", "!=", "Sospeso")
.where("tipo", "=", servizio.tipologia)
.where("id", "not in", (eb) =>
eb
.selectFrom("servizio_annunci")
.select("annunci_id")
.where("servizio_id", "=", servizioId),
);
if (adminOverride) {
return await qry.execute();
}
qry = qry.where("prezzo", "<=", servizio.budget * 1e2 * 1.2); // Allow 20% margin
2025-08-28 18:27:07 +02:00
const month = new Date().getMonth() + 1; // Months are 0-indexed in JS
if (servizio.entromese === month) {
qry = qry.where((eb) =>
eb.or([
eb("consegna", "=", 0),
eb("consegna", "=", servizio.entromese),
]),
);
} else if (servizio.entromese === 0) {
qry = qry.where((eb) =>
eb.or([eb("consegna", "=", 0), eb("consegna", "=", month)]),
);
} else {
qry = qry.where("consegna", "=", servizio.entromese);
}
return await qry.execute();
2025-08-28 18:27:07 +02:00
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Error fetching compatibile annunci: ${(e as Error).message}`,
});
}
2025-08-04 17:45:44 +02:00
};
export const getOrdineRicevutaData = async ({
2025-08-28 18:27:07 +02:00
ordineId,
2025-08-04 17:45:44 +02:00
}: {
2025-08-28 18:27:07 +02:00
ordineId: OrdiniOrdineId;
}): Promise<PurchaseData> => {
2025-08-28 18:27:07 +02:00
try {
const data = await db
.selectFrom("ordini")
2025-08-28 18:27:07 +02:00
.select([
"ordini.ordine_id",
"ordini.userid",
"ordini.amount_cent",
"ordini.sconto",
"ordini.created_at",
"ordini.paymentmethod",
2025-08-28 18:27:07 +02:00
])
.innerJoin("prezziario", "prezziario.idprezziario", "ordini.packid")
.select("prezziario.nome_it")
.where("ordini.ordine_id", "=", ordineId)
//.where("ordini.paymentstatus", "=", PaymentStatusEnum.success)
2025-08-28 18:27:07 +02:00
.executeTakeFirstOrThrow(
() => new Error(`Ordine not found - ordineId: ${ordineId}`),
);
2025-08-28 18:27:07 +02:00
const anagrafica = await db
.selectFrom("users")
.select("users.username")
.innerJoin("users_anagrafica", "users_anagrafica.userid", "users.id")
.select([
"users_anagrafica.via_residenza",
"users_anagrafica.civico_residenza",
"users_anagrafica.comune_residenza",
"users_anagrafica.provincia_residenza",
"users_anagrafica.cap_residenza",
"users_anagrafica.nazione_residenza",
"users_anagrafica.codice_fiscale",
])
.where("users.id", "=", data.userid)
.executeTakeFirstOrThrow(
() => new Error(`Anagrafica not found - userid: ${data.userid}`),
);
const comuni = await getComuni();
const nazioni = await getNazioni();
const parsedComune = parseDBComune(anagrafica.comune_residenza, comuni);
const parsedNazione = parseDBNazione(anagrafica.nazione_residenza, nazioni);
2025-08-28 18:27:07 +02:00
return {
clienteIndirizzo: `${anagrafica.via_residenza} ${anagrafica.civico_residenza}\n${parsedComune?.nome} (${anagrafica.provincia_residenza}) ${anagrafica.cap_residenza}\n${parsedNazione?.nome}`,
clienteNome: anagrafica.username,
codiceFiscale: anagrafica.codice_fiscale,
2025-08-29 16:18:32 +02:00
date: data.created_at,
packName: data.nome_it,
sconto: data.sconto,
amount_cent: data.amount_cent,
ordineId: data.ordine_id,
paymentMethod: (data.paymentmethod || "manual") as PaymentType,
};
2025-08-28 18:27:07 +02:00
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Error fetching ordine ricevuta data: ${(e as Error).message}`,
});
}
2025-08-04 17:45:44 +02:00
};
export type RichiesteData = Servizio &
Pick<Users, "username"> & {
annunci_servizio: (Override<
ServizioAnnunci,
{
created_at: string;
open_contatti_at: string | null;
user_confirmed_at: string | null;
accettato_conferma_at: string | null;
contratto_decorrenza: string | null;
contratto_scadenza: string | null;
}
> &
Pick<Annunci, "codice" | "titolo_it">)[];
ordini_servizio: (Override<
Ordini,
{
created_at: string;
paid_at: string | null;
}
> &
Pick<Prezziario, "testo_condizioni">)[];
};
export const getRichieste = async (): Promise<RichiesteData[]> => {
try {
return await db
.selectFrom("servizio")
.innerJoin("users", "users.id", "servizio.user_id")
.selectAll("servizio")
.select(["users.username"])
.select((eb) => [
jsonArrayFrom(
eb
.selectFrom("servizio_annunci")
.selectAll("servizio_annunci")
.innerJoin("annunci", "annunci.id", "servizio_annunci.annunci_id")
.select(["annunci.codice", "annunci.titolo_it"])
.whereRef(
"servizio.servizio_id",
"=",
"servizio_annunci.servizio_id",
)
.orderBy("servizio_annunci.created_at", "desc"),
).as("annunci_servizio"),
jsonArrayFrom(
eb
.selectFrom("ordini")
.selectAll("ordini")
.innerJoin("prezziario", "prezziario.idprezziario", "ordini.packid")
.select("prezziario.testo_condizioni")
.whereRef("ordini.servizio_id", "=", "servizio.servizio_id")
.orderBy("ordini.created_at", "desc"),
).as("ordini_servizio"),
])
.orderBy("servizio.created_at", "desc")
.execute();
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Error fetching richieste: ${(e as Error).message}`,
});
}
};