import type { NewServizio, ServizioServizioId, ServizioUpdate, } from "~/schemas/public/Servizio"; import { db } from "~/server/db"; import { TRPCError } from "@trpc/server"; import type { Users, UsersId } from "~/schemas/public/Users"; import type { UsersAnagraficaUpdate } from "~/schemas/public/UsersAnagrafica"; import { jsonArrayFrom, jsonObjectFrom } from "kysely/helpers/postgres"; import type { AnnunciId } from "~/schemas/public/Annunci"; import { add } from "date-fns"; import { z } from "zod/v4"; import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum"; import { NewMail, type NewMailProps } from "~/server/services/mailer"; import type { ServizioAnnunciUpdate } from "~/schemas/public/ServizioAnnunci"; import type { Payments, PaymentsId, PaymentsUpdate, } from "~/schemas/public/Payments"; import { sql } from "kysely"; import type { Prezziario, PrezziarioIdprezziario, } from "~/schemas/public/Prezziario"; import OrderTypeEnum from "~/schemas/public/OrderTypeEnum"; import type { OrdiniOrdineId } from "~/schemas/public/Ordini"; import PaymentStatusEnum from "~/schemas/public/PaymentStatusEnum"; import type { PurchaseData } from "~/components/acquisto_receipt"; import type { Context } from "~/server/api/trpc"; import { getUser } from "~/server/services/user.service"; import { env } from "~/env.mjs"; import { cleanupUnusedOrders } from "~/server/controllers/ordini.controller"; import { addEventToQueue, canSendNotification, nextTimeForNotification, } from "~/server/controllers/event_queue.controller"; import { SendMessage, type MinimalSMSData, } from "~/server/controllers/skebby.controller"; 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 { servizio, userData, hasPrevious: !!hasPrevious }; } 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 { servizio, userData, anagrafica, }; } 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({ servizioId, data: { isOkAcconto: true, decorrenza: new Date(), }, }); 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({ servizio_id: servizioData.servizio_id, packid: acconto.idprezziario, 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({ servizio_id: servizio.servizio_id, packid: saldo.idprezziario, 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 = { Transitorio: "CONS_TRAN" as PrezziarioIdprezziario, Agevolato: "CONS_3+2" as PrezziarioIdprezziario, "4 + 4": "CONS_4+4" as PrezziarioIdprezziario, "30 Giorni": "CONS_30GG" as PrezziarioIdprezziario, "Transi.Age": "CONS_3+2" as PrezziarioIdprezziario, Foresteria: "CONS_COMM" as PrezziarioIdprezziario, Commerciale: "CONS_COMM" 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({ servizio_id: servizio.servizio_id, packid: pack.idprezziario, 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.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) => ({ servizio_id: servizioId, annunci_id: annuncio, created_at: add(new Date(), { //trick per assicurare che la data sia sempre diversa seconds: idx, }), })), ) .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({ userId: utente.id, mailType: "generic", to: utente.email, subject: "Conferma lavorata per il servizio di ricerca affitto", props: { noreply: true, title: "Conferma lavorata per il servizio di ricerca affitto", testo: `La conferma è stata lavorata con successo. Puoi procedere con le prossime azioni.`, link: { href: `${env.NEXT_PUBLIC_BASE_URL}/area-riservata/dashboard#${servizioId}-${annuncioId}`, label: "Vai al servizio", }, }, }); } // USER ACCETTA CONFERMA EMAIL if (data.accettato_conferma_at) { await NewMail({ mailType: "generic", to: "web@infoalloggi.it", subject: `Utente ${utente.username} ha accettato le condizioni di conferma`, props: { noreply: true, title: `Utente ${utente.username} ha accettato le condizioni di conferma`, testo: `L'utente ha accettato le condizioni di conferma per la locazione. Puoi procedere con le prossime azioni.`, link: { href: `${env.NEXT_PUBLIC_BASE_URL}/area-riservata/admin/user-view/ricerca/${servizio.user_id}#${servizioId}-${annuncioId}`, label: "Vai al servizio", }, }, }); } 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 { user, anagrafica, annuncio, servizioId, annuncioId, }; }); const EmailProprietario: NewMailProps | null = data.annuncio.email ? { mailType: "emailContattoInteressato", to: data.annuncio.email, subject: "Un interessato ha ricevuto i Suoi contatti", props: { indirizzo: data.annuncio.indirizzo, nome_cognome: data.annuncio.locatore || "", nome_cognome_utente: data.user.username, telefono: data.user.telefono, sesso: data.anagrafica.sesso, }, } : null; const EmailInteressato: NewMailProps = { mailType: "generic", to: data.user.email, subject: "Contatti sbloccati per il servizio di ricerca affitto", props: { noreply: true, title: "Contatti sbloccati per il servizio di ricerca affitto", testo: `I contatti per l'annuncio ${data.annuncio.codice} sono stati sbloccati con successo.`, link: { href: `${env.NEXT_PUBLIC_BASE_URL}/area-riservata/dashboard#${servizioId}-${annuncioId}`, label: "Vai al servizio", }, }, }; const numeroProprietario = data.annuncio.numero && data.annuncio.numero.startsWith("+39") && data.annuncio.numero[3] == "3" ? data.annuncio.numero.split(",")[0] : null; const SmsProprietario: MinimalSMSData | null = numeroProprietario ? { recipient: [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}.`, } : 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({ lock_expires, data: { tipo: "email", data: EmailProprietario, }, }); } if (SmsProprietario) { await addEventToQueue({ lock_expires, data: { tipo: "sms", data: SmsProprietario, }, }); } } 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({ userId: utente.id, mailType: "generic", to: utente.email, subject: `Servizio Ricerca Affitto ${servizio.tipologia} interrotto`, props: { noreply: true, title: `Servizio Ricerca Affitto ${servizio.tipologia} interrotto`, testo: `Abbiamo ricevuto la tua richiesta di interruzione del servizio di ricerca affitto per la tipologia ${servizio.tipologia}. Il servizio è stato interrotto con successo.`, }, }); await NewMail({ mailType: "generic", to: "web@infoalloggi.it", subject: `Utente ${utente.username} ha richieto l'interruzione del servizio di ricerca affitto`, props: { noreply: true, title: `Utente ${utente.username} ha richieto l'interruzione del servizio di ricerca affitto`, testo: `L'utente ha richiesto l'interruzione del servizio di ricerca affitto per la tipologia ${servizio.tipologia}. Puoi procedere con le prossime azioni.`, link: { href: `${env.NEXT_PUBLIC_BASE_URL}/area-riservata/admin/user-view/ricerca/${servizio.user_id}#${servizioId}`, label: "Vai al servizio", }, }, }); 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 { status: "already_saved" as const, servizioId: servizi.servizio_id, }; } return { status: "found" as const, servizioId: servizi.servizio_id, 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({ userId, mailType: "onboardingServizio", to: email, props: { nome, token: servizioId, annunci: annunci.map((annuncio) => ({ titolo: annuncio.titolo_it, immagine: annuncio.url_immagini ? annuncio.url_immagini[0] || null : null, codice: annuncio.codice, prezzo: annuncio.prezzo, })), }, subject: "Procedi ora su Infoalloggi.it", }); 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", to: "web@infoalloggi.it", subject: `Nuova conferma immobile ${annuncio_codice.codice}`, props: { noreply: true, title: `Nuova conferma immobile ${annuncio_codice.codice}`, testo: `L'utente ${utente.username} ha confermato l'immobile con codice ${annuncio_codice.codice}.`, link: { href: `${env.NEXT_PUBLIC_BASE_URL}/area-riservata/admin/user-view/ricerca/${servizio.user_id}#${servizioId}-${annuncioId}`, label: "Visualizza Annuncio", }, }, }); await NewMail({ userId: utente.id, mailType: "generic", to: utente.email, subject: `Conferma immobile ${annuncio_codice.codice} ricevuta`, props: { noreply: true, title: `Conferma immobile ${annuncio_codice.codice} ricevuta`, 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.`, }, }); 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({ ordine_id: ordineId, servizio_id: ordine.servizio_id, amount_cent: pack.prezzo_cent, paymentname: pack.nome_it, 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 { servizio, ordine, payment, }; } 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", to: "web@infoalloggi.it", props: { nome, email, telefono, codice, messaggio, }, subject: "Contatto per annuncio", }); return { nome, email, 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 { paymentId: data.id, date: data.created_at, clienteNome: `${data.user.nome} ${data.user.cognome}`, 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}`, paymentMethod: data.paymentmethod, items: [ { description: data.paymentname, price: data.amount_cent / 100, // Convert cents to euros discount: 0, // Assuming no discount for simplicity }, ], } as PurchaseData; } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Error fetching ordine ricevuta data: " + (e as Error).message, }); } };