diff --git a/apps/db/migrations/37_rinnovi.up.sql b/apps/db/migrations/37_rinnovi.up.sql new file mode 100644 index 0000000..6e4d363 --- /dev/null +++ b/apps/db/migrations/37_rinnovi.up.sql @@ -0,0 +1,261 @@ +ALTER TABLE IF EXISTS public.ordini +ALTER COLUMN servizio_id +DROP NOT NULL; + +ALTER TABLE IF EXISTS public.ordini +ADD COLUMN IF NOT EXISTS rinnovo_id UUID; + +CREATE TABLE IF NOT EXISTS public.rinnovi ( + id UUID NOT NULL DEFAULT gen_random_uuid (), + "userId" UUID NOT NULL, + codice TEXT NOT NULL, + decorrenza TIMESTAMP WITHOUT TIME ZONE NOT NULL, + scadenza timestamp without time zone NOT NULL, + gestionale_id TEXT +); + +DO $$ BEGIN IF NOT EXISTS ( + SELECT 1 FROM pg_constraint + WHERE conname = 'rinnovo_user_fkey' + ) THEN + ALTER TABLE public.rinnovi + ADD CONSTRAINT rinnovo_user_fkey + FOREIGN KEY ("userId") REFERENCES public.users (id) + ON UPDATE CASCADE ON DELETE CASCADE; + END IF; +END $$; +-- Primary Key +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint + WHERE conname = 'rinnovi_pkey' + AND conrelid = 'public.rinnovi'::regclass + ) THEN + ALTER TABLE public.rinnovi + ADD CONSTRAINT rinnovi_pkey PRIMARY KEY (id); + END IF; +END $$; + +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'ordine_rinnovo_fkey') THEN + ALTER TABLE public.ordini + ADD CONSTRAINT ordine_rinnovo_fkey FOREIGN KEY (rinnovo_id) REFERENCES public.rinnovi (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE CASCADE NOT VALID; + END IF; +END; +$$; + +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'only_one_service') THEN + ALTER TABLE public.ordini + ADD CONSTRAINT only_one_service CHECK ( + ( + servizio_id IS NOT NULL + AND rinnovo_id IS NULL + ) + OR ( + servizio_id IS NULL + AND rinnovo_id IS NOT NULL + ) + ) NOT VALID; + END IF; +END; +$$; + +ALTER TABLE IF EXISTS public.prezziario +ADD COLUMN IF NOT EXISTS order_type "OrderTypeEnum"; + +ALTER TABLE IF EXISTS public.prezziario +ADD COLUMN IF NOT EXISTS service_type "TipologiaPosizioneEnum"; + +UPDATE public.prezziario +SET + service_type = 'Transitorio' +WHERE + idprezziario = 'ACCONTO_BD' + OR idprezziario LIKE 'SALDOBD%' + OR idprezziario = 'CONS_TRANS' + OR idprezziario = 'CONS_30GG'; + +UPDATE public.prezziario +SET + service_type = 'Stabile' +WHERE + idprezziario = 'ACCONTO_CA' + OR idprezziario LIKE 'SALDOCA%' + OR idprezziario = 'CONS_COMM' + OR idprezziario = 'CONS_4+4' + OR idprezziario = 'CONS_3+2'; + +UPDATE public.prezziario +SET + order_type = 'Acconto' +WHERE + idprezziario LIKE 'ACCONTO%'; + +UPDATE public.prezziario +SET + order_type = 'Saldo' +WHERE + idprezziario LIKE 'SALDO%'; + +UPDATE public.prezziario +SET + order_type = 'Consulenza' +WHERE + idprezziario LIKE 'CONS%'; + +ALTER TABLE IF EXISTS public.prezziario +DROP COLUMN IF EXISTS "isAcconto"; + +ALTER TABLE IF EXISTS public.prezziario +DROP COLUMN IF EXISTS "isSaldo"; + +ALTER TABLE IF EXISTS public.prezziario +DROP COLUMN IF EXISTS "isConsulenza"; + +ALTER TABLE IF EXISTS public.prezziario +DROP COLUMN IF EXISTS "isStabile"; + +ALTER TABLE IF EXISTS public.prezziario +DROP COLUMN IF EXISTS "isTransitorio"; + +ALTER TABLE IF EXISTS public.prezziario +ADD COLUMN IF NOT EXISTS service_variant integer; + +UPDATE public.prezziario +SET + service_variant = 4 +WHERE + idprezziario = 'SALDOBD_12MESI'; + +UPDATE public.prezziario +SET + service_variant = 0 +WHERE + idprezziario = 'SALDOBD_1MESE'; + +UPDATE public.prezziario +SET + service_variant = 1 +WHERE + idprezziario = 'SALDOBD_3MESI'; + +UPDATE public.prezziario +SET + service_variant = 2 +WHERE + idprezziario = 'SALDOBD_6MESI'; + +UPDATE public.prezziario +SET + service_variant = 3 +WHERE + idprezziario = 'SALDOBD_9MESI'; + +UPDATE public.prezziario +SET + service_variant = 0 +WHERE + idprezziario = 'SALDOCA500'; + +UPDATE public.prezziario +SET + service_variant = 1 +WHERE + idprezziario = 'SALDOCA500+'; + +UPDATE public.prezziario +SET + service_variant = 2 +WHERE + idprezziario = 'SALDOCA700+'; + +INSERT INTO + public.prezziario ( + idprezziario, + prezzo_cent, + testo_condizioni, + "isActive", + nome_it, + nome_en, + desc_it, + desc_en, + sconto, + order_type, + service_type, + service_variant + ) +VALUES ( + 'CERCO_BD365', + 46000, + 'CONDIZIONI_CERCHI', + TRUE, + 'Rinnovo permanenza 12 mesi', + 'Renewal 12 months', + 'Rinnovo permanenza 12 mesi', + 'Renewal 12 months', + 0, + 'Rinnovo', + 'Transitorio', + 4 + ), + ( + 'CERCO_BD30', + 8000, + 'CONDIZIONI_CERCHI', + TRUE, + 'Rinnovo permanenza 1 mese', + 'Renewal 1 month', + 'Rinnovo permanenza 1 mese', + 'Renewal 1 month', + 0, + 'Rinnovo', + 'Transitorio', + 0 + ), + ( + 'CERCO_BD90', + 25000, + 'CONDIZIONI_CERCHI', + TRUE, + 'Rinnovo permanenza 3 mesi', + 'Renewal 3 months', + 'Rinnovo permanenza 3 mesi', + 'Renewal 3 months', + 0, + 'Rinnovo', + 'Transitorio', + 1 + ), + ( + 'CERCO_BD180', + 36000, + 'CONDIZIONI_CERCHI', + TRUE, + 'Rinnovo permanenza 6 mesi', + 'Renewal 6 months', + 'Rinnovo permanenza 6 mesi', + 'Renewal 6 months', + 0, + 'Rinnovo', + 'Transitorio', + 2 + ), + ( + 'CERCO_BD270', + 41000, + 'CONDIZIONI_CERCHI', + TRUE, + 'Rinnovo permanenza 9 mesi', + 'Renewal 9 months', + 'Rinnovo permanenza 9 mesi', + 'Renewal 9 months', + 0, + 'Rinnovo', + 'Transitorio', + 3 + ) +ON CONFLICT (idprezziario) DO NOTHING; \ No newline at end of file diff --git a/apps/infoalloggi/src/components/area-riservata/dashboard.tsx b/apps/infoalloggi/src/components/area-riservata/dashboard.tsx index d91dc9f..3cd7e65 100644 --- a/apps/infoalloggi/src/components/area-riservata/dashboard.tsx +++ b/apps/infoalloggi/src/components/area-riservata/dashboard.tsx @@ -59,7 +59,7 @@ export const AdminDashboard = () => { }; export const UserDashboard = ({ userId }: { userId: UsersId }) => { - const { data, isLoading } = api.servizio.getAllServizioAnnunci.useQuery({ + const { data, isLoading } = api.servizio.getServizioRinnovi.useQuery({ userId, }); @@ -70,7 +70,7 @@ export const UserDashboard = ({ userId }: { userId: UsersId }) => {

Le tue ricerche

- +
diff --git a/apps/infoalloggi/src/components/servizio/annuncio_actions.tsx b/apps/infoalloggi/src/components/servizio/annuncio_actions.tsx index b947e73..4bc28ea 100644 --- a/apps/infoalloggi/src/components/servizio/annuncio_actions.tsx +++ b/apps/infoalloggi/src/components/servizio/annuncio_actions.tsx @@ -465,7 +465,7 @@ export const SaldoButton = ({ return ( + +
+ )} + + + ); +}; +const EditRinnovoModal = () => { + const { rinnovo, userId } = useRinnovo(); + const utils = api.useUtils(); + const router = useRouter(); + + const { mutate: edit } = api.servizio.updateRinnovo.useMutation({ + onError: (error) => { + toast.error(error.message); + }, + onSuccess: async () => { + toast.success("Rinnovo modificato con successo"); + await utils.servizio.invalidate(); + }, + }); + const { mutate: remove } = api.servizio.removeRinnovo.useMutation({ + onError: (error) => { + toast.error(error.message); + }, + onMutate: async () => { + await router.push(`/area-riservata/admin/user-view/ricerca/${userId}`); + }, + onSuccess: async () => { + toast.success("Rinnovo modificato con successo"); + await utils.servizio.invalidate(); + }, + }); + + return ( + + + + + + + Modifica Rinnovo + desc + + + remove({ rinnovoId: rinnovo.id })} + onSubmit={(v) => edit({ rinnovoId: rinnovo.id, data: v })} + /> + + + ); +}; + +export const NewRinnovoModal = ({ userId }: { userId: UsersId }) => { + const [open, setOpen] = useState(false); + const utils = api.useUtils(); + + const { mutate: add } = api.servizio.createRinnovo.useMutation({ + onError: (error) => { + toast.error(error.message); + }, + onSuccess: async () => { + toast.success("Rinnovo creato con successo"); + await utils.servizio.invalidate(); + + setOpen(false); + }, + }); + + return ( + + + + + + + Creazione Rinnovo + desc + + add({ data: { userId, ...v } })} /> + + + ); +}; diff --git a/apps/infoalloggi/src/components/servizio/servizi_header.tsx b/apps/infoalloggi/src/components/servizio/servizi_header.tsx index 18475ab..c6ba010 100644 --- a/apps/infoalloggi/src/components/servizio/servizi_header.tsx +++ b/apps/infoalloggi/src/components/servizio/servizi_header.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import toast from "react-hot-toast"; +import { NewRinnovoModal } from "~/components/servizio/rinnovo"; import { FormServizio, type FormValues } from "~/forms/FormServizio"; -import type { Servizio } from "~/schemas/public/Servizio"; import type { UsersId } from "~/schemas/public/Users"; import { api } from "~/utils/api"; import { Button } from "../ui/button"; @@ -15,37 +15,19 @@ import { } from "../ui/dialog"; export const ServiziHeader = ({ userId }: { userId: UsersId }) => { - const [open, setOpen] = useState(false); - const [editData, setEditData] = useState(undefined); - return (

Servizi cliente

- +
+ + +
); }; -const NewServizioModal = ({ - userId, - editData, - - setEditData, - open, - setOpen, -}: { - userId: UsersId; - editData: Servizio | undefined; - setEditData: (data: Servizio | undefined) => void; - open: boolean; - setOpen: (open: boolean) => void; -}) => { +const NewServizioModal = ({ userId }: { userId: UsersId }) => { + const [open, setOpen] = useState(false); const utils = api.useUtils(); const { mutate: add } = api.servizio.addServizio.useMutation({ @@ -56,40 +38,18 @@ const NewServizioModal = ({ toast.success("Servizio creato con successo"); await utils.servizio.invalidate(); - setEditData(undefined); setOpen(false); }, }); - const { mutate: update } = api.servizio.updateServizio.useMutation({ - onError: (error) => { - toast.error(error.message); - }, - onSuccess: async () => { - toast.success("Servizio modificato con successo"); - await utils.servizio.invalidate(); - setEditData(undefined); - setOpen(false); - }, - }); function onSubmit(fields: FormValues) { - if (editData) { - update({ - data: { - ...fields, - }, - servizioId: editData.servizio_id, - }); - } else { - add({ data: { ...fields, user_id: userId } }); - } + add({ data: { ...fields, user_id: userId } }); } return ( + + + + + + + + )} + /> + ( + +
+ Scadenza + +
+ + + + + + + + + + + +
+ )} + /> + +

+ Permanenza:{" "} + {formatDuration( + intervalToDuration({ + start: new Date(decorrenza), + end: add(new Date(scadenza), { days: 1 }), + }), + { locale: it, format: ["years", "months", "days"] }, + )} +

+

+ Attenzione: un cambiamento nella permanenza andrà ad aggiornare + automaticamente l'ordine associato se non ancora pagato. +

+ + ( + +
+ Gestionale ID + +
+ + + +
+ )} + /> +
+ {initialData && ( + { + if (onDelete) { + onDelete(); + } + }} + title="Elimina annuncio" + > + + + )} + + +
+ + + ); +}; diff --git a/apps/infoalloggi/src/pages/area-riservata/admin/user-view/ricerca/[userId].tsx b/apps/infoalloggi/src/pages/area-riservata/admin/user-view/ricerca/[userId].tsx index 075066c..3c8a57e 100644 --- a/apps/infoalloggi/src/pages/area-riservata/admin/user-view/ricerca/[userId].tsx +++ b/apps/infoalloggi/src/pages/area-riservata/admin/user-view/ricerca/[userId].tsx @@ -20,7 +20,7 @@ type RicercaUserProps = { const RicercaUser: NextPageWithLayout = ({ userId, }: RicercaUserProps) => { - const { data, isLoading } = api.servizio.getAllServizioAnnunci.useQuery({ + const { data, isLoading } = api.servizio.getServizioRinnovi.useQuery({ userId, }); if (isLoading) return ; @@ -29,8 +29,7 @@ const RicercaUser: NextPageWithLayout = ({
- - +
); }; diff --git a/apps/infoalloggi/src/pages/area-riservata/admin/user-view/rinnovo/[...slug].tsx b/apps/infoalloggi/src/pages/area-riservata/admin/user-view/rinnovo/[...slug].tsx new file mode 100644 index 0000000..31cb449 --- /dev/null +++ b/apps/infoalloggi/src/pages/area-riservata/admin/user-view/rinnovo/[...slug].tsx @@ -0,0 +1,94 @@ +import type { GetServerSideProps } from "next"; +import { AreaRiservataLayoutUserView } from "~/components/Layout"; +import { LoadingPage } from "~/components/loading"; +import { Rinnovo } from "~/components/servizio/rinnovo"; +import { Status500 } from "~/components/status-page"; +import { redirectTo500 } from "~/lib/utils"; +import type { NextPageWithLayout } from "~/pages/_app"; +import { RinnovoProvider } from "~/providers/RinnovoProvider"; +import type { RinnoviId } from "~/schemas/public/Rinnovi"; +import type { UsersId } from "~/schemas/public/Users"; +import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper"; +import { zRinnovoId, zUserId } from "~/server/utils/zod_types"; +import { api } from "~/utils/api"; + +type RinnovoUserProps = { + rinnovoId: RinnoviId; + userId: UsersId; +}; +/** + * Pagina di visualizzazione rinnovo per admin: /area-riservata/admin/user-view/rinnovo/[userId]/[rinnovoId] + */ +const RinnovoUser: NextPageWithLayout = ({ + rinnovoId, + userId, +}: RinnovoUserProps) => { + const { data, isLoading } = api.servizio.getRinnovo.useQuery({ + rinnovoId, + }); + if (isLoading) { + return ; + } + if (!data) { + return ; + } + return ( +
+ + + +
+ ); +}; +export default RinnovoUser; + +export const getServerSideProps = (async (context) => { + const user_id = context.params?.slug?.[0]; + const rinnovo_id = context.params?.slug?.[1]; + + if (typeof rinnovo_id !== "string" || typeof user_id !== "string") { + console.error("Error: rinnovoId or userId is not a string"); + return redirectTo500 + } + const parsedUserId = zUserId.safeParse(user_id); + if (!parsedUserId.success) { + console.error("Error parsing userId", parsedUserId.error); + return redirectTo500 + } + + const parsedRinnovoId = zRinnovoId.safeParse(rinnovo_id); + if (!parsedRinnovoId.success) { + console.error("Error parsing rinnovoId", parsedRinnovoId.error); + return redirectTo500 + } + const rinnovoId = parsedRinnovoId.data; + const userId = parsedUserId.data; + + const access_token = context.req.cookies.access_token; + + const helper = await TrpcAuthedFetchingIstance({ access_token }); + if (helper) { + await helper.trpc.servizio.getRinnovo.prefetch({ + rinnovoId, + }); + + return { + props: { + rinnovoId, + trpcState: helper.trpc.dehydrate(), + userId, + }, + }; + } + + return redirectTo500 +}) satisfies GetServerSideProps; + +RinnovoUser.getLayout = function getLayout(page) { + return {page}; +}; diff --git a/apps/infoalloggi/src/pages/area-riservata/rinnovo/[rinnovoId].tsx b/apps/infoalloggi/src/pages/area-riservata/rinnovo/[rinnovoId].tsx new file mode 100644 index 0000000..3f01d89 --- /dev/null +++ b/apps/infoalloggi/src/pages/area-riservata/rinnovo/[rinnovoId].tsx @@ -0,0 +1,92 @@ +import type { GetServerSideProps } from "next"; +import { UserDashboardFaq } from "~/components/area-riservata/dashboard"; +import { AreaRiservataLayout } from "~/components/Layout"; +import { LoadingPage } from "~/components/loading"; +import { Rinnovo } from "~/components/servizio/rinnovo"; +import { Status500 } from "~/components/status-page"; +import { redirectTo500 } from "~/lib/utils"; +import type { NextPageWithLayout } from "~/pages/_app"; +import { RinnovoProvider } from "~/providers/RinnovoProvider"; +import { useEnforcedSession } from "~/providers/SessionProvider"; +import type { RinnoviId } from "~/schemas/public/Rinnovi"; +import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper"; +import { zRinnovoId } from "~/server/utils/zod_types"; +import { api } from "~/utils/api"; + +type RinnovoPageProps = { + rinnovoId: RinnoviId; +}; + +/** + * Pagina del rinnovo: /area-riservata/rinnovo/[rinnovoId] + */ +const RinnovoPage: NextPageWithLayout = ({ + rinnovoId, +}: RinnovoPageProps) => { + const { user } = useEnforcedSession(); + const { data, isLoading } = api.servizio.getRinnovo.useQuery({ + rinnovoId, + }); + if (isLoading) { + return ; + } + if (!data) { + return ; + } + return ( +
+
+ + + +
+
+ +
+
+ ); +}; +export default RinnovoPage; + +export const getServerSideProps = (async (context) => { + const rinnovo_id = context.params?.rinnovoId; + + if (typeof rinnovo_id !== "string") { + console.error("Error: rinnovoId is not a string"); + return redirectTo500; + } + + const parsedRinnovoId = zRinnovoId.safeParse(rinnovo_id); + if (!parsedRinnovoId.success) { + console.error("Error parsing rinnovoId", parsedRinnovoId.error); + return redirectTo500; + } + const rinnovoId = parsedRinnovoId.data; + + const access_token = context.req.cookies.access_token; + + const helper = await TrpcAuthedFetchingIstance({ access_token }); + if (helper) { + await helper.trpc.servizio.getRinnovo.prefetch({ + rinnovoId, + }); + + return { + props: { + rinnovoId, + trpcState: helper.trpc.dehydrate(), + }, + }; + } + + return redirectTo500; +}) satisfies GetServerSideProps; + +RinnovoPage.getLayout = function getLayout(page) { + return {page}; +}; diff --git a/apps/infoalloggi/src/pages/servizio/pagamento-checkout/[ordineId].tsx b/apps/infoalloggi/src/pages/servizio/pagamento-checkout/[ordineId].tsx index c8bbd91..0a01a63 100644 --- a/apps/infoalloggi/src/pages/servizio/pagamento-checkout/[ordineId].tsx +++ b/apps/infoalloggi/src/pages/servizio/pagamento-checkout/[ordineId].tsx @@ -2,6 +2,7 @@ import type { GetServerSideProps } from "next"; import CheckoutForm from "~/components/checkout"; import { AreaRiservataLayout } from "~/components/Layout"; import stripe from "~/lib/stripe"; +import { redirectTo500 } from "~/lib/utils"; import type { NextPageWithLayout } from "~/pages/_app"; import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper"; import { zOrdineId } from "~/server/utils/zod_types"; @@ -48,23 +49,13 @@ export const getServerSideProps = (async (context) => { const id = context.params?.ordineId; if (typeof id !== "string") { console.error("Error: ordineId is not a string"); - return { - redirect: { - destination: "/500", - permanent: false, - }, - }; + return redirectTo500; } const parsed = zOrdineId.safeParse(id); if (!parsed.success) { console.error("Error parsing ordineId", parsed.error); - return { - redirect: { - destination: "/500", - permanent: false, - }, - }; + return redirectTo500; } const access_token = context.req.cookies.access_token; @@ -94,9 +85,7 @@ export const getServerSideProps = (async (context) => { customer: customer.id, description: paymentData.nome_it, metadata: { - packId: paymentData.packid, ordineId: paymentData.ordine_id, - servizioId: paymentData.servizio_id, userId: paymentData.userid, }, @@ -104,12 +93,7 @@ export const getServerSideProps = (async (context) => { }); if (!clientSecret) { - return { - redirect: { - destination: "/500", - permanent: false, - }, - }; + return redirectTo500; } return { @@ -121,10 +105,5 @@ export const getServerSideProps = (async (context) => { }, }; } - return { - redirect: { - destination: "/500", - permanent: false, - }, - }; + return redirectTo500; }) satisfies GetServerSideProps; diff --git a/apps/infoalloggi/src/pages/servizio/pagamento/[servizioId].tsx b/apps/infoalloggi/src/pages/servizio/pagamento.tsx similarity index 68% rename from apps/infoalloggi/src/pages/servizio/pagamento/[servizioId].tsx rename to apps/infoalloggi/src/pages/servizio/pagamento.tsx index 957ad3a..b0a3115 100644 --- a/apps/infoalloggi/src/pages/servizio/pagamento/[servizioId].tsx +++ b/apps/infoalloggi/src/pages/servizio/pagamento.tsx @@ -5,20 +5,20 @@ import { useRouter } from "next/router"; import z from "zod"; import { AreaRiservataLayout } from "~/components/Layout"; import { Button } from "~/components/ui/button"; -import { formatCurrency } from "~/lib/utils"; +import { formatCurrency, redirectTo500 } from "~/lib/utils"; import type { NextPageWithLayout } from "~/pages/_app"; import { useTranslation } from "~/providers/I18nProvider"; import OrderTypeEnum from "~/schemas/public/OrderTypeEnum"; import type { PreparedPaymentData } from "~/server/controllers/pagamenti.controller"; import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper"; -import { zServizioId } from "~/server/utils/zod_types"; +import { zRinnovoId, zServizioId } from "~/server/utils/zod_types"; type PreviewPurchaseProps = { stripeDisabled: boolean; data: PreparedPaymentData; }; /** - * /servizio/pagamento/[servizioId]?type= \ + * /servizio/pagamento?servizioId=|rinnovoId=&type= \ * Pagina di preparazione al pagamento. */ const PreviewPurchasePage: NextPageWithLayout = ({ @@ -81,83 +81,76 @@ PreviewPurchasePage.getLayout = function getLayout(page) { export default PreviewPurchasePage; export const getServerSideProps = (async (context) => { - const id = context.params?.servizioId; + const access_token = context.req.cookies.access_token; - if (typeof id !== "string") { - console.error("Error: servizioId is not a string"); - return { - redirect: { - destination: "/500", - permanent: false, - }, - }; - } - - const parsedServizioId = zServizioId.safeParse(id); - if (!parsedServizioId.success) { - console.error("Error parsing servizioId", parsedServizioId.error); - return { - redirect: { - destination: "/500", - permanent: false, - }, - }; + const helper = await TrpcAuthedFetchingIstance({ access_token }); + if (!helper) { + return redirectTo500; } + const flag = await helper.trpc.flags.GetFlagValue.fetch({ + id: "STRIPE_DISABLED", + }); const payment_type = context.query?.type; if (typeof payment_type !== "string") { console.error("Error: payment type is not a string"); - return { - redirect: { - destination: "/500", - permanent: false, - }, - }; + return redirectTo500; } const parsedType = z.enum(OrderTypeEnum).safeParse(payment_type); if (!parsedType.success) { console.error("Error parsing payment type", parsedType.error); - return { - redirect: { - destination: "/500", - permanent: false, - }, - }; + return redirectTo500; } + const type = parsedType.data; - const access_token = context.req.cookies.access_token; - - const helper = await TrpcAuthedFetchingIstance({ access_token }); - if (helper) { - const data = await helper.trpc.pagamenti.preparePayment.fetch({ - type: parsedType.data, - servizioId: parsedServizioId.data, - }); - if (!data.success) { - console.error("Error preparing payment", data.message); - return { - redirect: { - destination: `/500`, - permanent: false, - }, - }; + if (type === OrderTypeEnum.Rinnovo) { + const id = context.query?.rinnovoId; + if (typeof id !== "string") { + console.error("Error: rinnovoId is not a string"); + return redirectTo500; } - - const flag = await helper.trpc.flags.GetFlagValue.fetch({ - id: "STRIPE_DISABLED", + const parsedRinnovoId = zRinnovoId.safeParse(id); + if (!parsedRinnovoId.success) { + console.error("Error parsing rinnovoId", parsedRinnovoId.error); + return redirectTo500; + } + const result = await helper.trpc.pagamenti.preparePaymentRinnovi.fetch({ + rinnovoId: parsedRinnovoId.data, }); - + if (!result.success) { + console.error("Error preparing payment", result.message); + return redirectTo500; + } return { props: { stripeDisabled: flag || false, - data: data.data, + data: result.data, }, }; } + // Non è un rinnovo, deve essere un servizio normale + const id = context.query?.servizioId; + if (typeof id !== "string") { + console.error("Error: servizioId is not a string"); + return redirectTo500; + } + const parsedServizioId = zServizioId.safeParse(id); + if (!parsedServizioId.success) { + console.error("Error parsing servizioId", parsedServizioId.error); + return redirectTo500; + } + const result = await helper.trpc.pagamenti.preparePayment.fetch({ + type, + servizioId: parsedServizioId.data, + }); + if (!result.success) { + console.error("Error preparing payment", result.message); + return redirectTo500; + } return { - redirect: { - destination: "/500", - permanent: false, + props: { + stripeDisabled: flag || false, + data: result.data, }, }; }) satisfies GetServerSideProps; diff --git a/apps/infoalloggi/src/providers/RinnovoProvider.tsx b/apps/infoalloggi/src/providers/RinnovoProvider.tsx new file mode 100644 index 0000000..03ce2d0 --- /dev/null +++ b/apps/infoalloggi/src/providers/RinnovoProvider.tsx @@ -0,0 +1,39 @@ +import { createContext, useContext } from "react"; +import type { RinnoviId } from "~/schemas/public/Rinnovi"; +import type { UsersId } from "~/schemas/public/Users"; +import type { RinnovoData } from "~/server/services/rinnovi.service"; + +type RinnovoProviderProps = { + isAdmin: boolean; + userId: UsersId; + rinnovoId: RinnoviId; + rinnovo: RinnovoData; + children: React.ReactNode; +}; +type RinnovoContextProps = { + isAdmin: boolean; + userId: UsersId; + rinnovoId: RinnoviId; + rinnovo: RinnovoData; +}; + +const RinnovoContext = createContext(null); + +export const RinnovoProvider = ({ + children, + ...props +}: RinnovoProviderProps) => { + return ( + + {children} + + ); +}; + +export const useRinnovo = () => { + const rinnovoContext = useContext(RinnovoContext); + if (!rinnovoContext) { + throw new Error("useRinnovo must be used within a RinnovoProvider"); + } + return rinnovoContext; +}; diff --git a/apps/infoalloggi/src/schemas/public/Ordini.ts b/apps/infoalloggi/src/schemas/public/Ordini.ts index 51ace68..2dddb07 100644 --- a/apps/infoalloggi/src/schemas/public/Ordini.ts +++ b/apps/infoalloggi/src/schemas/public/Ordini.ts @@ -6,6 +6,7 @@ import type { PrezziarioIdprezziario } from './Prezziario'; import type { ServizioServizioId } from './Servizio'; import type { default as OrderTypeEnum } from './OrderTypeEnum'; import type { default as PaymentStatusEnum } from './PaymentStatusEnum'; +import type { RinnoviId } from './Rinnovi'; import type { ColumnType, Selectable, Insertable, Updateable } from 'kysely'; /** Identifier type for public.ordini */ @@ -21,7 +22,7 @@ export default interface OrdiniTable { packid: ColumnType; - servizio_id: ColumnType; + servizio_id: ColumnType; type: ColumnType; @@ -38,6 +39,8 @@ export default interface OrdiniTable { paymentstatus: ColumnType; sconto: ColumnType; + + rinnovo_id: ColumnType; } export type Ordini = Selectable; diff --git a/apps/infoalloggi/src/schemas/public/PublicSchema.ts b/apps/infoalloggi/src/schemas/public/PublicSchema.ts index 5b9aedf..87a9b60 100644 --- a/apps/infoalloggi/src/schemas/public/PublicSchema.ts +++ b/apps/infoalloggi/src/schemas/public/PublicSchema.ts @@ -14,6 +14,7 @@ import type { default as UserInvitesTable } from './UserInvites'; import type { default as EmailsTable } from './Emails'; import type { default as PotenzialiGroupsTable } from './PotenzialiGroups'; import type { default as ComuniTable } from './Comuni'; +import type { default as RinnoviTable } from './Rinnovi'; import type { default as EventQueueTable } from './EventQueue'; import type { default as OrdiniTable } from './Ordini'; import type { default as PrezziarioTable } from './Prezziario'; @@ -58,6 +59,8 @@ export default interface PublicSchema { comuni: ComuniTable; + rinnovi: RinnoviTable; + event_queue: EventQueueTable; ordini: OrdiniTable; diff --git a/apps/infoalloggi/src/schemas/public/Rinnovi.ts b/apps/infoalloggi/src/schemas/public/Rinnovi.ts new file mode 100644 index 0000000..4a0588a --- /dev/null +++ b/apps/infoalloggi/src/schemas/public/Rinnovi.ts @@ -0,0 +1,29 @@ +// @generated +// This file is automatically generated by Kanel. Do not modify manually. + +import type { UsersId } from './Users'; +import type { ColumnType, Selectable, Insertable, Updateable } from 'kysely'; + +/** Identifier type for public.rinnovi */ +export type RinnoviId = string & { __brand: 'public.rinnovi' }; + +/** Represents the table public.rinnovi */ +export default interface RinnoviTable { + id: ColumnType; + + userId: ColumnType; + + codice: ColumnType; + + decorrenza: ColumnType; + + gestionale_id: ColumnType; + + scadenza: ColumnType; +} + +export type Rinnovi = Selectable; + +export type NewRinnovi = Insertable; + +export type RinnoviUpdate = Updateable; diff --git a/apps/infoalloggi/src/server/api/routers/pagamenti.ts b/apps/infoalloggi/src/server/api/routers/pagamenti.ts index 14a27f8..2892f16 100644 --- a/apps/infoalloggi/src/server/api/routers/pagamenti.ts +++ b/apps/infoalloggi/src/server/api/routers/pagamenti.ts @@ -1,6 +1,6 @@ import { TRPCError } from "@trpc/server"; import { z } from "zod/v4"; -import type OrderTypeEnum from "~/schemas/public/OrderTypeEnum"; +import OrderTypeEnum from "~/schemas/public/OrderTypeEnum"; import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc"; import { getPagamentoDataForCheckout, @@ -10,7 +10,7 @@ import { import { db } from "~/server/db"; import { genPdfRicevutaCortesia } from "~/server/services/puppeteer.service"; -import { zOrdineId, zServizioId } from "~/server/utils/zod_types"; +import { zOrdineId, zRinnovoId, zServizioId } from "~/server/utils/zod_types"; // Create a new ratelimiter, that allows 10 requests per 10 seconds /* const ratelimit = new RateLimiterHandler({ @@ -42,7 +42,7 @@ export const pagamentiRouter = createTRPCRouter({ .input( z.object({ servizioId: zServizioId, - type: z.custom(), + type: z.enum(OrderTypeEnum), }), ) .output( @@ -58,7 +58,40 @@ export const pagamentiRouter = createTRPCRouter({ ]), ) .query(async ({ input }) => { - return await preparePayment(input.servizioId, input.type); + if (input.type === OrderTypeEnum.Rinnovo) { + return { + success: false, + message: "Use preparePaymentRinnovi for Rinnovo payments", + }; + } + return await preparePayment({ + type: input.type, + servizioId: input.servizioId, + }); + }), + preparePaymentRinnovi: protectedProcedure + .input( + z.object({ + rinnovoId: zRinnovoId, + }), + ) + .output( + z.discriminatedUnion("success", [ + z.object({ + success: z.literal(true), + data: z.custom(), + }), + z.object({ + success: z.literal(false), + message: z.string(), + }), + ]), + ) + .query(async ({ input }) => { + return await preparePayment({ + type: OrderTypeEnum.Rinnovo, + rinnovoId: input.rinnovoId, + }); }), getPagamentoDataForCheckout: protectedProcedure diff --git a/apps/infoalloggi/src/server/api/routers/servizio.ts b/apps/infoalloggi/src/server/api/routers/servizio.ts index 28b91dc..36c597b 100644 --- a/apps/infoalloggi/src/server/api/routers/servizio.ts +++ b/apps/infoalloggi/src/server/api/routers/servizio.ts @@ -1,5 +1,10 @@ import z from "zod"; import type { NewOrdini, OrdiniUpdate } from "~/schemas/public/Ordini"; +import type { + NewRinnovi, + Rinnovi, + RinnoviUpdate, +} from "~/schemas/public/Rinnovi"; import type { NewServizio, ServizioUpdate } from "~/schemas/public/Servizio"; import type { ServizioAnnunciUpdate } from "~/schemas/public/ServizioAnnunci"; import type { Users } from "~/schemas/public/Users"; @@ -27,6 +32,7 @@ import { interruzioneServizio, processServizioOnboard, SbloccaContatti, + type ServizioData, userAccettaConferma, userSendConfermaIntent, } from "~/server/controllers/servizio.controller"; @@ -37,6 +43,13 @@ import { removeOrder, updateOrder, } from "~/server/services/ordini.service"; +import { + createRinnovo, + deleteRinnovo, + getRinnoviUser, + getRinnovoData, + updateRinnovo, +} from "~/server/services/rinnovi.service"; import { addServizio, deleteServizio, @@ -49,6 +62,7 @@ import { import { zAnnuncioId, zOrdineId, + zRinnovoId, zServizioId, zUserId, } from "~/server/utils/zod_types"; @@ -406,4 +420,75 @@ export const servizioRouter = createTRPCRouter({ servizioId: input.servizioId, }); }), + getServizioRinnovi: protectedProcedure + .input( + z.object({ + userId: zUserId, + }), + ) + .query(async ({ input }) => { + const data: ( + | { d: Date; type: "servizio"; data: ServizioData } + | { d: Date; type: "rinnovo"; data: Rinnovi } + )[] = []; + + const servizio = await getAllServizioAnnunci(input.userId); + const rinnovi = await getRinnoviUser(input.userId); + servizio.forEach((s) => { + data.push({ + d: s.created_at, + type: "servizio", + data: s, + }); + }); + rinnovi.forEach((r) => { + data.push({ + d: r.decorrenza, + type: "rinnovo", + data: r, + }); + }); + data.sort((a, b) => b.d.getTime() - a.d.getTime()); + return data; + }), + createRinnovo: adminProcedure + .input( + z.object({ + data: z.custom(), + }), + ) + .mutation(async ({ input }) => { + return await createRinnovo(input.data); + }), + updateRinnovo: adminProcedure + .input( + z.object({ + data: z.custom(), + rinnovoId: zRinnovoId, + }), + ) + .mutation(async ({ input }) => { + return await updateRinnovo({ + rinnovoId: input.rinnovoId, + data: input.data, + }); + }), + getRinnovo: protectedProcedure + .input( + z.object({ + rinnovoId: zRinnovoId, + }), + ) + .query(async ({ input }) => { + return await getRinnovoData(input.rinnovoId); + }), + removeRinnovo: adminProcedure + .input( + z.object({ + rinnovoId: zRinnovoId, + }), + ) + .mutation(async ({ input }) => { + return await deleteRinnovo(input.rinnovoId); + }), }); diff --git a/apps/infoalloggi/src/server/controllers/pagamenti.controller.ts b/apps/infoalloggi/src/server/controllers/pagamenti.controller.ts index 3b23729..d4e9230 100644 --- a/apps/infoalloggi/src/server/controllers/pagamenti.controller.ts +++ b/apps/infoalloggi/src/server/controllers/pagamenti.controller.ts @@ -1,15 +1,20 @@ import { TRPCError } from "@trpc/server"; +import { add, intervalToDuration } from "date-fns"; import OrderTypeEnum from "~/schemas/public/OrderTypeEnum"; import type { OrdiniOrdineId } from "~/schemas/public/Ordini"; import type { Prezziario, PrezziarioIdprezziario, } from "~/schemas/public/Prezziario"; +import type { RinnoviId } from "~/schemas/public/Rinnovi"; import type { ServizioServizioId } from "~/schemas/public/Servizio"; import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum"; import { db } from "../db"; import { createOrdine } from "../services/ordini.service"; -import { getPrezziarioByIdHandler } from "../services/prezziario.service"; +import { + CONDIZIONI_DEFAULT, + getPrezziarioByIdHandler, +} from "../services/prezziario.service"; import { getServizioById } from "../services/servizio.service"; export type PreparedPaymentData = { @@ -21,297 +26,340 @@ export type PreparedPaymentData = { packDesc: string; condizioni: string; }; +export type preparePaymentInput = + | { + servizioId: ServizioServizioId; + type: Exclude; + } + | { + rinnovoId: RinnoviId; + type: OrderTypeEnum.Rinnovo; + }; + export const preparePayment = async ( - servizioId: ServizioServizioId, - type: OrderTypeEnum, + props: preparePaymentInput, ): Promise< | { success: true; data: PreparedPaymentData } | { success: false; message: string } > => { try { - const servizio = await getServizioById(servizioId); - const inactiveOrders = await db - .selectFrom("ordini") - .selectAll() - .where("servizio_id", "=", servizioId) - .where("isActive", "=", false) - .where("paymentstatus", "is", null) - .execute(); + switch (props.type) { + case OrderTypeEnum.Acconto: + case OrderTypeEnum.Saldo: + case OrderTypeEnum.Consulenza: + case OrderTypeEnum.Altro: { + const { servizioId, type } = props; + const servizio = await getServizioById(servizioId); + const inactiveOrders = await db + .selectFrom("ordini") + .selectAll() + .where("servizio_id", "=", servizioId) + .where("isActive", "=", false) + .where("paymentstatus", "is", null) + .execute(); - switch (type) { - case OrderTypeEnum.Acconto: { - if (!servizio.isOkAcconto) { - const acconto = await AccontoSolver(servizio.tipologia); - if (!acconto.success) { + switch (type) { + case OrderTypeEnum.Acconto: { + if (!servizio.isOkAcconto) { + const acconto = await AccontoSolver(servizio.tipologia); + if (!acconto.success) { + return { + success: false, + message: `Acconto non trovato per il servizio: ${acconto.message}`, + }; + } + + // servizio sta aspettando attivazione acconto + + const preExistingAcconto = inactiveOrders.find( + (ordine) => ordine.type === OrderTypeEnum.Acconto, + ); + + if (preExistingAcconto) { + // acconto già creato, procedere con questo + return { + success: true, + data: { + ordine_id: preExistingAcconto.ordine_id, + packid: preExistingAcconto.packid, + amount_cent: preExistingAcconto.amount_cent, + sconto: preExistingAcconto.sconto, + packName: acconto.pack.nome_it, + packDesc: acconto.pack.desc_it, + condizioni: + acconto.pack.testo_condizioni || CONDIZIONI_DEFAULT, + }, + }; + } + // nessun acconto preesistente, crearne uno nuovo + + const ordine = await createOrdine({ + servizio_id: servizioId, + amount_cent: acconto.pack.prezzo_cent, + packid: acconto.pack.idprezziario, + type: OrderTypeEnum.Acconto, + userid: servizio.user_id, + }); + return { + success: true, + data: { + ordine_id: ordine.ordine_id, + packid: acconto.pack.idprezziario, + amount_cent: acconto.pack.prezzo_cent, + sconto: 0, + packName: acconto.pack.nome_it, + packDesc: acconto.pack.desc_it, + condizioni: + acconto.pack.testo_condizioni || CONDIZIONI_DEFAULT, + }, + }; + } return { success: false, - message: `Acconto non trovato per il servizio: ${acconto.message}`, + message: + "Il servizio ha già un acconto attivo, non è possibile preparare un nuovo pagamento", }; } - if (!acconto.pack.testo_condizioni) { + case OrderTypeEnum.Saldo: { + if (!servizio.isOkAcconto) { + return { + success: false, + message: + "Non è possibile procedere con il pagamento del saldo prima di aver attivato l'acconto", + }; + } + if (!servizio.isOkSaldo) { + //find saldo pack based on servizio tipologia and permanenza/budget, if tipologia posizione + const saldo = await SaldoSolver(servizio); + if (!saldo.success) { + return { + success: false, + message: `Saldo non trovato per il servizio: ${saldo.message}`, + }; + } + + const preExistingSaldo = inactiveOrders.find( + (ordine) => ordine.type === OrderTypeEnum.Saldo, + ); + if (preExistingSaldo) { + // saldo già creato, procedere con questo + return { + success: true, + data: { + ordine_id: preExistingSaldo.ordine_id, + packid: preExistingSaldo.packid, + amount_cent: preExistingSaldo.amount_cent, + sconto: preExistingSaldo.sconto, + packName: saldo.pack.nome_it, + packDesc: saldo.pack.desc_it, + condizioni: + saldo.pack.testo_condizioni || CONDIZIONI_DEFAULT, + }, + }; + } + // nessun saldo preesistente, crearne uno nuovo + const ordine = await createOrdine({ + servizio_id: servizioId, + amount_cent: saldo.pack.prezzo_cent, + packid: saldo.pack.idprezziario, + type: OrderTypeEnum.Saldo, + userid: servizio.user_id, + }); + return { + success: true, + data: { + ordine_id: ordine.ordine_id, + packid: saldo.pack.idprezziario, + amount_cent: saldo.pack.prezzo_cent, + sconto: 0, + packName: saldo.pack.nome_it, + packDesc: saldo.pack.desc_it, + condizioni: saldo.pack.testo_condizioni || CONDIZIONI_DEFAULT, + }, + }; + } return { success: false, - message: `Condizioni non trovate per acconto: ${acconto.pack.nome_it}`, + message: + "Il servizio ha già un saldo attivo, non è possibile preparare un nuovo pagamento", }; } - // servizio sta aspettando attivazione acconto + case OrderTypeEnum.Consulenza: { + if (!servizio.isOkAcconto || !servizio.isOkSaldo) { + return { + success: false, + message: + "Non è possibile procedere con il pagamento della consulenza prima di aver attivato acconto e saldo", + }; + } + if (!servizio.isOkConsulenza) { + const existingConsulenza = inactiveOrders.find( + (ordine) => ordine.type === OrderTypeEnum.Consulenza, + ); + if (existingConsulenza) { + // consulenza già creato, procedere con questo + const consulenzaPack = await getPrezziarioByIdHandler( + existingConsulenza.packid, + ); + if (!consulenzaPack) { + return { + success: false, + message: `Pack della consulenza non trovato: ${existingConsulenza.packid}`, + }; + } - const preExistingAcconto = inactiveOrders.find( - (ordine) => ordine.type === OrderTypeEnum.Acconto, - ); + return { + success: true, + data: { + ordine_id: existingConsulenza.ordine_id, + packid: existingConsulenza.packid, + amount_cent: existingConsulenza.amount_cent, + sconto: existingConsulenza.sconto, + packName: consulenzaPack.nome_it, + packDesc: consulenzaPack.desc_it, + condizioni: + consulenzaPack.testo_condizioni || CONDIZIONI_DEFAULT, + }, + }; + } - if (preExistingAcconto) { - // acconto già creato, procedere con questo - return { - success: true, - data: { - ordine_id: preExistingAcconto.ordine_id, - packid: preExistingAcconto.packid, - amount_cent: preExistingAcconto.amount_cent, - sconto: preExistingAcconto.sconto, - packName: acconto.pack.nome_it, - packDesc: acconto.pack.desc_it, - condizioni: acconto.pack.testo_condizioni, - }, - }; - } - // nessun acconto preesistente, crearne uno nuovo + return { + success: false, + message: + "Il servizio non ha un pacchetto di consulenza, è necessario inserirlo prima di procedere", + }; + } - const ordine = await createOrdine({ - servizio_id: servizioId, - amount_cent: acconto.pack.prezzo_cent, - packid: acconto.pack.idprezziario, - type: OrderTypeEnum.Acconto, - userid: servizio.user_id, - }); - return { - success: true, - data: { - ordine_id: ordine.ordine_id, - packid: acconto.pack.idprezziario, - amount_cent: acconto.pack.prezzo_cent, - sconto: 0, - packName: acconto.pack.nome_it, - packDesc: acconto.pack.desc_it, - condizioni: acconto.pack.testo_condizioni, - }, - }; - } - return { - success: false, - message: - "Il servizio ha già un acconto attivo, non è possibile preparare un nuovo pagamento", - }; - } - case OrderTypeEnum.Saldo: { - if (!servizio.isOkAcconto) { - return { - success: false, - message: - "Non è possibile procedere con il pagamento del saldo prima di aver attivato l'acconto", - }; - } - if (!servizio.isOkSaldo) { - //find saldo pack based on servizio tipologia and permanenza/budget, if tipologia posizione - const saldo = await SaldoSolver(servizio); - if (!saldo.success) { return { success: false, - message: `Saldo non trovato per il servizio: ${saldo.message}`, + message: + "Il servizio ha già una consulenza attiva, non è possibile preparare un nuovo pagamento", }; } - if (!saldo.pack.testo_condizioni) { - return { - success: false, - message: `Condizioni non trovate per saldo: ${saldo.pack.nome_it}`, - }; - } - const preExistingSaldo = inactiveOrders.find( - (ordine) => ordine.type === OrderTypeEnum.Saldo, - ); - if (preExistingSaldo) { - // saldo già creato, procedere con questo - return { - success: true, - data: { - ordine_id: preExistingSaldo.ordine_id, - packid: preExistingSaldo.packid, - amount_cent: preExistingSaldo.amount_cent, - sconto: preExistingSaldo.sconto, - packName: saldo.pack.nome_it, - packDesc: saldo.pack.desc_it, - condizioni: saldo.pack.testo_condizioni, - }, - }; - } - // nessun saldo preesistente, crearne uno nuovo - const ordine = await createOrdine({ - servizio_id: servizioId, - amount_cent: saldo.pack.prezzo_cent, - packid: saldo.pack.idprezziario, - type: OrderTypeEnum.Saldo, - userid: servizio.user_id, - }); - return { - success: true, - data: { - ordine_id: ordine.ordine_id, - packid: saldo.pack.idprezziario, - amount_cent: saldo.pack.prezzo_cent, - sconto: 0, - packName: saldo.pack.nome_it, - packDesc: saldo.pack.desc_it, - condizioni: saldo.pack.testo_condizioni, - }, - }; - } - return { - success: false, - message: - "Il servizio ha già un saldo attivo, non è possibile preparare un nuovo pagamento", - }; - } - - case OrderTypeEnum.Consulenza: { - if (!servizio.isOkAcconto || !servizio.isOkSaldo) { - return { - success: false, - message: - "Non è possibile procedere con il pagamento della consulenza prima di aver attivato acconto e saldo", - }; - } - if (!servizio.isOkConsulenza) { - const existingConsulenza = inactiveOrders.find( - (ordine) => ordine.type === OrderTypeEnum.Consulenza, - ); - if (existingConsulenza) { - // consulenza già creato, procedere con questo - const consulenzaPack = await getPrezziarioByIdHandler( - existingConsulenza.packid, + case OrderTypeEnum.Altro: { + const existingAltro = inactiveOrders.find( + (ordine) => ordine.type === OrderTypeEnum.Altro, ); - if (!consulenzaPack) { + if (existingAltro) { + // altro già creato, procedere con questo + const altroPack = await getPrezziarioByIdHandler( + existingAltro.packid, + ); + if (!altroPack) { + return { + success: false, + message: `Pack dell'altro non trovato: ${existingAltro.packid}`, + }; + } + return { - success: false, - message: `Pack della consulenza non trovato: ${existingConsulenza.packid}`, - }; - } - if (!consulenzaPack.testo_condizioni) { - return { - success: false, - message: `Condizioni non trovate per consulenza: ${consulenzaPack.nome_it}`, + success: true, + data: { + ordine_id: existingAltro.ordine_id, + packid: existingAltro.packid, + amount_cent: existingAltro.amount_cent, + sconto: existingAltro.sconto, + packName: altroPack.nome_it, + packDesc: altroPack.desc_it, + condizioni: altroPack.testo_condizioni || CONDIZIONI_DEFAULT, + }, }; } + return { - success: true, - data: { - ordine_id: existingConsulenza.ordine_id, - packid: existingConsulenza.packid, - amount_cent: existingConsulenza.amount_cent, - sconto: existingConsulenza.sconto, - packName: consulenzaPack.nome_it, - packDesc: consulenzaPack.desc_it, - condizioni: consulenzaPack.testo_condizioni, - }, + success: false, + message: + "La preparazione del pagamento per questa tipologia non è supportata", }; } - - return { - success: false, - message: - "Il servizio non ha un pacchetto di consulenza, è necessario inserirlo prima di procedere", - }; + default: + throw new Error( + `Tipo di ordine non valido: ${type satisfies never}`, + ); } - - return { - success: false, - message: - "Il servizio ha già una consulenza attiva, non è possibile preparare un nuovo pagamento", - }; } case OrderTypeEnum.Rinnovo: { - const existingRinnovo = inactiveOrders.find( - (ordine) => ordine.type === OrderTypeEnum.Rinnovo, - ); - if (existingRinnovo) { + const { rinnovoId } = props; + + const ordiniRinnovo = await db + .selectFrom("ordini") + .selectAll() + .where("rinnovo_id", "=", rinnovoId) + .where("type", "=", OrderTypeEnum.Rinnovo) + .execute(); + + const alreadyPaid = ordiniRinnovo.some((ordine) => ordine.isActive); + if (alreadyPaid) { + return { + success: false, + message: + "Il rinnovo ha già un ordine attivo, non è possibile preparare un nuovo pagamento", + }; + } + + const unusedOrdine = ordiniRinnovo.find((ordine) => !ordine.isActive); + if (unusedOrdine) { // rinnovo già creato, procedere con questo const rinnovoPack = await getPrezziarioByIdHandler( - existingRinnovo.packid, + unusedOrdine.packid, ); if (!rinnovoPack) { return { success: false, - message: `Pack del rinnovo non trovato: ${existingRinnovo.packid}`, - }; - } - if (!rinnovoPack.testo_condizioni) { - return { - success: false, - message: `Condizioni non trovate per rinnovo: ${rinnovoPack.nome_it}`, + message: `Pack del rinnovo non trovato: ${unusedOrdine.packid}`, }; } + return { success: true, data: { - ordine_id: existingRinnovo.ordine_id, - packid: existingRinnovo.packid, - amount_cent: existingRinnovo.amount_cent, - sconto: existingRinnovo.sconto, + ordine_id: unusedOrdine.ordine_id, + packid: unusedOrdine.packid, + amount_cent: unusedOrdine.amount_cent, + sconto: unusedOrdine.sconto, packName: rinnovoPack.nome_it, packDesc: rinnovoPack.desc_it, - condizioni: rinnovoPack.testo_condizioni, + condizioni: rinnovoPack.testo_condizioni || CONDIZIONI_DEFAULT, }, }; } - return { - success: false, - message: - "Il servizio non ha un pacchetto di rinnovo, è necessario inserirlo prima di procedere", - }; - } - case OrderTypeEnum.Altro: { - const existingAltro = inactiveOrders.find( - (ordine) => ordine.type === OrderTypeEnum.Altro, - ); - if (existingAltro) { - // altro già creato, procedere con questo - const altroPack = await getPrezziarioByIdHandler( - existingAltro.packid, + // nessun rinnovo preesistente, crearne uno nuovo + const rinnovo = await db + .selectFrom("rinnovi") + .selectAll() + .where("id", "=", rinnovoId) + .executeTakeFirstOrThrow( + () => new Error(`Rinnovo non trovato - id: ${rinnovoId}`), ); - if (!altroPack) { - return { - success: false, - message: `Pack dell'altro non trovato: ${existingAltro.packid}`, - }; - } - if (!altroPack.testo_condizioni) { - return { - success: false, - message: `Condizioni non trovate per altro: ${altroPack.nome_it}`, - }; - } + const pack = await RinnovoSolver(rinnovo.decorrenza, rinnovo.scadenza); + if (!pack.success) { return { - success: true, - data: { - ordine_id: existingAltro.ordine_id, - packid: existingAltro.packid, - amount_cent: existingAltro.amount_cent, - sconto: existingAltro.sconto, - packName: altroPack.nome_it, - packDesc: altroPack.desc_it, - condizioni: altroPack.testo_condizioni, - }, + success: false, + message: `Pack non trovato per il rinnovo: ${pack.message}`, }; } + const ordine = await createOrdine({ + rinnovo_id: rinnovoId, + amount_cent: pack.pack.prezzo_cent, + packid: pack.pack.idprezziario, + type: OrderTypeEnum.Rinnovo, + userid: rinnovo.userId, + }); return { - success: false, - message: - "La preparazione del pagamento per questa tipologia non è supportata", + success: true, + data: { + ordine_id: ordine.ordine_id, + packid: pack.pack.idprezziario, + amount_cent: pack.pack.prezzo_cent, + sconto: 0, + packName: pack.pack.nome_it, + packDesc: pack.pack.desc_it, + condizioni: pack.pack.testo_condizioni || CONDIZIONI_DEFAULT, + }, }; } - default: - throw new Error(`Tipo di ordine non valido: ${type satisfies never}`); } } catch (e) { throw new TRPCError({ @@ -479,6 +527,51 @@ export const SaldoSolver = async ({ return { success: false, message: "Seleziona tipologia" }; } }; +function rinnovoPermanenzaVariant(permanenza: number): number { + if (permanenza <= 1) return 0; + if (permanenza <= 3) return 1; + if (permanenza <= 6) return 2; + if (permanenza <= 9) return 3; + return 4; +} + +const rinnovoDurationToPermanenza = (decorrenza: Date, scadenza: Date) => { + const dur = intervalToDuration({ + start: new Date(decorrenza), + end: add(new Date(scadenza), { days: 1 }), + }); + return (dur.years || 0) * 12 + (dur.months || 0) + (dur.days ? 1 : 0); +}; + +export const RinnovoSolver = async ( + decorrenza: Date, + scadenza: Date, +): Promise => { + try { + const permanenza = rinnovoDurationToPermanenza(decorrenza, scadenza); + + const pack = await db + .selectFrom("prezziario") + .selectAll() + .where("isActive", "=", true) + .where("order_type", "=", OrderTypeEnum.Rinnovo) + .where("service_type", "=", TipologiaPosizioneEnum.Transitorio) + .where("service_variant", "=", rinnovoPermanenzaVariant(permanenza)) + .limit(1) + .executeTakeFirst(); + + if (!pack) { + return { success: false, message: "Pack non trovato" }; + } + + return { success: true, pack }; + } catch (error) { + return { + success: false, + message: `Errore nel calcolo del rinnovo: ${(error as Error).message}`, + }; + } +}; export const getPacksPerServizio = async ( servizioId: ServizioServizioId, diff --git a/apps/infoalloggi/src/server/controllers/servizio.controller.ts b/apps/infoalloggi/src/server/controllers/servizio.controller.ts index ee9a654..dfffc24 100644 --- a/apps/infoalloggi/src/server/controllers/servizio.controller.ts +++ b/apps/infoalloggi/src/server/controllers/servizio.controller.ts @@ -810,7 +810,7 @@ export const SbloccaContatti = async ({ }, tipo: "email", }, - lock_expires: new Date(Date.now() + 1000), + lock_expires: new Date(Date.now() + 3000), }); if (canSendNotification()) { diff --git a/apps/infoalloggi/src/server/controllers/stripe.controller.ts b/apps/infoalloggi/src/server/controllers/stripe.controller.ts index 9dd9ade..3d2a3b4 100644 --- a/apps/infoalloggi/src/server/controllers/stripe.controller.ts +++ b/apps/infoalloggi/src/server/controllers/stripe.controller.ts @@ -9,6 +9,7 @@ import type { TestiEStringheStingaId } from "~/schemas/public/TestiEStringhe"; import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum"; import { db } from "~/server/db"; import { NewMail } from "~/server/services/mailer"; +import { CONDIZIONI_DEFAULT } from "~/server/services/prezziario.service"; import { getUser } from "~/server/services/user.service"; import { addEventToQueue } from "./event_queue.controller"; @@ -80,30 +81,13 @@ export const whIntentSucceededHandler = async ({ }) .where("intent_id", "=", pIntentId) .where("ordine_id", "=", ordineId) - .returning([ - "servizio_id", - "ordine_id", - "userid", - "type", - "created_at", - "packid", - ]) + .returningAll() .executeTakeFirstOrThrow(() => { throw new Error( `Payment not found - intent_id: ${pIntentId}, ordineId: ${ordineId}`, ); }); - const servizio = await trx - .selectFrom("servizio") - .select(["servizio.tipologia", "servizio.servizio_id"]) - .where("servizio_id", "=", ordine.servizio_id) - .executeTakeFirstOrThrow(() => { - throw new Error( - `Servizio not found - servizio_id: ${ordine.servizio_id}`, - ); - }); - const condizioni = await trx .selectFrom("prezziario") .select("testo_condizioni") @@ -114,13 +98,19 @@ export const whIntentSucceededHandler = async ({ db: trx, userId: ordine.userid, }); - const lock_expires = new Date(Date.now() + 60 * 1000); // Lock for 1 minute + const lock_expires = new Date(Date.now() + 3000); // Lock for 30 sec switch (ordine.type) { - case OrderTypeEnum.Acconto: + case OrderTypeEnum.Acconto: { + if (!ordine.servizio_id) { + throw new Error( + `No servizio_id for ordine_id: ${ordine.ordine_id}`, + ); + } + await trx .updateTable("servizio") - .where("servizio_id", "=", servizio.servizio_id) + .where("servizio_id", "=", ordine.servizio_id) .set({ decorrenza: new Date(), isOkAcconto: true, @@ -140,19 +130,16 @@ export const whIntentSucceededHandler = async ({ attachments: [ { filename: `condizioni_contrattuali_${ordine.created_at.toISOString()}.pdf`, - //ATTENTION: hardcoded condizioni key - generate: { type: "condizioni", stringId: (condizioni?.testo_condizioni || - "CONDIZIONI_CERCHI") as TestiEStringheStingaId, + CONDIZIONI_DEFAULT) as TestiEStringheStingaId, }, encoding: "base64", contentType: "application/pdf", }, { filename: `ricevuta_cortesia_${ordine.ordine_id}.pdf`, - generate: { type: "ricevuta_cortesia", ordineId: ordine.ordine_id, @@ -170,11 +157,27 @@ export const whIntentSucceededHandler = async ({ //SERVIZIO ATTIVATO 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}`, + ); + }); - case OrderTypeEnum.Saldo: await trx .updateTable("servizio") - .where("servizio_id", "=", servizio.servizio_id) + .where("servizio_id", "=", ordine.servizio_id) .set({ isOkConsulenza: servizio.tipologia === TipologiaPosizioneEnum.Transitorio, @@ -198,7 +201,6 @@ export const whIntentSucceededHandler = async ({ type: "ricevuta_cortesia", ordineId: ordine.ordine_id, }, - encoding: "base64", contentType: "application/pdf", }, @@ -209,12 +211,49 @@ export const whIntentSucceededHandler = async ({ }, lock_expires, }); - //PAGAMENTO SALDO CONFERMATO break; - case OrderTypeEnum.Consulenza: + } + 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`, + generate: { + type: "ricevuta_cortesia", + ordineId: ordine.ordine_id, + }, + encoding: "base64", + contentType: "application/pdf", + }, + ], + }, + userId: utente.id, + }, + }, + lock_expires, + }); + 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", "=", servizio.servizio_id) + .where("servizio_id", "=", ordine.servizio_id) .set({ isOkConsulenza: true, }) @@ -232,7 +271,6 @@ export const whIntentSucceededHandler = async ({ attachments: [ { filename: `ricevuta_cortesia_${ordine.ordine_id}.pdf`, - generate: { type: "ricevuta_cortesia", ordineId: ordine.ordine_id, @@ -247,38 +285,38 @@ export const whIntentSucceededHandler = async ({ }, lock_expires, }); - //CONSULENZA ATTIVATA break; + } case OrderTypeEnum.Altro: - // TODO: Handle other types - 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`, + // 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, - }); + // generate: { + // type: "ricevuta_cortesia", + // ordineId: ordine.ordine_id, + // }, + // encoding: "base64", + // contentType: "application/pdf", + // }, + // ], + // }, + // userId: utente.id, + // }, + // }, + // lock_expires, + // }); break; } await addEventToQueue({ @@ -315,7 +353,6 @@ export const whIntentSucceededHandler = async ({ }); return true; } catch (error) { - // Enhanced error logging console.error("whIntentSucceededHandler error:", { pIntentId, ordineId, diff --git a/apps/infoalloggi/src/server/services/ordini.service.ts b/apps/infoalloggi/src/server/services/ordini.service.ts index c585bc7..90b0f2e 100644 --- a/apps/infoalloggi/src/server/services/ordini.service.ts +++ b/apps/infoalloggi/src/server/services/ordini.service.ts @@ -1,6 +1,7 @@ import { TRPCError } from "@trpc/server"; +import type { Insertable, Selectable, Updateable } from "kysely"; +import type OrdiniTable from "~/schemas/public/Ordini"; import type { - NewOrdini, Ordini, OrdiniOrdineId, OrdiniUpdate, @@ -8,6 +9,22 @@ import type { import type { Users, UsersId } from "~/schemas/public/Users"; import { db } from "~/server/db"; +export type ServizioOrdiniTable = Omit; + +export type ServizioOrdini = Selectable; + +export type NewServizioOrdini = Insertable; + +export type ServizioOrdiniUpdate = Updateable; + +export type RinnovoOrdiniTable = Omit; + +export type RinnovoOrdini = Selectable; + +export type NewRinnovoOrdini = Insertable; + +export type RinnovoOrdiniUpdate = Updateable; + export type Ordini_w_Utente = Ordini & Pick; export const getOrdini = async ( userId: UsersId | undefined, @@ -51,7 +68,9 @@ export const getOrdineById = async (ordineId: OrdiniOrdineId) => { } }; -export const createOrdine = async (data: NewOrdini) => { +export const createOrdine = async ( + data: NewServizioOrdini | NewRinnovoOrdini, +) => { try { return await db .insertInto("ordini") diff --git a/apps/infoalloggi/src/server/services/prezziario.service.ts b/apps/infoalloggi/src/server/services/prezziario.service.ts index fb77c91..6de308c 100644 --- a/apps/infoalloggi/src/server/services/prezziario.service.ts +++ b/apps/infoalloggi/src/server/services/prezziario.service.ts @@ -6,9 +6,11 @@ import type { PrezziarioIdprezziario, PrezziarioUpdate, } from "~/schemas/public/Prezziario"; +import type { TestiEStringheStingaId } from "~/schemas/public/TestiEStringhe"; import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum"; import { db, type Querier } from "~/server/db"; +export const CONDIZIONI_DEFAULT = "CONDIZIONI_CERCHI" as TestiEStringheStingaId; export const getPrezziario = async () => { try { return await db diff --git a/apps/infoalloggi/src/server/services/rinnovi.service.ts b/apps/infoalloggi/src/server/services/rinnovi.service.ts new file mode 100644 index 0000000..71b0969 --- /dev/null +++ b/apps/infoalloggi/src/server/services/rinnovi.service.ts @@ -0,0 +1,171 @@ +import { TRPCError } from "@trpc/server"; +import { jsonArrayFrom } from "kysely/helpers/postgres"; +import OrderTypeEnum from "~/schemas/public/OrderTypeEnum"; +import type { + NewRinnovi, + RinnoviId, + RinnoviUpdate, +} from "~/schemas/public/Rinnovi"; +import type { UsersId } from "~/schemas/public/Users"; +import { RinnovoSolver } from "~/server/controllers/pagamenti.controller"; +import { db } from "~/server/db"; +import { + createOrdine, + type NewRinnovoOrdini, +} from "~/server/services/ordini.service"; + +export const getRinnoviUser = async (userId: UsersId) => { + try { + return await db + .selectFrom("rinnovi") + .where("userId", "=", userId) + .selectAll() + .execute(); + } catch (error) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: `Could not fetch rinnovi for user: ${(error as Error).message}`, + }); + } +}; + +export type RinnovoData = Awaited>; + +export const getRinnovoData = async (rinnovoId: RinnoviId) => { + try { + return await db + .selectFrom("rinnovi") + .where("id", "=", rinnovoId) + .selectAll("rinnovi") + .select((eb) => + jsonArrayFrom( + eb + .selectFrom("ordini") + .whereRef("ordini.rinnovo_id", "=", "rinnovi.id") + .selectAll("ordini"), + ).as("ordini"), + ) + .executeTakeFirstOrThrow( + () => new Error(`Rinnovo not found - id: ${rinnovoId}`), + ); + } catch (error) { + throw new TRPCError({ + code: "NOT_FOUND", + message: `Error getting Rinnovo by id: ${(error as Error).message}`, + }); + } +}; + +export const createRinnovo = async (data: NewRinnovi) => { + try { + const rinnovo = await db + .insertInto("rinnovi") + .values(data) + .returningAll() + .executeTakeFirstOrThrow(() => new Error("Failed to create rinnovo")); + + const pack = await RinnovoSolver(rinnovo.decorrenza, rinnovo.scadenza); + if (!pack.success) { + throw new Error("No suitable pack found for rinnovo"); + } + + const ordineData: NewRinnovoOrdini = { + rinnovo_id: rinnovo.id, + userid: rinnovo.userId, + type: OrderTypeEnum.Rinnovo, + amount_cent: pack.pack.prezzo_cent, + packid: pack.pack.idprezziario, + isActive: false, + }; + await createOrdine(ordineData); + + return rinnovo; + } catch (error) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: `Could not create rinnovo: ${(error as Error).message}`, + }); + } +}; + +export const updateRinnovo = async ({ + rinnovoId, + data, +}: { + rinnovoId: RinnoviId; + data: RinnoviUpdate; +}) => { + try { + if (data.decorrenza && data.scadenza) { + const rinnovo = await db + .selectFrom("rinnovi") + .where("id", "=", rinnovoId) + .selectAll() + .executeTakeFirstOrThrow( + () => new Error(`Rinnovo not found - id: ${rinnovoId}`), + ); + + if ( + new Date(data.decorrenza).getTime() !== + new Date(rinnovo.decorrenza).getTime() || + new Date(data.scadenza).getTime() !== + new Date(rinnovo.scadenza).getTime() + ) { + //no need to update ordine if permanenza is not being updated + + const existingPack = await RinnovoSolver( + rinnovo.decorrenza, + rinnovo.scadenza, + ); + if (!existingPack.success) { + throw new Error("No suitable pack found for rinnovo"); + } + const newPack = await RinnovoSolver( + new Date(data.decorrenza), + new Date(data.scadenza), + ); + if (!newPack.success) { + throw new Error("No suitable pack found for rinnovo"); + } + if (existingPack.pack.idprezziario !== newPack.pack.idprezziario) { + //update ordine with new pack and price if permanenza is being updated and ordine is not active yet + await db + .updateTable("ordini") + .set({ + amount_cent: newPack.pack.prezzo_cent, + packid: newPack.pack.idprezziario, + }) + .where("rinnovo_id", "=", rinnovoId) + .where("isActive", "=", false) + .execute(); + } + } + } + + return await db + .updateTable("rinnovi") + .set(data) + .where("id", "=", rinnovoId) + .returningAll() + .executeTakeFirstOrThrow( + () => new Error(`Failed to update rinnovo - id: ${rinnovoId}`), + ); + } catch (error) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: `Error updating rinnovo: ${(error as Error).message}`, + }); + } +}; + +export const deleteRinnovo = async (rinnovoId: RinnoviId) => { + try { + await db.deleteFrom("rinnovi").where("id", "=", rinnovoId).execute(); + return true; + } catch (error) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: `Error deleting rinnovo: ${(error as Error).message}`, + }); + } +}; diff --git a/apps/infoalloggi/src/server/utils/zod_types.ts b/apps/infoalloggi/src/server/utils/zod_types.ts index 760d5b7..c8f57d4 100644 --- a/apps/infoalloggi/src/server/utils/zod_types.ts +++ b/apps/infoalloggi/src/server/utils/zod_types.ts @@ -8,6 +8,7 @@ import type { FlagsId } from "~/schemas/public/Flags"; import type { MessagesMessageid } from "~/schemas/public/Messages"; import type { OrdiniOrdineId } from "~/schemas/public/Ordini"; import type { PrezziarioIdprezziario } from "~/schemas/public/Prezziario"; +import type { RinnoviId } from "~/schemas/public/Rinnovi"; import type { ServizioServizioId } from "~/schemas/public/Servizio"; import type { TestiEStringheStingaId } from "~/schemas/public/TestiEStringhe"; import type { UsersId } from "~/schemas/public/Users"; @@ -85,3 +86,8 @@ export const zServizioId = z.custom( // (val) => typeof val === "number", // "falied to validate NazioneId", // ); + +export const zRinnovoId = z.custom( + (val) => typeof val === "string", + "falied to validate RinnovoId", +);