feat: add dotenv package for environment variable management and update payment logic to use new database queries

This commit is contained in:
Marco Pedone 2026-03-12 16:24:16 +01:00
parent 547b1d6ce6
commit 6c53ef130c
8 changed files with 318 additions and 133 deletions

View file

@ -110,6 +110,7 @@
"@types/uuid": "^11.0.0",
"@vitejs/plugin-react": "^5.1.4",
"cross-env": "^10.1.0",
"dotenv": "^17.3.1",
"jiti": "^2.6.1",
"jsdom": "^28.1.0",
"kanel": "^3.16.1",
@ -8205,6 +8206,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/dotenv": {
"version": "17.3.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz",
"integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",

View file

@ -123,6 +123,7 @@
"@types/uuid": "^11.0.0",
"@vitejs/plugin-react": "^5.1.4",
"cross-env": "^10.1.0",
"dotenv": "^17.3.1",
"jiti": "^2.6.1",
"jsdom": "^28.1.0",
"kanel": "^3.16.1",

View file

@ -0,0 +1,124 @@
import { assert, describe, expect, it } from "vitest";
import type { PrezziarioIdprezziario } from "~/schemas/public/Prezziario";
import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
import {
AccontoSolver,
SaldoSolver,
} from "~/server/controllers/pagamenti.controller";
type TestInput = {
tipologia: TipologiaPosizioneEnum;
permanenza: number | null;
budget: number;
};
type TestCase = {
input: TestInput;
exp_acconto: PrezziarioIdprezziario;
exp_saldo: PrezziarioIdprezziario;
descrizione: string;
};
const testCases: TestCase[] = [
{
input: {
tipologia: TipologiaPosizioneEnum.Transitorio,
permanenza: 0,
budget: 500,
},
exp_acconto: "ACCONTO_BD" as PrezziarioIdprezziario,
exp_saldo: "SALDOBD_1MESE" as PrezziarioIdprezziario,
descrizione: "1MESE BD",
},
{
input: {
tipologia: TipologiaPosizioneEnum.Transitorio,
permanenza: 1,
budget: 500,
},
exp_acconto: "ACCONTO_BD" as PrezziarioIdprezziario,
exp_saldo: "SALDOBD_3MESI" as PrezziarioIdprezziario,
descrizione: "3MESI BD",
},
{
input: {
tipologia: TipologiaPosizioneEnum.Transitorio,
permanenza: 2,
budget: 500,
},
exp_acconto: "ACCONTO_BD" as PrezziarioIdprezziario,
exp_saldo: "SALDOBD_6MESI" as PrezziarioIdprezziario,
descrizione: "6MESI BD",
},
{
input: {
tipologia: TipologiaPosizioneEnum.Transitorio,
permanenza: 3,
budget: 500,
},
exp_acconto: "ACCONTO_BD" as PrezziarioIdprezziario,
exp_saldo: "SALDOBD_9MESI" as PrezziarioIdprezziario,
descrizione: "9MESI BD",
},
{
input: {
tipologia: TipologiaPosizioneEnum.Transitorio,
permanenza: 4,
budget: 500,
},
exp_acconto: "ACCONTO_BD" as PrezziarioIdprezziario,
exp_saldo: "SALDOBD_12MESI" as PrezziarioIdprezziario,
descrizione: "12MESI BD",
},
{
input: {
tipologia: TipologiaPosizioneEnum.Stabile,
permanenza: null,
budget: 500,
},
exp_acconto: "ACCONTO_CA" as PrezziarioIdprezziario,
exp_saldo: "SALDOCA500" as PrezziarioIdprezziario,
descrizione: "CA 500",
},
{
input: {
tipologia: TipologiaPosizioneEnum.Stabile,
permanenza: null,
budget: 600,
},
exp_acconto: "ACCONTO_CA" as PrezziarioIdprezziario,
exp_saldo: "SALDOCA500+" as PrezziarioIdprezziario,
descrizione: "CA 500+",
},
{
input: {
tipologia: TipologiaPosizioneEnum.Stabile,
permanenza: null,
budget: 800,
},
exp_acconto: "ACCONTO_CA" as PrezziarioIdprezziario,
exp_saldo: "SALDOCA700+" as PrezziarioIdprezziario,
descrizione: "CA 700+",
},
];
describe("Test AccontoSolver", () => {
it.each(testCases)("$descrizione", async ({ input, exp_acconto }) => {
const pack = await AccontoSolver(input.tipologia);
assert(pack.success, "AccontoSolver failed");
expect(pack.pack.idprezziario).toEqual(exp_acconto);
});
});
describe("Test SaldoSolver", () => {
it.each(testCases)("$descrizione", async ({ input, exp_saldo }) => {
const pack = await SaldoSolver({
tipologia: input.tipologia,
permanenza: input.permanenza,
budget: input.budget,
});
assert(pack.success, "SaldoSolver failed");
expect(pack.pack.idprezziario).toEqual(exp_saldo);
});
});

