1882 lines
46 KiB
TypeScript
1882 lines
46 KiB
TypeScript
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";
|
|
import { withImages, withVideos } from "~/utils/kysely-helper";
|
|
import { GetUserInterests } from "../services/interests.service";
|
|
import { getPrezziarioByIdHandler } from "../services/prezziario.service";
|
|
import type { AnnuncioRicerca } from "./annunci.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 { 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,
|
|
}: {
|
|
servizioId: ServizioServizioId;
|
|
userId: UsersId;
|
|
user: Pick<Users, "nome" | "cognome" | "email" | "telefono" | "username">;
|
|
anagrafica: UsersAnagraficaUpdate;
|
|
servizio: ServizioUpdate;
|
|
}): Promise<{ ordineIdForPayment: OrdiniOrdineId | null }> => {
|
|
try {
|
|
const servizioData = await db.transaction().execute(async (tx) => {
|
|
// Update user and anagrafica data
|
|
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();
|
|
}
|
|
// Update servizio data
|
|
return await tx
|
|
.updateTable("servizio")
|
|
.set(servizio)
|
|
.where("servizio_id", "=", servizioId)
|
|
.returningAll()
|
|
.executeTakeFirstOrThrow();
|
|
});
|
|
|
|
if (servizioData.skipPayment) {
|
|
// If skipping order, set servizio as active with decorrenza today
|
|
await updateServizio({
|
|
data: {
|
|
decorrenza: new Date(),
|
|
isOkAcconto: true,
|
|
isOkSaldo: true,
|
|
isOkConsulenza: true,
|
|
},
|
|
servizioId,
|
|
});
|
|
return { ordineIdForPayment: 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 { ordineIdForPayment: 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 { ordineIdForPayment: 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<number, PrezziarioIdprezziario> = {
|
|
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 GetPackError = { success: false; message: string };
|
|
type GetPackResult = { success: true; pack: Prezziario };
|
|
|
|
type ServizioPacks = {
|
|
acconto: GetPackResult | GetPackError;
|
|
saldo: GetPackResult | GetPackError;
|
|
};
|
|
export const getPacksPerServizio = async (
|
|
servizioId: ServizioServizioId,
|
|
): Promise<ServizioPacks> => {
|
|
const servizio = await getServizioById(servizioId);
|
|
if (!servizio) {
|
|
throw new TRPCError({
|
|
code: "NOT_FOUND",
|
|
message: "Servizio non trovato",
|
|
});
|
|
}
|
|
|
|
const getAcconto = async (): Promise<GetPackResult | GetPackError> => {
|
|
switch (servizio.tipologia) {
|
|
case TipologiaPosizioneEnum.Transitorio: {
|
|
const pack = await getPrezziarioByIdHandler({
|
|
id: "ACCONTO_BD" as PrezziarioIdprezziario,
|
|
});
|
|
|
|
if (!pack) {
|
|
return { success: false, message: "Pack non trovato" };
|
|
}
|
|
return {
|
|
success: true,
|
|
pack,
|
|
};
|
|
}
|
|
|
|
case TipologiaPosizioneEnum.Stabile: {
|
|
const pack = await getPrezziarioByIdHandler({
|
|
id: "ACCONTO_CA" as PrezziarioIdprezziario,
|
|
});
|
|
|
|
if (!pack) {
|
|
return { success: false, message: "Pack non trovato" };
|
|
}
|
|
return {
|
|
success: true,
|
|
pack,
|
|
};
|
|
}
|
|
default:
|
|
return { success: false, message: "Seleziona tipologia" };
|
|
}
|
|
};
|
|
|
|
const getSaldo = async (): Promise<GetPackResult | GetPackError> => {
|
|
switch (servizio.tipologia) {
|
|
case TipologiaPosizioneEnum.Transitorio: {
|
|
const mpp =
|
|
servizio.permanenza && SaldoTransitorioMapping[servizio.permanenza];
|
|
if (!servizio.permanenza || !mpp) {
|
|
return {
|
|
success: false,
|
|
message: "Permanenza non selezionata",
|
|
};
|
|
}
|
|
const pack = await getPrezziarioByIdHandler({ id: mpp });
|
|
if (!pack) {
|
|
return { success: false, message: "Pack non trovato" };
|
|
}
|
|
|
|
return { success: true, pack };
|
|
}
|
|
case TipologiaPosizioneEnum.Stabile: {
|
|
const pack = await getPrezziarioByIdHandler({
|
|
id: (servizio.budget >= 500
|
|
? "SALDOCA500+"
|
|
: "SALDOCA500") as PrezziarioIdprezziario,
|
|
});
|
|
if (!pack) {
|
|
return { success: false, message: "Pack non trovato" };
|
|
}
|
|
return {
|
|
success: true,
|
|
pack,
|
|
};
|
|
}
|
|
default:
|
|
return { success: false, message: "Seleziona tipologia" };
|
|
}
|
|
};
|
|
|
|
return {
|
|
acconto: await getAcconto(),
|
|
saldo: await getSaldo(),
|
|
};
|
|
};
|
|
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<typeof getAllServizioAnnunci>
|
|
>[number];
|
|
export const getAllServizioAnnunci = async (userId: UsersId) => {
|
|
try {
|
|
return await db
|
|
.selectFrom("servizio")
|
|
.where("user_id", "=", userId)
|
|
.leftJoin(
|
|
"users_anagrafica",
|
|
"servizio.user_id",
|
|
"users_anagrafica.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",
|
|
"servizio.skipPayment",
|
|
"servizio.skipControlloDoc",
|
|
"servizio.skipDocMotivazione",
|
|
"servizio.n_adulti",
|
|
"servizio.n_minori",
|
|
"servizio.entromese",
|
|
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"),
|
|
])
|
|
|
|
.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();
|
|
|
|
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<number>("annunci_id").as("count")])
|
|
.executeTakeFirst();
|
|
if (alreadyUnlocked && alreadyUnlocked.count >= 10) {
|
|
return false;
|
|
}
|
|
|
|
const data = await db.transaction().execute(async (tx) => {
|
|
await tx
|
|
.updateTable("servizio_annunci")
|
|
.set({
|
|
open_contatti_at: new Date(),
|
|
})
|
|
.where("servizio_id", "=", servizioId)
|
|
.where("annunci_id", "=", annuncioId)
|
|
.execute();
|
|
|
|
const servizio = await tx
|
|
.selectFrom("servizio")
|
|
.select("user_id")
|
|
.where("servizio_id", "=", servizioId)
|
|
.executeTakeFirstOrThrow(() => new Error("Servizio not found"));
|
|
|
|
const user = await tx
|
|
.selectFrom("users")
|
|
.select(["id", "username", "email", "telefono"])
|
|
.where("id", "=", servizio.user_id)
|
|
.executeTakeFirstOrThrow(() => new Error("User not found"));
|
|
|
|
const anagrafica = await tx
|
|
.selectFrom("users_anagrafica")
|
|
.select("sesso")
|
|
.where("userid", "=", servizio.user_id)
|
|
.executeTakeFirstOrThrow(() => new Error("Anagrafica not found"));
|
|
|
|
const annuncio = await tx
|
|
.selectFrom("annunci")
|
|
.where("id", "=", annuncioId)
|
|
.select([
|
|
"codice",
|
|
"indirizzo",
|
|
"email",
|
|
"numero",
|
|
"locatore",
|
|
"civico",
|
|
"comune",
|
|
])
|
|
.executeTakeFirstOrThrow(() => new Error("Annuncio not found"));
|
|
|
|
return {
|
|
anagrafica,
|
|
annuncio,
|
|
annuncioId,
|
|
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 EmailInteressato: NewMailProps = {
|
|
template: {
|
|
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",
|
|
},
|
|
},
|
|
mail: {
|
|
subject: "Contatti sbloccati per il servizio di ricerca affitto",
|
|
to: data.user.email,
|
|
},
|
|
userId: data.user.id,
|
|
};
|
|
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({
|
|
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<TipologiaPosizioneEnum>();
|
|
const parsedTipologia = parse.safeParse(tipologia);
|
|
if (!parsedTipologia.success) {
|
|
return { status: "invalid" as const };
|
|
}
|
|
const servizi = await db
|
|
.selectFrom("servizio")
|
|
.select(["tipologia", "servizio_id"])
|
|
.where("tipologia", "=", parsedTipologia.data)
|
|
.where("isOkAcconto", "=", true)
|
|
.where("isInterrotto", "=", false)
|
|
.where("decorrenza", "is not", null)
|
|
.where("user_id", "=", userId)
|
|
.select((eb) => [
|
|
jsonObjectFrom(
|
|
eb
|
|
.selectFrom("servizio_annunci")
|
|
.select("annunci_id")
|
|
.where("servizio_annunci.annunci_id", "=", annuncioId)
|
|
.whereRef("servizio.servizio_id", "=", "servizio_annunci.servizio_id")
|
|
.limit(1),
|
|
).as("alreadySaved"),
|
|
])
|
|
|
|
.executeTakeFirst();
|
|
|
|
if (!servizi) {
|
|
return { status: "not_found" as const };
|
|
}
|
|
|
|
if (servizi.alreadySaved) {
|
|
return {
|
|
servizioId: servizi.servizio_id,
|
|
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)
|
|
.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({
|
|
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 });
|
|
//ADMIN CONFERMA LAVORATA EMAIL
|
|
if (status) {
|
|
await NewMail({
|
|
template: {
|
|
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",
|
|
},
|
|
},
|
|
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 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}`,
|
|
});
|
|
}
|
|
};
|
|
|
|
export const getPagamentoDataForCheckout = async (paymentId: PaymentsId) => {
|
|
try {
|
|
return await db
|
|
.selectFrom("payments")
|
|
.selectAll("payments")
|
|
.innerJoin("users", "users.id", "payments.userid")
|
|
.select("users.email")
|
|
.innerJoin("ordini", "ordini.ordine_id", "payments.ordine_id")
|
|
.innerJoin("prezziario", "prezziario.idprezziario", "ordini.packid")
|
|
.select(["prezziario.nome_it", "prezziario.desc_it"])
|
|
.where("payments.id", "=", paymentId)
|
|
.executeTakeFirstOrThrow();
|
|
} catch (e) {
|
|
throw new TRPCError({
|
|
code: "NOT_FOUND",
|
|
message: `Payment not found: ${(e as Error).message}`,
|
|
});
|
|
}
|
|
};
|
|
|
|
type Pagamento = Payments & Pick<Users, "username">;
|
|
export const getAllPagamenti = async (): Promise<Pagamento[]> => {
|
|
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<Pagamento[]> => {
|
|
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({
|
|
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,
|
|
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}`,
|
|
});
|
|
}
|
|
};
|
|
|
|
export const getServizioOrdini = async (servizioId: ServizioServizioId) => {
|
|
try {
|
|
const data = await db
|
|
.selectFrom("ordini")
|
|
.selectAll("ordini")
|
|
.select((eb) => [
|
|
jsonArrayFrom(
|
|
eb
|
|
.selectFrom("payments")
|
|
.whereRef("payments.ordine_id", "=", "ordini.ordine_id")
|
|
.selectAll("payments")
|
|
.orderBy("created_at", "desc"),
|
|
).as("pagamenti"),
|
|
])
|
|
.where("servizio_id", "=", servizioId)
|
|
.orderBy("created_at", "desc")
|
|
.execute();
|
|
|
|
return data.map((ordine) => ({
|
|
...ordine,
|
|
pagamenti: ordine.pagamenti.map((payment) => ({
|
|
...payment,
|
|
created_at: new Date(payment.created_at),
|
|
paid_at: payment.paid_at ? new Date(payment.paid_at) : null,
|
|
})),
|
|
}));
|
|
} catch (e) {
|
|
throw new TRPCError({
|
|
code: "INTERNAL_SERVER_ERROR",
|
|
message: `Error fetching servizio ordini: ${(e as Error).message}`,
|
|
});
|
|
}
|
|
};
|