Files
Lorenzo SanesiandClaude Opus 5.5 b83ce039fb
Deploy stack su Proxmox / deploy (push) Failing after 3s
Piattaforma ITI TECHNOLOGY: monorepo, Medusa, API, sito, area riservata e deploy
Monorepo npm con apps/public (Angular SSR), apps/private (Angular SPA),
apps/api (Fastify), packages/shared e packages/styles; Medusa 2.21 in medusa/;
infrastruttura Docker (Postgres PostGIS+pgvector, Redis, Caddy, backup) e
workflow Gitea Actions per il deploy via SSH a ogni push su master.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018jQyT6dR9czzGwqb7FkG6u
2026-10-20 09:30:48 +02:00

211 lines
11 KiB
TypeScript

import { rm } from "node:fs/promises"
import { eq } from "drizzle-orm"
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"
import { PROVINCES } from "../src/configurator/rules.js"
import { installerProfiles, quoteRequests, users } from "../src/db/schema/index.js"
import { PROVINCE_CENTROIDS } from "../src/services/geo.js"
import { buildTestApp, createUserAndLogin, multipartBody, TEST_UPLOAD_DIR, type TestApp, truncateAll } from "./helpers.js"
import { MOCK_PRODUCT, MOCK_VARIANT } from "./medusa-mock.js"
const MONZA = { lat: 45.585, lng: 9.274 }
let t: TestApp
let admin: Record<string, string>
type Seed = { email: string; city: { lat: number; lng: number } | null; radius: number; status?: "pending" | "approved" | "rejected"; disabled?: boolean }
async function seedInstaller(s: Seed) {
const [u] = await t.app.db
.insert(users)
.values({
email: s.email,
passwordHash: "x",
role: "installer",
firstName: "I",
lastName: s.email,
disabledAt: s.disabled ? new Date() : null,
})
.returning()
await t.app.db.insert(installerProfiles).values({
userId: u!.id,
businessName: `Impianti ${s.email.split("@")[0]}`,
location: s.city,
serviceRadiusKm: s.radius,
status: s.status ?? "approved",
})
return u!
}
beforeAll(async () => {
t = await buildTestApp()
})
afterAll(async () => {
await t.app.close()
await rm(TEST_UPLOAD_DIR, { recursive: true, force: true })
})
beforeEach(async () => {
await t.app.flushSyncs()
await truncateAll()
t.medusa.reset()
t.email.clear()
t.app.shop.clearCache()
admin = (await createUserAndLogin(t.app, "admin@iti.local", "admin")).headers
})
describe("ricerca per zona (PostGIS)", () => {
beforeEach(async () => {
await seedInstaller({ email: "torino@example.com", city: { lat: 45.07, lng: 7.687 }, radius: 20 }) // ~135 km
await seedInstaller({ email: "bergamo@example.com", city: { lat: 45.698, lng: 9.677 }, radius: 40 }) // ~34 km
await seedInstaller({ email: "lecco@example.com", city: { lat: 45.856, lng: 9.398 }, radius: 10 }) // ~31 km, raggio piccolo
await seedInstaller({ email: "milano@example.com", city: { lat: 45.464, lng: 9.19 }, radius: 50 }) // ~15 km
await seedInstaller({ email: "attesa@example.com", city: MONZA, radius: 50, status: "pending" })
await seedInstaller({ email: "disabilitato@example.com", city: MONZA, radius: 50, disabled: true })
await seedInstaller({ email: "senzasede@example.com", city: null, radius: 300 })
})
it("solo gli installatori approvati entro il proprio raggio, in ordine di distanza", async () => {
const res = await t.app.inject({ method: "GET", url: `/admin/installers/nearby?lat=${MONZA.lat}&lng=${MONZA.lng}`, headers: admin })
expect(res.statusCode).toBe(200)
const list = res.json() as { email: string; distanceKm: number }[]
expect(list.map((i) => i.email)).toEqual(["milano@example.com", "bergamo@example.com"])
expect(list[0]!.distanceKm).toBeGreaterThan(13)
expect(list[0]!.distanceKm).toBeLessThan(17)
expect(list[1]!.distanceKm).toBeGreaterThan(30)
expect(list[1]!.distanceKm).toBeLessThan(37)
})
it("da una richiesta di preventivo: provincia → capoluogo → installatori", async () => {
const [r] = await t.app.db
.insert(quoteRequests)
.values({ email: "c@example.com", name: "C", province: "MB", configuratorInput: {} })
.returning()
const res = await t.app.inject({ method: "GET", url: `/admin/quote-requests/${r!.id}/installers`, headers: admin })
expect(res.json()).toMatchObject({ province: "MB", point: PROVINCE_CENTROIDS.MB })
expect(res.json().installers.map((i: { email: string }) => i.email)).toEqual(["milano@example.com", "bergamo@example.com"])
const [noProv] = await t.app.db.insert(quoteRequests).values({ email: "d@example.com", name: "D", configuratorInput: {} }).returning()
expect((await t.app.inject({ method: "GET", url: `/admin/quote-requests/${noProv!.id}/installers`, headers: admin })).json().error.code).toBe(
"PROVINCE_MISSING",
)
})
it("coordinate non valide o fuori dall'Italia → 400; solo admin", async () => {
expect((await t.app.inject({ method: "GET", url: "/admin/installers/nearby?lat=48.85&lng=2.35", headers: admin })).statusCode).toBe(400)
expect((await t.app.inject({ method: "GET", url: "/admin/installers/nearby?lat=abc&lng=9", headers: admin })).statusCode).toBe(400)
const customer = (await createUserAndLogin(t.app, "c@example.com")).headers
expect((await t.app.inject({ method: "GET", url: "/admin/installers/nearby?lat=45&lng=9", headers: customer })).statusCode).toBe(403)
})
it("ogni provincia ha le coordinate del capoluogo, in Italia", () => {
expect(PROVINCES.filter((p) => !PROVINCE_CENTROIDS[p.code])).toEqual([])
expect(Object.keys(PROVINCE_CENTROIDS)).toHaveLength(PROVINCES.length)
})
})
describe("profilo installatore", () => {
it("GET/PUT del proprio profilo; coordinate validate; stato e note non modificabili", async () => {
const inst = await createUserAndLogin(t.app, "inst@example.com", "installer")
await t.app.db.insert(installerProfiles).values({ userId: inst.user.id, businessName: "Impianti Srl", notes: "nota interna" })
const got = await t.app.inject({ method: "GET", url: "/installer/profile", headers: inst.headers })
expect(got.json()).toMatchObject({ businessName: "Impianti Srl", status: "pending", location: null, serviceRadiusKm: 50 })
expect(got.body).not.toContain("nota interna")
const body = {
businessName: "Impianti Rossi Srl",
vatNumber: "IT01234567890",
phone: "0391234567",
address: "Via Italia 1, Monza",
location: MONZA,
serviceRadiusKm: 35,
certifications: ["FER", "F-GAS", "FER"],
status: "approved",
}
const put = await t.app.inject({ method: "PUT", url: "/installer/profile", headers: inst.headers, payload: body })
expect(put.statusCode).toBe(200)
expect(put.json()).toMatchObject({ businessName: "Impianti Rossi Srl", location: MONZA, serviceRadiusKm: 35, certifications: ["FER", "F-GAS"], status: "pending" })
for (const bad of [{ location: { lat: 48.85, lng: 2.35 } }, { location: { lat: 95, lng: 9 } }, { serviceRadiusKm: 0 }]) {
expect((await t.app.inject({ method: "PUT", url: "/installer/profile", headers: inst.headers, payload: { ...body, ...bad } })).statusCode).toBe(400)
}
const customer = (await createUserAndLogin(t.app, "c@example.com")).headers
expect((await t.app.inject({ method: "GET", url: "/installer/profile", headers: customer })).statusCode).toBe(403)
})
})
describe("approvazione", () => {
const PDF = Buffer.from("%PDF-1.7\n%%EOF\n")
async function installerDoc() {
const body = multipartBody({ title: "Listino tecnico", visibility: "installer" }, [{ filename: "l.pdf", contentType: "application/pdf", content: PDF }])
const res = await t.app.inject({ method: "POST", url: "/admin/documents", payload: body.payload, headers: { ...body.headers, ...admin } })
return res.json().id as string
}
const price = async (headers: Record<string, string>) =>
(await t.app.inject({ method: "GET", url: `/shop/products/${MOCK_PRODUCT.handle}`, headers })).json().variants[0].price
it("pending: niente documenti installer né prezzi riservati; dopo l'approvazione sì (stesso token); dopo il rifiuto no", async () => {
const inst = await createUserAndLogin(t.app, "inst@example.com", "installer")
await t.app.db.insert(installerProfiles).values({ userId: inst.user.id, businessName: "Impianti Srl" })
const docId = await installerDoc()
await t.app.flushSyncs()
expect((await t.app.inject({ method: "GET", url: `/documents/${docId}/download`, headers: inst.headers })).statusCode).toBe(403)
expect(await price(inst.headers)).toBe(MOCK_VARIANT.price * 100)
const approved = await t.app.inject({ method: "POST", url: `/admin/installers/${inst.user.id}/approve`, headers: admin, payload: {} })
expect(approved.statusCode).toBe(200)
expect(approved.json()).toMatchObject({ status: "approved", medusaSynced: true })
expect(approved.json().approvedAt).not.toBeNull()
const customerId = t.medusa.accountByEmail("inst@example.com")!.id
expect(t.medusa.installerGroup.has(customerId)).toBe(true)
expect(t.email.last("installer-approved")?.to).toBe("inst@example.com")
t.app.shop.clearCache()
expect((await t.app.inject({ method: "GET", url: `/documents/${docId}/download`, headers: inst.headers })).statusCode).toBe(200)
expect(await price(inst.headers)).toBe(MOCK_VARIANT.installerPrice * 100)
const rejected = await t.app.inject({
method: "POST",
url: `/admin/installers/${inst.user.id}/reject`,
headers: admin,
payload: { reason: "Certificazioni scadute" },
})
expect(rejected.json()).toMatchObject({ status: "rejected", approvedAt: null, medusaSynced: true })
expect(t.medusa.installerGroup.has(customerId)).toBe(false)
expect(t.email.last("installer-rejected")?.text).toContain("Certificazioni scadute")
t.app.shop.clearCache()
expect((await t.app.inject({ method: "GET", url: `/documents/${docId}/download`, headers: inst.headers })).statusCode).toBe(403)
expect(await price(inst.headers)).toBe(MOCK_VARIANT.price * 100)
})
it("Medusa giù all'approvazione: approvato comunque (medusaSynced false), gruppo allineato al login successivo", async () => {
const inst = await createUserAndLogin(t.app, "inst@example.com", "installer")
await t.app.db.insert(installerProfiles).values({ userId: inst.user.id, businessName: "Impianti Srl" })
await t.app.flushSyncs()
t.medusa.down = true
const res = await t.app.inject({ method: "POST", url: `/admin/installers/${inst.user.id}/approve`, headers: admin, payload: {} })
expect(res.json()).toMatchObject({ status: "approved", medusaSynced: false })
t.medusa.down = false
await t.app.inject({ method: "POST", url: "/auth/login", payload: { email: "inst@example.com", password: "Password-di-test-1" } })
await t.app.flushSyncs()
const customerId = t.medusa.accountByEmail("inst@example.com")!.id
expect(t.medusa.installerGroup.has(customerId)).toBe(true)
})
it("approvare un customer che ha un profilo lo rende installer; elenco per stato e note interne", async () => {
const c = await createUserAndLogin(t.app, "ex-cliente@example.com", "customer")
await t.app.db.insert(installerProfiles).values({ userId: c.user.id, businessName: "Nuova Ditta" })
expect((await t.app.inject({ method: "GET", url: "/admin/installers?status=pending", headers: admin })).json().total).toBe(1)
await t.app.inject({ method: "POST", url: `/admin/installers/${c.user.id}/approve`, headers: admin, payload: { notes: "Verificata visura" } })
const [u] = await t.app.db.select().from(users).where(eq(users.id, c.user.id))
expect(u!.role).toBe("installer")
const detail = (await t.app.inject({ method: "GET", url: `/admin/installers/${c.user.id}`, headers: admin })).json()
expect(detail).toMatchObject({ status: "approved", notes: "Verificata visura", email: "ex-cliente@example.com" })
expect((await t.app.inject({ method: "GET", url: "/admin/installers?status=pending", headers: admin })).json().total).toBe(0)
})
})