feat: enhance payment preparation logic and add rinnovo handling
- Refactor `preparePayment` function to handle multiple order types including Rinnovo. - Introduce new `RinnovoSolver` function to calculate renewal packs based on duration. - Add `createRinnovo` service to manage the creation of renewal orders. - Update `whIntentSucceededHandler` to handle payment confirmations for Rinnovo and other order types. - Introduce default conditions for packs in `prezziario.service.ts`. - Add new utility functions for managing renewal durations and permanenza. - Update database interactions to ensure proper handling of orders and renewals. - Enhance error handling and logging throughout the payment and renewal processes.
This commit is contained in:
parent
7e57f128f4
commit
1307f352a4
25 changed files with 1770 additions and 447 deletions
261
apps/db/migrations/37_rinnovi.up.sql
Normal file
261
apps/db/migrations/37_rinnovi.up.sql
Normal file
|
|
@ -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;
|
||||||
|
|
@ -59,7 +59,7 @@ export const AdminDashboard = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UserDashboard = ({ userId }: { userId: UsersId }) => {
|
export const UserDashboard = ({ userId }: { userId: UsersId }) => {
|
||||||
const { data, isLoading } = api.servizio.getAllServizioAnnunci.useQuery({
|
const { data, isLoading } = api.servizio.getServizioRinnovi.useQuery({
|
||||||
userId,
|
userId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -70,7 +70,7 @@ export const UserDashboard = ({ userId }: { userId: UsersId }) => {
|
||||||
<h3 className="font-bold text-2xl">Le tue ricerche</h3>
|
<h3 className="font-bold text-2xl">Le tue ricerche</h3>
|
||||||
|
|
||||||
<div className="flex w-full flex-1 grow flex-col space-y-5">
|
<div className="flex w-full flex-1 grow flex-col space-y-5">
|
||||||
<ServizioList isAdmin={false} servizi={data || []} userId={userId} />
|
<ServizioList data={data || []} isAdmin={false} userId={userId} />
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-auto">
|
<div className="mx-auto">
|
||||||
<UserDashboardFaq />
|
<UserDashboardFaq />
|
||||||
|
|
|
||||||
|
|
@ -465,7 +465,7 @@ export const SaldoButton = ({
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
className="w-full sm:w-fit"
|
className="w-full sm:w-fit"
|
||||||
href={`/servizio/pagamento/${servizioId}?type=${OrderTypeEnum.Saldo}`}
|
href={`/servizio/pagamento?servizioId=${servizioId}&type=${OrderTypeEnum.Saldo}`}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
aria-label={locale === "it" ? "Procedi al saldo" : "Proceed to payment"}
|
aria-label={locale === "it" ? "Procedi al saldo" : "Proceed to payment"}
|
||||||
|
|
@ -492,7 +492,7 @@ const SaldoConsulenzaButton = ({
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
className="w-full sm:w-fit"
|
className="w-full sm:w-fit"
|
||||||
href={`/servizio/pagamento/${servizioId}?type=${OrderTypeEnum.Consulenza}`}
|
href={`/servizio/pagamento?servizioId=${servizioId}&type=${OrderTypeEnum.Consulenza}`}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
aria-label={
|
aria-label={
|
||||||
|
|
|
||||||
179
apps/infoalloggi/src/components/servizio/rinnovo.tsx
Normal file
179
apps/infoalloggi/src/components/servizio/rinnovo.tsx
Normal file
|
|
@ -0,0 +1,179 @@
|
||||||
|
import { add, formatDuration, intervalToDuration } from "date-fns";
|
||||||
|
import { it } from "date-fns/locale";
|
||||||
|
import { ArrowRight, CalendarClock, Clock, PackageCheck } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useState } from "react";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "~/components/ui/dialog";
|
||||||
|
import { FormRinnovo } from "~/forms/FormRinnovo";
|
||||||
|
import { useRinnovo } from "~/providers/RinnovoProvider";
|
||||||
|
import OrderTypeEnum from "~/schemas/public/OrderTypeEnum";
|
||||||
|
import type { UsersId } from "~/schemas/public/Users";
|
||||||
|
import { api } from "~/utils/api";
|
||||||
|
|
||||||
|
export const Rinnovo = () => {
|
||||||
|
const { rinnovo, isAdmin } = useRinnovo();
|
||||||
|
const hasPaid = rinnovo.ordini
|
||||||
|
.filter((o) => o.type === OrderTypeEnum.Rinnovo)
|
||||||
|
.find((o) => o.isActive);
|
||||||
|
return (
|
||||||
|
<div className="w-full space-y-8">
|
||||||
|
<div className="flex flex-col flex-wrap items-start justify-between gap-4 sm:flex-row">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<h3 className="font-semibold text-2xl">
|
||||||
|
Rinnovo permanenza {rinnovo.codice}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Clock className="size-5" />
|
||||||
|
<span>
|
||||||
|
Decorrenza: {rinnovo.decorrenza.toLocaleDateString("it-IT")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<CalendarClock className="size-5" />
|
||||||
|
<span>
|
||||||
|
Scadenza: {rinnovo.scadenza.toLocaleDateString("it-IT")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
Permanenza:{" "}
|
||||||
|
{formatDuration(
|
||||||
|
intervalToDuration({
|
||||||
|
start: new Date(rinnovo.decorrenza),
|
||||||
|
end: add(new Date(rinnovo.scadenza), { days: 1 }),
|
||||||
|
}),
|
||||||
|
{ locale: it, format: ["years", "months", "days"] },
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{isAdmin && <EditRinnovoModal />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full justify-center">
|
||||||
|
{hasPaid ? (
|
||||||
|
<div className="flex w-full max-w-md flex-col items-center justify-center gap-4 text-center">
|
||||||
|
<PackageCheck className="size-46 stroke-1" />
|
||||||
|
<p className="font-medium text-green-600 text-lg">
|
||||||
|
Pagamento ricevuto!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex w-full max-w-md flex-col items-center justify-center gap-4 text-center">
|
||||||
|
<PackageCheck className="size-46 stroke-1" />
|
||||||
|
|
||||||
|
<Link
|
||||||
|
aria-label="Attiva Servizio"
|
||||||
|
className="w-full"
|
||||||
|
href={`/servizio/pagamento?rinnovoId=${rinnovo.id}&type=${OrderTypeEnum.Rinnovo}`}
|
||||||
|
>
|
||||||
|
<Button className="w-full text-lg" variant="info">
|
||||||
|
<span>Procedi al pagamento</span>
|
||||||
|
<ArrowRight />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
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 (
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="outline">Modifica Rinnovo</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="w-full px-4 py-10 sm:max-w-5xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Modifica Rinnovo</DialogTitle>
|
||||||
|
<DialogDescription className="sr-only">desc</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<FormRinnovo
|
||||||
|
initialData={rinnovo}
|
||||||
|
onDelete={() => remove({ rinnovoId: rinnovo.id })}
|
||||||
|
onSubmit={(v) => edit({ rinnovoId: rinnovo.id, data: v })}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<Dialog onOpenChange={setOpen} open={open}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setOpen(true);
|
||||||
|
}}
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
Nuovo Rinnovo
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="w-full px-4 py-10 sm:max-w-5xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Creazione Rinnovo</DialogTitle>
|
||||||
|
<DialogDescription className="sr-only">desc</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<FormRinnovo onSubmit={(v) => add({ data: { userId, ...v } })} />
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
|
import { NewRinnovoModal } from "~/components/servizio/rinnovo";
|
||||||
import { FormServizio, type FormValues } from "~/forms/FormServizio";
|
import { FormServizio, type FormValues } from "~/forms/FormServizio";
|
||||||
import type { Servizio } from "~/schemas/public/Servizio";
|
|
||||||
import type { UsersId } from "~/schemas/public/Users";
|
import type { UsersId } from "~/schemas/public/Users";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
|
|
@ -15,37 +15,19 @@ import {
|
||||||
} from "../ui/dialog";
|
} from "../ui/dialog";
|
||||||
|
|
||||||
export const ServiziHeader = ({ userId }: { userId: UsersId }) => {
|
export const ServiziHeader = ({ userId }: { userId: UsersId }) => {
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const [editData, setEditData] = useState<Servizio | undefined>(undefined);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-wrap items-center justify-between gap-2">
|
<div className="flex w-full flex-wrap items-center justify-between gap-2">
|
||||||
<h3 className="font-semibold text-xl">Servizi cliente</h3>
|
<h3 className="font-semibold text-xl">Servizi cliente</h3>
|
||||||
<NewServizioModal
|
<div className="flex items-center gap-2">
|
||||||
editData={editData}
|
<NewServizioModal userId={userId} />
|
||||||
open={open}
|
<NewRinnovoModal userId={userId} />
|
||||||
setEditData={setEditData}
|
</div>
|
||||||
setOpen={setOpen}
|
|
||||||
userId={userId}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const NewServizioModal = ({
|
const NewServizioModal = ({ userId }: { userId: UsersId }) => {
|
||||||
userId,
|
const [open, setOpen] = useState(false);
|
||||||
editData,
|
|
||||||
|
|
||||||
setEditData,
|
|
||||||
open,
|
|
||||||
setOpen,
|
|
||||||
}: {
|
|
||||||
userId: UsersId;
|
|
||||||
editData: Servizio | undefined;
|
|
||||||
setEditData: (data: Servizio | undefined) => void;
|
|
||||||
open: boolean;
|
|
||||||
setOpen: (open: boolean) => void;
|
|
||||||
}) => {
|
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
|
||||||
const { mutate: add } = api.servizio.addServizio.useMutation({
|
const { mutate: add } = api.servizio.addServizio.useMutation({
|
||||||
|
|
@ -56,40 +38,18 @@ const NewServizioModal = ({
|
||||||
toast.success("Servizio creato con successo");
|
toast.success("Servizio creato con successo");
|
||||||
await utils.servizio.invalidate();
|
await utils.servizio.invalidate();
|
||||||
|
|
||||||
setEditData(undefined);
|
|
||||||
setOpen(false);
|
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) {
|
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 (
|
return (
|
||||||
<Dialog onOpenChange={setOpen} open={open}>
|
<Dialog onOpenChange={setOpen} open={open}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditData(undefined);
|
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
}}
|
}}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|
@ -104,7 +64,7 @@ const NewServizioModal = ({
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="overflow-y-auto">
|
<div className="overflow-y-auto">
|
||||||
<FormServizio
|
<FormServizio
|
||||||
initialData={editData}
|
initialData={undefined}
|
||||||
isLimited={false}
|
isLimited={false}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -429,7 +429,7 @@ const Main = () => {
|
||||||
<Link
|
<Link
|
||||||
aria-label="Attiva Servizio"
|
aria-label="Attiva Servizio"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
href={`/servizio/pagamento/${servizio.servizio_id}?type=${OrderTypeEnum.Acconto}`}
|
href={`/servizio/pagamento?servizioId=${servizio.servizio_id}&type=${OrderTypeEnum.Acconto}`}
|
||||||
>
|
>
|
||||||
<Button className="w-full text-lg" variant="info">
|
<Button className="w-full text-lg" variant="info">
|
||||||
<span>Procedi al pagamento</span>
|
<span>Procedi al pagamento</span>
|
||||||
|
|
|
||||||
246
apps/infoalloggi/src/forms/FormRinnovo.tsx
Normal file
246
apps/infoalloggi/src/forms/FormRinnovo.tsx
Normal file
|
|
@ -0,0 +1,246 @@
|
||||||
|
import { add, format, formatDuration, intervalToDuration } from "date-fns";
|
||||||
|
import { it } from "date-fns/locale";
|
||||||
|
import { CalendarIcon, Trash2 } from "lucide-react";
|
||||||
|
import { z } from "zod/v4";
|
||||||
|
import { Confirm } from "~/components/confirm";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "~/components/custom_ui/form";
|
||||||
|
import Input from "~/components/custom_ui/input";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import { Calendar } from "~/components/ui/calendar";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "~/components/ui/popover";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
import { useZodForm } from "~/lib/zodForm";
|
||||||
|
import { useTranslation } from "~/providers/I18nProvider";
|
||||||
|
import type { Rinnovi } from "~/schemas/public/Rinnovi";
|
||||||
|
|
||||||
|
const Schema = z.object({
|
||||||
|
codice: z.string().min(4, "Il codice è obbligatorio"),
|
||||||
|
decorrenza: z.date(),
|
||||||
|
scadenza: z.date(),
|
||||||
|
gestionale_id: z.string().nullable(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type FormValues = z.infer<typeof Schema>;
|
||||||
|
|
||||||
|
export const FormRinnovo = ({
|
||||||
|
initialData,
|
||||||
|
onSubmit,
|
||||||
|
onDelete,
|
||||||
|
}: {
|
||||||
|
initialData?: Rinnovi;
|
||||||
|
onSubmit: (fields: FormValues) => void;
|
||||||
|
onDelete?: () => void;
|
||||||
|
}) => {
|
||||||
|
const { locale, t } = useTranslation();
|
||||||
|
|
||||||
|
z.config(z.locales[locale]());
|
||||||
|
const defaultValues: FormValues = initialData
|
||||||
|
? {
|
||||||
|
codice: initialData.codice,
|
||||||
|
decorrenza: initialData.decorrenza,
|
||||||
|
scadenza: initialData.scadenza,
|
||||||
|
gestionale_id: initialData.gestionale_id,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
codice: "",
|
||||||
|
decorrenza: new Date(),
|
||||||
|
scadenza: new Date(),
|
||||||
|
gestionale_id: null,
|
||||||
|
};
|
||||||
|
const form = useZodForm(Schema, {
|
||||||
|
defaultValues: defaultValues,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [decorrenza, scadenza] = form.watch(["decorrenza", "scadenza"]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form className="space-y-8 px-0.5" onSubmit={form.handleSubmit(onSubmit)}>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="codice"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<div className="flex flex-wrap items-center gap-x-2">
|
||||||
|
<FormLabel htmlFor="codice">Codice</FormLabel>
|
||||||
|
<FormMessage />
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="" {...field} id="codice" />
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="decorrenza"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex w-full flex-col">
|
||||||
|
<div className="flex flex-wrap items-center gap-x-2">
|
||||||
|
<FormLabel htmlFor="decorrenza">Decorrenza</FormLabel>
|
||||||
|
<FormMessage />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<FormControl>
|
||||||
|
<Button
|
||||||
|
className={cn(
|
||||||
|
"h-10 w-full bg-card pl-3 text-left font-medium",
|
||||||
|
!field.value && "text-muted-foreground",
|
||||||
|
"rounded-lg border border-neutral-300 shadow-xs outline-hidden file:border-0 file:bg-transparent file:font-medium file:text-sm disabled:cursor-not-allowed disabled:opacity-50 dark:focus:border-transparent",
|
||||||
|
)}
|
||||||
|
id="decorrenza"
|
||||||
|
variant={"outline"}
|
||||||
|
>
|
||||||
|
{field.value ? (
|
||||||
|
format(field.value, "PPP", {
|
||||||
|
locale: it,
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<span>{t.anagrafica.scegli_data}</span>
|
||||||
|
)}
|
||||||
|
<CalendarIcon className="ml-auto size-4 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</FormControl>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent align="end" className="w-auto p-0">
|
||||||
|
<Calendar
|
||||||
|
autoFocus
|
||||||
|
captionLayout="dropdown"
|
||||||
|
defaultMonth={field.value || new Date()}
|
||||||
|
endMonth={new Date(2050, 12)}
|
||||||
|
locale={it}
|
||||||
|
mode="single"
|
||||||
|
onSelect={field.onChange}
|
||||||
|
selected={field.value || undefined}
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="scadenza"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex w-full flex-col">
|
||||||
|
<div className="flex flex-wrap items-center gap-x-2">
|
||||||
|
<FormLabel htmlFor="scadenza">Scadenza</FormLabel>
|
||||||
|
<FormMessage />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<FormControl>
|
||||||
|
<Button
|
||||||
|
className={cn(
|
||||||
|
"h-10 w-full bg-card pl-3 text-left font-medium",
|
||||||
|
!field.value && "text-muted-foreground",
|
||||||
|
"rounded-lg border border-neutral-300 shadow-xs outline-hidden file:border-0 file:bg-transparent file:font-medium file:text-sm disabled:cursor-not-allowed disabled:opacity-50 dark:focus:border-transparent",
|
||||||
|
)}
|
||||||
|
id="scadenza"
|
||||||
|
variant={"outline"}
|
||||||
|
>
|
||||||
|
{field.value ? (
|
||||||
|
format(field.value, "PPP", {
|
||||||
|
locale: it,
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<span>{t.anagrafica.scegli_data}</span>
|
||||||
|
)}
|
||||||
|
<CalendarIcon className="ml-auto size-4 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</FormControl>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent align="end" className="w-auto p-0">
|
||||||
|
<Calendar
|
||||||
|
autoFocus
|
||||||
|
captionLayout="dropdown"
|
||||||
|
defaultMonth={field.value || new Date()}
|
||||||
|
endMonth={new Date(2050, 12)}
|
||||||
|
locale={it}
|
||||||
|
mode="single"
|
||||||
|
onSelect={field.onChange}
|
||||||
|
selected={field.value || undefined}
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Permanenza:{" "}
|
||||||
|
{formatDuration(
|
||||||
|
intervalToDuration({
|
||||||
|
start: new Date(decorrenza),
|
||||||
|
end: add(new Date(scadenza), { days: 1 }),
|
||||||
|
}),
|
||||||
|
{ locale: it, format: ["years", "months", "days"] },
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Attenzione: un cambiamento nella permanenza andrà ad aggiornare
|
||||||
|
automaticamente l'ordine associato se non ancora pagato.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="gestionale_id"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<div className="flex flex-wrap items-center gap-x-2">
|
||||||
|
<FormLabel htmlFor="gestionale_id">Gestionale ID</FormLabel>
|
||||||
|
<FormMessage />
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder=""
|
||||||
|
{...field}
|
||||||
|
id="gestionale_id"
|
||||||
|
value={field.value || undefined}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center justify-end gap-2">
|
||||||
|
{initialData && (
|
||||||
|
<Confirm
|
||||||
|
description="Sei sicuro di voler eliminare l'annuncio?"
|
||||||
|
onConfirm={() => {
|
||||||
|
if (onDelete) {
|
||||||
|
onDelete();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title="Elimina annuncio"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
aria-label="Delete Annuncio"
|
||||||
|
className="w-fit"
|
||||||
|
variant="destructive"
|
||||||
|
>
|
||||||
|
<Trash2 />
|
||||||
|
<span>Elimina</span>
|
||||||
|
</Button>
|
||||||
|
</Confirm>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button type="submit">Salva</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -20,7 +20,7 @@ type RicercaUserProps = {
|
||||||
const RicercaUser: NextPageWithLayout<RicercaUserProps> = ({
|
const RicercaUser: NextPageWithLayout<RicercaUserProps> = ({
|
||||||
userId,
|
userId,
|
||||||
}: RicercaUserProps) => {
|
}: RicercaUserProps) => {
|
||||||
const { data, isLoading } = api.servizio.getAllServizioAnnunci.useQuery({
|
const { data, isLoading } = api.servizio.getServizioRinnovi.useQuery({
|
||||||
userId,
|
userId,
|
||||||
});
|
});
|
||||||
if (isLoading) return <LoadingPage />;
|
if (isLoading) return <LoadingPage />;
|
||||||
|
|
@ -29,8 +29,7 @@ const RicercaUser: NextPageWithLayout<RicercaUserProps> = ({
|
||||||
<div className="flex flex-col gap-5">
|
<div className="flex flex-col gap-5">
|
||||||
<ServiziHeader userId={userId} />
|
<ServiziHeader userId={userId} />
|
||||||
<AnnunciRichiesti userId={userId} />
|
<AnnunciRichiesti userId={userId} />
|
||||||
|
<ServizioList data={data || []} isAdmin userId={userId} />
|
||||||
<ServizioList isAdmin servizi={data || []} userId={userId} />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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<RinnovoUserProps> = ({
|
||||||
|
rinnovoId,
|
||||||
|
userId,
|
||||||
|
}: RinnovoUserProps) => {
|
||||||
|
const { data, isLoading } = api.servizio.getRinnovo.useQuery({
|
||||||
|
rinnovoId,
|
||||||
|
});
|
||||||
|
if (isLoading) {
|
||||||
|
return <LoadingPage />;
|
||||||
|
}
|
||||||
|
if (!data) {
|
||||||
|
return <Status500 />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="mx-auto max-w-8xl grow space-y-4">
|
||||||
|
<RinnovoProvider
|
||||||
|
isAdmin={true}
|
||||||
|
rinnovo={data}
|
||||||
|
rinnovoId={rinnovoId}
|
||||||
|
userId={userId}
|
||||||
|
>
|
||||||
|
<Rinnovo />
|
||||||
|
</RinnovoProvider>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
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<RinnovoUserProps>;
|
||||||
|
|
||||||
|
RinnovoUser.getLayout = function getLayout(page) {
|
||||||
|
return <AreaRiservataLayoutUserView>{page}</AreaRiservataLayoutUserView>;
|
||||||
|
};
|
||||||
|
|
@ -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<RinnovoPageProps> = ({
|
||||||
|
rinnovoId,
|
||||||
|
}: RinnovoPageProps) => {
|
||||||
|
const { user } = useEnforcedSession();
|
||||||
|
const { data, isLoading } = api.servizio.getRinnovo.useQuery({
|
||||||
|
rinnovoId,
|
||||||
|
});
|
||||||
|
if (isLoading) {
|
||||||
|
return <LoadingPage />;
|
||||||
|
}
|
||||||
|
if (!data) {
|
||||||
|
return <Status500 />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="flex w-full flex-1 flex-col items-start justify-center gap-3 overflow-auto p-2 md:gap-6">
|
||||||
|
<div className="flex w-full flex-1 grow flex-col space-y-5">
|
||||||
|
<RinnovoProvider
|
||||||
|
isAdmin={false}
|
||||||
|
rinnovo={data}
|
||||||
|
rinnovoId={rinnovoId}
|
||||||
|
userId={user.id}
|
||||||
|
>
|
||||||
|
<Rinnovo />
|
||||||
|
</RinnovoProvider>
|
||||||
|
</div>
|
||||||
|
<div className="mx-auto">
|
||||||
|
<UserDashboardFaq />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
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<RinnovoPageProps>;
|
||||||
|
|
||||||
|
RinnovoPage.getLayout = function getLayout(page) {
|
||||||
|
return <AreaRiservataLayout>{page}</AreaRiservataLayout>;
|
||||||
|
};
|
||||||
|
|
@ -2,6 +2,7 @@ import type { GetServerSideProps } from "next";
|
||||||
import CheckoutForm from "~/components/checkout";
|
import CheckoutForm from "~/components/checkout";
|
||||||
import { AreaRiservataLayout } from "~/components/Layout";
|
import { AreaRiservataLayout } from "~/components/Layout";
|
||||||
import stripe from "~/lib/stripe";
|
import stripe from "~/lib/stripe";
|
||||||
|
import { redirectTo500 } from "~/lib/utils";
|
||||||
import type { NextPageWithLayout } from "~/pages/_app";
|
import type { NextPageWithLayout } from "~/pages/_app";
|
||||||
import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper";
|
import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper";
|
||||||
import { zOrdineId } from "~/server/utils/zod_types";
|
import { zOrdineId } from "~/server/utils/zod_types";
|
||||||
|
|
@ -48,23 +49,13 @@ export const getServerSideProps = (async (context) => {
|
||||||
const id = context.params?.ordineId;
|
const id = context.params?.ordineId;
|
||||||
if (typeof id !== "string") {
|
if (typeof id !== "string") {
|
||||||
console.error("Error: ordineId is not a string");
|
console.error("Error: ordineId is not a string");
|
||||||
return {
|
return redirectTo500;
|
||||||
redirect: {
|
|
||||||
destination: "/500",
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsed = zOrdineId.safeParse(id);
|
const parsed = zOrdineId.safeParse(id);
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
console.error("Error parsing ordineId", parsed.error);
|
console.error("Error parsing ordineId", parsed.error);
|
||||||
return {
|
return redirectTo500;
|
||||||
redirect: {
|
|
||||||
destination: "/500",
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const access_token = context.req.cookies.access_token;
|
const access_token = context.req.cookies.access_token;
|
||||||
|
|
@ -94,9 +85,7 @@ export const getServerSideProps = (async (context) => {
|
||||||
customer: customer.id,
|
customer: customer.id,
|
||||||
description: paymentData.nome_it,
|
description: paymentData.nome_it,
|
||||||
metadata: {
|
metadata: {
|
||||||
packId: paymentData.packid,
|
|
||||||
ordineId: paymentData.ordine_id,
|
ordineId: paymentData.ordine_id,
|
||||||
servizioId: paymentData.servizio_id,
|
|
||||||
userId: paymentData.userid,
|
userId: paymentData.userid,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -104,12 +93,7 @@ export const getServerSideProps = (async (context) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!clientSecret) {
|
if (!clientSecret) {
|
||||||
return {
|
return redirectTo500;
|
||||||
redirect: {
|
|
||||||
destination: "/500",
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -121,10 +105,5 @@ export const getServerSideProps = (async (context) => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return redirectTo500;
|
||||||
redirect: {
|
|
||||||
destination: "/500",
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}) satisfies GetServerSideProps<PagamentoPageProps>;
|
}) satisfies GetServerSideProps<PagamentoPageProps>;
|
||||||
|
|
|
||||||
|
|
@ -5,20 +5,20 @@ import { useRouter } from "next/router";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { AreaRiservataLayout } from "~/components/Layout";
|
import { AreaRiservataLayout } from "~/components/Layout";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import { formatCurrency } from "~/lib/utils";
|
import { formatCurrency, redirectTo500 } from "~/lib/utils";
|
||||||
import type { NextPageWithLayout } from "~/pages/_app";
|
import type { NextPageWithLayout } from "~/pages/_app";
|
||||||
import { useTranslation } from "~/providers/I18nProvider";
|
import { useTranslation } from "~/providers/I18nProvider";
|
||||||
import OrderTypeEnum from "~/schemas/public/OrderTypeEnum";
|
import OrderTypeEnum from "~/schemas/public/OrderTypeEnum";
|
||||||
import type { PreparedPaymentData } from "~/server/controllers/pagamenti.controller";
|
import type { PreparedPaymentData } from "~/server/controllers/pagamenti.controller";
|
||||||
import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper";
|
import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper";
|
||||||
import { zServizioId } from "~/server/utils/zod_types";
|
import { zRinnovoId, zServizioId } from "~/server/utils/zod_types";
|
||||||
|
|
||||||
type PreviewPurchaseProps = {
|
type PreviewPurchaseProps = {
|
||||||
stripeDisabled: boolean;
|
stripeDisabled: boolean;
|
||||||
data: PreparedPaymentData;
|
data: PreparedPaymentData;
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* /servizio/pagamento/[servizioId]?type=<OrderTypeEnum> \
|
* /servizio/pagamento?servizioId=<servizioId>|rinnovoId=<rinnovoId>&type=<OrderTypeEnum> \
|
||||||
* Pagina di preparazione al pagamento.
|
* Pagina di preparazione al pagamento.
|
||||||
*/
|
*/
|
||||||
const PreviewPurchasePage: NextPageWithLayout<PreviewPurchaseProps> = ({
|
const PreviewPurchasePage: NextPageWithLayout<PreviewPurchaseProps> = ({
|
||||||
|
|
@ -81,83 +81,76 @@ PreviewPurchasePage.getLayout = function getLayout(page) {
|
||||||
export default PreviewPurchasePage;
|
export default PreviewPurchasePage;
|
||||||
|
|
||||||
export const getServerSideProps = (async (context) => {
|
export const getServerSideProps = (async (context) => {
|
||||||
const id = context.params?.servizioId;
|
|
||||||
|
|
||||||
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 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,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const access_token = context.req.cookies.access_token;
|
const access_token = context.req.cookies.access_token;
|
||||||
|
|
||||||
const helper = await TrpcAuthedFetchingIstance({ access_token });
|
const helper = await TrpcAuthedFetchingIstance({ access_token });
|
||||||
if (helper) {
|
if (!helper) {
|
||||||
const data = await helper.trpc.pagamenti.preparePayment.fetch({
|
return redirectTo500;
|
||||||
type: parsedType.data,
|
|
||||||
servizioId: parsedServizioId.data,
|
|
||||||
});
|
|
||||||
if (!data.success) {
|
|
||||||
console.error("Error preparing payment", data.message);
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: `/500`,
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const flag = await helper.trpc.flags.GetFlagValue.fetch({
|
const flag = await helper.trpc.flags.GetFlagValue.fetch({
|
||||||
id: "STRIPE_DISABLED",
|
id: "STRIPE_DISABLED",
|
||||||
});
|
});
|
||||||
|
const payment_type = context.query?.type;
|
||||||
|
if (typeof payment_type !== "string") {
|
||||||
|
console.error("Error: payment type is not a string");
|
||||||
|
return redirectTo500;
|
||||||
|
}
|
||||||
|
const parsedType = z.enum(OrderTypeEnum).safeParse(payment_type);
|
||||||
|
if (!parsedType.success) {
|
||||||
|
console.error("Error parsing payment type", parsedType.error);
|
||||||
|
return redirectTo500;
|
||||||
|
}
|
||||||
|
const type = parsedType.data;
|
||||||
|
|
||||||
|
if (type === OrderTypeEnum.Rinnovo) {
|
||||||
|
const id = context.query?.rinnovoId;
|
||||||
|
if (typeof id !== "string") {
|
||||||
|
console.error("Error: rinnovoId is not a string");
|
||||||
|
return redirectTo500;
|
||||||
|
}
|
||||||
|
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 {
|
return {
|
||||||
props: {
|
props: {
|
||||||
stripeDisabled: flag || false,
|
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 {
|
return {
|
||||||
redirect: {
|
props: {
|
||||||
destination: "/500",
|
stripeDisabled: flag || false,
|
||||||
permanent: false,
|
data: result.data,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}) satisfies GetServerSideProps<PreviewPurchaseProps>;
|
}) satisfies GetServerSideProps<PreviewPurchaseProps>;
|
||||||
39
apps/infoalloggi/src/providers/RinnovoProvider.tsx
Normal file
39
apps/infoalloggi/src/providers/RinnovoProvider.tsx
Normal file
|
|
@ -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<RinnovoContextProps | null>(null);
|
||||||
|
|
||||||
|
export const RinnovoProvider = ({
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: RinnovoProviderProps) => {
|
||||||
|
return (
|
||||||
|
<RinnovoContext.Provider value={{ ...props }}>
|
||||||
|
{children}
|
||||||
|
</RinnovoContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useRinnovo = () => {
|
||||||
|
const rinnovoContext = useContext(RinnovoContext);
|
||||||
|
if (!rinnovoContext) {
|
||||||
|
throw new Error("useRinnovo must be used within a RinnovoProvider");
|
||||||
|
}
|
||||||
|
return rinnovoContext;
|
||||||
|
};
|
||||||
|
|
@ -6,6 +6,7 @@ import type { PrezziarioIdprezziario } from './Prezziario';
|
||||||
import type { ServizioServizioId } from './Servizio';
|
import type { ServizioServizioId } from './Servizio';
|
||||||
import type { default as OrderTypeEnum } from './OrderTypeEnum';
|
import type { default as OrderTypeEnum } from './OrderTypeEnum';
|
||||||
import type { default as PaymentStatusEnum } from './PaymentStatusEnum';
|
import type { default as PaymentStatusEnum } from './PaymentStatusEnum';
|
||||||
|
import type { RinnoviId } from './Rinnovi';
|
||||||
import type { ColumnType, Selectable, Insertable, Updateable } from 'kysely';
|
import type { ColumnType, Selectable, Insertable, Updateable } from 'kysely';
|
||||||
|
|
||||||
/** Identifier type for public.ordini */
|
/** Identifier type for public.ordini */
|
||||||
|
|
@ -21,7 +22,7 @@ export default interface OrdiniTable {
|
||||||
|
|
||||||
packid: ColumnType<PrezziarioIdprezziario, PrezziarioIdprezziario, PrezziarioIdprezziario>;
|
packid: ColumnType<PrezziarioIdprezziario, PrezziarioIdprezziario, PrezziarioIdprezziario>;
|
||||||
|
|
||||||
servizio_id: ColumnType<ServizioServizioId, ServizioServizioId, ServizioServizioId>;
|
servizio_id: ColumnType<ServizioServizioId | null, ServizioServizioId | null, ServizioServizioId | null>;
|
||||||
|
|
||||||
type: ColumnType<OrderTypeEnum, OrderTypeEnum, OrderTypeEnum>;
|
type: ColumnType<OrderTypeEnum, OrderTypeEnum, OrderTypeEnum>;
|
||||||
|
|
||||||
|
|
@ -38,6 +39,8 @@ export default interface OrdiniTable {
|
||||||
paymentstatus: ColumnType<PaymentStatusEnum | null, PaymentStatusEnum | null, PaymentStatusEnum | null>;
|
paymentstatus: ColumnType<PaymentStatusEnum | null, PaymentStatusEnum | null, PaymentStatusEnum | null>;
|
||||||
|
|
||||||
sconto: ColumnType<number, number | undefined, number>;
|
sconto: ColumnType<number, number | undefined, number>;
|
||||||
|
|
||||||
|
rinnovo_id: ColumnType<RinnoviId | null, RinnoviId | null, RinnoviId | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Ordini = Selectable<OrdiniTable>;
|
export type Ordini = Selectable<OrdiniTable>;
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import type { default as UserInvitesTable } from './UserInvites';
|
||||||
import type { default as EmailsTable } from './Emails';
|
import type { default as EmailsTable } from './Emails';
|
||||||
import type { default as PotenzialiGroupsTable } from './PotenzialiGroups';
|
import type { default as PotenzialiGroupsTable } from './PotenzialiGroups';
|
||||||
import type { default as ComuniTable } from './Comuni';
|
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 EventQueueTable } from './EventQueue';
|
||||||
import type { default as OrdiniTable } from './Ordini';
|
import type { default as OrdiniTable } from './Ordini';
|
||||||
import type { default as PrezziarioTable } from './Prezziario';
|
import type { default as PrezziarioTable } from './Prezziario';
|
||||||
|
|
@ -58,6 +59,8 @@ export default interface PublicSchema {
|
||||||
|
|
||||||
comuni: ComuniTable;
|
comuni: ComuniTable;
|
||||||
|
|
||||||
|
rinnovi: RinnoviTable;
|
||||||
|
|
||||||
event_queue: EventQueueTable;
|
event_queue: EventQueueTable;
|
||||||
|
|
||||||
ordini: OrdiniTable;
|
ordini: OrdiniTable;
|
||||||
|
|
|
||||||
29
apps/infoalloggi/src/schemas/public/Rinnovi.ts
Normal file
29
apps/infoalloggi/src/schemas/public/Rinnovi.ts
Normal file
|
|
@ -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<RinnoviId, RinnoviId | undefined, RinnoviId>;
|
||||||
|
|
||||||
|
userId: ColumnType<UsersId, UsersId, UsersId>;
|
||||||
|
|
||||||
|
codice: ColumnType<string, string, string>;
|
||||||
|
|
||||||
|
decorrenza: ColumnType<Date, Date | string, Date | string>;
|
||||||
|
|
||||||
|
gestionale_id: ColumnType<string | null, string | null, string | null>;
|
||||||
|
|
||||||
|
scadenza: ColumnType<Date, Date | string, Date | string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Rinnovi = Selectable<RinnoviTable>;
|
||||||
|
|
||||||
|
export type NewRinnovi = Insertable<RinnoviTable>;
|
||||||
|
|
||||||
|
export type RinnoviUpdate = Updateable<RinnoviTable>;
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { z } from "zod/v4";
|
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 { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
|
||||||
import {
|
import {
|
||||||
getPagamentoDataForCheckout,
|
getPagamentoDataForCheckout,
|
||||||
|
|
@ -10,7 +10,7 @@ import {
|
||||||
|
|
||||||
import { db } from "~/server/db";
|
import { db } from "~/server/db";
|
||||||
import { genPdfRicevutaCortesia } from "~/server/services/puppeteer.service";
|
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
|
// Create a new ratelimiter, that allows 10 requests per 10 seconds
|
||||||
/*
|
/*
|
||||||
const ratelimit = new RateLimiterHandler({
|
const ratelimit = new RateLimiterHandler({
|
||||||
|
|
@ -42,7 +42,7 @@ export const pagamentiRouter = createTRPCRouter({
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
servizioId: zServizioId,
|
servizioId: zServizioId,
|
||||||
type: z.custom<OrderTypeEnum>(),
|
type: z.enum(OrderTypeEnum),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.output(
|
.output(
|
||||||
|
|
@ -58,7 +58,40 @@ export const pagamentiRouter = createTRPCRouter({
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
.query(async ({ input }) => {
|
.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<PreparedPaymentData>(),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
success: z.literal(false),
|
||||||
|
message: z.string(),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
return await preparePayment({
|
||||||
|
type: OrderTypeEnum.Rinnovo,
|
||||||
|
rinnovoId: input.rinnovoId,
|
||||||
|
});
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getPagamentoDataForCheckout: protectedProcedure
|
getPagamentoDataForCheckout: protectedProcedure
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import type { NewOrdini, OrdiniUpdate } from "~/schemas/public/Ordini";
|
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 { NewServizio, ServizioUpdate } from "~/schemas/public/Servizio";
|
||||||
import type { ServizioAnnunciUpdate } from "~/schemas/public/ServizioAnnunci";
|
import type { ServizioAnnunciUpdate } from "~/schemas/public/ServizioAnnunci";
|
||||||
import type { Users } from "~/schemas/public/Users";
|
import type { Users } from "~/schemas/public/Users";
|
||||||
|
|
@ -27,6 +32,7 @@ import {
|
||||||
interruzioneServizio,
|
interruzioneServizio,
|
||||||
processServizioOnboard,
|
processServizioOnboard,
|
||||||
SbloccaContatti,
|
SbloccaContatti,
|
||||||
|
type ServizioData,
|
||||||
userAccettaConferma,
|
userAccettaConferma,
|
||||||
userSendConfermaIntent,
|
userSendConfermaIntent,
|
||||||
} from "~/server/controllers/servizio.controller";
|
} from "~/server/controllers/servizio.controller";
|
||||||
|
|
@ -37,6 +43,13 @@ import {
|
||||||
removeOrder,
|
removeOrder,
|
||||||
updateOrder,
|
updateOrder,
|
||||||
} from "~/server/services/ordini.service";
|
} from "~/server/services/ordini.service";
|
||||||
|
import {
|
||||||
|
createRinnovo,
|
||||||
|
deleteRinnovo,
|
||||||
|
getRinnoviUser,
|
||||||
|
getRinnovoData,
|
||||||
|
updateRinnovo,
|
||||||
|
} from "~/server/services/rinnovi.service";
|
||||||
import {
|
import {
|
||||||
addServizio,
|
addServizio,
|
||||||
deleteServizio,
|
deleteServizio,
|
||||||
|
|
@ -49,6 +62,7 @@ import {
|
||||||
import {
|
import {
|
||||||
zAnnuncioId,
|
zAnnuncioId,
|
||||||
zOrdineId,
|
zOrdineId,
|
||||||
|
zRinnovoId,
|
||||||
zServizioId,
|
zServizioId,
|
||||||
zUserId,
|
zUserId,
|
||||||
} from "~/server/utils/zod_types";
|
} from "~/server/utils/zod_types";
|
||||||
|
|
@ -406,4 +420,75 @@ export const servizioRouter = createTRPCRouter({
|
||||||
servizioId: input.servizioId,
|
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<NewRinnovi>(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
return await createRinnovo(input.data);
|
||||||
|
}),
|
||||||
|
updateRinnovo: adminProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
data: z.custom<RinnoviUpdate>(),
|
||||||
|
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);
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,20 @@
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { add, intervalToDuration } from "date-fns";
|
||||||
import OrderTypeEnum from "~/schemas/public/OrderTypeEnum";
|
import OrderTypeEnum from "~/schemas/public/OrderTypeEnum";
|
||||||
import type { OrdiniOrdineId } from "~/schemas/public/Ordini";
|
import type { OrdiniOrdineId } from "~/schemas/public/Ordini";
|
||||||
import type {
|
import type {
|
||||||
Prezziario,
|
Prezziario,
|
||||||
PrezziarioIdprezziario,
|
PrezziarioIdprezziario,
|
||||||
} from "~/schemas/public/Prezziario";
|
} from "~/schemas/public/Prezziario";
|
||||||
|
import type { RinnoviId } from "~/schemas/public/Rinnovi";
|
||||||
import type { ServizioServizioId } from "~/schemas/public/Servizio";
|
import type { ServizioServizioId } from "~/schemas/public/Servizio";
|
||||||
import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
|
import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
|
||||||
import { db } from "../db";
|
import { db } from "../db";
|
||||||
import { createOrdine } from "../services/ordini.service";
|
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";
|
import { getServizioById } from "../services/servizio.service";
|
||||||
|
|
||||||
export type PreparedPaymentData = {
|
export type PreparedPaymentData = {
|
||||||
|
|
@ -21,14 +26,29 @@ export type PreparedPaymentData = {
|
||||||
packDesc: string;
|
packDesc: string;
|
||||||
condizioni: string;
|
condizioni: string;
|
||||||
};
|
};
|
||||||
|
export type preparePaymentInput =
|
||||||
|
| {
|
||||||
|
servizioId: ServizioServizioId;
|
||||||
|
type: Exclude<OrderTypeEnum, OrderTypeEnum.Rinnovo>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
rinnovoId: RinnoviId;
|
||||||
|
type: OrderTypeEnum.Rinnovo;
|
||||||
|
};
|
||||||
|
|
||||||
export const preparePayment = async (
|
export const preparePayment = async (
|
||||||
servizioId: ServizioServizioId,
|
props: preparePaymentInput,
|
||||||
type: OrderTypeEnum,
|
|
||||||
): Promise<
|
): Promise<
|
||||||
| { success: true; data: PreparedPaymentData }
|
| { success: true; data: PreparedPaymentData }
|
||||||
| { success: false; message: string }
|
| { success: false; message: string }
|
||||||
> => {
|
> => {
|
||||||
try {
|
try {
|
||||||
|
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 servizio = await getServizioById(servizioId);
|
||||||
const inactiveOrders = await db
|
const inactiveOrders = await db
|
||||||
.selectFrom("ordini")
|
.selectFrom("ordini")
|
||||||
|
|
@ -48,12 +68,6 @@ export const preparePayment = async (
|
||||||
message: `Acconto non trovato per il servizio: ${acconto.message}`,
|
message: `Acconto non trovato per il servizio: ${acconto.message}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (!acconto.pack.testo_condizioni) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: `Condizioni non trovate per acconto: ${acconto.pack.nome_it}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// servizio sta aspettando attivazione acconto
|
// servizio sta aspettando attivazione acconto
|
||||||
|
|
||||||
|
|
@ -72,7 +86,8 @@ export const preparePayment = async (
|
||||||
sconto: preExistingAcconto.sconto,
|
sconto: preExistingAcconto.sconto,
|
||||||
packName: acconto.pack.nome_it,
|
packName: acconto.pack.nome_it,
|
||||||
packDesc: acconto.pack.desc_it,
|
packDesc: acconto.pack.desc_it,
|
||||||
condizioni: acconto.pack.testo_condizioni,
|
condizioni:
|
||||||
|
acconto.pack.testo_condizioni || CONDIZIONI_DEFAULT,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -94,7 +109,8 @@ export const preparePayment = async (
|
||||||
sconto: 0,
|
sconto: 0,
|
||||||
packName: acconto.pack.nome_it,
|
packName: acconto.pack.nome_it,
|
||||||
packDesc: acconto.pack.desc_it,
|
packDesc: acconto.pack.desc_it,
|
||||||
condizioni: acconto.pack.testo_condizioni,
|
condizioni:
|
||||||
|
acconto.pack.testo_condizioni || CONDIZIONI_DEFAULT,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -121,12 +137,7 @@ export const preparePayment = async (
|
||||||
message: `Saldo non trovato per il servizio: ${saldo.message}`,
|
message: `Saldo non trovato per il servizio: ${saldo.message}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (!saldo.pack.testo_condizioni) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: `Condizioni non trovate per saldo: ${saldo.pack.nome_it}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const preExistingSaldo = inactiveOrders.find(
|
const preExistingSaldo = inactiveOrders.find(
|
||||||
(ordine) => ordine.type === OrderTypeEnum.Saldo,
|
(ordine) => ordine.type === OrderTypeEnum.Saldo,
|
||||||
);
|
);
|
||||||
|
|
@ -141,7 +152,8 @@ export const preparePayment = async (
|
||||||
sconto: preExistingSaldo.sconto,
|
sconto: preExistingSaldo.sconto,
|
||||||
packName: saldo.pack.nome_it,
|
packName: saldo.pack.nome_it,
|
||||||
packDesc: saldo.pack.desc_it,
|
packDesc: saldo.pack.desc_it,
|
||||||
condizioni: saldo.pack.testo_condizioni,
|
condizioni:
|
||||||
|
saldo.pack.testo_condizioni || CONDIZIONI_DEFAULT,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -162,7 +174,7 @@ export const preparePayment = async (
|
||||||
sconto: 0,
|
sconto: 0,
|
||||||
packName: saldo.pack.nome_it,
|
packName: saldo.pack.nome_it,
|
||||||
packDesc: saldo.pack.desc_it,
|
packDesc: saldo.pack.desc_it,
|
||||||
condizioni: saldo.pack.testo_condizioni,
|
condizioni: saldo.pack.testo_condizioni || CONDIZIONI_DEFAULT,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -196,12 +208,7 @@ export const preparePayment = async (
|
||||||
message: `Pack della consulenza non trovato: ${existingConsulenza.packid}`,
|
message: `Pack della consulenza non trovato: ${existingConsulenza.packid}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (!consulenzaPack.testo_condizioni) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: `Condizioni non trovate per consulenza: ${consulenzaPack.nome_it}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
|
|
@ -211,7 +218,8 @@ export const preparePayment = async (
|
||||||
sconto: existingConsulenza.sconto,
|
sconto: existingConsulenza.sconto,
|
||||||
packName: consulenzaPack.nome_it,
|
packName: consulenzaPack.nome_it,
|
||||||
packDesc: consulenzaPack.desc_it,
|
packDesc: consulenzaPack.desc_it,
|
||||||
condizioni: consulenzaPack.testo_condizioni,
|
condizioni:
|
||||||
|
consulenzaPack.testo_condizioni || CONDIZIONI_DEFAULT,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -229,46 +237,6 @@ export const preparePayment = async (
|
||||||
"Il servizio ha già una consulenza attiva, non è possibile preparare un nuovo pagamento",
|
"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) {
|
|
||||||
// rinnovo già creato, procedere con questo
|
|
||||||
const rinnovoPack = await getPrezziarioByIdHandler(
|
|
||||||
existingRinnovo.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}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
ordine_id: existingRinnovo.ordine_id,
|
|
||||||
packid: existingRinnovo.packid,
|
|
||||||
amount_cent: existingRinnovo.amount_cent,
|
|
||||||
sconto: existingRinnovo.sconto,
|
|
||||||
packName: rinnovoPack.nome_it,
|
|
||||||
packDesc: rinnovoPack.desc_it,
|
|
||||||
condizioni: rinnovoPack.testo_condizioni,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message:
|
|
||||||
"Il servizio non ha un pacchetto di rinnovo, è necessario inserirlo prima di procedere",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case OrderTypeEnum.Altro: {
|
case OrderTypeEnum.Altro: {
|
||||||
const existingAltro = inactiveOrders.find(
|
const existingAltro = inactiveOrders.find(
|
||||||
(ordine) => ordine.type === OrderTypeEnum.Altro,
|
(ordine) => ordine.type === OrderTypeEnum.Altro,
|
||||||
|
|
@ -284,12 +252,7 @@ export const preparePayment = async (
|
||||||
message: `Pack dell'altro non trovato: ${existingAltro.packid}`,
|
message: `Pack dell'altro non trovato: ${existingAltro.packid}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (!altroPack.testo_condizioni) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: `Condizioni non trovate per altro: ${altroPack.nome_it}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
|
|
@ -299,7 +262,7 @@ export const preparePayment = async (
|
||||||
sconto: existingAltro.sconto,
|
sconto: existingAltro.sconto,
|
||||||
packName: altroPack.nome_it,
|
packName: altroPack.nome_it,
|
||||||
packDesc: altroPack.desc_it,
|
packDesc: altroPack.desc_it,
|
||||||
condizioni: altroPack.testo_condizioni,
|
condizioni: altroPack.testo_condizioni || CONDIZIONI_DEFAULT,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -311,7 +274,92 @@ export const preparePayment = async (
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
throw new Error(`Tipo di ordine non valido: ${type satisfies never}`);
|
throw new Error(
|
||||||
|
`Tipo di ordine non valido: ${type satisfies never}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case OrderTypeEnum.Rinnovo: {
|
||||||
|
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(
|
||||||
|
unusedOrdine.packid,
|
||||||
|
);
|
||||||
|
if (!rinnovoPack) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `Pack del rinnovo non trovato: ${unusedOrdine.packid}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
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_DEFAULT,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// nessun rinnovo preesistente, crearne uno nuovo
|
||||||
|
const rinnovo = await db
|
||||||
|
.selectFrom("rinnovi")
|
||||||
|
.selectAll()
|
||||||
|
.where("id", "=", rinnovoId)
|
||||||
|
.executeTakeFirstOrThrow(
|
||||||
|
() => new Error(`Rinnovo non trovato - id: ${rinnovoId}`),
|
||||||
|
);
|
||||||
|
const pack = await RinnovoSolver(rinnovo.decorrenza, rinnovo.scadenza);
|
||||||
|
if (!pack.success) {
|
||||||
|
return {
|
||||||
|
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: 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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
|
|
@ -479,6 +527,51 @@ export const SaldoSolver = async ({
|
||||||
return { success: false, message: "Seleziona tipologia" };
|
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<GetPackResult | GetPackError> => {
|
||||||
|
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 (
|
export const getPacksPerServizio = async (
|
||||||
servizioId: ServizioServizioId,
|
servizioId: ServizioServizioId,
|
||||||
|
|
|
||||||
|
|
@ -810,7 +810,7 @@ export const SbloccaContatti = async ({
|
||||||
},
|
},
|
||||||
tipo: "email",
|
tipo: "email",
|
||||||
},
|
},
|
||||||
lock_expires: new Date(Date.now() + 1000),
|
lock_expires: new Date(Date.now() + 3000),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (canSendNotification()) {
|
if (canSendNotification()) {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import type { TestiEStringheStingaId } from "~/schemas/public/TestiEStringhe";
|
||||||
import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
|
import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
|
||||||
import { db } from "~/server/db";
|
import { db } from "~/server/db";
|
||||||
import { NewMail } from "~/server/services/mailer";
|
import { NewMail } from "~/server/services/mailer";
|
||||||
|
import { CONDIZIONI_DEFAULT } from "~/server/services/prezziario.service";
|
||||||
import { getUser } from "~/server/services/user.service";
|
import { getUser } from "~/server/services/user.service";
|
||||||
import { addEventToQueue } from "./event_queue.controller";
|
import { addEventToQueue } from "./event_queue.controller";
|
||||||
|
|
||||||
|
|
@ -80,30 +81,13 @@ export const whIntentSucceededHandler = async ({
|
||||||
})
|
})
|
||||||
.where("intent_id", "=", pIntentId)
|
.where("intent_id", "=", pIntentId)
|
||||||
.where("ordine_id", "=", ordineId)
|
.where("ordine_id", "=", ordineId)
|
||||||
.returning([
|
.returningAll()
|
||||||
"servizio_id",
|
|
||||||
"ordine_id",
|
|
||||||
"userid",
|
|
||||||
"type",
|
|
||||||
"created_at",
|
|
||||||
"packid",
|
|
||||||
])
|
|
||||||
.executeTakeFirstOrThrow(() => {
|
.executeTakeFirstOrThrow(() => {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Payment not found - intent_id: ${pIntentId}, ordineId: ${ordineId}`,
|
`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
|
const condizioni = await trx
|
||||||
.selectFrom("prezziario")
|
.selectFrom("prezziario")
|
||||||
.select("testo_condizioni")
|
.select("testo_condizioni")
|
||||||
|
|
@ -114,13 +98,19 @@ export const whIntentSucceededHandler = async ({
|
||||||
db: trx,
|
db: trx,
|
||||||
userId: ordine.userid,
|
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) {
|
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
|
await trx
|
||||||
.updateTable("servizio")
|
.updateTable("servizio")
|
||||||
.where("servizio_id", "=", servizio.servizio_id)
|
.where("servizio_id", "=", ordine.servizio_id)
|
||||||
.set({
|
.set({
|
||||||
decorrenza: new Date(),
|
decorrenza: new Date(),
|
||||||
isOkAcconto: true,
|
isOkAcconto: true,
|
||||||
|
|
@ -140,19 +130,16 @@ export const whIntentSucceededHandler = async ({
|
||||||
attachments: [
|
attachments: [
|
||||||
{
|
{
|
||||||
filename: `condizioni_contrattuali_${ordine.created_at.toISOString()}.pdf`,
|
filename: `condizioni_contrattuali_${ordine.created_at.toISOString()}.pdf`,
|
||||||
//ATTENTION: hardcoded condizioni key
|
|
||||||
|
|
||||||
generate: {
|
generate: {
|
||||||
type: "condizioni",
|
type: "condizioni",
|
||||||
stringId: (condizioni?.testo_condizioni ||
|
stringId: (condizioni?.testo_condizioni ||
|
||||||
"CONDIZIONI_CERCHI") as TestiEStringheStingaId,
|
CONDIZIONI_DEFAULT) as TestiEStringheStingaId,
|
||||||
},
|
},
|
||||||
encoding: "base64",
|
encoding: "base64",
|
||||||
contentType: "application/pdf",
|
contentType: "application/pdf",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
filename: `ricevuta_cortesia_${ordine.ordine_id}.pdf`,
|
filename: `ricevuta_cortesia_${ordine.ordine_id}.pdf`,
|
||||||
|
|
||||||
generate: {
|
generate: {
|
||||||
type: "ricevuta_cortesia",
|
type: "ricevuta_cortesia",
|
||||||
ordineId: ordine.ordine_id,
|
ordineId: ordine.ordine_id,
|
||||||
|
|
@ -170,11 +157,27 @@ export const whIntentSucceededHandler = async ({
|
||||||
//SERVIZIO ATTIVATO
|
//SERVIZIO ATTIVATO
|
||||||
|
|
||||||
break;
|
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
|
await trx
|
||||||
.updateTable("servizio")
|
.updateTable("servizio")
|
||||||
.where("servizio_id", "=", servizio.servizio_id)
|
.where("servizio_id", "=", ordine.servizio_id)
|
||||||
.set({
|
.set({
|
||||||
isOkConsulenza:
|
isOkConsulenza:
|
||||||
servizio.tipologia === TipologiaPosizioneEnum.Transitorio,
|
servizio.tipologia === TipologiaPosizioneEnum.Transitorio,
|
||||||
|
|
@ -198,7 +201,6 @@ export const whIntentSucceededHandler = async ({
|
||||||
type: "ricevuta_cortesia",
|
type: "ricevuta_cortesia",
|
||||||
ordineId: ordine.ordine_id,
|
ordineId: ordine.ordine_id,
|
||||||
},
|
},
|
||||||
|
|
||||||
encoding: "base64",
|
encoding: "base64",
|
||||||
contentType: "application/pdf",
|
contentType: "application/pdf",
|
||||||
},
|
},
|
||||||
|
|
@ -209,12 +211,49 @@ export const whIntentSucceededHandler = async ({
|
||||||
},
|
},
|
||||||
lock_expires,
|
lock_expires,
|
||||||
});
|
});
|
||||||
//PAGAMENTO SALDO CONFERMATO
|
|
||||||
break;
|
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
|
await trx
|
||||||
.updateTable("servizio")
|
.updateTable("servizio")
|
||||||
.where("servizio_id", "=", servizio.servizio_id)
|
.where("servizio_id", "=", ordine.servizio_id)
|
||||||
.set({
|
.set({
|
||||||
isOkConsulenza: true,
|
isOkConsulenza: true,
|
||||||
})
|
})
|
||||||
|
|
@ -232,7 +271,6 @@ export const whIntentSucceededHandler = async ({
|
||||||
attachments: [
|
attachments: [
|
||||||
{
|
{
|
||||||
filename: `ricevuta_cortesia_${ordine.ordine_id}.pdf`,
|
filename: `ricevuta_cortesia_${ordine.ordine_id}.pdf`,
|
||||||
|
|
||||||
generate: {
|
generate: {
|
||||||
type: "ricevuta_cortesia",
|
type: "ricevuta_cortesia",
|
||||||
ordineId: ordine.ordine_id,
|
ordineId: ordine.ordine_id,
|
||||||
|
|
@ -247,38 +285,38 @@ export const whIntentSucceededHandler = async ({
|
||||||
},
|
},
|
||||||
lock_expires,
|
lock_expires,
|
||||||
});
|
});
|
||||||
//CONSULENZA ATTIVATA
|
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case OrderTypeEnum.Altro:
|
case OrderTypeEnum.Altro:
|
||||||
// TODO: Handle other types
|
// Handle "Altro" type if needed
|
||||||
await addEventToQueue({
|
// await addEventToQueue({
|
||||||
data: {
|
// data: {
|
||||||
tipo: "email",
|
// tipo: "email",
|
||||||
data: {
|
// data: {
|
||||||
template: {
|
// template: {
|
||||||
mailType: "pagamentoConferma",
|
// mailType: "pagamentoConferma",
|
||||||
},
|
// },
|
||||||
mail: {
|
// mail: {
|
||||||
subject: "Pagamento Confermato - Infoalloggi.it",
|
// subject: "Pagamento Confermato - Infoalloggi.it",
|
||||||
to: utente.email,
|
// to: utente.email,
|
||||||
attachments: [
|
// attachments: [
|
||||||
{
|
// {
|
||||||
filename: `ricevuta_cortesia_${ordine.ordine_id}.pdf`,
|
// filename: `ricevuta_cortesia_${ordine.ordine_id}.pdf`,
|
||||||
|
|
||||||
generate: {
|
// generate: {
|
||||||
type: "ricevuta_cortesia",
|
// type: "ricevuta_cortesia",
|
||||||
ordineId: ordine.ordine_id,
|
// ordineId: ordine.ordine_id,
|
||||||
},
|
// },
|
||||||
encoding: "base64",
|
// encoding: "base64",
|
||||||
contentType: "application/pdf",
|
// contentType: "application/pdf",
|
||||||
},
|
// },
|
||||||
],
|
// ],
|
||||||
},
|
// },
|
||||||
userId: utente.id,
|
// userId: utente.id,
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
lock_expires,
|
// lock_expires,
|
||||||
});
|
// });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
await addEventToQueue({
|
await addEventToQueue({
|
||||||
|
|
@ -315,7 +353,6 @@ export const whIntentSucceededHandler = async ({
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Enhanced error logging
|
|
||||||
console.error("whIntentSucceededHandler error:", {
|
console.error("whIntentSucceededHandler error:", {
|
||||||
pIntentId,
|
pIntentId,
|
||||||
ordineId,
|
ordineId,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import type { Insertable, Selectable, Updateable } from "kysely";
|
||||||
|
import type OrdiniTable from "~/schemas/public/Ordini";
|
||||||
import type {
|
import type {
|
||||||
NewOrdini,
|
|
||||||
Ordini,
|
Ordini,
|
||||||
OrdiniOrdineId,
|
OrdiniOrdineId,
|
||||||
OrdiniUpdate,
|
OrdiniUpdate,
|
||||||
|
|
@ -8,6 +9,22 @@ import type {
|
||||||
import type { Users, UsersId } from "~/schemas/public/Users";
|
import type { Users, UsersId } from "~/schemas/public/Users";
|
||||||
import { db } from "~/server/db";
|
import { db } from "~/server/db";
|
||||||
|
|
||||||
|
export type ServizioOrdiniTable = Omit<OrdiniTable, "rinnovo_id">;
|
||||||
|
|
||||||
|
export type ServizioOrdini = Selectable<ServizioOrdiniTable>;
|
||||||
|
|
||||||
|
export type NewServizioOrdini = Insertable<ServizioOrdiniTable>;
|
||||||
|
|
||||||
|
export type ServizioOrdiniUpdate = Updateable<ServizioOrdiniTable>;
|
||||||
|
|
||||||
|
export type RinnovoOrdiniTable = Omit<OrdiniTable, "servizio_id">;
|
||||||
|
|
||||||
|
export type RinnovoOrdini = Selectable<RinnovoOrdiniTable>;
|
||||||
|
|
||||||
|
export type NewRinnovoOrdini = Insertable<RinnovoOrdiniTable>;
|
||||||
|
|
||||||
|
export type RinnovoOrdiniUpdate = Updateable<RinnovoOrdiniTable>;
|
||||||
|
|
||||||
export type Ordini_w_Utente = Ordini & Pick<Users, "username">;
|
export type Ordini_w_Utente = Ordini & Pick<Users, "username">;
|
||||||
export const getOrdini = async (
|
export const getOrdini = async (
|
||||||
userId: UsersId | undefined,
|
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 {
|
try {
|
||||||
return await db
|
return await db
|
||||||
.insertInto("ordini")
|
.insertInto("ordini")
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,11 @@ import type {
|
||||||
PrezziarioIdprezziario,
|
PrezziarioIdprezziario,
|
||||||
PrezziarioUpdate,
|
PrezziarioUpdate,
|
||||||
} from "~/schemas/public/Prezziario";
|
} from "~/schemas/public/Prezziario";
|
||||||
|
import type { TestiEStringheStingaId } from "~/schemas/public/TestiEStringhe";
|
||||||
import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
|
import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
|
||||||
import { db, type Querier } from "~/server/db";
|
import { db, type Querier } from "~/server/db";
|
||||||
|
|
||||||
|
export const CONDIZIONI_DEFAULT = "CONDIZIONI_CERCHI" as TestiEStringheStingaId;
|
||||||
export const getPrezziario = async () => {
|
export const getPrezziario = async () => {
|
||||||
try {
|
try {
|
||||||
return await db
|
return await db
|
||||||
|
|
|
||||||
171
apps/infoalloggi/src/server/services/rinnovi.service.ts
Normal file
171
apps/infoalloggi/src/server/services/rinnovi.service.ts
Normal file
|
|
@ -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<ReturnType<typeof getRinnovoData>>;
|
||||||
|
|
||||||
|
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}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -8,6 +8,7 @@ import type { FlagsId } from "~/schemas/public/Flags";
|
||||||
import type { MessagesMessageid } from "~/schemas/public/Messages";
|
import type { MessagesMessageid } from "~/schemas/public/Messages";
|
||||||
import type { OrdiniOrdineId } from "~/schemas/public/Ordini";
|
import type { OrdiniOrdineId } from "~/schemas/public/Ordini";
|
||||||
import type { PrezziarioIdprezziario } from "~/schemas/public/Prezziario";
|
import type { PrezziarioIdprezziario } from "~/schemas/public/Prezziario";
|
||||||
|
import type { RinnoviId } from "~/schemas/public/Rinnovi";
|
||||||
import type { ServizioServizioId } from "~/schemas/public/Servizio";
|
import type { ServizioServizioId } from "~/schemas/public/Servizio";
|
||||||
import type { TestiEStringheStingaId } from "~/schemas/public/TestiEStringhe";
|
import type { TestiEStringheStingaId } from "~/schemas/public/TestiEStringhe";
|
||||||
import type { UsersId } from "~/schemas/public/Users";
|
import type { UsersId } from "~/schemas/public/Users";
|
||||||
|
|
@ -85,3 +86,8 @@ export const zServizioId = z.custom<ServizioServizioId>(
|
||||||
// (val) => typeof val === "number",
|
// (val) => typeof val === "number",
|
||||||
// "falied to validate NazioneId",
|
// "falied to validate NazioneId",
|
||||||
// );
|
// );
|
||||||
|
|
||||||
|
export const zRinnovoId = z.custom<RinnoviId>(
|
||||||
|
(val) => typeof val === "string",
|
||||||
|
"falied to validate RinnovoId",
|
||||||
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue