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

422 lines
10 KiB
TypeScript
Raw Normal View History

2025-08-28 18:27:07 +02:00
import { TRPCError } from "@trpc/server";
import { add, format } from "date-fns";
import { env } from "~/env";
import stripe from "~/lib/stripe";
2025-08-28 18:27:07 +02:00
import OrderTypeEnum from "~/schemas/public/OrderTypeEnum";
import type { Ordini, OrdiniOrdineId } from "~/schemas/public/Ordini";
2025-08-04 17:45:44 +02:00
import PaymentStatusEnum from "~/schemas/public/PaymentStatusEnum";
import type { TestiEStringheStingaId } from "~/schemas/public/TestiEStringhe";
2025-08-04 17:45:44 +02:00
import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
2025-08-28 18:27:07 +02:00
import { db } from "~/server/db";
2025-08-04 17:45:44 +02:00
import { NewMail } from "~/server/services/mailer";
import { CONDIZIONI_DEFAULT } from "~/server/services/prezziario.service";
2025-08-04 17:45:44 +02:00
import { getUser } from "~/server/services/user.service";
import { addEventToQueue } from "./event_queue.controller";
2025-08-04 17:45:44 +02:00
export const stripeHealthCheck = async () => {
try {
const endpoints = await stripe.webhookEndpoints.list();
return {
success: true,
endpoints,
secretKey: env.STRIPE_SECRET_KEY,
publicKey: env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY,
};
} catch (e) {
console.error("Stripe health check failed:", e);
return {
success: false,
endpoints: null,
secretKey: env.STRIPE_SECRET_KEY,
publicKey: env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY,
};
}
};
2025-08-04 17:45:44 +02:00
export const whIntentCreatedHandler = async ({
2025-08-28 18:27:07 +02:00
pIntentId,
ordineId,
2025-08-04 17:45:44 +02:00
}: {
pIntentId: Ordini["intent_id"];
ordineId: OrdiniOrdineId;
2025-08-04 17:45:44 +02:00
}) => {
try {
return await db
.updateTable("ordini")
.set({
intent_id: pIntentId,
paymentstatus: PaymentStatusEnum.processing,
})
.where("ordine_id", "=", ordineId)
.returning("ordine_id")
.execute();
} catch (error) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Error whIntentCreatedHandler: ${(error as Error).message}`,
});
}
2025-08-04 17:45:44 +02:00
};
export const whIntentSucceededHandler = async ({
2025-08-28 18:27:07 +02:00
pIntentId,
ordineId,
2025-08-28 18:27:07 +02:00
pm_id,
2025-08-04 17:45:44 +02:00
}: {
pIntentId: Ordini["intent_id"];
ordineId: OrdiniOrdineId;
2025-08-28 18:27:07 +02:00
pm_id: string;
2025-08-04 17:45:44 +02:00
}) => {
2025-08-28 18:27:07 +02:00
try {
const paymentMethod = await stripe.paymentMethods.retrieve(pm_id);
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
await db.transaction().execute(async (trx) => {
const ordine = await trx
.updateTable("ordini")
2025-08-28 18:27:07 +02:00
.set({
paid_at: new Date(),
paymentmethod: paymentMethod.type,
2025-08-29 16:18:32 +02:00
paymentstatus: PaymentStatusEnum.success,
isActive: true,
2025-08-28 18:27:07 +02:00
})
.where("intent_id", "=", pIntentId)
.where("ordine_id", "=", ordineId)
.returningAll()
.executeTakeFirstOrThrow(() => {
throw new Error(
`Payment not found - intent_id: ${pIntentId}, ordineId: ${ordineId}`,
);
});
2025-08-04 17:45:44 +02:00
const condizioni = await trx
.selectFrom("prezziario")
.select("testo_condizioni")
.where("idprezziario", "=", ordine.packid)
.executeTakeFirst();
2025-08-28 18:27:07 +02:00
const utente = await getUser({
db: trx,
userId: ordine.userid,
});
const lock_expires = new Date(Date.now() + 3000); // Lock for 30 sec
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
switch (ordine.type) {
case OrderTypeEnum.Acconto: {
if (!ordine.servizio_id) {
throw new Error(
`No servizio_id for ordine_id: ${ordine.ordine_id}`,
);
}
2025-08-28 18:27:07 +02:00
await trx
.updateTable("servizio")
.where("servizio_id", "=", ordine.servizio_id)
2025-08-28 18:27:07 +02:00
.set({
decorrenza: new Date(),
isOkAcconto: true,
})
.execute();
2025-08-04 17:45:44 +02:00
await addEventToQueue({
data: {
tipo: "email",
data: {
template: {
mailType: "servizioAttivato",
},
mail: {
subject: "Pagamento Confermato - Acconto Infoalloggi.it",
to: utente.email,
attachments: [
{
filename: `condizioni_contrattuali_${ordine.created_at.toISOString()}.pdf`,
2026-03-04 20:45:21 +01:00
generate: {
type: "condizioni",
stringId: (condizioni?.testo_condizioni ||
CONDIZIONI_DEFAULT) as TestiEStringheStingaId,
2026-03-04 20:45:21 +01:00
},
encoding: "base64",
contentType: "application/pdf",
},
{
filename: `ricevuta_cortesia_${ordine.ordine_id}.pdf`,
2026-03-04 20:45:21 +01:00
generate: {
type: "ricevuta_cortesia",
ordineId: ordine.ordine_id,
},
encoding: "base64",
contentType: "application/pdf",
},
],
},
userId: utente.id,
},
},
lock_expires,
2025-08-28 18:27:07 +02:00
});
await addEventToQueue({
data: {
tipo: "email",
data: {
template: {
mailType: "avvisoInterruzione",
},
mail: {
subject: "Avviso Interruzione",
to: utente.email,
},
userId: utente.id,
},
},
lock_expires: add(new Date(), { days: 10 }),
});
//SERVIZIO ATTIVATO
2025-08-28 18:27:07 +02:00
break;
}
case OrderTypeEnum.Saldo: {
if (!ordine.servizio_id) {
throw new Error(
`No servizio_id for ordine_id: ${ordine.ordine_id}`,
);
}
const servizio = await trx
.selectFrom("servizio")
.select(["servizio.tipologia"])
.where("servizio_id", "=", ordine.servizio_id)
.executeTakeFirstOrThrow(() => {
throw new Error(
`Servizio not found - servizio_id: ${ordine.servizio_id}`,
);
});
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
await trx
.updateTable("servizio")
.where("servizio_id", "=", ordine.servizio_id)
2025-08-28 18:27:07 +02:00
.set({
isOkConsulenza:
servizio.tipologia === TipologiaPosizioneEnum.Transitorio,
2025-08-29 16:18:32 +02:00
isOkSaldo: true,
2025-08-28 18:27:07 +02:00
})
.execute();
await addEventToQueue({
data: {
tipo: "email",
data: {
template: {
mailType: "pagamentoConferma",
},
mail: {
subject: "Pagamento Confermato - Saldo Infoalloggi.it",
to: utente.email,
attachments: [
{
filename: `ricevuta_cortesia_${ordine.ordine_id}.pdf`,
2026-03-04 20:45:21 +01:00
generate: {
type: "ricevuta_cortesia",
ordineId: ordine.ordine_id,
},
encoding: "base64",
contentType: "application/pdf",
},
],
},
userId: utente.id,
},
},
lock_expires,
2025-08-28 18:27:07 +02:00
});
break;
}
case OrderTypeEnum.Rinnovo: {
// nessuna azione aggiuntiva richiesta
await addEventToQueue({
data: {
tipo: "email",
data: {
template: {
mailType: "pagamentoConferma",
},
mail: {
subject: "Pagamento Confermato - Rinnovo Infoalloggi.it",
to: utente.email,
attachments: [
{
filename: `ricevuta_cortesia_${ordine.ordine_id}.pdf`,
2026-03-04 20:45:21 +01:00
generate: {
type: "ricevuta_cortesia",
ordineId: ordine.ordine_id,
},
encoding: "base64",
contentType: "application/pdf",
},
],
},
userId: utente.id,
},
},
lock_expires,
2025-08-28 18:27:07 +02:00
});
break;
}
case OrderTypeEnum.Consulenza: {
if (!ordine.servizio_id) {
throw new Error(
`No servizio_id for ordine_id: ${ordine.ordine_id}`,
);
}
await trx
.updateTable("servizio")
.where("servizio_id", "=", ordine.servizio_id)
.set({
isOkConsulenza: true,
})
.execute();
await addEventToQueue({
data: {
tipo: "email",
data: {
template: {
mailType: "pagamentoConferma",
},
mail: {
subject: "Pagamento Confermato - Consulenza Infoalloggi.it",
to: utente.email,
attachments: [
{
filename: `ricevuta_cortesia_${ordine.ordine_id}.pdf`,
2026-03-04 20:45:21 +01:00
generate: {
type: "ricevuta_cortesia",
ordineId: ordine.ordine_id,
},
encoding: "base64",
contentType: "application/pdf",
},
],
},
userId: utente.id,
},
2025-08-28 18:27:07 +02:00
},
lock_expires,
2025-08-28 18:27:07 +02:00
});
break;
}
case OrderTypeEnum.Altro:
// Handle "Altro" type if needed
// await addEventToQueue({
// data: {
// tipo: "email",
// data: {
// template: {
// mailType: "pagamentoConferma",
// },
// mail: {
// subject: "Pagamento Confermato - Infoalloggi.it",
// to: utente.email,
// attachments: [
// {
// filename: `ricevuta_cortesia_${ordine.ordine_id}.pdf`,
// generate: {
// type: "ricevuta_cortesia",
// ordineId: ordine.ordine_id,
// },
// encoding: "base64",
// contentType: "application/pdf",
// },
// ],
// },
// userId: utente.id,
// },
// },
// lock_expires,
// });
break;
2025-08-28 18:27:07 +02:00
}
await addEventToQueue({
data: {
tipo: "email",
data: {
template: {
mailType: "generic",
props: {
title: "Nuovo pagamento ricevuto",
testo: `È stato ricevuto un nuovo pagamento.`,
},
},
mail: {
subject: "Ric pagamento",
to: "web@infoalloggi.it",
attachments: [
{
filename: `ricevuta_cortesia_${utente.username.replace(/\s+/g, "-")}-${format(ordine.created_at, "dd-MM-yyyy")}.pdf`,
generate: {
type: "ricevuta_cortesia",
ordineId: ordine.ordine_id,
},
encoding: "base64",
contentType: "application/pdf",
},
],
},
},
},
lock_expires,
});
2025-08-28 18:27:07 +02:00
});
return true;
} catch (error) {
console.error("whIntentSucceededHandler error:", {
pIntentId,
ordineId,
pm_id,
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
});
2025-08-28 18:27:07 +02:00
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Error updating payment intent: ${(error as Error).message}`,
cause: error,
2025-08-28 18:27:07 +02:00
});
}
2025-08-04 17:45:44 +02:00
};
export const whIntentFailedHandler = async ({
2025-08-28 18:27:07 +02:00
pIntentId,
2025-08-04 17:45:44 +02:00
}: {
pIntentId: Ordini["intent_id"];
2025-08-04 17:45:44 +02:00
}) => {
2025-08-28 18:27:07 +02:00
const userId = await db
.updateTable("ordini")
2025-08-28 18:27:07 +02:00
.set({
paymentstatus: PaymentStatusEnum.failed,
})
.returning("userid")
.where("paymentstatus", "=", PaymentStatusEnum.processing)
2025-08-28 18:27:07 +02:00
.where("intent_id", "=", pIntentId)
.execute();
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
if (userId[0]) {
const utente = await getUser({
db,
userId: userId[0].userid,
});
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
await NewMail({
template: {
mailType: "pagamentoErrore",
},
mail: {
subject: "Pagamento Fallito - Infoalloggi.it",
to: utente.email,
},
2025-08-29 16:18:32 +02:00
userId: utente.id,
2025-08-28 18:27:07 +02:00
});
}
return true;
2025-08-04 17:45:44 +02:00
};