import { TRPCError } from "@trpc/server"; import { add } from "date-fns"; import { sql } from "kysely"; 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 { AnnunciId } from "~/schemas/public/Annunci"; import OrderTypeEnum from "~/schemas/public/OrderTypeEnum"; import type { OrdiniOrdineId } from "~/schemas/public/Ordini"; import PaymentStatusEnum from "~/schemas/public/PaymentStatusEnum"; import type { Payments, PaymentsId, PaymentsUpdate, } from "~/schemas/public/Payments"; import type { Prezziario, PrezziarioIdprezziario, } from "~/schemas/public/Prezziario"; import type { NewServizio, ServizioServizioId, ServizioUpdate, } from "~/schemas/public/Servizio"; import type { ServizioAnnunciUpdate } from "~/schemas/public/ServizioAnnunci"; import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum"; import type { Users, UsersId } from "~/schemas/public/Users"; import type { UsersAnagraficaUpdate } from "~/schemas/public/UsersAnagrafica"; import type { Context } from "~/server/api/trpc"; import { addEventToQueue, canSendNotification, nextTimeForNotification, } from "~/server/controllers/event_queue.controller"; import { cleanupUnusedOrders } from "~/server/controllers/ordini.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"; export const addServizio = async (data: NewServizio) => { try { return await db .insertInto("servizio") .values(data) .returningAll() .executeTakeFirstOrThrow(); } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: `Error adding new servizio: ${(e as Error).message}`, }); } }; export const getServizioById = async (servizioId: ServizioServizioId) => { try { return await db .selectFrom("servizio") .selectAll() .where("servizio_id", "=", servizioId) .executeTakeFirstOrThrow(); } catch (e) { throw new TRPCError({ code: "NOT_FOUND", message: `Servizio not found: ${(e as Error).message}`, }); } }; export const updateServizio = async ({ servizioId, data, }: { servizioId: ServizioServizioId; data: ServizioUpdate; }) => { try { return await db .updateTable("servizio") .set(data) .where("servizio_id", "=", servizioId) .returningAll() .executeTakeFirstOrThrow(); } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: `Error updating servizio: ${(e as Error).message}`, }); } }; export const deleteServizio = async (servizioId: ServizioServizioId) => { try { await db .deleteFrom("servizio") .where("servizio_id", "=", servizioId) .executeTakeFirstOrThrow(); return true; } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: `Error deleting servizio: ${(e as Error).message}`, }); } }; 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 getPreOnboardData = 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(); if (!userData) { throw new TRPCError({ code: "NOT_FOUND", message: "User not found", }); } const hasPrevious = await db .selectFrom("servizio") .select("servizio_id") .where("user_id", "=", servizio.user_id) .where("servizio_id", "!=", servizioId) .where("isOkAcconto", "=", true) .executeTakeFirst(); return { hasPrevious: !!hasPrevious, servizio, userData }; } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: `Error fetching pre-onboard data: ${(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(); 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 postAcquistoDataHandler = async ({ servizioId, userId, user, anagrafica, servizio, skipOrdine = false, }: { servizioId: ServizioServizioId; userId: UsersId; user: Pick; anagrafica: UsersAnagraficaUpdate; servizio: ServizioUpdate; skipOrdine?: boolean; }) => { try { const servizioData = await db.transaction().execute(async (tx) => { await tx .updateTable("users") .set(user) .where("id", "=", userId) .executeTakeFirstOrThrow(); const { idanagrafica, ...rest } = anagrafica; if (idanagrafica) { await tx .updateTable("users_anagrafica") .set({ ...rest, userid: userId, }) .where("idanagrafica", "=", idanagrafica) .returningAll() .executeTakeFirstOrThrow(); } else { await tx .insertInto("users_anagrafica") .values({ ...rest, userid: userId, }) .returningAll() .executeTakeFirstOrThrow(); } await tx .updateTable("users_anagrafica") .set(anagrafica) .where("userid", "=", userId) .executeTakeFirstOrThrow(); return await tx .updateTable("servizio") .set(servizio) .where("servizio_id", "=", servizioId) .returningAll() .executeTakeFirstOrThrow(); }); if (skipOrdine) { await updateServizio({ data: { decorrenza: new Date(), isOkAcconto: true, }, servizioId, }); return null; } //cleanup unused payments await db .deleteFrom("payments") .where("servizio_id", "=", servizioId) .where("paymentstatus", "is", null) .execute(); if ( !servizioData || servizioData.isInterrotto || servizioData.isOkAcconto || servizioData.decorrenza !== null ) { throw new TRPCError({ code: "NOT_FOUND", message: "Cannot create payment for this servizioData", }); } const packId = servizioData.tipologia === TipologiaPosizioneEnum.Transitorio ? ("ACCONTO_BD" as PrezziarioIdprezziario) : ("ACCONTO_CA" as PrezziarioIdprezziario); const acconto = await db .selectFrom("prezziario") .select("idprezziario") .where("isActive", "=", true) .where("idprezziario", "=", packId) .executeTakeFirst(); if (!acconto) { throw new TRPCError({ code: "NOT_FOUND", message: "Acconto not found", }); } const existingOrder = await db .selectFrom("ordini") .select("ordini.ordine_id") .where("ordini.servizio_id", "=", servizioData.servizio_id) .where("ordini.packid", "=", acconto.idprezziario) .where("ordini.type", "=", OrderTypeEnum.Acconto) .where("ordini.isActive", "=", false) .orderBy("ordini.created_at", "desc") .executeTakeFirst(); if (existingOrder) { await db .deleteFrom("ordini") .where("ordini.isActive", "=", false) .where("ordine_id", "!=", existingOrder.ordine_id) .execute(); return existingOrder.ordine_id; } const order = await db .insertInto("ordini") .values({ packid: acconto.idprezziario, servizio_id: servizioData.servizio_id, type: OrderTypeEnum.Acconto, userid: userId, }) .returning("ordine_id") .executeTakeFirstOrThrow(); return order.ordine_id; } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: `Error updating data for acquisto: ${(e as Error).message}`, }); } }; const SaldoTransitorioMapping: Record = { 0: "SALDOBD_1MESE" as PrezziarioIdprezziario, 1: "SALDOBD_3MESI" as PrezziarioIdprezziario, 2: "SALDOBD_6MESI" as PrezziarioIdprezziario, 3: "SALDOBD_9MESI" as PrezziarioIdprezziario, 4: "SALDOBD_12MESI" as PrezziarioIdprezziario, }; export const setupSaldoServizio = async ({ servizioId, annuncioId, }: { servizioId: ServizioServizioId; annuncioId: AnnunciId; }) => { try { return await db.transaction().execute(async (tx) => { // Cleanup previous payments and orders await cleanupUnusedOrders({ db: tx }); const servizio = await tx .selectFrom("servizio") .selectAll() .where("servizio_id", "=", servizioId) .executeTakeFirstOrThrow(); if ( !servizio.isOkAcconto || servizio.isInterrotto || servizio.decorrenza == null || servizio.isOkSaldo ) { throw new TRPCError({ code: "NOT_FOUND", message: "Servizio not found", }); } const annuncio = await tx .selectFrom("servizio_annunci") .selectAll() .where("servizio_id", "=", servizioId) .where("annunci_id", "=", annuncioId) .executeTakeFirstOrThrow(); if ( annuncio.user_confirmed_at == null || annuncio.accettato_conferma_at == null ) { throw new TRPCError({ code: "NOT_FOUND", message: "Annuncio not found", }); } let saldo: | Pick< Prezziario, "idprezziario" | "prezzo_cent" | "nome_it" | "nome_en" | "sconto" > | undefined; const qry = tx .selectFrom("prezziario") .select(["idprezziario", "prezzo_cent", "nome_it", "nome_en", "sconto"]) .where("isActive", "=", true); if (servizio.tipologia === TipologiaPosizioneEnum.Transitorio) { const mpp = servizio.permanenza && SaldoTransitorioMapping[servizio.permanenza]; if (!servizio.permanenza || !mpp) { throw new TRPCError({ code: "NOT_FOUND", message: "Permanenza not set for transitorio servizio", }); } saldo = await qry.where("idprezziario", "=", mpp).executeTakeFirst(); } else if (servizio.tipologia === TipologiaPosizioneEnum.Stabile) { if (servizio.budget >= 500) { saldo = await qry .where("idprezziario", "=", "SALDOCA500+" as PrezziarioIdprezziario) .executeTakeFirst(); } else { saldo = await qry .where("idprezziario", "=", "SALDOCA500" as PrezziarioIdprezziario) .executeTakeFirst(); } } if (!saldo) { throw new TRPCError({ code: "NOT_FOUND", message: "Acconto not found", }); } await tx .deleteFrom("payments") .where("servizio_id", "=", servizioId) .where("paymentstatus", "is", null) .execute(); const order = await db .insertInto("ordini") .values({ packid: saldo.idprezziario, servizio_id: servizio.servizio_id, type: OrderTypeEnum.Saldo, userid: servizio.user_id, }) .returning("ordine_id") .executeTakeFirstOrThrow(); return order.ordine_id; }); } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: `Error setting up saldo servizio: ${(e as Error).message}`, }); } }; const ConsulenzaPriceMapping = { "4 + 4": "CONS_4+4" as PrezziarioIdprezziario, "30 Giorni": "CONS_30GG" as PrezziarioIdprezziario, Agevolato: "CONS_3+2" as PrezziarioIdprezziario, Commerciale: "CONS_COMM" as PrezziarioIdprezziario, Foresteria: "CONS_COMM" as PrezziarioIdprezziario, "Transi.Age": "CONS_3+2" as PrezziarioIdprezziario, Transitorio: "CONS_TRAN" as PrezziarioIdprezziario, }; type TipoEnum = keyof typeof ConsulenzaPriceMapping; export const setupSaldoConsulenzaServizio = async ({ servizioId, annuncioId, }: { servizioId: ServizioServizioId; annuncioId: AnnunciId; }) => { try { return await db.transaction().execute(async (tx) => { // Cleanup previous payments and orders await cleanupUnusedOrders({ db: tx }); const servizio = await tx .selectFrom("servizio") .selectAll() .where("servizio_id", "=", servizioId) .executeTakeFirstOrThrow(); if ( !servizio.isOkAcconto || servizio.isInterrotto || servizio.decorrenza == null || !servizio.isOkSaldo || servizio.isOkConsulenza ) { throw new TRPCError({ code: "NOT_FOUND", message: "Non serve saldo per questo servizio", }); } const annuncio = await tx .selectFrom("servizio_annunci") .selectAll() .where("servizio_id", "=", servizioId) .where("annunci_id", "=", annuncioId) .executeTakeFirstOrThrow(); if ( annuncio.user_confirmed_at == null || annuncio.accettato_conferma_at == null || !annuncio.contratto_tipo ) { throw new TRPCError({ code: "NOT_FOUND", message: "Annuncio not found or contratto not set", }); } if ( !Object.keys(ConsulenzaPriceMapping).includes(annuncio.contratto_tipo) ) { throw new TRPCError({ code: "NOT_FOUND", message: "Consulenza type not found", }); } const pack = await tx .selectFrom("prezziario") .select(["idprezziario", "prezzo_cent", "nome_it", "nome_en", "sconto"]) .where("isActive", "=", true) .where( "idprezziario", "=", ConsulenzaPriceMapping[annuncio.contratto_tipo as TipoEnum], ) .executeTakeFirst(); if (!pack) { throw new TRPCError({ code: "NOT_FOUND", message: "Pack not found", }); } await tx .deleteFrom("payments") .where("servizio_id", "=", servizioId) .where("paymentstatus", "is", null) .execute(); const order = await db .insertInto("ordini") .values({ packid: pack.idprezziario, servizio_id: servizio.servizio_id, type: OrderTypeEnum.Consulenza, userid: servizio.user_id, }) .returning("ordine_id") .executeTakeFirstOrThrow(); return order.ordine_id; }); } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: `Error setting up consulenza servizio: ${(e as Error).message}`, }); } }; export type ServizioData = Awaited< ReturnType >[number]; export const getAllServizioAnnunci = async (userId: UsersId) => { try { return await db .selectFrom("servizio") .where("user_id", "=", userId) .select((eb) => [ "servizio.servizio_id", "servizio.created_at", "servizio.decorrenza", "servizio.tipologia", "servizio.budget", "servizio.permanenza", "servizio.isInterrotto", "servizio.isOkAcconto", "servizio.isOkSaldo", "servizio.isOkConsulenza", jsonArrayFrom( eb .selectFrom("servizio_annunci") .selectAll("servizio_annunci") .innerJoin("annunci", "annunci.id", "servizio_annunci.annunci_id") .select([ "annunci.id", "annunci.codice", "annunci.prezzo", "annunci.desc_en", "annunci.desc_it", "annunci.url_immagini", "annunci.titolo_en", "annunci.titolo_it", "annunci.tipo", "annunci.consegna", "annunci.stato", "annunci.web", ]) //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().nullsFirst(), ) .orderBy("servizio_annunci.created_at", "asc") .whereRef( "servizio_annunci.servizio_id", "=", "servizio.servizio_id", ), ).as("annunci"), ]) .orderBy("servizio.created_at", "desc") .execute(); } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: `Error fetching servizio annunci: ${(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 getServizioAnnuncio = async ({ servizioId, annuncioId, }: { servizioId: ServizioServizioId; annuncioId: AnnunciId; }) => { try { return await db .selectFrom("servizio_annunci") .selectAll() .where("servizio_id", "=", servizioId) .where("annunci_id", "=", annuncioId) .executeTakeFirstOrThrow(); } catch (e) { throw new TRPCError({ code: "NOT_FOUND", message: `Servizio not found: ${(e as Error).message}`, }); } }; export const updateServizioAnnuncio = async ({ servizioId, annuncioId, data, }: { servizioId: ServizioServizioId; annuncioId: AnnunciId; data: ServizioAnnunciUpdate; }) => { try { await db .updateTable("servizio_annunci") .set(data) .where("servizio_id", "=", servizioId) .where("annunci_id", "=", annuncioId) .execute(); const servizio = await getServizioById(servizioId); const utente = await getUser({ db, userId: servizio.user_id }); //ADMIN CONFERMA LAVORATA EMAIL if (data.hasConfermaAdmin) { await NewMail({ mailType: "generic", props: { link: { href: `${env.BASE_URL}/area-riservata/dashboard#${servizioId}-${annuncioId}`, 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", }, subject: "Conferma lavorata per il servizio di ricerca affitto", to: utente.email, userId: utente.id, }); } // USER ACCETTA CONFERMA EMAIL if (data.accettato_conferma_at) { await NewMail({ 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`, }, subject: `Utente ${utente.username} ha accettato le condizioni di conferma`, to: "web@infoalloggi.it", }); } return true; } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: `Error updating servizio from 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"]) .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 deleteServizioAnnuncio = async ( servizioId: ServizioServizioId, annuncioId: AnnunciId, ) => { try { await db .deleteFrom("servizio_annunci") .where("servizio_id", "=", servizioId) .where("annunci_id", "=", annuncioId) .execute(); return true; } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: `Error deleting servizio from annuncio: ${(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 ? { 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, }, subject: "Un interessato ha ricevuto i Suoi contatti", to: data.annuncio.email, } : null; const EmailInteressato: NewMailProps = { mailType: "generic", props: { link: { href: `${env.BASE_URL}/area-riservata/dashboard#${servizioId}-${annuncioId}`, label: "Vai al servizio", }, noreply: true, testo: `I contatti per l'annuncio ${data.annuncio.codice} sono stati sbloccati con successo.`, title: "Contatti sbloccati per il servizio di ricerca affitto", }, subject: "Contatti sbloccati per il servizio di ricerca affitto", to: data.user.email, }; 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 NewMail(EmailInteressato); 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) .executeTakeFirstOrThrow(); // NOTIFICHE EMAIL INTERRUZIONE SERVIZIO const utente = await getUser({ db, userId: servizio.user_id }); await NewMail({ 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`, }, subject: `Servizio Ricerca Affitto ${servizio.tipologia} interrotto`, to: utente.email, userId: utente.id, }); await NewMail({ 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`, }, 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 sendServizioEmail = async ({ servizioId, userId, }: { servizioId: ServizioServizioId; userId: UsersId; }) => { try { const user = await db .selectFrom("users") .select(["email", "nome"]) .where("id", "=", userId) .executeTakeFirstOrThrow(); if (!user) { throw new TRPCError({ code: "NOT_FOUND", message: "User not found", }); } const annunci = await db .selectFrom("servizio_annunci") .innerJoin("annunci", "annunci.id", "servizio_annunci.annunci_id") .select([ "annunci.titolo_it", "annunci.url_immagini", "annunci.codice", "annunci.prezzo", ]) .where("servizio_annunci.servizio_id", "=", servizioId) .execute(); const { email, nome } = user; await NewMail({ mailType: "onboardingServizio", props: { annunci: annunci.map((annuncio) => ({ codice: annuncio.codice, immagine: annuncio.url_immagini ? annuncio.url_immagini[0] || null : null, prezzo: annuncio.prezzo, titolo: annuncio.titolo_it, })), nome, token: servizioId, }, subject: "Procedi ora su Infoalloggi.it", to: email, userId, }); await db .updateTable("servizio") .set({ isSent: true, }) .where("servizio_id", "=", servizioId) .executeTakeFirstOrThrow(); return true; } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: `Error sending email: ${(e as Error).message}`, }); } }; export const userConfirmImmobile = 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) .executeTakeFirstOrThrow(); // 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(); await NewMail({ 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}`, }, subject: `Nuova conferma immobile ${annuncio_codice.codice}`, to: "web@infoalloggi.it", }); await NewMail({ mailType: "generic", props: { noreply: true, testo: `La tua conferma per l'immobile con codice ${annuncio_codice.codice} è stata ricevuta. Il nostro team la esaminerà a breve. Riceverai una notifica via email per procedere.`, title: `Conferma immobile ${annuncio_codice.codice} ricevuta`, }, subject: `Conferma immobile ${annuncio_codice.codice} ricevuta`, 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 isOrdineAwaitingPayment = async (ordineId: OrdiniOrdineId) => { try { const ordine = await db .selectFrom("ordini") .select("ordine_id") .where("ordine_id", "=", ordineId) .where("isActive", "=", false) .executeTakeFirst(); if (!ordine) { console.error("Ordine not found:", ordineId); return false; } const payment = await db .selectFrom("payments") .select("id") .where("ordine_id", "=", ordineId) .where("paymentstatus", "=", PaymentStatusEnum.success) .executeTakeFirst(); if (!payment) { return true; } console.error("Payment already exists for ordine:", ordineId); return false; } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: `Error checking ordine payment status: ${(e as Error).message}`, }); } }; export const getServizioPagamentoData = async (ordineId: OrdiniOrdineId) => { try { await db .deleteFrom("payments") .where("ordine_id", "=", ordineId) .where("paymentstatus", "is", null) .execute(); const ordine = await db .selectFrom("ordini") .selectAll() .where("ordine_id", "=", ordineId) .executeTakeFirstOrThrow(); const pack = await db .selectFrom("prezziario") .selectAll() .where("idprezziario", "=", ordine.packid) .executeTakeFirstOrThrow(); const payment = await db .insertInto("payments") .values({ amount_cent: pack.prezzo_cent, ordine_id: ordineId, paymentname: pack.nome_it, servizio_id: ordine.servizio_id, userid: ordine.userid, }) .returningAll() .executeTakeFirstOrThrow(); const servizio = await db .selectFrom("servizio") .selectAll() .where("servizio.servizio_id", "=", payment.servizio_id) .executeTakeFirstOrThrow(); if (!servizio) { throw new TRPCError({ code: "NOT_FOUND", message: "Servizio not found", }); } return { ordine, payment, servizio, }; } catch (e) { throw new TRPCError({ code: "NOT_FOUND", message: `Servizio not found: ${(e as Error).message}`, }); } }; type Pagamento = Payments & Pick; export const getAllPagamenti = async (): Promise => { try { return await db .selectFrom("payments") .selectAll() .innerJoin("users", "users.id", "payments.userid") .select("users.username") .orderBy("payments.created_at", "desc") .execute(); } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: `Error fetching user pagamenti: ${(e as Error).message}`, }); } }; export const updatePagamento = async ({ paymentId, data, }: { paymentId: PaymentsId; data: PaymentsUpdate; }) => { try { return await db .updateTable("payments") .set(data) .where("id", "=", paymentId) .returningAll() .executeTakeFirstOrThrow(); } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: `Error updating pagamento: ${(e as Error).message}`, }); } }; export const removePagamento = async (paymentId: PaymentsId) => { try { await db.deleteFrom("payments").where("id", "=", paymentId).execute(); return true; } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: `Error removing pagamento: ${(e as Error).message}`, }); } }; export const getUserPagamenti = async ( userId: UsersId, ): Promise => { try { return await db .selectFrom("payments") .selectAll() .select([sql.lit("").as("username")]) .where("payments.userid", "=", userId) .orderBy("payments.created_at", "desc") .execute(); } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: `Error fetching user pagamenti: ${(e as Error).message}`, }); } }; export const SendContactEmail = async ({ codice, nome, email, telefono, messaggio, }: { codice: string; nome: string; email: string; telefono: string; messaggio: string; }) => { try { await NewMail({ mailType: "contattoAnnuncioEmail", props: { codice, email, messaggio, nome, telefono, }, 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: ServizioServizioId) => { try { const servizio = await getServizioById(servizioId); if (!servizio) { throw new TRPCError({ code: "NOT_FOUND", message: "Servizio not found", }); } let qry = db .selectFrom("annunci") .select([ "annunci.id", "annunci.codice", "annunci.prezzo", "annunci.desc_en", "annunci.desc_it", "annunci.url_immagini", "annunci.titolo_en", "annunci.titolo_it", "annunci.tipo", "annunci.consegna", "annunci.stato", "annunci.web", ]) .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), ) .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); } const annunci = await qry.execute(); return annunci; } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: `Error fetching compatibile annunci: ${(e as Error).message}`, }); } }; export const getOrdineRicevutaData = async ({ ordineId, ctx, }: { ordineId: OrdiniOrdineId; ctx: Context; }) => { try { const data = await db .selectFrom("payments") .select([ "payments.id", "payments.userid", "payments.amount_cent", "payments.paymentname", "payments.created_at", "payments.paymentmethod", ]) .select((eb) => [ jsonObjectFrom( eb .selectFrom("users") .whereRef("users.id", "=", "payments.userid") .select(["users.nome", "users.cognome"]) .limit(1), ).as("user"), jsonObjectFrom( eb .selectFrom("users_anagrafica") .whereRef("users_anagrafica.userid", "=", "payments.userid") .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", ]) .limit(1), ).as("anagrafica"), ]) .where("payments.ordine_id", "=", ordineId) .where("payments.paymentstatus", "=", PaymentStatusEnum.success) .executeTakeFirstOrThrow(); if (!ctx.session) { throw new TRPCError({ code: "UNAUTHORIZED", message: "User not authenticated", }); } if (!ctx.session.isAdmin && data.userid !== ctx.session.id) { throw new TRPCError({ code: "FORBIDDEN", message: "You do not have permission to access this ordine", }); } if (!data || !data.user || !data.anagrafica) { throw new TRPCError({ code: "NOT_FOUND", message: "Ordine not found", }); } return { clienteIndirizzo: `${data.anagrafica.via_residenza} ${data.anagrafica.civico_residenza}\n${data.anagrafica.comune_residenza} (${data.anagrafica.provincia_residenza}) ${data.anagrafica.cap_residenza}\n${data.anagrafica.nazione_residenza}`, clienteNome: `${data.user.nome} ${data.user.cognome}`, date: data.created_at, items: [ { description: data.paymentname, discount: 0, // Assuming no discount for simplicity price: data.amount_cent / 100, // Convert cents to euros }, ], paymentId: data.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}`, }); } };