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 }) => {
|
||||
const { data, isLoading } = api.servizio.getAllServizioAnnunci.useQuery({
|
||||
const { data, isLoading } = api.servizio.getServizioRinnovi.useQuery({
|
||||
userId,
|
||||
});
|
||||
|
||||
|
|
@ -70,7 +70,7 @@ export const UserDashboard = ({ userId }: { userId: UsersId }) => {
|
|||
<h3 className="font-bold text-2xl">Le tue ricerche</h3>
|
||||
|
||||
<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 className="mx-auto">
|
||||
<UserDashboardFaq />
|
||||
|
|
|
|||
|
|
@ -465,7 +465,7 @@ export const SaldoButton = ({
|
|||
return (
|
||||
<Link
|
||||
className="w-full sm:w-fit"
|
||||
href={`/servizio/pagamento/${servizioId}?type=${OrderTypeEnum.Saldo}`}
|
||||
href={`/servizio/pagamento?servizioId=${servizioId}&type=${OrderTypeEnum.Saldo}`}
|
||||
>
|
||||
<Button
|
||||
aria-label={locale === "it" ? "Procedi al saldo" : "Proceed to payment"}
|
||||
|
|
@ -492,7 +492,7 @@ const SaldoConsulenzaButton = ({
|
|||
return (
|
||||
<Link
|
||||
className="w-full sm:w-fit"
|
||||
href={`/servizio/pagamento/${servizioId}?type=${OrderTypeEnum.Consulenza}`}
|
||||
href={`/servizio/pagamento?servizioId=${servizioId}&type=${OrderTypeEnum.Consulenza}`}
|
||||
>
|
||||
<Button
|
||||
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 toast from "react-hot-toast";
|
||||
import { NewRinnovoModal } from "~/components/servizio/rinnovo";
|
||||
import { FormServizio, type FormValues } from "~/forms/FormServizio";
|
||||
import type { Servizio } from "~/schemas/public/Servizio";
|
||||
import type { UsersId } from "~/schemas/public/Users";
|
||||
import { api } from "~/utils/api";
|
||||
import { Button } from "../ui/button";
|
||||
|
|
@ -15,37 +15,19 @@ import {
|
|||
} from "../ui/dialog";
|
||||
|
||||
export const ServiziHeader = ({ userId }: { userId: UsersId }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [editData, setEditData] = useState<Servizio | undefined>(undefined);
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-wrap items-center justify-between gap-2">
|
||||
<h3 className="font-semibold text-xl">Servizi cliente</h3>
|
||||
<NewServizioModal
|
||||
editData={editData}
|
||||
open={open}
|
||||
setEditData={setEditData}
|
||||
setOpen={setOpen}
|
||||
userId={userId}
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<NewServizioModal userId={userId} />
|
||||
<NewRinnovoModal userId={userId} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const NewServizioModal = ({
|
||||
userId,
|
||||
editData,
|
||||
|
||||
setEditData,
|
||||
open,
|
||||
setOpen,
|
||||
}: {
|
||||
userId: UsersId;
|
||||
editData: Servizio | undefined;
|
||||
setEditData: (data: Servizio | undefined) => void;
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
}) => {
|
||||
const NewServizioModal = ({ userId }: { userId: UsersId }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const utils = api.useUtils();
|
||||
|
||||
const { mutate: add } = api.servizio.addServizio.useMutation({
|
||||
|
|
@ -56,40 +38,18 @@ const NewServizioModal = ({
|
|||
toast.success("Servizio creato con successo");
|
||||
await utils.servizio.invalidate();
|
||||
|
||||
setEditData(undefined);
|
||||
setOpen(false);
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: update } = api.servizio.updateServizio.useMutation({
|
||||
onError: (error) => {
|
||||
toast.error(error.message);
|
||||
},
|
||||
onSuccess: async () => {
|
||||
toast.success("Servizio modificato con successo");
|
||||
await utils.servizio.invalidate();
|
||||
setEditData(undefined);
|
||||
setOpen(false);
|
||||
},
|
||||
});
|
||||
function onSubmit(fields: FormValues) {
|
||||
if (editData) {
|
||||
update({
|
||||
data: {
|
||||
...fields,
|
||||
},
|
||||
servizioId: editData.servizio_id,
|
||||
});
|
||||
} else {
|
||||
add({ data: { ...fields, user_id: userId } });
|
||||
}
|
||||
add({ data: { ...fields, user_id: userId } });
|
||||
}
|
||||
return (
|
||||
<Dialog onOpenChange={setOpen} open={open}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setEditData(undefined);
|
||||
setOpen(true);
|
||||
}}
|
||||
variant="outline"
|
||||
|
|
@ -104,7 +64,7 @@ const NewServizioModal = ({
|
|||
</DialogHeader>
|
||||
<div className="overflow-y-auto">
|
||||
<FormServizio
|
||||
initialData={editData}
|
||||
initialData={undefined}
|
||||
isLimited={false}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -429,7 +429,7 @@ const Main = () => {
|
|||
<Link
|
||||
aria-label="Attiva Servizio"
|
||||
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">
|
||||
<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> = ({
|
||||
userId,
|
||||
}: RicercaUserProps) => {
|
||||
const { data, isLoading } = api.servizio.getAllServizioAnnunci.useQuery({
|
||||
const { data, isLoading } = api.servizio.getServizioRinnovi.useQuery({
|
||||
userId,
|
||||
});
|
||||
if (isLoading) return <LoadingPage />;
|
||||
|
|
@ -29,8 +29,7 @@ const RicercaUser: NextPageWithLayout<RicercaUserProps> = ({
|
|||
<div className="flex flex-col gap-5">
|
||||
<ServiziHeader userId={userId} />
|
||||
<AnnunciRichiesti userId={userId} />
|
||||
|
||||
<ServizioList isAdmin servizi={data || []} userId={userId} />
|
||||
<ServizioList data={data || []} isAdmin userId={userId} />
|
||||
</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 { AreaRiservataLayout } from "~/components/Layout";
|
||||
import stripe from "~/lib/stripe";
|
||||
import { redirectTo500 } from "~/lib/utils";
|
||||
import type { NextPageWithLayout } from "~/pages/_app";
|
||||
import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper";
|
||||
import { zOrdineId } from "~/server/utils/zod_types";
|
||||
|
|
@ -48,23 +49,13 @@ export const getServerSideProps = (async (context) => {
|
|||
const id = context.params?.ordineId;
|
||||
if (typeof id !== "string") {
|
||||
console.error("Error: ordineId is not a string");
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/500",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
return redirectTo500;
|
||||
}
|
||||
|
||||
const parsed = zOrdineId.safeParse(id);
|
||||
if (!parsed.success) {
|
||||
console.error("Error parsing ordineId", parsed.error);
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/500",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
return redirectTo500;
|
||||
}
|
||||
|
||||
const access_token = context.req.cookies.access_token;
|
||||
|
|
@ -94,9 +85,7 @@ export const getServerSideProps = (async (context) => {
|
|||
customer: customer.id,
|
||||
description: paymentData.nome_it,
|
||||
metadata: {
|
||||
packId: paymentData.packid,
|
||||
ordineId: paymentData.ordine_id,
|
||||
servizioId: paymentData.servizio_id,
|
||||
userId: paymentData.userid,
|
||||
},
|
||||
|
||||
|
|
@ -104,12 +93,7 @@ export const getServerSideProps = (async (context) => {
|
|||
});
|
||||
|
||||
if (!clientSecret) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/500",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
return redirectTo500;
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -121,10 +105,5 @@ export const getServerSideProps = (async (context) => {
|
|||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/500",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
return redirectTo500;
|
||||
}) satisfies GetServerSideProps<PagamentoPageProps>;
|
||||
|
|
|
|||
|
|
@ -5,20 +5,20 @@ import { useRouter } from "next/router";
|
|||
import z from "zod";
|
||||
import { AreaRiservataLayout } from "~/components/Layout";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { formatCurrency } from "~/lib/utils";
|
||||
import { formatCurrency, redirectTo500 } from "~/lib/utils";
|
||||
import type { NextPageWithLayout } from "~/pages/_app";
|
||||
import { useTranslation } from "~/providers/I18nProvider";
|
||||
import OrderTypeEnum from "~/schemas/public/OrderTypeEnum";
|
||||
import type { PreparedPaymentData } from "~/server/controllers/pagamenti.controller";
|
||||
import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper";
|
||||
import { zServizioId } from "~/server/utils/zod_types";
|
||||
import { zRinnovoId, zServizioId } from "~/server/utils/zod_types";
|
||||
|
||||
type PreviewPurchaseProps = {
|
||||
stripeDisabled: boolean;
|
||||
data: PreparedPaymentData;
|
||||
};
|
||||
/**
|
||||
* /servizio/pagamento/[servizioId]?type=<OrderTypeEnum> \
|
||||
* /servizio/pagamento?servizioId=<servizioId>|rinnovoId=<rinnovoId>&type=<OrderTypeEnum> \
|
||||
* Pagina di preparazione al pagamento.
|
||||
*/
|
||||
const PreviewPurchasePage: NextPageWithLayout<PreviewPurchaseProps> = ({
|
||||
|
|
@ -81,83 +81,76 @@ PreviewPurchasePage.getLayout = function getLayout(page) {
|
|||
export default PreviewPurchasePage;
|
||||
|
||||
export const getServerSideProps = (async (context) => {
|
||||
const id = context.params?.servizioId;
|
||||
const access_token = context.req.cookies.access_token;
|
||||
|
||||
if (typeof id !== "string") {
|
||||
console.error("Error: servizioId is not a string");
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/500",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const parsedServizioId = zServizioId.safeParse(id);
|
||||
if (!parsedServizioId.success) {
|
||||
console.error("Error parsing servizioId", parsedServizioId.error);
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/500",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
const helper = await TrpcAuthedFetchingIstance({ access_token });
|
||||
if (!helper) {
|
||||
return redirectTo500;
|
||||
}
|
||||
|
||||
const flag = await helper.trpc.flags.GetFlagValue.fetch({
|
||||
id: "STRIPE_DISABLED",
|
||||
});
|
||||
const payment_type = context.query?.type;
|
||||
if (typeof payment_type !== "string") {
|
||||
console.error("Error: payment type is not a string");
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/500",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
return redirectTo500;
|
||||
}
|
||||
const parsedType = z.enum(OrderTypeEnum).safeParse(payment_type);
|
||||
if (!parsedType.success) {
|
||||
console.error("Error parsing payment type", parsedType.error);
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/500",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
return redirectTo500;
|
||||
}
|
||||
const type = parsedType.data;
|
||||
|
||||
const access_token = context.req.cookies.access_token;
|
||||
|
||||
const helper = await TrpcAuthedFetchingIstance({ access_token });
|
||||
if (helper) {
|
||||
const data = await helper.trpc.pagamenti.preparePayment.fetch({
|
||||
type: parsedType.data,
|
||||
servizioId: parsedServizioId.data,
|
||||
});
|
||||
if (!data.success) {
|
||||
console.error("Error preparing payment", data.message);
|
||||
return {
|
||||
redirect: {
|
||||
destination: `/500`,
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
if (type === OrderTypeEnum.Rinnovo) {
|
||||
const id = context.query?.rinnovoId;
|
||||
if (typeof id !== "string") {
|
||||
console.error("Error: rinnovoId is not a string");
|
||||
return redirectTo500;
|
||||
}
|
||||
|
||||
const flag = await helper.trpc.flags.GetFlagValue.fetch({
|
||||
id: "STRIPE_DISABLED",
|
||||
const parsedRinnovoId = zRinnovoId.safeParse(id);
|
||||
if (!parsedRinnovoId.success) {
|
||||
console.error("Error parsing rinnovoId", parsedRinnovoId.error);
|
||||
return redirectTo500;
|
||||
}
|
||||
const result = await helper.trpc.pagamenti.preparePaymentRinnovi.fetch({
|
||||
rinnovoId: parsedRinnovoId.data,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
console.error("Error preparing payment", result.message);
|
||||
return redirectTo500;
|
||||
}
|
||||
return {
|
||||
props: {
|
||||
stripeDisabled: flag || false,
|
||||
data: data.data,
|
||||
data: result.data,
|
||||
},
|
||||
};
|
||||
}
|
||||
// Non è un rinnovo, deve essere un servizio normale
|
||||
const id = context.query?.servizioId;
|
||||
if (typeof id !== "string") {
|
||||
console.error("Error: servizioId is not a string");
|
||||
return redirectTo500;
|
||||
}
|
||||
const parsedServizioId = zServizioId.safeParse(id);
|
||||
if (!parsedServizioId.success) {
|
||||
console.error("Error parsing servizioId", parsedServizioId.error);
|
||||
return redirectTo500;
|
||||
}
|
||||
const result = await helper.trpc.pagamenti.preparePayment.fetch({
|
||||
type,
|
||||
servizioId: parsedServizioId.data,
|
||||
});
|
||||
if (!result.success) {
|
||||
console.error("Error preparing payment", result.message);
|
||||
return redirectTo500;
|
||||
}
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/500",
|
||||
permanent: false,
|
||||
props: {
|
||||
stripeDisabled: flag || false,
|
||||
data: result.data,
|
||||
},
|
||||
};
|
||||
}) satisfies GetServerSideProps<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 { default as OrderTypeEnum } from './OrderTypeEnum';
|
||||
import type { default as PaymentStatusEnum } from './PaymentStatusEnum';
|
||||
import type { RinnoviId } from './Rinnovi';
|
||||
import type { ColumnType, Selectable, Insertable, Updateable } from 'kysely';
|
||||
|
||||
/** Identifier type for public.ordini */
|
||||
|
|
@ -21,7 +22,7 @@ export default interface OrdiniTable {
|
|||
|
||||
packid: ColumnType<PrezziarioIdprezziario, PrezziarioIdprezziario, PrezziarioIdprezziario>;
|
||||
|
||||
servizio_id: ColumnType<ServizioServizioId, ServizioServizioId, ServizioServizioId>;
|
||||
servizio_id: ColumnType<ServizioServizioId | null, ServizioServizioId | null, ServizioServizioId | null>;
|
||||
|
||||
type: ColumnType<OrderTypeEnum, OrderTypeEnum, OrderTypeEnum>;
|
||||
|
||||
|
|
@ -38,6 +39,8 @@ export default interface OrdiniTable {
|
|||
paymentstatus: ColumnType<PaymentStatusEnum | null, PaymentStatusEnum | null, PaymentStatusEnum | null>;
|
||||
|
||||
sconto: ColumnType<number, number | undefined, number>;
|
||||
|
||||
rinnovo_id: ColumnType<RinnoviId | null, RinnoviId | null, RinnoviId | null>;
|
||||
}
|
||||
|
||||
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 PotenzialiGroupsTable } from './PotenzialiGroups';
|
||||
import type { default as ComuniTable } from './Comuni';
|
||||
import type { default as RinnoviTable } from './Rinnovi';
|
||||
import type { default as EventQueueTable } from './EventQueue';
|
||||
import type { default as OrdiniTable } from './Ordini';
|
||||
import type { default as PrezziarioTable } from './Prezziario';
|
||||
|
|
@ -58,6 +59,8 @@ export default interface PublicSchema {
|
|||
|
||||
comuni: ComuniTable;
|
||||
|
||||
rinnovi: RinnoviTable;
|
||||
|
||||
event_queue: EventQueueTable;
|
||||
|
||||
ordini: OrdiniTable;
|
||||
|
|
|
|||
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 { z } from "zod/v4";
|
||||
import type OrderTypeEnum from "~/schemas/public/OrderTypeEnum";
|
||||
import OrderTypeEnum from "~/schemas/public/OrderTypeEnum";
|
||||
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
|
||||
import {
|
||||
getPagamentoDataForCheckout,
|
||||
|
|
@ -10,7 +10,7 @@ import {
|
|||
|
||||
import { db } from "~/server/db";
|
||||
import { genPdfRicevutaCortesia } from "~/server/services/puppeteer.service";
|
||||
import { zOrdineId, zServizioId } from "~/server/utils/zod_types";
|
||||
import { zOrdineId, zRinnovoId, zServizioId } from "~/server/utils/zod_types";
|
||||
// Create a new ratelimiter, that allows 10 requests per 10 seconds
|
||||
/*
|
||||
const ratelimit = new RateLimiterHandler({
|
||||
|
|
@ -42,7 +42,7 @@ export const pagamentiRouter = createTRPCRouter({
|
|||
.input(
|
||||
z.object({
|
||||
servizioId: zServizioId,
|
||||
type: z.custom<OrderTypeEnum>(),
|
||||
type: z.enum(OrderTypeEnum),
|
||||
}),
|
||||
)
|
||||
.output(
|
||||
|
|
@ -58,7 +58,40 @@ export const pagamentiRouter = createTRPCRouter({
|
|||
]),
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
return await preparePayment(input.servizioId, input.type);
|
||||
if (input.type === OrderTypeEnum.Rinnovo) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Use preparePaymentRinnovi for Rinnovo payments",
|
||||
};
|
||||
}
|
||||
return await preparePayment({
|
||||
type: input.type,
|
||||
servizioId: input.servizioId,
|
||||
});
|
||||
}),
|
||||
preparePaymentRinnovi: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
rinnovoId: zRinnovoId,
|
||||
}),
|
||||
)
|
||||
.output(
|
||||
z.discriminatedUnion("success", [
|
||||
z.object({
|
||||
success: z.literal(true),
|
||||
data: z.custom<PreparedPaymentData>(),
|
||||
}),
|
||||
z.object({
|
||||
success: z.literal(false),
|
||||
message: z.string(),
|
||||
}),
|
||||
]),
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
return await preparePayment({
|
||||
type: OrderTypeEnum.Rinnovo,
|
||||
rinnovoId: input.rinnovoId,
|
||||
});
|
||||
}),
|
||||
|
||||
getPagamentoDataForCheckout: protectedProcedure
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
import z from "zod";
|
||||
import type { NewOrdini, OrdiniUpdate } from "~/schemas/public/Ordini";
|
||||
import type {
|
||||
NewRinnovi,
|
||||
Rinnovi,
|
||||
RinnoviUpdate,
|
||||
} from "~/schemas/public/Rinnovi";
|
||||
import type { NewServizio, ServizioUpdate } from "~/schemas/public/Servizio";
|
||||
import type { ServizioAnnunciUpdate } from "~/schemas/public/ServizioAnnunci";
|
||||
import type { Users } from "~/schemas/public/Users";
|
||||
|
|
@ -27,6 +32,7 @@ import {
|
|||
interruzioneServizio,
|
||||
processServizioOnboard,
|
||||
SbloccaContatti,
|
||||
type ServizioData,
|
||||
userAccettaConferma,
|
||||
userSendConfermaIntent,
|
||||
} from "~/server/controllers/servizio.controller";
|
||||
|
|
@ -37,6 +43,13 @@ import {
|
|||
removeOrder,
|
||||
updateOrder,
|
||||
} from "~/server/services/ordini.service";
|
||||
import {
|
||||
createRinnovo,
|
||||
deleteRinnovo,
|
||||
getRinnoviUser,
|
||||
getRinnovoData,
|
||||
updateRinnovo,
|
||||
} from "~/server/services/rinnovi.service";
|
||||
import {
|
||||
addServizio,
|
||||
deleteServizio,
|
||||
|
|
@ -49,6 +62,7 @@ import {
|
|||
import {
|
||||
zAnnuncioId,
|
||||
zOrdineId,
|
||||
zRinnovoId,
|
||||
zServizioId,
|
||||
zUserId,
|
||||
} from "~/server/utils/zod_types";
|
||||
|
|
@ -406,4 +420,75 @@ export const servizioRouter = createTRPCRouter({
|
|||
servizioId: input.servizioId,
|
||||
});
|
||||
}),
|
||||
getServizioRinnovi: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
userId: zUserId,
|
||||
}),
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
const data: (
|
||||
| { d: Date; type: "servizio"; data: ServizioData }
|
||||
| { d: Date; type: "rinnovo"; data: Rinnovi }
|
||||
)[] = [];
|
||||
|
||||
const servizio = await getAllServizioAnnunci(input.userId);
|
||||
const rinnovi = await getRinnoviUser(input.userId);
|
||||
servizio.forEach((s) => {
|
||||
data.push({
|
||||
d: s.created_at,
|
||||
type: "servizio",
|
||||
data: s,
|
||||
});
|
||||
});
|
||||
rinnovi.forEach((r) => {
|
||||
data.push({
|
||||
d: r.decorrenza,
|
||||
type: "rinnovo",
|
||||
data: r,
|
||||
});
|
||||
});
|
||||
data.sort((a, b) => b.d.getTime() - a.d.getTime());
|
||||
return data;
|
||||
}),
|
||||
createRinnovo: adminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
data: z.custom<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 { add, intervalToDuration } from "date-fns";
|
||||
import OrderTypeEnum from "~/schemas/public/OrderTypeEnum";
|
||||
import type { OrdiniOrdineId } from "~/schemas/public/Ordini";
|
||||
import type {
|
||||
Prezziario,
|
||||
PrezziarioIdprezziario,
|
||||
} from "~/schemas/public/Prezziario";
|
||||
import type { RinnoviId } from "~/schemas/public/Rinnovi";
|
||||
import type { ServizioServizioId } from "~/schemas/public/Servizio";
|
||||
import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
|
||||
import { db } from "../db";
|
||||
import { createOrdine } from "../services/ordini.service";
|
||||
import { getPrezziarioByIdHandler } from "../services/prezziario.service";
|
||||
import {
|
||||
CONDIZIONI_DEFAULT,
|
||||
getPrezziarioByIdHandler,
|
||||
} from "../services/prezziario.service";
|
||||
import { getServizioById } from "../services/servizio.service";
|
||||
|
||||
export type PreparedPaymentData = {
|
||||
|
|
@ -21,297 +26,340 @@ export type PreparedPaymentData = {
|
|||
packDesc: string;
|
||||
condizioni: string;
|
||||
};
|
||||
export type preparePaymentInput =
|
||||
| {
|
||||
servizioId: ServizioServizioId;
|
||||
type: Exclude<OrderTypeEnum, OrderTypeEnum.Rinnovo>;
|
||||
}
|
||||
| {
|
||||
rinnovoId: RinnoviId;
|
||||
type: OrderTypeEnum.Rinnovo;
|
||||
};
|
||||
|
||||
export const preparePayment = async (
|
||||
servizioId: ServizioServizioId,
|
||||
type: OrderTypeEnum,
|
||||
props: preparePaymentInput,
|
||||
): Promise<
|
||||
| { success: true; data: PreparedPaymentData }
|
||||
| { success: false; message: string }
|
||||
> => {
|
||||
try {
|
||||
const servizio = await getServizioById(servizioId);
|
||||
const inactiveOrders = await db
|
||||
.selectFrom("ordini")
|
||||
.selectAll()
|
||||
.where("servizio_id", "=", servizioId)
|
||||
.where("isActive", "=", false)
|
||||
.where("paymentstatus", "is", null)
|
||||
.execute();
|
||||
switch (props.type) {
|
||||
case OrderTypeEnum.Acconto:
|
||||
case OrderTypeEnum.Saldo:
|
||||
case OrderTypeEnum.Consulenza:
|
||||
case OrderTypeEnum.Altro: {
|
||||
const { servizioId, type } = props;
|
||||
const servizio = await getServizioById(servizioId);
|
||||
const inactiveOrders = await db
|
||||
.selectFrom("ordini")
|
||||
.selectAll()
|
||||
.where("servizio_id", "=", servizioId)
|
||||
.where("isActive", "=", false)
|
||||
.where("paymentstatus", "is", null)
|
||||
.execute();
|
||||
|
||||
switch (type) {
|
||||
case OrderTypeEnum.Acconto: {
|
||||
if (!servizio.isOkAcconto) {
|
||||
const acconto = await AccontoSolver(servizio.tipologia);
|
||||
if (!acconto.success) {
|
||||
switch (type) {
|
||||
case OrderTypeEnum.Acconto: {
|
||||
if (!servizio.isOkAcconto) {
|
||||
const acconto = await AccontoSolver(servizio.tipologia);
|
||||
if (!acconto.success) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Acconto non trovato per il servizio: ${acconto.message}`,
|
||||
};
|
||||
}
|
||||
|
||||
// servizio sta aspettando attivazione acconto
|
||||
|
||||
const preExistingAcconto = inactiveOrders.find(
|
||||
(ordine) => ordine.type === OrderTypeEnum.Acconto,
|
||||
);
|
||||
|
||||
if (preExistingAcconto) {
|
||||
// acconto già creato, procedere con questo
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
ordine_id: preExistingAcconto.ordine_id,
|
||||
packid: preExistingAcconto.packid,
|
||||
amount_cent: preExistingAcconto.amount_cent,
|
||||
sconto: preExistingAcconto.sconto,
|
||||
packName: acconto.pack.nome_it,
|
||||
packDesc: acconto.pack.desc_it,
|
||||
condizioni:
|
||||
acconto.pack.testo_condizioni || CONDIZIONI_DEFAULT,
|
||||
},
|
||||
};
|
||||
}
|
||||
// nessun acconto preesistente, crearne uno nuovo
|
||||
|
||||
const ordine = await createOrdine({
|
||||
servizio_id: servizioId,
|
||||
amount_cent: acconto.pack.prezzo_cent,
|
||||
packid: acconto.pack.idprezziario,
|
||||
type: OrderTypeEnum.Acconto,
|
||||
userid: servizio.user_id,
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
ordine_id: ordine.ordine_id,
|
||||
packid: acconto.pack.idprezziario,
|
||||
amount_cent: acconto.pack.prezzo_cent,
|
||||
sconto: 0,
|
||||
packName: acconto.pack.nome_it,
|
||||
packDesc: acconto.pack.desc_it,
|
||||
condizioni:
|
||||
acconto.pack.testo_condizioni || CONDIZIONI_DEFAULT,
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
message: `Acconto non trovato per il servizio: ${acconto.message}`,
|
||||
message:
|
||||
"Il servizio ha già un acconto attivo, non è possibile preparare un nuovo pagamento",
|
||||
};
|
||||
}
|
||||
if (!acconto.pack.testo_condizioni) {
|
||||
case OrderTypeEnum.Saldo: {
|
||||
if (!servizio.isOkAcconto) {
|
||||
return {
|
||||
success: false,
|
||||
message:
|
||||
"Non è possibile procedere con il pagamento del saldo prima di aver attivato l'acconto",
|
||||
};
|
||||
}
|
||||
if (!servizio.isOkSaldo) {
|
||||
//find saldo pack based on servizio tipologia and permanenza/budget, if tipologia posizione
|
||||
const saldo = await SaldoSolver(servizio);
|
||||
if (!saldo.success) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Saldo non trovato per il servizio: ${saldo.message}`,
|
||||
};
|
||||
}
|
||||
|
||||
const preExistingSaldo = inactiveOrders.find(
|
||||
(ordine) => ordine.type === OrderTypeEnum.Saldo,
|
||||
);
|
||||
if (preExistingSaldo) {
|
||||
// saldo già creato, procedere con questo
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
ordine_id: preExistingSaldo.ordine_id,
|
||||
packid: preExistingSaldo.packid,
|
||||
amount_cent: preExistingSaldo.amount_cent,
|
||||
sconto: preExistingSaldo.sconto,
|
||||
packName: saldo.pack.nome_it,
|
||||
packDesc: saldo.pack.desc_it,
|
||||
condizioni:
|
||||
saldo.pack.testo_condizioni || CONDIZIONI_DEFAULT,
|
||||
},
|
||||
};
|
||||
}
|
||||
// nessun saldo preesistente, crearne uno nuovo
|
||||
const ordine = await createOrdine({
|
||||
servizio_id: servizioId,
|
||||
amount_cent: saldo.pack.prezzo_cent,
|
||||
packid: saldo.pack.idprezziario,
|
||||
type: OrderTypeEnum.Saldo,
|
||||
userid: servizio.user_id,
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
ordine_id: ordine.ordine_id,
|
||||
packid: saldo.pack.idprezziario,
|
||||
amount_cent: saldo.pack.prezzo_cent,
|
||||
sconto: 0,
|
||||
packName: saldo.pack.nome_it,
|
||||
packDesc: saldo.pack.desc_it,
|
||||
condizioni: saldo.pack.testo_condizioni || CONDIZIONI_DEFAULT,
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
message: `Condizioni non trovate per acconto: ${acconto.pack.nome_it}`,
|
||||
message:
|
||||
"Il servizio ha già un saldo attivo, non è possibile preparare un nuovo pagamento",
|
||||
};
|
||||
}
|
||||
|
||||
// servizio sta aspettando attivazione acconto
|
||||
case OrderTypeEnum.Consulenza: {
|
||||
if (!servizio.isOkAcconto || !servizio.isOkSaldo) {
|
||||
return {
|
||||
success: false,
|
||||
message:
|
||||
"Non è possibile procedere con il pagamento della consulenza prima di aver attivato acconto e saldo",
|
||||
};
|
||||
}
|
||||
if (!servizio.isOkConsulenza) {
|
||||
const existingConsulenza = inactiveOrders.find(
|
||||
(ordine) => ordine.type === OrderTypeEnum.Consulenza,
|
||||
);
|
||||
if (existingConsulenza) {
|
||||
// consulenza già creato, procedere con questo
|
||||
const consulenzaPack = await getPrezziarioByIdHandler(
|
||||
existingConsulenza.packid,
|
||||
);
|
||||
if (!consulenzaPack) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Pack della consulenza non trovato: ${existingConsulenza.packid}`,
|
||||
};
|
||||
}
|
||||
|
||||
const preExistingAcconto = inactiveOrders.find(
|
||||
(ordine) => ordine.type === OrderTypeEnum.Acconto,
|
||||
);
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
ordine_id: existingConsulenza.ordine_id,
|
||||
packid: existingConsulenza.packid,
|
||||
amount_cent: existingConsulenza.amount_cent,
|
||||
sconto: existingConsulenza.sconto,
|
||||
packName: consulenzaPack.nome_it,
|
||||
packDesc: consulenzaPack.desc_it,
|
||||
condizioni:
|
||||
consulenzaPack.testo_condizioni || CONDIZIONI_DEFAULT,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (preExistingAcconto) {
|
||||
// acconto già creato, procedere con questo
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
ordine_id: preExistingAcconto.ordine_id,
|
||||
packid: preExistingAcconto.packid,
|
||||
amount_cent: preExistingAcconto.amount_cent,
|
||||
sconto: preExistingAcconto.sconto,
|
||||
packName: acconto.pack.nome_it,
|
||||
packDesc: acconto.pack.desc_it,
|
||||
condizioni: acconto.pack.testo_condizioni,
|
||||
},
|
||||
};
|
||||
}
|
||||
// nessun acconto preesistente, crearne uno nuovo
|
||||
return {
|
||||
success: false,
|
||||
message:
|
||||
"Il servizio non ha un pacchetto di consulenza, è necessario inserirlo prima di procedere",
|
||||
};
|
||||
}
|
||||
|
||||
const ordine = await createOrdine({
|
||||
servizio_id: servizioId,
|
||||
amount_cent: acconto.pack.prezzo_cent,
|
||||
packid: acconto.pack.idprezziario,
|
||||
type: OrderTypeEnum.Acconto,
|
||||
userid: servizio.user_id,
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
ordine_id: ordine.ordine_id,
|
||||
packid: acconto.pack.idprezziario,
|
||||
amount_cent: acconto.pack.prezzo_cent,
|
||||
sconto: 0,
|
||||
packName: acconto.pack.nome_it,
|
||||
packDesc: acconto.pack.desc_it,
|
||||
condizioni: acconto.pack.testo_condizioni,
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
message:
|
||||
"Il servizio ha già un acconto attivo, non è possibile preparare un nuovo pagamento",
|
||||
};
|
||||
}
|
||||
case OrderTypeEnum.Saldo: {
|
||||
if (!servizio.isOkAcconto) {
|
||||
return {
|
||||
success: false,
|
||||
message:
|
||||
"Non è possibile procedere con il pagamento del saldo prima di aver attivato l'acconto",
|
||||
};
|
||||
}
|
||||
if (!servizio.isOkSaldo) {
|
||||
//find saldo pack based on servizio tipologia and permanenza/budget, if tipologia posizione
|
||||
const saldo = await SaldoSolver(servizio);
|
||||
if (!saldo.success) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Saldo non trovato per il servizio: ${saldo.message}`,
|
||||
message:
|
||||
"Il servizio ha già una consulenza attiva, non è possibile preparare un nuovo pagamento",
|
||||
};
|
||||
}
|
||||
if (!saldo.pack.testo_condizioni) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Condizioni non trovate per saldo: ${saldo.pack.nome_it}`,
|
||||
};
|
||||
}
|
||||
const preExistingSaldo = inactiveOrders.find(
|
||||
(ordine) => ordine.type === OrderTypeEnum.Saldo,
|
||||
);
|
||||
if (preExistingSaldo) {
|
||||
// saldo già creato, procedere con questo
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
ordine_id: preExistingSaldo.ordine_id,
|
||||
packid: preExistingSaldo.packid,
|
||||
amount_cent: preExistingSaldo.amount_cent,
|
||||
sconto: preExistingSaldo.sconto,
|
||||
packName: saldo.pack.nome_it,
|
||||
packDesc: saldo.pack.desc_it,
|
||||
condizioni: saldo.pack.testo_condizioni,
|
||||
},
|
||||
};
|
||||
}
|
||||
// nessun saldo preesistente, crearne uno nuovo
|
||||
const ordine = await createOrdine({
|
||||
servizio_id: servizioId,
|
||||
amount_cent: saldo.pack.prezzo_cent,
|
||||
packid: saldo.pack.idprezziario,
|
||||
type: OrderTypeEnum.Saldo,
|
||||
userid: servizio.user_id,
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
ordine_id: ordine.ordine_id,
|
||||
packid: saldo.pack.idprezziario,
|
||||
amount_cent: saldo.pack.prezzo_cent,
|
||||
sconto: 0,
|
||||
packName: saldo.pack.nome_it,
|
||||
packDesc: saldo.pack.desc_it,
|
||||
condizioni: saldo.pack.testo_condizioni,
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
message:
|
||||
"Il servizio ha già un saldo attivo, non è possibile preparare un nuovo pagamento",
|
||||
};
|
||||
}
|
||||
|
||||
case OrderTypeEnum.Consulenza: {
|
||||
if (!servizio.isOkAcconto || !servizio.isOkSaldo) {
|
||||
return {
|
||||
success: false,
|
||||
message:
|
||||
"Non è possibile procedere con il pagamento della consulenza prima di aver attivato acconto e saldo",
|
||||
};
|
||||
}
|
||||
if (!servizio.isOkConsulenza) {
|
||||
const existingConsulenza = inactiveOrders.find(
|
||||
(ordine) => ordine.type === OrderTypeEnum.Consulenza,
|
||||
);
|
||||
if (existingConsulenza) {
|
||||
// consulenza già creato, procedere con questo
|
||||
const consulenzaPack = await getPrezziarioByIdHandler(
|
||||
existingConsulenza.packid,
|
||||
case OrderTypeEnum.Altro: {
|
||||
const existingAltro = inactiveOrders.find(
|
||||
(ordine) => ordine.type === OrderTypeEnum.Altro,
|
||||
);
|
||||
if (!consulenzaPack) {
|
||||
if (existingAltro) {
|
||||
// altro già creato, procedere con questo
|
||||
const altroPack = await getPrezziarioByIdHandler(
|
||||
existingAltro.packid,
|
||||
);
|
||||
if (!altroPack) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Pack dell'altro non trovato: ${existingAltro.packid}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: `Pack della consulenza non trovato: ${existingConsulenza.packid}`,
|
||||
};
|
||||
}
|
||||
if (!consulenzaPack.testo_condizioni) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Condizioni non trovate per consulenza: ${consulenzaPack.nome_it}`,
|
||||
success: true,
|
||||
data: {
|
||||
ordine_id: existingAltro.ordine_id,
|
||||
packid: existingAltro.packid,
|
||||
amount_cent: existingAltro.amount_cent,
|
||||
sconto: existingAltro.sconto,
|
||||
packName: altroPack.nome_it,
|
||||
packDesc: altroPack.desc_it,
|
||||
condizioni: altroPack.testo_condizioni || CONDIZIONI_DEFAULT,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
ordine_id: existingConsulenza.ordine_id,
|
||||
packid: existingConsulenza.packid,
|
||||
amount_cent: existingConsulenza.amount_cent,
|
||||
sconto: existingConsulenza.sconto,
|
||||
packName: consulenzaPack.nome_it,
|
||||
packDesc: consulenzaPack.desc_it,
|
||||
condizioni: consulenzaPack.testo_condizioni,
|
||||
},
|
||||
success: false,
|
||||
message:
|
||||
"La preparazione del pagamento per questa tipologia non è supportata",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message:
|
||||
"Il servizio non ha un pacchetto di consulenza, è necessario inserirlo prima di procedere",
|
||||
};
|
||||
default:
|
||||
throw new Error(
|
||||
`Tipo di ordine non valido: ${type satisfies never}`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message:
|
||||
"Il servizio ha già una consulenza attiva, non è possibile preparare un nuovo pagamento",
|
||||
};
|
||||
}
|
||||
case OrderTypeEnum.Rinnovo: {
|
||||
const existingRinnovo = inactiveOrders.find(
|
||||
(ordine) => ordine.type === OrderTypeEnum.Rinnovo,
|
||||
);
|
||||
if (existingRinnovo) {
|
||||
const { rinnovoId } = props;
|
||||
|
||||
const ordiniRinnovo = await db
|
||||
.selectFrom("ordini")
|
||||
.selectAll()
|
||||
.where("rinnovo_id", "=", rinnovoId)
|
||||
.where("type", "=", OrderTypeEnum.Rinnovo)
|
||||
.execute();
|
||||
|
||||
const alreadyPaid = ordiniRinnovo.some((ordine) => ordine.isActive);
|
||||
if (alreadyPaid) {
|
||||
return {
|
||||
success: false,
|
||||
message:
|
||||
"Il rinnovo ha già un ordine attivo, non è possibile preparare un nuovo pagamento",
|
||||
};
|
||||
}
|
||||
|
||||
const unusedOrdine = ordiniRinnovo.find((ordine) => !ordine.isActive);
|
||||
if (unusedOrdine) {
|
||||
// rinnovo già creato, procedere con questo
|
||||
const rinnovoPack = await getPrezziarioByIdHandler(
|
||||
existingRinnovo.packid,
|
||||
unusedOrdine.packid,
|
||||
);
|
||||
if (!rinnovoPack) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Pack del rinnovo non trovato: ${existingRinnovo.packid}`,
|
||||
};
|
||||
}
|
||||
if (!rinnovoPack.testo_condizioni) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Condizioni non trovate per rinnovo: ${rinnovoPack.nome_it}`,
|
||||
message: `Pack del rinnovo non trovato: ${unusedOrdine.packid}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
ordine_id: existingRinnovo.ordine_id,
|
||||
packid: existingRinnovo.packid,
|
||||
amount_cent: existingRinnovo.amount_cent,
|
||||
sconto: existingRinnovo.sconto,
|
||||
ordine_id: unusedOrdine.ordine_id,
|
||||
packid: unusedOrdine.packid,
|
||||
amount_cent: unusedOrdine.amount_cent,
|
||||
sconto: unusedOrdine.sconto,
|
||||
packName: rinnovoPack.nome_it,
|
||||
packDesc: rinnovoPack.desc_it,
|
||||
condizioni: rinnovoPack.testo_condizioni,
|
||||
condizioni: rinnovoPack.testo_condizioni || CONDIZIONI_DEFAULT,
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
message:
|
||||
"Il servizio non ha un pacchetto di rinnovo, è necessario inserirlo prima di procedere",
|
||||
};
|
||||
}
|
||||
case OrderTypeEnum.Altro: {
|
||||
const existingAltro = inactiveOrders.find(
|
||||
(ordine) => ordine.type === OrderTypeEnum.Altro,
|
||||
);
|
||||
if (existingAltro) {
|
||||
// altro già creato, procedere con questo
|
||||
const altroPack = await getPrezziarioByIdHandler(
|
||||
existingAltro.packid,
|
||||
// nessun rinnovo preesistente, crearne uno nuovo
|
||||
const rinnovo = await db
|
||||
.selectFrom("rinnovi")
|
||||
.selectAll()
|
||||
.where("id", "=", rinnovoId)
|
||||
.executeTakeFirstOrThrow(
|
||||
() => new Error(`Rinnovo non trovato - id: ${rinnovoId}`),
|
||||
);
|
||||
if (!altroPack) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Pack dell'altro non trovato: ${existingAltro.packid}`,
|
||||
};
|
||||
}
|
||||
if (!altroPack.testo_condizioni) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Condizioni non trovate per altro: ${altroPack.nome_it}`,
|
||||
};
|
||||
}
|
||||
const pack = await RinnovoSolver(rinnovo.decorrenza, rinnovo.scadenza);
|
||||
if (!pack.success) {
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
ordine_id: existingAltro.ordine_id,
|
||||
packid: existingAltro.packid,
|
||||
amount_cent: existingAltro.amount_cent,
|
||||
sconto: existingAltro.sconto,
|
||||
packName: altroPack.nome_it,
|
||||
packDesc: altroPack.desc_it,
|
||||
condizioni: altroPack.testo_condizioni,
|
||||
},
|
||||
success: false,
|
||||
message: `Pack non trovato per il rinnovo: ${pack.message}`,
|
||||
};
|
||||
}
|
||||
|
||||
const ordine = await createOrdine({
|
||||
rinnovo_id: rinnovoId,
|
||||
amount_cent: pack.pack.prezzo_cent,
|
||||
packid: pack.pack.idprezziario,
|
||||
type: OrderTypeEnum.Rinnovo,
|
||||
userid: rinnovo.userId,
|
||||
});
|
||||
return {
|
||||
success: false,
|
||||
message:
|
||||
"La preparazione del pagamento per questa tipologia non è supportata",
|
||||
success: true,
|
||||
data: {
|
||||
ordine_id: ordine.ordine_id,
|
||||
packid: pack.pack.idprezziario,
|
||||
amount_cent: pack.pack.prezzo_cent,
|
||||
sconto: 0,
|
||||
packName: pack.pack.nome_it,
|
||||
packDesc: pack.pack.desc_it,
|
||||
condizioni: pack.pack.testo_condizioni || CONDIZIONI_DEFAULT,
|
||||
},
|
||||
};
|
||||
}
|
||||
default:
|
||||
throw new Error(`Tipo di ordine non valido: ${type satisfies never}`);
|
||||
}
|
||||
} catch (e) {
|
||||
throw new TRPCError({
|
||||
|
|
@ -479,6 +527,51 @@ export const SaldoSolver = async ({
|
|||
return { success: false, message: "Seleziona tipologia" };
|
||||
}
|
||||
};
|
||||
function rinnovoPermanenzaVariant(permanenza: number): number {
|
||||
if (permanenza <= 1) return 0;
|
||||
if (permanenza <= 3) return 1;
|
||||
if (permanenza <= 6) return 2;
|
||||
if (permanenza <= 9) return 3;
|
||||
return 4;
|
||||
}
|
||||
|
||||
const rinnovoDurationToPermanenza = (decorrenza: Date, scadenza: Date) => {
|
||||
const dur = intervalToDuration({
|
||||
start: new Date(decorrenza),
|
||||
end: add(new Date(scadenza), { days: 1 }),
|
||||
});
|
||||
return (dur.years || 0) * 12 + (dur.months || 0) + (dur.days ? 1 : 0);
|
||||
};
|
||||
|
||||
export const RinnovoSolver = async (
|
||||
decorrenza: Date,
|
||||
scadenza: Date,
|
||||
): Promise<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 (
|
||||
servizioId: ServizioServizioId,
|
||||
|
|
|
|||
|
|
@ -810,7 +810,7 @@ export const SbloccaContatti = async ({
|
|||
},
|
||||
tipo: "email",
|
||||
},
|
||||
lock_expires: new Date(Date.now() + 1000),
|
||||
lock_expires: new Date(Date.now() + 3000),
|
||||
});
|
||||
|
||||
if (canSendNotification()) {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import type { TestiEStringheStingaId } from "~/schemas/public/TestiEStringhe";
|
|||
import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
|
||||
import { db } from "~/server/db";
|
||||
import { NewMail } from "~/server/services/mailer";
|
||||
import { CONDIZIONI_DEFAULT } from "~/server/services/prezziario.service";
|
||||
import { getUser } from "~/server/services/user.service";
|
||||
import { addEventToQueue } from "./event_queue.controller";
|
||||
|
||||
|
|
@ -80,30 +81,13 @@ export const whIntentSucceededHandler = async ({
|
|||
})
|
||||
.where("intent_id", "=", pIntentId)
|
||||
.where("ordine_id", "=", ordineId)
|
||||
.returning([
|
||||
"servizio_id",
|
||||
"ordine_id",
|
||||
"userid",
|
||||
"type",
|
||||
"created_at",
|
||||
"packid",
|
||||
])
|
||||
.returningAll()
|
||||
.executeTakeFirstOrThrow(() => {
|
||||
throw new Error(
|
||||
`Payment not found - intent_id: ${pIntentId}, ordineId: ${ordineId}`,
|
||||
);
|
||||
});
|
||||
|
||||
const servizio = await trx
|
||||
.selectFrom("servizio")
|
||||
.select(["servizio.tipologia", "servizio.servizio_id"])
|
||||
.where("servizio_id", "=", ordine.servizio_id)
|
||||
.executeTakeFirstOrThrow(() => {
|
||||
throw new Error(
|
||||
`Servizio not found - servizio_id: ${ordine.servizio_id}`,
|
||||
);
|
||||
});
|
||||
|
||||
const condizioni = await trx
|
||||
.selectFrom("prezziario")
|
||||
.select("testo_condizioni")
|
||||
|
|
@ -114,13 +98,19 @@ export const whIntentSucceededHandler = async ({
|
|||
db: trx,
|
||||
userId: ordine.userid,
|
||||
});
|
||||
const lock_expires = new Date(Date.now() + 60 * 1000); // Lock for 1 minute
|
||||
const lock_expires = new Date(Date.now() + 3000); // Lock for 30 sec
|
||||
|
||||
switch (ordine.type) {
|
||||
case OrderTypeEnum.Acconto:
|
||||
case OrderTypeEnum.Acconto: {
|
||||
if (!ordine.servizio_id) {
|
||||
throw new Error(
|
||||
`No servizio_id for ordine_id: ${ordine.ordine_id}`,
|
||||
);
|
||||
}
|
||||
|
||||
await trx
|
||||
.updateTable("servizio")
|
||||
.where("servizio_id", "=", servizio.servizio_id)
|
||||
.where("servizio_id", "=", ordine.servizio_id)
|
||||
.set({
|
||||
decorrenza: new Date(),
|
||||
isOkAcconto: true,
|
||||
|
|
@ -140,19 +130,16 @@ export const whIntentSucceededHandler = async ({
|
|||
attachments: [
|
||||
{
|
||||
filename: `condizioni_contrattuali_${ordine.created_at.toISOString()}.pdf`,
|
||||
//ATTENTION: hardcoded condizioni key
|
||||
|
||||
generate: {
|
||||
type: "condizioni",
|
||||
stringId: (condizioni?.testo_condizioni ||
|
||||
"CONDIZIONI_CERCHI") as TestiEStringheStingaId,
|
||||
CONDIZIONI_DEFAULT) as TestiEStringheStingaId,
|
||||
},
|
||||
encoding: "base64",
|
||||
contentType: "application/pdf",
|
||||
},
|
||||
{
|
||||
filename: `ricevuta_cortesia_${ordine.ordine_id}.pdf`,
|
||||
|
||||
generate: {
|
||||
type: "ricevuta_cortesia",
|
||||
ordineId: ordine.ordine_id,
|
||||
|
|
@ -170,11 +157,27 @@ export const whIntentSucceededHandler = async ({
|
|||
//SERVIZIO ATTIVATO
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case OrderTypeEnum.Saldo: {
|
||||
if (!ordine.servizio_id) {
|
||||
throw new Error(
|
||||
`No servizio_id for ordine_id: ${ordine.ordine_id}`,
|
||||
);
|
||||
}
|
||||
const servizio = await trx
|
||||
.selectFrom("servizio")
|
||||
.select(["servizio.tipologia"])
|
||||
.where("servizio_id", "=", ordine.servizio_id)
|
||||
.executeTakeFirstOrThrow(() => {
|
||||
throw new Error(
|
||||
`Servizio not found - servizio_id: ${ordine.servizio_id}`,
|
||||
);
|
||||
});
|
||||
|
||||
case OrderTypeEnum.Saldo:
|
||||
await trx
|
||||
.updateTable("servizio")
|
||||
.where("servizio_id", "=", servizio.servizio_id)
|
||||
.where("servizio_id", "=", ordine.servizio_id)
|
||||
.set({
|
||||
isOkConsulenza:
|
||||
servizio.tipologia === TipologiaPosizioneEnum.Transitorio,
|
||||
|
|
@ -198,7 +201,6 @@ export const whIntentSucceededHandler = async ({
|
|||
type: "ricevuta_cortesia",
|
||||
ordineId: ordine.ordine_id,
|
||||
},
|
||||
|
||||
encoding: "base64",
|
||||
contentType: "application/pdf",
|
||||
},
|
||||
|
|
@ -209,12 +211,49 @@ export const whIntentSucceededHandler = async ({
|
|||
},
|
||||
lock_expires,
|
||||
});
|
||||
//PAGAMENTO SALDO CONFERMATO
|
||||
break;
|
||||
case OrderTypeEnum.Consulenza:
|
||||
}
|
||||
case OrderTypeEnum.Rinnovo: {
|
||||
// nessuna azione aggiuntiva richiesta
|
||||
await addEventToQueue({
|
||||
data: {
|
||||
tipo: "email",
|
||||
data: {
|
||||
template: {
|
||||
mailType: "pagamentoConferma",
|
||||
},
|
||||
mail: {
|
||||
subject: "Pagamento Confermato - Rinnovo Infoalloggi.it",
|
||||
to: utente.email,
|
||||
attachments: [
|
||||
{
|
||||
filename: `ricevuta_cortesia_${ordine.ordine_id}.pdf`,
|
||||
generate: {
|
||||
type: "ricevuta_cortesia",
|
||||
ordineId: ordine.ordine_id,
|
||||
},
|
||||
encoding: "base64",
|
||||
contentType: "application/pdf",
|
||||
},
|
||||
],
|
||||
},
|
||||
userId: utente.id,
|
||||
},
|
||||
},
|
||||
lock_expires,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case OrderTypeEnum.Consulenza: {
|
||||
if (!ordine.servizio_id) {
|
||||
throw new Error(
|
||||
`No servizio_id for ordine_id: ${ordine.ordine_id}`,
|
||||
);
|
||||
}
|
||||
|
||||
await trx
|
||||
.updateTable("servizio")
|
||||
.where("servizio_id", "=", servizio.servizio_id)
|
||||
.where("servizio_id", "=", ordine.servizio_id)
|
||||
.set({
|
||||
isOkConsulenza: true,
|
||||
})
|
||||
|
|
@ -232,7 +271,6 @@ export const whIntentSucceededHandler = async ({
|
|||
attachments: [
|
||||
{
|
||||
filename: `ricevuta_cortesia_${ordine.ordine_id}.pdf`,
|
||||
|
||||
generate: {
|
||||
type: "ricevuta_cortesia",
|
||||
ordineId: ordine.ordine_id,
|
||||
|
|
@ -247,38 +285,38 @@ export const whIntentSucceededHandler = async ({
|
|||
},
|
||||
lock_expires,
|
||||
});
|
||||
//CONSULENZA ATTIVATA
|
||||
break;
|
||||
}
|
||||
case OrderTypeEnum.Altro:
|
||||
// TODO: Handle other types
|
||||
await addEventToQueue({
|
||||
data: {
|
||||
tipo: "email",
|
||||
data: {
|
||||
template: {
|
||||
mailType: "pagamentoConferma",
|
||||
},
|
||||
mail: {
|
||||
subject: "Pagamento Confermato - Infoalloggi.it",
|
||||
to: utente.email,
|
||||
attachments: [
|
||||
{
|
||||
filename: `ricevuta_cortesia_${ordine.ordine_id}.pdf`,
|
||||
// Handle "Altro" type if needed
|
||||
// await addEventToQueue({
|
||||
// data: {
|
||||
// tipo: "email",
|
||||
// data: {
|
||||
// template: {
|
||||
// mailType: "pagamentoConferma",
|
||||
// },
|
||||
// mail: {
|
||||
// subject: "Pagamento Confermato - Infoalloggi.it",
|
||||
// to: utente.email,
|
||||
// attachments: [
|
||||
// {
|
||||
// filename: `ricevuta_cortesia_${ordine.ordine_id}.pdf`,
|
||||
|
||||
generate: {
|
||||
type: "ricevuta_cortesia",
|
||||
ordineId: ordine.ordine_id,
|
||||
},
|
||||
encoding: "base64",
|
||||
contentType: "application/pdf",
|
||||
},
|
||||
],
|
||||
},
|
||||
userId: utente.id,
|
||||
},
|
||||
},
|
||||
lock_expires,
|
||||
});
|
||||
// generate: {
|
||||
// type: "ricevuta_cortesia",
|
||||
// ordineId: ordine.ordine_id,
|
||||
// },
|
||||
// encoding: "base64",
|
||||
// contentType: "application/pdf",
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// userId: utente.id,
|
||||
// },
|
||||
// },
|
||||
// lock_expires,
|
||||
// });
|
||||
break;
|
||||
}
|
||||
await addEventToQueue({
|
||||
|
|
@ -315,7 +353,6 @@ export const whIntentSucceededHandler = async ({
|
|||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
// Enhanced error logging
|
||||
console.error("whIntentSucceededHandler error:", {
|
||||
pIntentId,
|
||||
ordineId,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { TRPCError } from "@trpc/server";
|
||||
import type { Insertable, Selectable, Updateable } from "kysely";
|
||||
import type OrdiniTable from "~/schemas/public/Ordini";
|
||||
import type {
|
||||
NewOrdini,
|
||||
Ordini,
|
||||
OrdiniOrdineId,
|
||||
OrdiniUpdate,
|
||||
|
|
@ -8,6 +9,22 @@ import type {
|
|||
import type { Users, UsersId } from "~/schemas/public/Users";
|
||||
import { db } from "~/server/db";
|
||||
|
||||
export type ServizioOrdiniTable = Omit<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 const getOrdini = async (
|
||||
userId: UsersId | undefined,
|
||||
|
|
@ -51,7 +68,9 @@ export const getOrdineById = async (ordineId: OrdiniOrdineId) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const createOrdine = async (data: NewOrdini) => {
|
||||
export const createOrdine = async (
|
||||
data: NewServizioOrdini | NewRinnovoOrdini,
|
||||
) => {
|
||||
try {
|
||||
return await db
|
||||
.insertInto("ordini")
|
||||
|
|
|
|||
|
|
@ -6,9 +6,11 @@ import type {
|
|||
PrezziarioIdprezziario,
|
||||
PrezziarioUpdate,
|
||||
} from "~/schemas/public/Prezziario";
|
||||
import type { TestiEStringheStingaId } from "~/schemas/public/TestiEStringhe";
|
||||
import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
|
||||
import { db, type Querier } from "~/server/db";
|
||||
|
||||
export const CONDIZIONI_DEFAULT = "CONDIZIONI_CERCHI" as TestiEStringheStingaId;
|
||||
export const getPrezziario = async () => {
|
||||
try {
|
||||
return await db
|
||||
|
|
|
|||
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 { OrdiniOrdineId } from "~/schemas/public/Ordini";
|
||||
import type { PrezziarioIdprezziario } from "~/schemas/public/Prezziario";
|
||||
import type { RinnoviId } from "~/schemas/public/Rinnovi";
|
||||
import type { ServizioServizioId } from "~/schemas/public/Servizio";
|
||||
import type { TestiEStringheStingaId } from "~/schemas/public/TestiEStringhe";
|
||||
import type { UsersId } from "~/schemas/public/Users";
|
||||
|
|
@ -85,3 +86,8 @@ export const zServizioId = z.custom<ServizioServizioId>(
|
|||
// (val) => typeof val === "number",
|
||||
// "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