import { TRPCError } from "@trpc/server"; import { add } from "date-fns"; 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 { Annunci, AnnunciId } from "~/schemas/public/Annunci"; import OrderTypeEnum from "~/schemas/public/OrderTypeEnum"; import type { Ordini, OrdiniOrdineId } from "~/schemas/public/Ordini"; import PaymentStatusEnum from "~/schemas/public/PaymentStatusEnum"; import type { Prezziario } from "~/schemas/public/Prezziario"; import type { Servizio, 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"; import type { Users, UsersId } from "~/schemas/public/Users"; import type { UsersAnagraficaUpdate } from "~/schemas/public/UsersAnagrafica"; import type { UsersStorageUserStorageId } from "~/schemas/public/UsersStorage"; import { addEventToQueue, canSendNotification, nextTimeForNotification, } from "~/server/controllers/event_queue.controller"; import { type MinimalSMSData, SendMessage, } from "~/server/controllers/skebby.controller"; 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 { SaldoSolver } from "./pagamenti.controller"; export const getServiziByUserId = async (userId: UsersId) => { 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}`, }); } }; export const getDataPerAcquisto = async (servizioId: ServizioServizioId) => { 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}`), ); 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 { anagrafica, servizio, userData, }; } catch (e) { throw new TRPCError({ code: "NOT_FOUND", message: `Servizio not found: ${(e as Error).message}`, }); } }; export const processServizioOnboard = async ({ servizioId, userId, user, anagrafica, servizio, }: { servizioId: ServizioServizioId; userId: UsersId; user: Pick; anagrafica: UsersAnagraficaUpdate; servizio: ServizioUpdate; }) => { try { await db.transaction().execute(async (tx) => { // Update user and anagrafica data await tx .updateTable("users") .set(user) .where("id", "=", userId) .execute(); 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}`, ), ); } else { await tx .insertInto("users_anagrafica") .values({ ...rest, userid: userId, }) .returningAll() .executeTakeFirstOrThrow( () => new Error("Failed to insert new users_anagrafica"), ); } // Update servizio data return await tx .updateTable("servizio") .set({ ...servizio, onboardOk: true }) .where("servizio_id", "=", servizioId) .returningAll() .executeTakeFirstOrThrow( () => new Error(`Failed to update servizio - servizioId: ${servizioId}`), ); }); return true; } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: `Error updating data for acquisto: ${(e as Error).message}`, }); } }; /** * 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, ) => { 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}`), ); await tx .updateTable("servizio") .set({ decorrenza: new Date(), isOkAcconto: true, }) .where("servizio_id", "=", ordine.servizio_id) .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; }); } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: `Error activating servizio without payment: ${(e as Error).message}`, }); } }; 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)[]; }; export const getAllServizioAnnunci = async ( userId: UsersId, ): Promise => { try { const data = await db .selectFrom("servizio") .where("user_id", "=", userId) .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") .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; } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: `Error fetching servizio annunci: ${(e as Error).message}`, }); } }; export const getServizioDataById = async ( servizioId: ServizioServizioId, ): Promise => { 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}`, }); } }; export const addServizioAnnunci = async ({ servizioId, annunci, }: { servizioId: ServizioServizioId; annunci: AnnunciId[]; }) => { 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, }), servizio_id: servizioId, })), ) .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}`, }); } }; export const getAnnunciAvailableToAdd = async ( servizioId: ServizioServizioId, ) => { 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"]) .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}`, }); } }; export const SbloccaContatti = async ({ servizioId, annuncioId, }: { servizioId: ServizioServizioId; annuncioId: AnnunciId; }) => { try { const alreadyUnlocked = await db .selectFrom("servizio_annunci") .where("servizio_id", "=", servizioId) .where("open_contatti_at", "is not", null) .select((eb) => [eb.fn.count("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, servizioId, user, }; }); 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, }, } : 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}.`, recipient: [numeroProprietario], } : 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() + 1000), }); if (canSendNotification()) { if (EmailProprietario) { await NewMail(EmailProprietario); } if (SmsProprietario) { await SendMessage(SmsProprietario); } } else { const lock_expires = nextTimeForNotification(); if (EmailProprietario) { await addEventToQueue({ data: { data: EmailProprietario, tipo: "email", }, lock_expires, }); } if (SmsProprietario) { await addEventToQueue({ data: { data: SmsProprietario, tipo: "sms", }, lock_expires, }); } } return true; } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: `Error unlocking contatti: ${(e as Error).message}`, }); } }; export const interruzioneServizio = async (servizioId: ServizioServizioId) => { 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(); // 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, }, userId: utente.id, }); 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`, }, }, mail: { subject: `Utente ${utente.username} ha richieto l'interruzione del servizio di ricerca affitto`, to: "web@infoalloggi.it", }, }); return true; } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: `Error interruzione servizio: ${(e as Error).message}`, }); } }; export const InteractionLogicTipologia = async ({ tipologia, userId, annuncioId, }: { tipologia: string; userId: UsersId; annuncioId: AnnunciId; }) => { const parse = z.custom(); 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, status: "already_saved" as const, }; } return { servizioId: servizi.servizio_id, status: "found" as const, tipologia: servizi.tipologia, }; }; export const userSendConfermaIntent = async ({ servizioId, annuncioId, }: { servizioId: ServizioServizioId; annuncioId: AnnunciId; }) => { 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(); // 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}`, ), ); 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}`, }, }, mail: { subject: `Nuova conferma immobile ${annuncio_codice.codice}`, to: "web@infoalloggi.it", }, }); 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, }, userId: utente.id, }); return true; } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: `Error confirming immobile: ${(e as Error).message}`, }); } }; 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(servizio); 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}`, }); } }; export const SendContattoAnnuncioEmail = async ({ codice, nome, email, telefono, messaggio, }: { codice: string; nome: string; email: string; telefono: string; messaggio: string; }) => { try { await NewMail({ template: { mailType: "contattoAnnuncioEmail", props: { codice, email, messaggio, nome, telefono, }, }, mail: { subject: "Contatto per annuncio", to: "web@infoalloggi.it", }, }); return { email, nome, telefono, }; } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: `Error sending email: ${(e as Error).message}`, }); } }; export const getCompatibileAnnunci = async ({ servizioId, adminOverride = false, }: { servizioId: ServizioServizioId; adminOverride?: boolean; }): Promise<(AnnuncioRicerca & { alreadyRequested: boolean })[]> => { 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); 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"), ]) .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 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(); } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: `Error fetching compatibile annunci: ${(e as Error).message}`, }); } }; export const getOrdineRicevutaData = async ({ ordineId, }: { ordineId: OrdiniOrdineId; }) => { try { const data = await db .selectFrom("ordini") .select([ "ordini.ordine_id", "ordini.userid", "ordini.amount_cent", "ordini.sconto", "ordini.created_at", "ordini.paymentmethod", ]) .innerJoin("prezziario", "prezziario.idprezziario", "ordini.packid") .select("prezziario.nome_it") .where("ordini.ordine_id", "=", ordineId) .where("ordini.paymentstatus", "=", PaymentStatusEnum.success) .executeTakeFirstOrThrow( () => new Error(`Ordine not found - ordineId: ${ordineId}`), ); 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", ]) .where("users.id", "=", data.userid) .executeTakeFirstOrThrow( () => new Error(`Anagrafica not found - userid: ${data.userid}`), ); return { clienteIndirizzo: `${anagrafica.via_residenza} ${anagrafica.civico_residenza}\n${anagrafica.comune_residenza} (${anagrafica.provincia_residenza}) ${anagrafica.cap_residenza}\n${anagrafica.nazione_residenza}`, clienteNome: anagrafica.username, date: data.created_at, packName: data.nome_it, sconto: data.sconto, amount_cent: data.amount_cent, ordineId: data.ordine_id, paymentMethod: data.paymentmethod, } as PurchaseData; } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: `Error fetching ordine ricevuta data: ${(e as Error).message}`, }); } };