View file

@ -5,16 +5,13 @@ import type {
Prezziario,
PrezziarioIdprezziario,
} from "~/schemas/public/Prezziario";
import type { Servizio, ServizioServizioId } from "~/schemas/public/Servizio";
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 { getServizioById } from "../services/servizio.service";
export const ACCONTO_TRANSITORIO = "ACCONTO_BD" as PrezziarioIdprezziario;
export const ACCONTO_STABILE = "ACCONTO_CA" as PrezziarioIdprezziario;
export type PreparedPaymentData = {
ordine_id: OrdiniOrdineId;
packid: PrezziarioIdprezziario;
@ -27,7 +24,10 @@ export type PreparedPaymentData = {
export const preparePayment = async (
servizioId: ServizioServizioId,
type: OrderTypeEnum,
): Promise<{success: true, data: PreparedPaymentData} | {success:false, message: string}> => {
): Promise<
| { success: true; data: PreparedPaymentData }
| { success: false; message: string }
> => {
try {
const servizio = await getServizioById(servizioId);
const inactiveOrders = await db
@ -41,13 +41,12 @@ export const preparePayment = async (
switch (type) {
case OrderTypeEnum.Acconto: {
if (!servizio.isOkAcconto) {
const acconto = await AccontoSolver(servizio);
const acconto = await AccontoSolver(servizio.tipologia);
if (!acconto.success) {
return {
success: false,
message: `Acconto non trovato per il servizio: ${acconto.message}`,
};
}
if (!acconto.pack.testo_condizioni) {
return {
@ -64,15 +63,18 @@ export const preparePayment = async (
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,
}};
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
@ -83,29 +85,32 @@ export const preparePayment = async (
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: 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{
return {
success: false,
message: "Il servizio ha già un acconto attivo, non è possibile preparare un nuovo pagamento"
}
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",
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
@ -127,15 +132,18 @@ export const preparePayment = async (
);
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,
}};
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({
@ -145,27 +153,32 @@ export const preparePayment = async (
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: 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{
return {
success: false,
message: "Il servizio ha già un saldo attivo, non è possibile preparare un nuovo pagamento"
}
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",
message:
"Non è possibile procedere con il pagamento della consulenza prima di aver attivato acconto e saldo",
};
}
if (!servizio.isOkConsulenza) {
@ -181,7 +194,7 @@ export const preparePayment = async (
return {
success: false,
message: `Pack della consulenza non trovato: ${existingConsulenza.packid}`,
}
};
}
if (!consulenzaPack.testo_condizioni) {
return {
@ -202,19 +215,21 @@ export const preparePayment = async (
},
};
}
return {
success: false,
message: "Il servizio non ha un pacchetto di consulenza, è necessario inserirlo prima di procedere",
message:
"Il servizio non ha un pacchetto di consulenza, è necessario inserirlo prima di procedere",
};
}
return {
success: false,
message: "Il servizio ha già una consulenza attiva, non è possibile preparare un nuovo pagamento",
message:
"Il servizio ha già una consulenza attiva, non è possibile preparare un nuovo pagamento",
};
}
case OrderTypeEnum.Rinnovo : {
case OrderTypeEnum.Rinnovo: {
const existingRinnovo = inactiveOrders.find(
(ordine) => ordine.type === OrderTypeEnum.Rinnovo,
);
@ -227,7 +242,7 @@ export const preparePayment = async (
return {
success: false,
message: `Pack del rinnovo non trovato: ${existingRinnovo.packid}`,
}
};
}
if (!rinnovoPack.testo_condizioni) {
return {
@ -250,10 +265,11 @@ export const preparePayment = async (
}
return {
success: false,
message: "Il servizio non ha un pacchetto di rinnovo, è necessario inserirlo prima di procedere",
message:
"Il servizio non ha un pacchetto di rinnovo, è necessario inserirlo prima di procedere",
};
}
case OrderTypeEnum.Altro : {
case OrderTypeEnum.Altro: {
const existingAltro = inactiveOrders.find(
(ordine) => ordine.type === OrderTypeEnum.Altro,
);
@ -266,14 +282,13 @@ export const preparePayment = async (
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}`,
};
}
return {
success: true,
@ -291,7 +306,8 @@ export const preparePayment = async (
return {
success: false,
message: "La preparazione del pagamento per questa tipologia non è supportata",
message:
"La preparazione del pagamento per questa tipologia non è supportata",
};
}
default:
@ -333,13 +349,13 @@ export const getPagamentoDataForCheckout = async (ordineId: OrdiniOrdineId) => {
}
};
const SaldoTransitorioMapping: Record<number, PrezziarioIdprezziario> = {
0: "SALDOBD_1MESE" as PrezziarioIdprezziario,
1: "SALDOBD_3MESI" as PrezziarioIdprezziario,
2: "SALDOBD_6MESI" as PrezziarioIdprezziario,
3: "SALDOBD_9MESI" as PrezziarioIdprezziario,
4: "SALDOBD_12MESI" as PrezziarioIdprezziario,
};
// const SaldoTransitorioMapping: Record<number, PrezziarioIdprezziario> = {
// 0: "SALDOBD_1MESE" as PrezziarioIdprezziario,
// 1: "SALDOBD_3MESI" as PrezziarioIdprezziario,
// 2: "SALDOBD_6MESI" as PrezziarioIdprezziario,
// 3: "SALDOBD_9MESI" as PrezziarioIdprezziario,
// 4: "SALDOBD_12MESI" as PrezziarioIdprezziario,
// };
// const ConsulenzaPriceMapping = {
// "4 + 4": "CONS_4+4" as PrezziarioIdprezziario,
@ -359,12 +375,19 @@ type ServizioPacks = {
saldo: GetPackResult | GetPackError;
};
const AccontoSolver = async (
servizio: Servizio,
export const AccontoSolver = async (
tipologia: TipologiaPosizioneEnum,
): Promise<GetPackResult | GetPackError> => {
switch (servizio.tipologia) {
switch (tipologia) {
case TipologiaPosizioneEnum.Transitorio: {
const pack = await getPrezziarioByIdHandler(ACCONTO_TRANSITORIO);
const pack = await db
.selectFrom("prezziario")
.selectAll()
.where("isActive", "=", true)
.where("order_type", "=", OrderTypeEnum.Acconto)
.where("service_type", "=", TipologiaPosizioneEnum.Transitorio)
.limit(1)
.executeTakeFirst();
if (!pack) {
return { success: false, message: "Pack non trovato" };
@ -376,7 +399,14 @@ const AccontoSolver = async (
}
case TipologiaPosizioneEnum.Stabile: {
const pack = await getPrezziarioByIdHandler(ACCONTO_STABILE);
const pack = await db
.selectFrom("prezziario")
.selectAll()
.where("isActive", "=", true)
.where("order_type", "=", OrderTypeEnum.Acconto)
.where("service_type", "=", TipologiaPosizioneEnum.Stabile)
.limit(1)
.executeTakeFirst();
if (!pack) {
return { success: false, message: "Pack non trovato" };
@ -391,20 +421,34 @@ const AccontoSolver = async (
}
};
export const SaldoSolver = async (
servizio: Servizio,
): Promise<GetPackResult | GetPackError> => {
switch (servizio.tipologia) {
export const SaldoSolver = async ({
tipologia,
permanenza,
budget,
}: {
tipologia: TipologiaPosizioneEnum;
permanenza: number | null;
budget: number;
}): Promise<GetPackResult | GetPackError> => {
switch (tipologia) {
case TipologiaPosizioneEnum.Transitorio: {
const mpp =
servizio.permanenza && SaldoTransitorioMapping[servizio.permanenza];
if (!servizio.permanenza || !mpp) {
if (permanenza === null) {
return {
success: false,
message: "Permanenza non selezionata",
};
}
const pack = await getPrezziarioByIdHandler(mpp);
const pack = await db
.selectFrom("prezziario")
.selectAll()
.where("isActive", "=", true)
.where("order_type", "=", OrderTypeEnum.Saldo)
.where("service_type", "=", TipologiaPosizioneEnum.Transitorio)
.where("service_variant", "=", permanenza)
.limit(1)
.executeTakeFirst();
if (!pack) {
return { success: false, message: "Pack non trovato" };
}
@ -412,13 +456,17 @@ export const SaldoSolver = async (
return { success: true, pack };
}
case TipologiaPosizioneEnum.Stabile: {
const pack = await getPrezziarioByIdHandler(
(servizio.budget > 700
? "SALDOCA700+"
: servizio.budget > 500
? "SALDOCA500+"
: "SALDOCA500") as PrezziarioIdprezziario,
);
const budget_step = budget > 700 ? 2 : budget > 500 ? 1 : 0;
const pack = await db
.selectFrom("prezziario")
.selectAll()
.where("isActive", "=", true)
.where("order_type", "=", OrderTypeEnum.Saldo)
.where("service_type", "=", TipologiaPosizioneEnum.Stabile)
.where("service_variant", "=", budget_step)
.limit(1)
.executeTakeFirst();
if (!pack) {
return { success: false, message: "Pack non trovato" };
}
@ -443,7 +491,11 @@ export const getPacksPerServizio = async (
});
}
return {
acconto: await AccontoSolver(servizio),
saldo: await SaldoSolver(servizio),
acconto: await AccontoSolver(servizio.tipologia),
saldo: await SaldoSolver({
tipologia: servizio.tipologia,
permanenza: servizio.permanenza,
budget: servizio.budget,
}),
};
};

View file

@ -1101,7 +1101,11 @@ export const adminUpdateConfermaStatus = async ({
const servizio = await getServizioById(servizioId);
const utente = await getUser({ db, userId: servizio.user_id });
const saldo = await SaldoSolver(servizio);
const saldo = await SaldoSolver({
tipologia: servizio.tipologia,
permanenza: servizio.permanenza,
budget: servizio.budget,
});
if (saldo.success) {
await createOrdine({
servizio_id: servizioId,

View file

@ -1,5 +1,5 @@
import { TRPCError } from "@trpc/server";
import { sql } from "kysely";
import OrderTypeEnum from "~/schemas/public/OrderTypeEnum";
import type {
NewPrezziario,
Prezziario,
@ -127,20 +127,13 @@ export const getPrezziarioByTipologia = async ({
}): Promise<PrezziarioByTipologia> => {
try {
const { acconto, saldi } = await db.transaction().execute(async (trx) => {
let qry = trx
const acconto = await trx
.selectFrom("prezziario")
.select(["idprezziario", "prezzo_cent", "nome_it", "nome_en", "sconto"])
.where("isAcconto", "=", true)
.where("isSaldo", "=", false)
.where("isActive", "=", true);
if (tipologia === TipologiaPosizioneEnum.Transitorio) {
qry = qry.where("isTransitorio", "=", true);
} else if (tipologia === TipologiaPosizioneEnum.Stabile) {
qry = qry.where("isStabile", "=", true);
}
const acconto = await qry.executeTakeFirst();
.where("order_type", "=", OrderTypeEnum.Acconto)
.where("service_type", "=", tipologia)
.where("isActive", "=", true)
.executeTakeFirst();
if (!acconto) {
throw new TRPCError({
@ -148,25 +141,15 @@ export const getPrezziarioByTipologia = async ({
message: "Acconto not found",
});
}
let qry2 = trx
const saldi = await trx
.selectFrom("prezziario")
.select(["idprezziario", "prezzo_cent", "nome_it", "nome_en", "sconto"])
.where("isAcconto", "=", false)
.where("isSaldo", "=", true)
.where("isActive", "=", true);
if (tipologia === TipologiaPosizioneEnum.Transitorio) {
qry2 = qry2.where("isTransitorio", "=", true);
} else if (tipologia === TipologiaPosizioneEnum.Stabile) {
qry2 = qry2.where("isStabile", "=", true);
}
// query with order by numeric part of idprezziario, handling '+' at the end
const saldi = await qry2
.orderBy(
sql`CAST(REGEXP_REPLACE(idprezziario, '[^0-9]+', '', 'g') AS INTEGER),
CASE WHEN idprezziario LIKE '%+%' THEN 1 ELSE 0 END`,
)
.where("order_type", "=", OrderTypeEnum.Saldo)
.where("service_type", "=", tipologia)
.where("isActive", "=", true)
.orderBy("service_variant", "asc")
.execute();
//const saldi = await qry2.orderBy("prezzo_cent", "asc").execute();
if (!saldi) {
throw new TRPCError({
@ -193,11 +176,9 @@ export const getPrezziarioConsulenza = async ({ db }: { db: Querier }) => {
const prezzi = await trx
.selectFrom("prezziario")
.select(["idprezziario", "prezzo_cent", "nome_it", "nome_en", "sconto"])
.where("isConsulenza", "=", true)
.where("isAcconto", "=", false)
.where("isSaldo", "=", false)
.where("order_type", "=", OrderTypeEnum.Consulenza)
.where("isActive", "=", true)
.where("isStabile", "=", true)
.where("service_type", "=", TipologiaPosizioneEnum.Stabile)
.orderBy("prezzo_cent", "asc")
.execute();

View file

@ -7,6 +7,7 @@ import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
import { getPrezziarioByIdHandler } from "./prezziario.service";
import type { AnnunciId } from "~/schemas/public/Annunci";
import type { ServizioAnnunciUpdate } from "~/schemas/public/ServizioAnnunci";
import { db } from "../db";
export const addServizio = async (data: NewServizio) => {
try {
@ -19,11 +20,15 @@ export const addServizio = async (data: NewServizio) => {
() => new Error("Failed to insert new servizio into database"),
);
const acconto = await getPrezziarioByIdHandler(
data.tipologia === TipologiaPosizioneEnum.Transitorio
? ACCONTO_TRANSITORIO
: ACCONTO_STABILE,
);
const acconto = await db
.selectFrom("prezziario")
.selectAll()
.where("isActive", "=", true)
.where("order_type", "=", OrderTypeEnum.Acconto)
.where("service_type", "=", data.tipologia)
.limit(1)
.executeTakeFirst();
if (!acconto) {
throw new Error("Acconto not found for new servizio");
}

View file

@ -1,10 +1,14 @@
import react from "@vitejs/plugin-react";
import { config } from "dotenv";
import tsconfigPaths from "vite-tsconfig-paths";
import { defineConfig } from "vitest/config";
config();
export default defineConfig({
plugins: [tsconfigPaths(), react()],
test: {
environment: "jsdom",
environment: "node",
env: process.env,
},
});